Files
minstrel/internal/db/queries/system_playlists.sql
T
bvandeusen 9e02878b61
test-go / test (push) Successful in 29s
test-go / integration (push) Successful in 4m37s
feat(playlists): For You composition v2 — multi-seed blend + weighted fresh tail
Two approved composition changes (#1269), mechanism only — the
taste/fresh share stays data-decided (#1252) and pick_kind
attribution is unchanged.

Multi-seed blending: each day's build now seeds from up to 3 of the
user's top-5 tracks (pickDailySeeds, the generalized daily shuffle)
instead of one rotating anchor, so the mix spans neighborhoods within
a day and stops feeling bipolar as the rotation swings between
dissimilar seeds. Per-seed pools merge first-seen-deduped; the head
is filled best-first under 50/30/20 per-seed quotas (60/40 for two
seeds) so one neighborhood can't monopolize it, with thin-seed quota
spilling best-first.

Score-weighted fresh tail: the tail sample (rank 2*headN onward) was
uniform — the 380th-best candidate as likely as the 101st. It now
uses deterministic Efraimidis-Spirakis keys with weight halving every
50 ranks, so freshness keeps its "you'll probably enjoy this" half
while still rotating daily.

The retired single-seed picker's one other caller, You-might-like,
moves to pickDailySeeds(n=1) — a single neighborhood per day is right
for a short shelf, and the behavior note is inline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TsF3cNoKrqCYsU78cXC8U6
2026-07-03 09:02:35 -04:00

263 lines
9.3 KiB
SQL

-- M7 #352 slice 2: system-generated playlist queries.
-- name: ListActiveUsersForSystemPlaylists :many
-- Active = had a play in the last 7 days. The cron iterates this list.
SELECT u.id FROM users u
WHERE EXISTS (
SELECT 1 FROM play_events pe
WHERE pe.user_id = u.id
AND pe.started_at > now() - INTERVAL '7 days'
);
-- name: GetSystemPlaylistRun :one
SELECT user_id, last_run_at, last_run_date, in_flight, last_error
FROM system_playlist_runs
WHERE user_id = $1;
-- name: TryClaimSystemPlaylistRun :one
-- Atomic test-and-set on in_flight. Returns the row if we won the claim,
-- or pgx.ErrNoRows if someone else is already building.
INSERT INTO system_playlist_runs (user_id, in_flight)
VALUES ($1, true)
ON CONFLICT (user_id) DO UPDATE
SET in_flight = true
WHERE system_playlist_runs.in_flight = false
RETURNING user_id, last_run_at, last_run_date, in_flight, last_error;
-- name: FinishSystemPlaylistRun :exec
-- Mark a successful build complete.
UPDATE system_playlist_runs
SET last_run_at = $2,
last_run_date = $3,
in_flight = false,
last_error = NULL
WHERE user_id = $1;
-- name: FailSystemPlaylistRun :exec
-- Mark a failed build complete (in_flight=false, error captured).
UPDATE system_playlist_runs
SET in_flight = false,
last_error = $2
WHERE user_id = $1;
-- name: ClearStaleSystemPlaylistInFlight :exec
-- Cron startup recovery: clear any in_flight=true rows from a previously
-- crashed process. Safe because at startup, by definition, nothing is
-- still building.
UPDATE system_playlist_runs SET in_flight = false WHERE in_flight = true;
-- name: PickSeedArtists :many
-- Top-5 most-engaged distinct artist candidates, tiered so the
-- "Songs like X" mixes never silently vanish (#1255): the old hard
-- 7-day window emptied the seed pool after a quiet week, and the
-- daily atomic-replace build then deleted every existing mix until
-- the user played something again. Same fallback shape as
-- PickTopPlayedTracksForUser (For You's seeds):
-- tier 0 engagement in the last 7 days
-- tier 1 (only if tier 0 empty) last 30 days
-- tier 2 (only if tiers 0+1 empty) all-time
-- tier 3 (only if 0-2 empty) liked artists with no play history
-- All returned rows share one tier; produceSeedMixes maps it onto the
-- rule-#131 pick-kind ladder and stamps the built tracks, so metrics
-- can compare mixes seeded from fresh vs stale engagement.
-- Score = unskipped-play count + 5 if user has liked the artist. The
-- Go-side picker (pickSeedArtistsForDay) shuffles the 5
-- daily-deterministically and takes 3 so the mix set rotates.
WITH liked AS (
SELECT gla.artist_id FROM general_likes_artists gla WHERE gla.user_id = $1
),
recent7 AS (
SELECT t.artist_id,
COUNT(*) FILTER (WHERE pe.was_skipped = false) AS play_count,
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 '7 days'
AND t.artist_id IS NOT NULL
GROUP BY t.artist_id
),
recent30 AS (
SELECT t.artist_id,
COUNT(*) FILTER (WHERE pe.was_skipped = false) AS play_count,
1 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 t.artist_id IS NOT NULL
GROUP BY t.artist_id
),
alltime AS (
SELECT t.artist_id,
COUNT(*) FILTER (WHERE pe.was_skipped = false) AS play_count,
2 AS tier
FROM play_events pe
JOIN tracks t ON t.id = pe.track_id
WHERE pe.user_id = $1
AND t.artist_id IS NOT NULL
GROUP BY t.artist_id
),
likedonly AS (
SELECT l.artist_id, 0::bigint AS play_count, 3 AS tier
FROM liked l
),
chosen AS (
SELECT artist_id, play_count, tier FROM recent7
UNION ALL
SELECT artist_id, play_count, tier FROM recent30
WHERE NOT EXISTS (SELECT 1 FROM recent7)
UNION ALL
SELECT artist_id, play_count, tier FROM alltime
WHERE NOT EXISTS (SELECT 1 FROM recent7)
AND NOT EXISTS (SELECT 1 FROM recent30)
UNION ALL
SELECT artist_id, play_count, tier FROM likedonly
WHERE NOT EXISTS (SELECT 1 FROM recent7)
AND NOT EXISTS (SELECT 1 FROM recent30)
AND NOT EXISTS (SELECT 1 FROM alltime)
)
SELECT c.artist_id,
(c.play_count + CASE WHEN l.artist_id IS NOT NULL THEN 5 ELSE 0 END)::bigint AS score,
c.tier::int AS tier
FROM chosen c
LEFT JOIN liked l ON l.artist_id = c.artist_id
ORDER BY score DESC, c.artist_id
LIMIT 5;
-- name: PickTopPlayedTracksForUser :many
-- 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 (pickDailySeeds) draws the day's seeds from these.
-- 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
-- "Songs like X" seed selection. Returns the user's most-played non-skipped
-- track by artist X in the last 7 days. Falls back to the artist's most-
-- recent album's first track if no plays exist.
-- Cast to uuid so sqlc infers pgtype.UUID rather than interface{}.
SELECT COALESCE(
(SELECT t.id
FROM play_events pe
JOIN tracks t ON t.id = pe.track_id
WHERE pe.user_id = $1
AND t.artist_id = $2
AND pe.started_at > now() - INTERVAL '7 days'
AND pe.was_skipped = false
GROUP BY t.id
ORDER BY COUNT(*) DESC, t.id
LIMIT 1),
(SELECT t.id
FROM tracks t
JOIN albums a ON a.id = t.album_id
WHERE t.artist_id = $2
ORDER BY a.release_date DESC NULLS LAST,
t.disc_number NULLS LAST,
t.track_number NULLS LAST,
t.id
LIMIT 1)
)::uuid AS id;
-- name: PickTopAlbumCoverForArtistByUser :one
-- "Songs like X" cover. Most-played album by user; tie-break by most-recent
-- release. NULL if the artist has no albums or all covers are NULL.
-- Note: albums uses cover_art_path (not cover_path).
SELECT a.cover_art_path
FROM albums a
LEFT JOIN tracks t ON t.album_id = a.id
LEFT JOIN play_events pe
ON pe.track_id = t.id
AND pe.user_id = $1
AND pe.was_skipped = false
WHERE a.artist_id = $2
AND a.cover_art_path IS NOT NULL
GROUP BY a.id, a.release_date, a.cover_art_path
ORDER BY COUNT(pe.id) DESC, a.release_date DESC NULLS LAST, a.id
LIMIT 1;
-- name: DeleteSystemPlaylistsForUser :exec
-- Atomic-replace step. CASCADE on playlist_tracks.playlist_id handles
-- the join rows.
DELETE FROM playlists WHERE user_id = $1 AND kind = 'system';
-- name: ListPlaylistsByUserAndKind :many
-- Used in integration tests; production filter applied in Go
-- (api.handleListPlaylists fetches via Service.List and filters by Kind in
-- memory so it can also surface other users' public playlists alongside
-- the caller's own kind-filtered ones).
-- Sorted by updated_at DESC.
SELECT id, user_id, name, description, is_public, kind, system_variant,
seed_artist_id, cover_path, track_count, duration_sec,
created_at, updated_at
FROM playlists
WHERE user_id = $1
AND ($2::text = 'all' OR kind = $2)
ORDER BY updated_at DESC;
-- name: CreateSystemPlaylist :one
-- Inserts a system-generated playlist. Used by BuildSystemPlaylists.
-- For 'for_you' variant, pass seed_artist_id as NULL (zero-value pgtype.UUID
-- with Valid=false).
INSERT INTO playlists (
user_id, name, description, is_public,
kind, system_variant, seed_artist_id, cover_path
) VALUES ($1, $2, '', false, 'system', $3, $4, $5)
RETURNING *;
-- name: GetSystemPlaylistByVariantForUser :one
-- Returns the user's system playlist for the given variant
-- ('for_you' / 'songs_like_artist' / 'discover'), or pgx.ErrNoRows
-- when the user hasn't built that variant yet. For
-- 'songs_like_artist' (which can have multiple rows per user, one
-- per seed), this returns whichever LIMIT 1 picks — callers that
-- need all instances should use a different query.
SELECT p.id, p.user_id, p.name, p.kind, p.system_variant,
p.seed_artist_id, p.cover_path, p.track_count
FROM playlists p
WHERE p.user_id = $1
AND p.kind = 'system'
AND p.system_variant = $2
ORDER BY p.created_at DESC
LIMIT 1;