mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
A speaker registers each DLNA media server as a STORED_MUSIC source whose account is "<UDN>/0", and reconciles its source list against marge (/full + /sources). AddSource deduped STORED_MUSIC by provider ID alone, so registering a second media server overwrote the first in the datastore; the first then disappeared from /full + /sources and the speaker dropped it. Only one media server could ever stay registered. - STORED_MUSIC now replaces only when the account (SourceKey.Account) matches, so distinct servers coexist and re-adding the same server updates in place. Other (singleton) providers keep replace-by-provider. - Generate source IDs from crypto/rand instead of a per-second timestamp. SaveConfiguredSources dedups by ID, so two sources created in the same instant would otherwise collide and one would be silently dropped; a timestamp (even nanosecond) is fragile on coarse clocks, so use 64 bits of randomness with a timestamp fallback only if the RNG fails. - Add a regression test for two coexisting media servers + same-account update. Diagnosed from speaker + service logs: setMusicServiceAccount succeeds locally, the speaker pushes AddSource to marge (streaming.bose.com, DNS-intercepted to AfterTouch), then re-fetches /full + /sources; that list returned only the latest STORED_MUSIC source, so the speaker pruned the previously-added one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
98 lines
3.0 KiB
Go
98 lines
3.0 KiB
Go
package marge
|
|
|
|
import (
|
|
"os"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/gesellix/bose-soundtouch/pkg/service/constants"
|
|
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
|
|
)
|
|
|
|
// TestAddSource_MultipleStoredMusicServersCoexist is a regression test for the
|
|
// media-server eviction bug: AddSource deduped STORED_MUSIC by provider ID, so
|
|
// registering a second DLNA media server overwrote the first. The first then
|
|
// vanished from /full + /sources and the speaker dropped it, meaning only one
|
|
// media server could ever stay registered.
|
|
//
|
|
// Each media server is a separate account (username = "<UDN>/0"), so two
|
|
// distinct servers must coexist, while re-adding the same server (same account)
|
|
// updates in place.
|
|
func TestAddSource_MultipleStoredMusicServersCoexist(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "addsource-storedmusic-*")
|
|
if err != nil {
|
|
t.Fatalf("temp dir: %v", err)
|
|
}
|
|
|
|
defer func() { _ = os.RemoveAll(tempDir) }()
|
|
|
|
ds := datastore.NewDataStore(tempDir)
|
|
account := "6919733"
|
|
device := "A81B6A536A98"
|
|
|
|
if mkErr := os.MkdirAll(ds.AccountDeviceDir(account, device), 0o755); mkErr != nil {
|
|
t.Fatalf("mkdir device dir: %v", mkErr)
|
|
}
|
|
|
|
sm := strconv.Itoa(constants.StoredMusicProviderID)
|
|
|
|
const (
|
|
fritzAcct = "fa095ecc-e13e-40e7-8e6c-e0286d5bc000/0"
|
|
testAcct = "4d696e69-444c-164e-9d41-72ecda78e4c1/0"
|
|
)
|
|
|
|
// Register two different media servers.
|
|
if _, err := AddSource(ds, account, fritzAcct, sm, "", "", "fritz"); err != nil {
|
|
t.Fatalf("add server 1: %v", err)
|
|
}
|
|
|
|
if _, err := AddSource(ds, account, testAcct, sm, "", "", "AfterTouch Test Library"); err != nil {
|
|
t.Fatalf("add server 2: %v", err)
|
|
}
|
|
|
|
// storedMusicAccounts returns the set of STORED_MUSIC source accounts the
|
|
// datastore would serve via /full + /sources. SourceKey.Account is the
|
|
// persisted identity (the display name lives on the speaker, set via
|
|
// setMusicServiceAccount, and does not round-trip here).
|
|
storedMusicAccounts := func() map[string]bool {
|
|
sources, gerr := ds.GetConfiguredSources(account, device)
|
|
if gerr != nil {
|
|
t.Fatalf("get sources: %v", gerr)
|
|
}
|
|
|
|
out := map[string]bool{}
|
|
|
|
for _, s := range sources {
|
|
if s.SourceProviderID == sm {
|
|
out[s.SourceKey.Account] = true
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
got := storedMusicAccounts()
|
|
if len(got) != 2 {
|
|
t.Fatalf("expected 2 STORED_MUSIC sources, got %d: %+v", len(got), got)
|
|
}
|
|
|
|
if !got[fritzAcct] {
|
|
t.Errorf("first media server was evicted (account %q missing)", fritzAcct)
|
|
}
|
|
|
|
if !got[testAcct] {
|
|
t.Errorf("second media server not registered (account %q missing)", testAcct)
|
|
}
|
|
|
|
// Re-adding the SAME server (same account) updates in place; it must not
|
|
// create a duplicate or drop the other server.
|
|
if _, err := AddSource(ds, account, fritzAcct, sm, "", "", "fritz (renamed)"); err != nil {
|
|
t.Fatalf("re-add server 1: %v", err)
|
|
}
|
|
|
|
got = storedMusicAccounts()
|
|
if len(got) != 2 || !got[fritzAcct] || !got[testAcct] {
|
|
t.Fatalf("re-adding the same server should keep exactly both accounts; got %+v", got)
|
|
}
|
|
}
|