a98a106f6f
Adds 'discover' as an accepted system_variant on the playlists table — alongside 'for_you' and 'songs_like_artist' — and three sqlc bucket queries that back the new playlist's daily candidate selection. The three buckets surface tracks the user hasn't played and hasn't liked: - Dormant artists: artists this user has played < 10 times total. Surfaces the long tail of their library. - Cross-user likes: tracks any OTHER user has liked but this one hasn't engaged with. Empty on single-user servers; the Go-side allocator redistributes the deficit across the other two buckets. - Random unheard: pure random sample as the safety net and the cold-start fallback. All three share exclusion filters: not-played by this user, not liked by this user, not in lidarr_quarantine for this user. All order by md5(track_id || dateStr) for daily determinism — same pattern as the existing tieBreakHash logic in system.go. LIMIT values are generous (80/60/200) so the per-album (<=2) / per-artist (<=3) caps and slot redistribution in T2 have headroom. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
99 lines
3.4 KiB
SQL
99 lines
3.4 KiB
SQL
-- Discover playlist bucket queries. Each returns up to its LIMIT
|
|
-- count of (track_id, album_id, artist_id) triples ordered
|
|
-- daily-deterministically via md5(track_id || dateStr). The Go-side
|
|
-- bucket allocator (internal/playlists/discover.go) applies
|
|
-- per-album (<=2) and per-artist (<=3) caps then redistributes any
|
|
-- bucket deficit equally across the others. LIMIT values are
|
|
-- generous so the caps + redistribution have headroom.
|
|
|
|
-- name: ListDormantArtistTracksForDiscover :many
|
|
-- Tracks whose artist this user has played fewer than 10 times total.
|
|
-- The artist threshold defines "dormant" — we want to surface the
|
|
-- long tail of artists in the user's library that they rarely listen
|
|
-- to. Excludes tracks the user has played, liked, or has quarantined.
|
|
-- $1 = user_id, $2 = date string for md5 ordering.
|
|
WITH user_artist_counts AS (
|
|
SELECT t.artist_id, COUNT(*) AS play_count
|
|
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.artist_id
|
|
),
|
|
dormant_artists AS (
|
|
SELECT a.id
|
|
FROM artists a
|
|
LEFT JOIN user_artist_counts uac ON uac.artist_id = a.id
|
|
WHERE COALESCE(uac.play_count, 0) < 10
|
|
)
|
|
SELECT t.id, t.album_id, t.artist_id
|
|
FROM tracks t
|
|
JOIN dormant_artists da ON da.id = t.artist_id
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM play_events pe
|
|
WHERE pe.user_id = $1
|
|
AND pe.track_id = t.id
|
|
AND pe.was_skipped = false
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM general_likes gl
|
|
WHERE gl.user_id = $1 AND gl.track_id = t.id
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM lidarr_quarantine q
|
|
WHERE q.user_id = $1 AND q.track_id = t.id
|
|
)
|
|
ORDER BY md5(t.id::text || $2::text)
|
|
LIMIT 80;
|
|
|
|
-- name: ListCrossUserLikedTracksForDiscover :many
|
|
-- Tracks any OTHER user has liked, that this user hasn't played,
|
|
-- liked, or had quarantined. On single-user servers this returns 0
|
|
-- rows (the caller's slot redistribution rolls the deficit into the
|
|
-- other two buckets).
|
|
-- $1 = user_id, $2 = date string for md5 ordering.
|
|
SELECT DISTINCT t.id, t.album_id, t.artist_id
|
|
FROM general_likes gl
|
|
JOIN tracks t ON t.id = gl.track_id
|
|
WHERE gl.user_id != $1
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM play_events pe
|
|
WHERE pe.user_id = $1
|
|
AND pe.track_id = t.id
|
|
AND pe.was_skipped = false
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM general_likes ml
|
|
WHERE ml.user_id = $1 AND ml.track_id = t.id
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM lidarr_quarantine q
|
|
WHERE q.user_id = $1 AND q.track_id = t.id
|
|
)
|
|
ORDER BY md5(t.id::text || $2::text)
|
|
LIMIT 60;
|
|
|
|
-- name: ListRandomUnheardTracksForDiscover :many
|
|
-- Random sample from the entire library. Same exclusion filters as
|
|
-- the other two buckets. The generous LIMIT (200) gives the caller
|
|
-- headroom for the per-album/artist caps and the slot redistribution.
|
|
-- $1 = user_id, $2 = date string for md5 ordering.
|
|
SELECT t.id, t.album_id, t.artist_id
|
|
FROM tracks t
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM play_events pe
|
|
WHERE pe.user_id = $1
|
|
AND pe.track_id = t.id
|
|
AND pe.was_skipped = false
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM general_likes gl
|
|
WHERE gl.user_id = $1 AND gl.track_id = t.id
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM lidarr_quarantine q
|
|
WHERE q.user_id = $1 AND q.track_id = t.id
|
|
)
|
|
ORDER BY md5(t.id::text || $2::text)
|
|
LIMIT 200;
|