Skip to content

Commit

Permalink
refactor: use lyricsapi package (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
raitonoberu committed Apr 16, 2024
1 parent 9b19c8d commit 30f434f
Show file tree
Hide file tree
Showing 3 changed files with 23 additions and 197 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
215 changes: 18 additions & 197 deletions services/spotify/spotify.go
Original file line number Diff line number Diff line change
@@ -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 := &currentBody{}
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,
Expand All @@ -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"`
}

0 comments on commit 30f434f

Please sign in to comment.