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) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-06-14 21:48:02 +02:00
co-authored by Claude Opus 4.8
parent b8de0f90f0
commit d6257e6108
2 changed files with 49 additions and 1 deletions
+23 -1
View File
@@ -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,
@@ -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)
}
}
}