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) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-06-02 21:08:05 +02:00
co-authored by Claude Opus 4.8
parent 8232fd1401
commit 8ea2461265
2 changed files with 71 additions and 1 deletions
+6 -1
View File
@@ -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))
}
+65
View File
@@ -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)
}
})
}
}