package spotify import ( "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "sync" "time" ) const spotifyAPIBase = "https://api.spotify.com/v1" type SpotifyPlaylist struct { Name string `json:"name"` Description string `json:"description"` Public bool `json:"public"` Images []struct { Url string `json:"url"` Height int `json:"height"` Width int `json:"width"` } `json:"images"` Items struct { Limit int `json:"limit"` Offset int `json:"offset"` Total int `json:"total"` Items []struct { Item SpotifyTrack `json:"item"` } `json:"items"` } `json:"items"` ExternalURLs struct { Spotify string `json:"spotify"` } `json:"external_urls"` } type SpotifyTrack struct { ID string `json:"id"` Name string `json:"name"` Artists []struct { Name string `json:"name"` } `json:"artists"` ExternalIDs struct { Isrc string `json:"isrc"` } `json:"external_ids"` } type SpotifyClient struct { clientID string clientSecret string token string mu sync.Mutex httpClient *http.Client } type spotifyTokenResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` } func NewSpotifyClient(clientID, clientSecret string) (*SpotifyClient, error) { c := &SpotifyClient{ clientID: clientID, clientSecret: clientSecret, httpClient: &http.Client{Timeout: 30 * time.Second}, } if err := c.renewToken(); err != nil { return nil, err } return c, nil } func (c *SpotifyClient) GetPlaylist(playlistID string) (*SpotifyPlaylist, error) { // TODO: handle rate limit // TODO: handle request limits reqURL := fmt.Sprintf("%s/playlists/%s", spotifyAPIBase, playlistID) req, err := http.NewRequest("GET", reqURL, nil) if err != nil { return nil, err } resp, err := c.do(req) if err != nil { return nil, err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("status code: %d: %s", resp.StatusCode, body) } var playlist SpotifyPlaylist if err := json.Unmarshal(body, &playlist); err != nil { return nil, err } return &playlist, nil } func (c *SpotifyClient) renewToken() error { creds := base64.StdEncoding.EncodeToString([]byte(c.clientID + ":" + c.clientSecret)) data := url.Values{} data.Set("grant_type", "client_credentials") req, err := http.NewRequest("POST", "https://accounts.spotify.com/api/token", strings.NewReader(data.Encode())) if err != nil { return err } req.Header.Set("Authorization", "Basic "+creds) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := c.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { return fmt.Errorf("auth failed: %d: %s", resp.StatusCode, body) } var token spotifyTokenResponse if err := json.Unmarshal(body, &token); err != nil { return err } c.token = token.AccessToken return nil } func (c *SpotifyClient) do(req *http.Request) (*http.Response, error) { resp, err := c.doOnce(req) if err != nil { return nil, err } if resp.StatusCode == http.StatusUnauthorized { resp.Body.Close() if err := c.renewToken(); err != nil { return nil, err } return c.doOnce(req) } return resp, nil } func (c *SpotifyClient) doOnce(req *http.Request) (*http.Response, error) { c.mu.Lock() req.Header.Set("Authorization", "Bearer "+c.token) c.mu.Unlock() return c.httpClient.Do(req) }