Files
spotify-ws/internal/websocket/poller.go
2025-10-03 00:15:02 +02:00

109 lines
2.5 KiB
Go

package websocket
import (
"context"
"log/slog"
"sync"
"time"
"spotify-ws/internal/spotify"
"golang.org/x/net/websocket"
)
// Poller is responsible for fetching data from the Spotify API periodically.
type Poller struct {
client *spotify.Client
hub *Hub
lastState *spotify.CurrentlyPlaying
mu sync.RWMutex
}
// NewPoller creates a new Poller.
func NewPoller(client *spotify.Client, hub *Hub) *Poller {
return &Poller{
client: client,
hub: hub,
}
}
// Run starts the polling loop. It must be run in a separate goroutine.
func (p *Poller) Run(ctx context.Context) {
slog.Info("poller started")
defer slog.Info("poller stopped")
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
p.UpdateState(ctx)
}
}
}
// UpdateState fetches the latest state, compares it, and broadcasts if needed.
func (p *Poller) UpdateState(ctx context.Context) {
current, err := p.client.CurrentlyPlaying(ctx)
if err != nil {
slog.Error("failed to get currently playing track", "error", err)
return
}
p.mu.Lock()
hasChanged := p.hasStateChanged(current)
if hasChanged {
p.lastState = current
}
p.mu.Unlock()
if hasChanged {
if !p.hub.realtime {
trackName := "Nothing"
if current.Item != nil {
trackName = current.Item.Name
}
slog.Info("state changed, broadcasting update", "isPlaying", current.IsPlaying, "track", trackName)
}
p.hub.Broadcast(current)
}
}
// SendLastState sends the cached state to a single new client.
func (p *Poller) SendLastState(ws *websocket.Conn) {
p.mu.RLock()
defer p.mu.RUnlock()
if p.lastState == nil {
return
}
clientPayload := newPlaybackState(p.lastState, p.hub.realtime)
if err := websocket.JSON.Send(ws, clientPayload); err != nil {
slog.Warn("failed to send initial state to client", "error", err, "remoteAddr", ws.RemoteAddr())
}
}
// hasStateChanged performs a robust comparison between the new and old states.
// This function must be called within a lock.
func (p *Poller) hasStateChanged(current *spotify.CurrentlyPlaying) bool {
if p.lastState == nil {
return true
}
if p.hub.realtime && current.IsPlaying && current.Item != nil {
return true
}
if p.lastState.IsPlaying != current.IsPlaying {
return true
}
if (p.lastState.Item == nil) != (current.Item == nil) {
return true
}
if p.lastState.Item != nil && current.Item != nil && p.lastState.Item.ID != current.Item.ID {
return true
}
return false
}