mirror of
https://github.com/skidoodle/spotify-ws
synced 2025-10-09 05:22:43 +02:00
109 lines
2.5 KiB
Go
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
|
|
}
|