mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
fix(web): strip placeholder SourceAccount before replaying recents
Speakers echo back the source name as SourceAccount when no real credential is set (e.g. SourceAccount="TUNEIN" for a TUNEIN source). HandleDevicePlay was forwarding this verbatim, causing the speaker to try authenticating with the source name as a TuneIn account and returning INVALID_SOURCE. Clear SourceAccount when it equals Source; preserve it when it differs (real credentials such as Spotify or STORED_MUSIC UUIDs). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
11a6515f4d
commit
abe9079382
@@ -1031,13 +1031,12 @@ func (app *WebApp) HandleDevicePlay(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
contentItem := &models.ContentItem{
|
||||
Source: req.Source,
|
||||
Type: req.Type,
|
||||
Location: req.Location,
|
||||
SourceAccount: req.SourceAccount,
|
||||
ItemName: req.ItemName,
|
||||
ContainerArt: req.ContainerArt,
|
||||
IsPresetable: req.IsPresetable,
|
||||
Source: req.Source,
|
||||
Type: req.Type,
|
||||
Location: req.Location,
|
||||
ItemName: req.ItemName,
|
||||
ContainerArt: req.ContainerArt,
|
||||
IsPresetable: req.IsPresetable,
|
||||
}
|
||||
|
||||
if err := device.Client.SelectContentItem(contentItem); err != nil {
|
||||
@@ -1124,6 +1123,11 @@ func (app *WebApp) HandlePlayRadioBrowser(w http.ResponseWriter, r *http.Request
|
||||
ItemName: req.Name,
|
||||
IsPresetable: true,
|
||||
}
|
||||
// Only pass SourceAccount when it's a real credential, not the placeholder
|
||||
// value that speakers echo back (source name == source account, e.g. "TUNEIN").
|
||||
if req.SourceAccount != "" && req.SourceAccount != req.Source {
|
||||
contentItem.SourceAccount = req.SourceAccount
|
||||
}
|
||||
|
||||
if err := device.Client.SelectContentItem(contentItem); err != nil {
|
||||
app.sendError(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
@@ -4,6 +4,7 @@ package soundtouchweb
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -622,3 +623,84 @@ func BenchmarkSendError(b *testing.B) {
|
||||
app.sendError(w, "Test error", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleDevicePlay_SourceAccountFiltering verifies that a SourceAccount
|
||||
// equal to Source (the placeholder speakers echo back, e.g. "TUNEIN") is
|
||||
// stripped before the ContentItem XML is sent to the speaker, while a real
|
||||
// credential (SourceAccount != Source) is preserved.
|
||||
func TestHandleDevicePlay_SourceAccountFiltering(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
sourceAccount string
|
||||
wantSourceAccount string // empty means the XML attr must be absent
|
||||
}{
|
||||
{
|
||||
name: "placeholder echoed back — stripped",
|
||||
source: "TUNEIN",
|
||||
sourceAccount: "TUNEIN",
|
||||
wantSourceAccount: "",
|
||||
},
|
||||
{
|
||||
name: "real credential — preserved",
|
||||
source: "TUNEIN",
|
||||
sourceAccount: "real-account-id",
|
||||
wantSourceAccount: "real-account-id",
|
||||
},
|
||||
{
|
||||
name: "empty account — stays empty",
|
||||
source: "TUNEIN",
|
||||
sourceAccount: "",
|
||||
wantSourceAccount: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var capturedBody string
|
||||
|
||||
// Fake speaker that captures the /select POST body.
|
||||
speaker := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/select" {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
capturedBody = string(b)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer speaker.Close()
|
||||
|
||||
speakerClient := client.NewClient(&client.Config{Host: speaker.URL})
|
||||
|
||||
app := NewWebApp()
|
||||
deviceInfo := &models.DeviceInfo{Name: "Test Speaker"}
|
||||
conn := webtypes.NewDeviceConnection(speakerClient, deviceInfo)
|
||||
conn.SetStatus(&webtypes.DeviceStatus{IsConnected: true, LastActivity: time.Now()})
|
||||
app.AddDevice("play-device", conn)
|
||||
|
||||
body := strings.NewReader(`{
|
||||
"source":"` + tt.source + `",
|
||||
"type":"stationurl",
|
||||
"location":"/v1/playback/station/s6634",
|
||||
"sourceAccount":"` + tt.sourceAccount + `",
|
||||
"itemName":"Venice Classic Radio"
|
||||
}`)
|
||||
req := httptest.NewRequest("POST", "/api/device-play/play-device", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req = withChiParams(req, map[string]string{"id": "play-device"})
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
app.HandleDevicePlay(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
// SourceAccount XML attribute is always emitted (no omitempty on the struct
|
||||
// tag), so check its value rather than its presence/absence.
|
||||
want := `sourceAccount="` + tt.wantSourceAccount + `"`
|
||||
if !strings.Contains(capturedBody, want) {
|
||||
t.Errorf("XML should contain %q, got: %s", want, capturedBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user