fix(player): skip stereo polling on unsupported models

This commit is contained in:
Lukáš Lipinský
2026-08-30 22:27:56 +02:00
committed by Tobias Gesellchen
parent ce66b103b4
commit 3b04a6eb57
3 changed files with 76 additions and 6 deletions
@@ -52,6 +52,16 @@ type stereoPairMemberView struct {
Available bool `json:"available"`
}
func stereoPairCapable(info *models.DeviceInfo) bool {
if info == nil {
return false
}
typeName := strings.ToLower(strings.TrimSpace(info.Type))
return typeName == "st10" || typeName == "soundtouch 10"
}
// deviceViewSnapshot projects the physical registry into logical control
// targets for the HTTP API and the global player WebSocket.
func (app *WebApp) deviceViewSnapshot() map[string]deviceView {
+18 -4
View File
@@ -303,7 +303,13 @@ func (app *WebApp) UpdateDeviceStatus(_ string, conn *webtypes.DeviceConnection)
return
}
groupGeneration := conn.BeginGroupRefresh()
// /getGroup is ST10-only; ST20/ST30 may accept the request but never reply.
stereoCapable := stereoPairCapable(conn.DeviceInfo)
var groupGeneration uint64
if stereoCapable {
groupGeneration = conn.BeginGroupRefresh()
}
// Phase 1: slow network fetches. Local vars only, no shared state
// is touched yet. Errors are recorded so the merge below can tell
@@ -313,7 +319,15 @@ func (app *WebApp) UpdateDeviceStatus(_ string, conn *webtypes.DeviceConnection)
presets, presetsErr := conn.Client.GetPresets()
sources, sourcesErr := conn.Client.GetSources()
bass, bassErr := conn.Client.GetBass()
group, groupErr := conn.Client.GetGroup()
var (
group *models.Group
groupErr error
)
if stereoCapable {
group, groupErr = conn.Client.GetGroup()
}
// Phase 2: fast merge. Only fields we successfully fetched
// overwrite; everything else keeps the value other goroutines may
@@ -346,7 +360,7 @@ func (app *WebApp) UpdateDeviceStatus(_ string, conn *webtypes.DeviceConnection)
statusUpdated = true
}
statusUpdated = statusUpdated || groupErr == nil
statusUpdated = statusUpdated || (stereoCapable && groupErr == nil)
// Mark as connected if we successfully got at least one
// status from this round. Mirrors prior behaviour.
@@ -354,7 +368,7 @@ func (app *WebApp) UpdateDeviceStatus(_ string, conn *webtypes.DeviceConnection)
s.LastActivity = time.Now()
})
if groupErr == nil {
if stereoCapable && groupErr == nil {
conn.ApplyPolledGroup(groupGeneration, group)
}
}
+48 -2
View File
@@ -3,6 +3,7 @@ package soundtouchweb
import (
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
@@ -23,7 +24,7 @@ func TestUpdateDeviceStatusRefreshesGroup(t *testing.T) {
</group>`)
defer server.Close()
conn := webtypes.NewDeviceConnection(client.NewClientFromHost(server.URL), nil)
conn := webtypes.NewDeviceConnection(client.NewClientFromHost(server.URL), &models.DeviceInfo{Type: "SoundTouch 10"})
NewWebApp().UpdateDeviceStatus("device-1", conn)
status := conn.Status()
@@ -48,7 +49,7 @@ func TestUpdateDeviceStatusPreservesGroupOnError(t *testing.T) {
server := newStatusTestServer(t, http.StatusInternalServerError, "group unavailable")
defer server.Close()
conn := webtypes.NewDeviceConnection(client.NewClientFromHost(server.URL), nil)
conn := webtypes.NewDeviceConnection(client.NewClientFromHost(server.URL), &models.DeviceInfo{Type: "SoundTouch 10"})
existing := &models.Group{ID: "pair-old", Name: "Existing Pair"}
conn.SetStatus(&webtypes.DeviceStatus{Group: existing})
@@ -64,6 +65,51 @@ func TestUpdateDeviceStatusPreservesGroupOnError(t *testing.T) {
}
}
func TestUpdateDeviceStatusSkipsGroupForNonStereoModel(t *testing.T) {
var groupRequested atomic.Bool
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/getGroup" {
groupRequested.Store(true)
http.Error(w, "unsupported endpoint", http.StatusInternalServerError)
return
}
responses := map[string]string{
"/now_playing": `<nowPlaying source="STANDBY"><playStatus>STOP_STATE</playStatus></nowPlaying>`,
"/volume": `<volume><targetvolume>10</targetvolume><actualvolume>10</actualvolume><muteenabled>false</muteenabled></volume>`,
"/presets": `<presets/>`,
"/sources": `<sources/>`,
"/bass": `<bass><targetbass>0</targetbass><actualbass>0</actualbass></bass>`,
}
body, ok := responses[r.URL.Path]
if !ok {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/xml")
_, _ = w.Write([]byte(body))
}))
defer server.Close()
for _, model := range []string{"SoundTouch 20", "SoundTouch 30"} {
t.Run(model, func(t *testing.T) {
conn := webtypes.NewDeviceConnection(client.NewClientFromHost(server.URL), &models.DeviceInfo{Type: model})
NewWebApp().UpdateDeviceStatus("device-1", conn)
if !conn.Status().IsConnected {
t.Fatal("successful ordinary status requests should mark a non-stereo model connected")
}
})
}
if groupRequested.Load() {
t.Fatal("UpdateDeviceStatus requested /getGroup for a non-stereo model")
}
}
func TestApplyGroupUpdatedEventReplacesGroup(t *testing.T) {
conn := webtypes.NewDeviceConnection(nil, nil)
previousActivity := time.Unix(1, 0)