mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 16:46:17 +00:00
fix(datastore): dedup recents by ID in SaveRecents (stop same-recent pile-up)
A speaker<->marge recents sync could re-store the same recent (same ID) multiple times — observed live as one STORED_MUSIC track appearing 4x in the speaker's /recents, the service's stored Recents.xml, and /full. The duplicates crowd the capped (10) recents list and evict other sources (e.g. a freshly played Spotify track never appears). SaveConfiguredSources already dedups by ID; SaveRecents did not, so dupes introduced by any path (AddRecent move-to-front, syncRecents from the speaker's /full, setup/health) persisted and fed back through the sync loop. SaveRecents now dedups by ID (first occurrence wins) at the single chokepoint all callers share, so the list self-heals on the next write. Regression test added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
393b31ad93
commit
6ad75657e8
@@ -1436,6 +1436,27 @@ func (ds *DataStore) SaveRecents(account, device string, recents []models.Servic
|
||||
Recents []RecentXML `xml:"recent"`
|
||||
}
|
||||
|
||||
// Deduplicate by ID before saving; first occurrence wins. A speaker<->marge
|
||||
// recents sync can otherwise re-store the same recent (same ID) multiple
|
||||
// times — it then crowds the capped list and evicts other sources from the
|
||||
// speaker's recents. Mirrors SaveConfiguredSources.
|
||||
seen := make(map[string]bool)
|
||||
deduped := make([]models.ServiceRecent, 0, len(recents))
|
||||
|
||||
for i := range recents {
|
||||
if id := recents[i].ID; id != "" {
|
||||
if seen[id] {
|
||||
continue
|
||||
}
|
||||
|
||||
seen[id] = true
|
||||
}
|
||||
|
||||
deduped = append(deduped, recents[i])
|
||||
}
|
||||
|
||||
recents = deduped
|
||||
|
||||
wrap := RecentsXML{
|
||||
Recents: make([]RecentXML, 0, len(recents)),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
)
|
||||
|
||||
// TestSaveRecents_DeduplicatesByID is a regression test for the recents
|
||||
// duplication bug: a speaker<->marge sync could re-store the same recent (same
|
||||
// ID) multiple times, crowding the capped list and evicting other sources from
|
||||
// the speaker's recents. SaveRecents must dedup by ID (first occurrence wins).
|
||||
func TestSaveRecents_DeduplicatesByID(t *testing.T) {
|
||||
tmp, err := os.MkdirTemp("", "recents-dedup-*")
|
||||
if err != nil {
|
||||
t.Fatalf("temp dir: %v", err)
|
||||
}
|
||||
|
||||
defer func() { _ = os.RemoveAll(tmp) }()
|
||||
|
||||
ds := NewDataStore(tmp)
|
||||
account, device := "6919733", "A81B6A536A98"
|
||||
|
||||
mk := func(id, name string) models.ServiceRecent {
|
||||
var r models.ServiceRecent
|
||||
r.ID = id
|
||||
r.Name = name
|
||||
r.Source = "7"
|
||||
r.SourceAccount = "4d696e69-444c-164e-9d41-72ecda78e4c1/0"
|
||||
r.Location = "1$4$2 TRACK"
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Same ID four times (the observed live state), plus two distinct recents.
|
||||
in := []models.ServiceRecent{
|
||||
mk("260614006", "03 - Salvation"),
|
||||
mk("260614006", "03 - Salvation"),
|
||||
mk("260614006", "03 - Salvation"),
|
||||
mk("260614006", "03 - Salvation"),
|
||||
mk("260614004", "06 - Back Burner"),
|
||||
mk("260613001", "Artifact"),
|
||||
}
|
||||
|
||||
if err := ds.SaveRecents(account, device, in); err != nil {
|
||||
t.Fatalf("SaveRecents: %v", err)
|
||||
}
|
||||
|
||||
out, err := ds.GetRecents(account, device)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRecents: %v", err)
|
||||
}
|
||||
|
||||
counts := map[string]int{}
|
||||
for _, r := range out {
|
||||
counts[r.ID]++
|
||||
}
|
||||
|
||||
if counts["260614006"] != 1 {
|
||||
t.Errorf("duplicate recent not deduped: id 260614006 appears %d times (want 1)", counts["260614006"])
|
||||
}
|
||||
|
||||
if len(out) != 3 {
|
||||
t.Errorf("expected 3 distinct recents, got %d: %+v", len(out), counts)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user