Files
ripsort/internal/playlist/spotify.go
2026-05-07 16:26:03 +02:00

167 lines
3.5 KiB
Go

package playlist
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)
}