feat(server/playlists): Discover system playlist
Adds the third system playlist variant: 'discover'. Surfaces 100 tracks the operator has not played and not liked, biased toward artists they rarely play (< 10 plays) and complemented by tracks liked by other users plus a random unheard sample. Cold start collapses to all-random; single-user servers redistribute the cross-user-likes allocation equally across the other two buckets. The build runs as a fourth phase inside BuildSystemPlaylists alongside For You and the seed-artist mixes. Same daily-deterministic md5(track_id || dateStr) ordering as the other variants. Per-album (<=2) and per-artist (<=3) caps prevent collapse onto a single prolific artist. Buckets interleave round-robin so the playlist order doesn't front-load one bucket's flavour. Post-commit collage generation (already wired) gives Discover the same 4-cell cover treatment as the other system playlists — no new collage code needed. Tests cover the slot-redistribution table (all-available, cold-start, single-user, partial-dormant, fully-empty), the per-album/per-artist caps, the round-robin interleave, and a DB-backed cold-start integration test that asserts a Discover playlist lands with non-zero tracks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -105,6 +105,8 @@ func TestBuildSystemPlaylists_SufficientActivity(t *testing.T) {
|
||||
if !r.SeedArtistID.Valid {
|
||||
t.Errorf("songs_like_artist row should have non-NULL seed_artist_id")
|
||||
}
|
||||
case "discover":
|
||||
// Discover playlist is valid — no seed_artist_id required.
|
||||
default:
|
||||
t.Errorf("unknown system_variant=%q", *r.SystemVariant)
|
||||
}
|
||||
@@ -320,6 +322,57 @@ func TestListActiveUsersForSystemPlaylists(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSystemPlaylists_DiscoverColdStart(t *testing.T) {
|
||||
// User has no plays / likes → Discover's dormant + cross-user
|
||||
// buckets are empty (or minimal), all slots come from random unheard.
|
||||
pool := newPool(t)
|
||||
logger := discardLogger()
|
||||
u, _ := seedActiveLibrary(t, pool, "discover_cold", 4, 5)
|
||||
// seedActiveLibrary seeds 3 plays per track; delete them so dormant
|
||||
// bucket (< 10 plays per artist) and random bucket both operate on
|
||||
// a true cold-start user with no prior play history.
|
||||
if _, err := pool.Exec(context.Background(),
|
||||
"DELETE FROM play_events WHERE user_id = $1", u.ID); err != nil {
|
||||
t.Fatalf("clear plays: %v", err)
|
||||
}
|
||||
|
||||
if err := playlists.BuildSystemPlaylists(context.Background(), pool, logger, u.ID, time.Now().UTC(), t.TempDir()); err != nil {
|
||||
t.Fatalf("build: %v", err)
|
||||
}
|
||||
|
||||
rows, err := dbq.New(pool).ListPlaylistsByUserAndKind(context.Background(), dbq.ListPlaylistsByUserAndKindParams{
|
||||
UserID: u.ID, Column2: "system",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
var foundDiscover bool
|
||||
for _, r := range rows {
|
||||
if r.SystemVariant != nil && *r.SystemVariant == "discover" {
|
||||
foundDiscover = true
|
||||
if r.TrackCount == 0 {
|
||||
t.Errorf("Discover playlist has zero tracks on cold start (random bucket should fill)")
|
||||
}
|
||||
if r.TrackCount > 100 {
|
||||
t.Errorf("Discover track_count = %d, want <= 100", r.TrackCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundDiscover {
|
||||
t.Errorf("expected a 'discover' system playlist; got variants: %v", systemVariantsOf(rows))
|
||||
}
|
||||
}
|
||||
|
||||
func systemVariantsOf(rows []dbq.ListPlaylistsByUserAndKindRow) []string {
|
||||
out := make([]string, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
if r.SystemVariant != nil {
|
||||
out = append(out, *r.SystemVariant)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestStartupRecoveryClearsStaleInFlight(t *testing.T) {
|
||||
pool := newPool(t)
|
||||
ctx := context.Background()
|
||||
|
||||
Reference in New Issue
Block a user