fix(playlists): dedup tracks across discover buckets

Discover playlists could surface the same track twice with the
duplicates landing back-to-back — a "first song plays, then plays
again, skip works" symptom user reported on v2026.05.13.0. Root
cause: interleaveBuckets rotates one track per pass per bucket but
never tracks which IDs it has already emitted, so a track that's
both a dormant-artist pick AND a random-unheard pick comes out
once from each bucket.

On a single-user server the crossUser bucket is empty, so the
redistribute step rolls its slots into dormant + random. Their
output then interleaves d0, r0, d1, r1, … — and when d0 == r0
(common: a dormant-artist track is also valid for random-unheard)
the result is [X, X, …] with adjacent duplicates.

Fix: track seen track IDs across all buckets while interleaving;
skip already-taken IDs and advance to the next index in that
bucket. Dedup priority is bucket order, so a track in both
dormant and random comes from dormant.

Regression test covers the single-user case directly. The existing
round-robin test still passes — no shared IDs in that fixture.

Note: stale duplicates already written to drift / served as cached
playlists will clear naturally on the next playlist rebuild (the
03:00-local refresh, or any manual /api/me/playlists/refresh).
This commit is contained in:
2026-05-14 07:50:35 -04:00
parent 2ebe6229b7
commit c29d25d1cb
2 changed files with 48 additions and 1 deletions
+24
View File
@@ -137,3 +137,27 @@ func TestInterleaveBuckets_RoundRobin(t *testing.T) {
}
}
}
func TestInterleaveBuckets_DedupsAcrossBuckets(t *testing.T) {
// Single-user server scenario: crossUser bucket empty, dormant +
// random share tracks. Without dedup, shared tracks land at
// adjacent interleaved positions — the duplication users reported
// on the Discover playlist in v2026.05.13.0.
dormant := []discoverTrack{{ID: uuidN(1)}, {ID: uuidN(2)}, {ID: uuidN(3)}}
crossUser := []discoverTrack{} // empty bucket, single-user server
random := []discoverTrack{{ID: uuidN(1)}, {ID: uuidN(2)}, {ID: uuidN(4)}}
got := interleaveBuckets(dormant, crossUser, random)
// Tracks 1 and 2 appear in both dormant and random — must be
// emitted once each (from dormant, the earlier bucket). Track 3
// is dormant-only, track 4 is random-only.
if len(got) != 4 {
t.Fatalf("len = %d, want 4 (3 unique from dormant + 1 unique from random)", len(got))
}
wantOrder := []byte{1, 2, 3, 4}
for i, w := range wantOrder {
if got[i].ID.Bytes[15] != w {
t.Errorf("got[%d].ID = %d, want %d", i, got[i].ID.Bytes[15], w)
}
}
}