fix(playlists): robust For-You seed + deep fill; young-library mix fallbacks

For-You silently vanished after ~7 days of not listening (seed query
required a non-skip play in the last 7 days) and capped at ~40 on
self-hosted libraries with no ListenBrainz similarity data.

- PickTopPlayedTracksForUser: tiered seed — last 30d top plays, else
  all-time top plays, else liked tracks. For-You only disappears now
  if the account has zero plays AND zero likes.
- produceForYou uses deeper candidate source limits (raised random/
  tag/similar K) so it reaches ~100 even with empty lb_similar /
  similar_artists; richer when LB enrichment is present.
- Rediscover: tiered — 6-month-dormant, else ≥5-play 30-day-dormant.
- On This Day: floor 60→30 days, window ±7→±10 doy; still skips
  cleanly (no rows → no playlist) on insufficient history.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 12:48:20 -04:00
parent 1e2c486356
commit 2e7b81fdfe
5 changed files with 200 additions and 69 deletions
+44 -14
View File
@@ -73,20 +73,50 @@ SELECT p.artist_id,
LIMIT 5;
-- name: PickTopPlayedTracksForUser :many
-- For-You candidate seeds. Returns the user's top-5 most-played
-- non-skipped tracks in the last 7 days; tie-break by track_id for
-- determinism. The Go-side picker (pickForYouSeedForDay) chooses one
-- of the returned rows as today's seed via userIDHash so the
-- candidate pool rotates day-to-day while staying stable within a
-- day.
SELECT t.id
FROM play_events pe
JOIN tracks t ON t.id = pe.track_id
WHERE pe.user_id = $1
AND pe.started_at > now() - INTERVAL '7 days'
AND pe.was_skipped = false
GROUP BY t.id
ORDER BY COUNT(*) DESC, t.id
-- For-You candidate seeds, tiered so For-You never silently vanishes:
-- tier 0 top non-skip plays in the last 30 days
-- tier 1 (only if tier 0 empty) all-time top non-skip plays
-- tier 2 (only if tiers 0+1 empty) liked tracks
-- Returns up to 5 ids; tie-break by track_id for determinism. The
-- Go-side picker (pickForYouSeedForDay) rotates one per day via
-- userIDHash. Widened from a hard 7-day window, which made For-You
-- disappear after a week of not listening and never recover on a
-- self-hosted library with sparse history.
WITH recent AS (
SELECT t.id, COUNT(*) AS c, 0 AS tier
FROM play_events pe
JOIN tracks t ON t.id = pe.track_id
WHERE pe.user_id = $1
AND pe.started_at > now() - INTERVAL '30 days'
AND pe.was_skipped = false
GROUP BY t.id
),
alltime AS (
SELECT t.id, COUNT(*) AS c, 1 AS tier
FROM play_events pe
JOIN tracks t ON t.id = pe.track_id
WHERE pe.user_id = $1
AND pe.was_skipped = false
GROUP BY t.id
),
liked AS (
SELECT gl.track_id AS id, 0::bigint AS c, 2 AS tier
FROM general_likes gl
WHERE gl.user_id = $1
),
chosen AS (
SELECT id, c, tier FROM recent
UNION ALL
SELECT id, c, tier FROM alltime
WHERE NOT EXISTS (SELECT 1 FROM recent)
UNION ALL
SELECT id, c, tier FROM liked
WHERE NOT EXISTS (SELECT 1 FROM recent)
AND NOT EXISTS (SELECT 1 FROM alltime)
)
SELECT id
FROM chosen
ORDER BY tier, c DESC, id
LIMIT 5;
-- name: PickTopPlayedTrackForArtistByUser :one