From 8ea24612655fd1428f914747f035d12890b84b2e Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Tue, 2 Jun 2026 20:58:37 +0200 Subject: [PATCH] fix(web): forward account param when selecting a source (#444) The /api/control/{host}/source handler hardcoded an empty sourceAccount, so devices that share source="AUX" across multiple jacks (e.g. the ST-5 CD/Aux inputs, disambiguated by AUX/AUX1/AUX2) always received sourceAccount="AUX" and rejected the wrong jack with internal error 1005. Read the account query parameter and forward it to SelectSource, matching what the frontend already sends and what the CLI already does. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/service/soundtouchweb/handler.go | 7 ++- pkg/service/soundtouchweb/handler_test.go | 65 +++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/pkg/service/soundtouchweb/handler.go b/pkg/service/soundtouchweb/handler.go index 2bb7059..2cb6ff5 100644 --- a/pkg/service/soundtouchweb/handler.go +++ b/pkg/service/soundtouchweb/handler.go @@ -413,12 +413,17 @@ func (app *WebApp) handleSourceControl(w http.ResponseWriter, r *http.Request, d return } + // Forward the optional account parameter as sourceAccount. Devices with + // multiple jacks that share source="AUX" (e.g. ST-5 CD/Aux inputs) + // disambiguate them via distinct sourceAccount values (AUX, AUX1, …). + accountParam := r.URL.Query().Get("account") + if device.Client == nil { app.sendError(w, "Device client not available", http.StatusInternalServerError) return } - err := device.Client.SelectSource(sourceParam, "") + err := device.Client.SelectSource(sourceParam, accountParam) app.sendControlResponse(w, err, fmt.Sprintf("Selected source %s", sourceParam)) } diff --git a/pkg/service/soundtouchweb/handler_test.go b/pkg/service/soundtouchweb/handler_test.go index b131b82..c3e7691 100644 --- a/pkg/service/soundtouchweb/handler_test.go +++ b/pkg/service/soundtouchweb/handler_test.go @@ -704,3 +704,68 @@ func TestHandleDevicePlay_SourceAccountFiltering(t *testing.T) { }) } } + +// TestHandleSourceControl_ForwardsAccount verifies that the account query +// parameter is forwarded as sourceAccount in the /select XML. Devices like +// the ST-5 expose multiple AUX jacks that share source="AUX" and are only +// disambiguated by distinct sourceAccount values (AUX, AUX1, …). Regression +// test for issue #444, where the handler dropped the account parameter. +func TestHandleSourceControl_ForwardsAccount(t *testing.T) { + tests := []struct { + name string + query string + wantSourceAccount string + }{ + { + name: "AUX with explicit account — forwarded", + query: "name=AUX&account=AUX1", + wantSourceAccount: "AUX1", + }, + { + name: "AUX without account — defaults to AUX", + query: "name=AUX", + wantSourceAccount: "AUX", + }, + } + + 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("source-device", conn) + + req := httptest.NewRequest("GET", "/api/control/source-device/source?"+tt.query, nil) + req = withChiParams(req, map[string]string{"id": "source-device", "action": "source"}) + w := httptest.NewRecorder() + + app.HandleAPIControl(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + if want := `source="AUX"`; !strings.Contains(capturedBody, want) { + t.Errorf("XML should contain %q, got: %s", want, capturedBody) + } + if want := `sourceAccount="` + tt.wantSourceAccount + `"`; !strings.Contains(capturedBody, want) { + t.Errorf("XML should contain %q, got: %s", want, capturedBody) + } + }) + } +}