diff --git a/pkg/client/websocket.go b/pkg/client/websocket.go index cd6c25c..71d5018 100644 --- a/pkg/client/websocket.go +++ b/pkg/client/websocket.go @@ -241,6 +241,15 @@ func (ws *WebSocketClient) connectWithConfig(config *WebSocketConfig) error { ws.conn = conn ws.connected = true + // Extend the read deadline on every pong so the connection survives + // quiet periods between speaker events. Without this, the 60-second + // read deadline in readLoop fires reliably after one ping cycle (30 s + // ping interval + 5 s reconnect = ~65 s disconnect loop). + conn.SetPongHandler(func(string) error { + _ = conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + return nil + }) + // Start background goroutines for connection management go ws.readLoop(config) go ws.pingLoop(config) diff --git a/pkg/service/soundtouchweb/discovery.go b/pkg/service/soundtouchweb/discovery.go index 4e687cf..1cc2b24 100644 --- a/pkg/service/soundtouchweb/discovery.go +++ b/pkg/service/soundtouchweb/discovery.go @@ -71,6 +71,18 @@ func (app *WebApp) AddDeviceByHost(host string, port int, source string) { go app.UpdateDeviceStatus(host, conn) + // Poll via HTTP every 30 s as a fallback for WebSocket events that the + // speaker does not emit (e.g. Spotify Connect track changes) and for the + // window between a WS disconnect and its reconnect. + go func() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + for range ticker.C { + app.UpdateDeviceStatus(host, conn) + } + }() + log.Printf("Added %s device %s (%s) at %s:%d", sanitizeLog(source), sanitizeLog(info.Name), sanitizeLog(info.Type), sanitizeLog(host), port) } diff --git a/pkg/service/soundtouchweb/static/js/components/NowPlaying.js b/pkg/service/soundtouchweb/static/js/components/NowPlaying.js index f59b774..0a243cf 100644 --- a/pkg/service/soundtouchweb/static/js/components/NowPlaying.js +++ b/pkg/service/soundtouchweb/static/js/components/NowPlaying.js @@ -93,7 +93,8 @@ export function NowPlaying({ nowPlaying, deviceId, presets }) { if (nowPlaying?.PlayStatus !== 'PLAY_STATE') return; const id = setInterval(() => setPosition(p => p + 1), 1000); return () => clearInterval(id); - }, [nowPlaying?.Time?.Position, nowPlaying?.PlayStatus]); + }, [nowPlaying?.Time?.Position, nowPlaying?.PlayStatus, + nowPlaying?.TrackID, nowPlaying?.ContentItem?.Location]); if (!nowPlaying || nowPlaying.Source === 'STANDBY') { return html`
`; @@ -121,7 +122,7 @@ export function NowPlaying({ nowPlaying, deviceId, presets }) { - ${fmt(position)} / ${fmt(total)} + ${fmt(Math.min(position, total))} / ${fmt(total)} `} diff --git a/pkg/service/soundtouchweb/websocket.go b/pkg/service/soundtouchweb/websocket.go index 24d722e..028c567 100644 --- a/pkg/service/soundtouchweb/websocket.go +++ b/pkg/service/soundtouchweb/websocket.go @@ -216,6 +216,11 @@ func (app *WebApp) ConnectDeviceWebSocket(deviceID string, conn *webtypes.Device log.Printf("WebSocket connected for device %s", sanitizeLog(deviceID)) + // Fetch current state immediately: speakers do not replay events on + // new WebSocket connections, so anything that changed while we were + // disconnected would otherwise stay stale until the next WS event. + go app.UpdateDeviceStatus(deviceID, conn) + // Reset backoff after a successful connect so the next failure // starts at the lowest cadence again. backoff = initialBackoff