From d6257e61080fd11ba4c4beef8494ee18d728f7b4 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sun, 14 Jun 2026 19:27:24 +0200 Subject: [PATCH] fix(player): set STORED_MUSIC type when replaying a recent (fix INVALID_SOURCE) Replaying a STORED_MUSIC item from Recents sent the speaker a ContentItem with an empty type (recents carry no contentItemType for STORED_MUSIC), and the speaker rejects an empty-type STORED_MUSIC select with INVALID_SOURCE. The library play paths work because they pass type "track"/"dir". HandleDevicePlay now derives the type from the speaker-native location, which ends with the item kind (e.g. "1$4$2 TRACK" -> "track"), when the caller didn't supply one. (The recents account itself is already correct via the #503 fix.) Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/service/soundtouchweb/handler.go | 24 ++++++++++++++++- .../soundtouchweb/storedmusic_replay_test.go | 26 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 pkg/service/soundtouchweb/storedmusic_replay_test.go diff --git a/pkg/service/soundtouchweb/handler.go b/pkg/service/soundtouchweb/handler.go index 920feec..17f7896 100644 --- a/pkg/service/soundtouchweb/handler.go +++ b/pkg/service/soundtouchweb/handler.go @@ -1114,6 +1114,19 @@ func (app *WebApp) HandleDeviceRecents(w http.ResponseWriter, r *http.Request) { } } +// storedMusicTypeForReplay derives a STORED_MUSIC ContentItem type from the +// speaker-native location, which ends with the item kind (e.g. "1$4$2 TRACK" +// or a container's "… DIR"). Recents don't store the type, and the speaker +// rejects an empty-type STORED_MUSIC select with INVALID_SOURCE. Falls back to +// "track" when the location has no kind suffix. +func storedMusicTypeForReplay(location string) string { + if fields := strings.Fields(location); len(fields) >= 2 { + return strings.ToLower(fields[len(fields)-1]) + } + + return "track" +} + // HandleDevicePlay plays an arbitrary content item on a device. Generic // counterpart to HandlePlayTuneIn — used by the Recents panel to replay // items the speaker reports under /recents, regardless of their source. @@ -1151,9 +1164,18 @@ func (app *WebApp) HandleDevicePlay(w http.ResponseWriter, r *http.Request) { return } + // Recents don't carry a contentItemType for STORED_MUSIC, and the speaker + // rejects an empty-type STORED_MUSIC select with INVALID_SOURCE. The + // speaker-native location ends with the item kind (e.g. "1$4$2 TRACK"), so + // derive the type from it when the caller didn't supply one. + ciType := req.Type + if ciType == "" && req.Source == "STORED_MUSIC" { + ciType = storedMusicTypeForReplay(req.Location) + } + contentItem := &models.ContentItem{ Source: req.Source, - Type: req.Type, + Type: ciType, Location: req.Location, ItemName: req.ItemName, ContainerArt: req.ContainerArt, diff --git a/pkg/service/soundtouchweb/storedmusic_replay_test.go b/pkg/service/soundtouchweb/storedmusic_replay_test.go new file mode 100644 index 0000000..a16ded2 --- /dev/null +++ b/pkg/service/soundtouchweb/storedmusic_replay_test.go @@ -0,0 +1,26 @@ +package soundtouchweb + +import "testing" + +// TestStoredMusicTypeForReplay covers deriving the ContentItem type from a +// STORED_MUSIC recent's location when the stored type is empty. Without a type +// the speaker rejects the select with INVALID_SOURCE. +func TestStoredMusicTypeForReplay(t *testing.T) { + cases := []struct { + location string + want string + }{ + {"1$4$2 TRACK", "track"}, + {"5:audio5:part13:5521:5 TRACK", "track"}, + {"1 DIR", "dir"}, + {"1 CONTAINER", "container"}, + {"noSuffix", "track"}, // fallback + {"", "track"}, // fallback + } + + for _, c := range cases { + if got := storedMusicTypeForReplay(c.location); got != c.want { + t.Errorf("storedMusicTypeForReplay(%q) = %q, want %q", c.location, got, c.want) + } + } +}