diff --git a/go.mod b/go.mod index d4d57b4..e108bc5 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/fhs/gompd v1.0.1 github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 github.com/muesli/reflow v0.3.0 + github.com/raitonoberu/lyricsapi v0.0.0-20240416172642-b6d0748007a6 github.com/spf13/cobra v1.8.0 gopkg.in/yaml.v2 v2.4.0 nhooyr.io/websocket v1.8.10 diff --git a/go.sum b/go.sum index aef8338..678b159 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,10 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/raitonoberu/lyricsapi v0.0.0-20240413165628-659da1d2b906 h1:7lgUpQkYK65IHJ9whNFP2nsC236hrWB7eEQxHKXhER0= +github.com/raitonoberu/lyricsapi v0.0.0-20240413165628-659da1d2b906/go.mod h1:OZuR43VBUauOq6BfM6M8Frcw7JPOSa2wKHP6lIqomhg= +github.com/raitonoberu/lyricsapi v0.0.0-20240416172642-b6d0748007a6 h1:7W6KMthUiKLpv1O+WvV9raMu3iKIY0e+EB1N8dLIToI= +github.com/raitonoberu/lyricsapi v0.0.0-20240416172642-b6d0748007a6/go.mod h1:OZuR43VBUauOq6BfM6M8Frcw7JPOSa2wKHP6lIqomhg= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= diff --git a/services/spotify/spotify.go b/services/spotify/spotify.go index c4edc2b..faf0221 100644 --- a/services/spotify/spotify.go +++ b/services/spotify/spotify.go @@ -1,75 +1,37 @@ package spotify import ( - "encoding/json" - "errors" - "io" - "net/http" - "net/url" + "strings" + "sptlrx/lyrics" "sptlrx/player" - "strings" - "time" -) -var ( - ErrInvalidCookie = errors.New("invalid or empty cookie provided") + lyricsapi "github.com/raitonoberu/lyricsapi/lyrics" ) -const tokenUrl = "https://open.spotify.com/get_access_token?reason=transport&productType=web_player" -const lyricsUrl = "https://spclient.wg.spotify.com/color-lyrics/v2/track/" -const stateUrl = "https://api.spotify.com/v1/me/player/currently-playing" -const searchUrl = "https://api.spotify.com/v1/search?" +var ErrInvalidCookie = lyricsapi.ErrInvalidCookie func New(cookie string) (*Client, error) { if cookie == "" { return nil, ErrInvalidCookie } - return &Client{cookie: cookie}, nil + return &Client{lyricsapi.NewLyricsApi(cookie)}, nil } // Client implements both player.Player and lyrics.Provider type Client struct { - cookie string - token string - expiresIn time.Time + api *lyricsapi.LyricsApi } func (c *Client) State() (*player.State, error) { - err := c.checkToken() + result, err := c.api.State() if err != nil { return nil, err } - - req, _ := http.NewRequest("GET", stateUrl, nil) - req.Header = http.Header{ - "referer": {"https://open.spotify.com/"}, - "origin": {"https://open.spotify.com/"}, - "accept": {"application/json"}, - "accept-language": {"en"}, - "app-platform": {"WebPlayer"}, - "sec-ch-ua-mobile": {"?0"}, - - "Authorization": {"Bearer " + c.token}, - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - result := ¤tBody{} - err = json.NewDecoder(resp.Body).Decode(result) - if err != nil { - if err == io.EOF { - // stopped - return nil, nil - } - return nil, err - } - if result.Item == nil { + if result == nil || result.Item == nil { return nil, nil } + return &player.State{ ID: "spotify:" + result.Item.ID, Position: result.Progress, @@ -78,167 +40,26 @@ func (c *Client) State() (*player.State, error) { } func (c *Client) Lyrics(id, query string) ([]lyrics.Line, error) { + var ( + result *lyricsapi.LyricsResult + err error + ) if strings.HasPrefix(id, "spotify:") { - return c.lyrics(id[8:]) - } - id, err := c.search(query) - if err != nil { - return nil, err - } - return c.lyrics(id) -} - -func (c *Client) search(query string) (string, error) { - err := c.checkToken() - if err != nil { - return "", err - } - - url := searchUrl + url.Values{ - "limit": {"1"}, - "type": {"track"}, - "q": {query}, - }.Encode() - req, _ := http.NewRequest("GET", url, nil) - req.Header.Set("Authorization", "Bearer "+c.token) - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - result := &searchBody{} - err = json.NewDecoder(resp.Body).Decode(result) - if err != nil { - return "", err - } - if result.Tracks.Total == 0 { - return "", nil - } - return result.Tracks.Items[0].ID, nil -} - -func (c *Client) lyrics(spotifyID string) ([]lyrics.Line, error) { - err := c.checkToken() - if err != nil { - return nil, err + result, err = c.api.GetByID(id[8:]) + } else { + result, err = c.api.GetByName(query) } - req, _ := http.NewRequest("GET", lyricsUrl+spotifyID, nil) - req.Header = http.Header{ - "referer": {"https://open.spotify.com/"}, - "origin": {"https://open.spotify.com/"}, - "accept": {"application/json"}, - "accept-language": {"en"}, - "app-platform": {"WebPlayer"}, - "sec-ch-ua-mobile": {"?0"}, - - "Authorization": {"Bearer " + c.token}, - } - resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } - defer resp.Body.Close() - - result := &lyricsBody{} - err = json.NewDecoder(resp.Body).Decode(result) - if err != nil { - if err == io.EOF { - // no lyrics - return nil, nil - } - return nil, err + if result == nil || len(result.Lyrics.Lines) == 0 { + return nil, nil } lines := make([]lyrics.Line, len(result.Lyrics.Lines)) for i, l := range result.Lyrics.Lines { lines[i] = lyrics.Line(l) } - return lines, nil } - -func (c *Client) checkToken() error { - if !c.tokenExpired() { - return nil - } - return c.updateToken() -} - -func (c *Client) tokenExpired() bool { - return c.token == "" || time.Now().After(c.expiresIn) -} - -func (c *Client) updateToken() error { - req, _ := http.NewRequest("GET", tokenUrl, nil) - req.Header = http.Header{ - "referer": {"https://open.spotify.com/"}, - "origin": {"https://open.spotify.com/"}, - "accept": {"application/json"}, - "accept-language": {"en"}, - "app-platform": {"WebPlayer"}, - "sec-fetch-dest": {"empty"}, - "sec-fetch-mode": {"cors"}, - "sec-fetch-site": {"same-origin"}, - "spotify-app-version": {"1.1.54.35.ge9dace1d"}, - "cookie": {c.cookie}, - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - result := &tokenBody{} - err = json.NewDecoder(resp.Body).Decode(result) - if err != nil { - return err - } - - if result.IsAnonymous { - return ErrInvalidCookie - } - - if result.AccessToken == "" { - return errors.New("couldn't get access token") - } - - c.token = result.AccessToken - c.expiresIn = time.Unix(0, result.ExpiresIn*int64(time.Millisecond)) - - return nil -} - -type tokenBody struct { - AccessToken string `json:"accessToken"` - ExpiresIn int64 `json:"accessTokenExpirationTimestampMs"` - IsAnonymous bool `json:"isAnonymous"` -} - -type lyricsBody struct { - Lyrics struct { - Lines []struct { - Time int `json:"startTimeMs,string"` - Words string `json:"words"` - } `json:"lines"` - } `json:"lyrics"` -} - -type currentBody struct { - Progress int `json:"progress_ms"` - Playing bool `json:"is_playing"` - Item *struct { - ID string `json:"id"` - } `json:"item"` -} - -type searchBody struct { - Tracks struct { - Items []struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"items"` - Total int `json:"total"` - } `json:"tracks"` -}