This commit is contained in:
2025-10-03 00:15:02 +02:00
parent c9806d5fe9
commit 71e16acf9a
20 changed files with 599 additions and 419 deletions

View File

@@ -0,0 +1,74 @@
package spotify
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"golang.org/x/oauth2"
)
const (
tokenURL = "https://accounts.spotify.com/api/token"
currentlyPlayingURL = "https://api.spotify.com/v1/me/player/currently-playing"
)
// Client is a thread-safe client for interacting with the Spotify API.
type Client struct {
httpClient *http.Client
}
// NewClient creates a new Spotify API client using the refresh token flow.
// The returned client is safe for concurrent use.
func NewClient(ctx context.Context, clientID, clientSecret, refreshToken string) *Client {
conf := &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
Endpoint: oauth2.Endpoint{
TokenURL: tokenURL,
},
}
token := &oauth2.Token{
RefreshToken: refreshToken,
}
// The TokenSource is concurrency-safe and handles token refreshes automatically.
tokenSource := conf.TokenSource(ctx, token)
return &Client{
httpClient: oauth2.NewClient(ctx, tokenSource),
}
}
// CurrentlyPlaying fetches the user's currently playing track from the Spotify API.
func (c *Client) CurrentlyPlaying(ctx context.Context) (*CurrentlyPlaying, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, currentlyPlayingURL, nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer func() {
if err := resp.Body.Close(); err != nil {
slog.Warn("failed to close spotify api response body", "error", err)
}
}()
// When nothing is playing, Spotify returns 204 No Content.
// We normalize this to a consistent struct response for the caller.
if resp.StatusCode == http.StatusNoContent {
return &CurrentlyPlaying{IsPlaying: false, Item: nil}, nil
}
var currentlyPlaying CurrentlyPlaying
if err := json.NewDecoder(resp.Body).Decode(&currentlyPlaying); err != nil {
return nil, err
}
return &currentlyPlaying, nil
}

25
internal/spotify/model.go Normal file
View File

@@ -0,0 +1,25 @@
package spotify
// TrackItem represents the track object from the Spotify API.
type TrackItem struct {
ID string `json:"id"`
Name string `json:"name"`
DurationMs int `json:"duration_ms"`
Artists []struct {
Name string `json:"name"`
} `json:"artists"`
Album struct {
Images []struct {
URL string `json:"url"`
} `json:"images"`
} `json:"album"`
}
// CurrentlyPlaying represents the currently playing object from the Spotify API.
// The Item field is a pointer to handle cases where nothing is playing (item is null).
type CurrentlyPlaying struct {
IsPlaying bool `json:"is_playing"`
ProgressMs int `json:"progress_ms"`
Timestamp int64 `json:"timestamp"`
Item *TrackItem `json:"item"`
}