4b150a277e
Confirmed against prod: exactly one track (17 plays, cold since May 21) met the c>=5 + 30d-cold bar, and three process defects turned that into a 1-track playlist instead of the locked placeholder. - ListRediscoverTracks: collapse the two-tier UNION into one blended pool. The old shallow-tier gate (WHERE NOT EXISTS deep) was all-or-nothing — one 6-month row suppressed the entire 30-day tier — and deep was a strict subset of shallow anyway. Eligibility drops to >=3 non-skip plays (on a weeks-old history the >=5-play tracks are precisely the ones still in rotation); ordering prefers >=6mo cold, then >=5 plays, then raw count. - Minimum viable mix floor for all five discovery mixes: below minLen (15; 5 for the album-coherent NewForYou/FirstListens) the variant is withheld so Home renders the 'listen more to unlock' placeholder instead of a mix that reads as built-wrong. - /api/events: clamp client-supplied 'at' to [user.created_at, now+5m]. Unbounded client clocks could write arbitrarily old plays and poison the 6-month ordering (prod data verified clean — no scrub needed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TsF3cNoKrqCYsU78cXC8U6
317 lines
9.5 KiB
Go
317 lines
9.5 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: system_mixes.sql
|
|
|
|
package dbq
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const listDeepCutsTracks = `-- name: ListDeepCutsTracks :many
|
|
|
|
WITH affinity_artists AS (
|
|
SELECT artist_id FROM general_likes_artists WHERE user_id = $1
|
|
UNION
|
|
SELECT t.artist_id
|
|
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
|
|
HAVING COUNT(*) >= 5
|
|
),
|
|
play_counts AS (
|
|
SELECT track_id, COUNT(*) AS c
|
|
FROM play_events
|
|
WHERE user_id = $1 AND was_skipped = false
|
|
GROUP BY track_id
|
|
)
|
|
SELECT t.id, t.album_id, t.artist_id
|
|
FROM tracks t
|
|
JOIN affinity_artists aa ON aa.artist_id = t.artist_id
|
|
LEFT JOIN play_counts pc ON pc.track_id = t.id
|
|
WHERE COALESCE(pc.c, 0) <= 2
|
|
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
|
|
`
|
|
|
|
type ListDeepCutsTracksParams struct {
|
|
UserID pgtype.UUID
|
|
Column2 string
|
|
}
|
|
|
|
type ListDeepCutsTracksRow struct {
|
|
ID pgtype.UUID
|
|
AlbumID pgtype.UUID
|
|
ArtistID pgtype.UUID
|
|
}
|
|
|
|
// Candidate queries for the discovery system-playlist mixes
|
|
// (#419-423). Each returns (id, album_id, artist_id) rows; the Go
|
|
// producer caps + truncates and converts to rankedCandidate. Daily-
|
|
// deterministic md5(id||dateStr) ordering (where used) keeps a mix
|
|
// stable within a day while rotating across days; shuffle-on-play
|
|
// gives per-play variety on top.
|
|
// #419 Deep Cuts: low-play tracks (<=2 plays) from artists the user
|
|
// has liked OR played heavily (>=5 non-skip plays across the artist).
|
|
// $1 user_id, $2 date string.
|
|
func (q *Queries) ListDeepCutsTracks(ctx context.Context, arg ListDeepCutsTracksParams) ([]ListDeepCutsTracksRow, error) {
|
|
rows, err := q.db.Query(ctx, listDeepCutsTracks, arg.UserID, arg.Column2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListDeepCutsTracksRow
|
|
for rows.Next() {
|
|
var i ListDeepCutsTracksRow
|
|
if err := rows.Scan(&i.ID, &i.AlbumID, &i.ArtistID); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listFirstListensTracks = `-- name: ListFirstListensTracks :many
|
|
WITH heard_albums AS (
|
|
SELECT DISTINCT t.album_id
|
|
FROM play_events pe JOIN tracks t ON t.id = pe.track_id
|
|
WHERE pe.user_id = $1
|
|
),
|
|
played_artists AS (
|
|
SELECT DISTINCT t.artist_id
|
|
FROM play_events pe JOIN tracks t ON t.id = pe.track_id
|
|
WHERE pe.user_id = $1
|
|
)
|
|
SELECT t.id, t.album_id, t.artist_id
|
|
FROM tracks t
|
|
JOIN albums al ON al.id = t.album_id
|
|
WHERE NOT EXISTS (SELECT 1 FROM heard_albums h WHERE h.album_id = al.id)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM lidarr_quarantine q
|
|
WHERE q.user_id = $1 AND q.track_id = t.id
|
|
)
|
|
ORDER BY
|
|
(CASE
|
|
WHEN EXISTS (
|
|
SELECT 1 FROM general_likes_artists gla
|
|
WHERE gla.user_id = $1 AND gla.artist_id = al.artist_id
|
|
) THEN 0
|
|
WHEN al.artist_id IN (SELECT artist_id FROM played_artists) THEN 1
|
|
ELSE 2
|
|
END),
|
|
al.id, t.disc_number NULLS FIRST, t.track_number NULLS FIRST
|
|
LIMIT 300
|
|
`
|
|
|
|
type ListFirstListensTracksRow struct {
|
|
ID pgtype.UUID
|
|
AlbumID pgtype.UUID
|
|
ArtistID pgtype.UUID
|
|
}
|
|
|
|
// #423 First Listens: albums the user has never played any track of.
|
|
// Tiered: liked-artist albums first, then played-artist albums, then
|
|
// the rest — album-coherent within each tier. Not diversity-capped
|
|
// (whole-album discovery). $1 user_id.
|
|
func (q *Queries) ListFirstListensTracks(ctx context.Context, userID pgtype.UUID) ([]ListFirstListensTracksRow, error) {
|
|
rows, err := q.db.Query(ctx, listFirstListensTracks, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListFirstListensTracksRow
|
|
for rows.Next() {
|
|
var i ListFirstListensTracksRow
|
|
if err := rows.Scan(&i.ID, &i.AlbumID, &i.ArtistID); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listNewForYouTracks = `-- name: ListNewForYouTracks :many
|
|
WITH affinity_artists AS (
|
|
SELECT artist_id FROM general_likes_artists WHERE user_id = $1
|
|
UNION
|
|
SELECT t.artist_id
|
|
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
|
|
HAVING COUNT(*) >= 3
|
|
)
|
|
SELECT t.id, t.album_id, t.artist_id
|
|
FROM tracks t
|
|
JOIN albums al ON al.id = t.album_id
|
|
JOIN affinity_artists aa ON aa.artist_id = al.artist_id
|
|
WHERE al.created_at >= now() - interval '30 days'
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM lidarr_quarantine q
|
|
WHERE q.user_id = $1 AND q.track_id = t.id
|
|
)
|
|
ORDER BY al.created_at DESC, t.disc_number NULLS FIRST, t.track_number NULLS FIRST
|
|
LIMIT 200
|
|
`
|
|
|
|
type ListNewForYouTracksRow struct {
|
|
ID pgtype.UUID
|
|
AlbumID pgtype.UUID
|
|
ArtistID pgtype.UUID
|
|
}
|
|
|
|
// #421 New for you: tracks from albums added in the last 30 days
|
|
// whose artist the user has liked OR played (>=3 non-skip). Album-
|
|
// coherent (newest album first, then disc/track) — the producer does
|
|
// NOT diversity-cap these; they're meant as whole-album discovery.
|
|
// $1 user_id.
|
|
func (q *Queries) ListNewForYouTracks(ctx context.Context, userID pgtype.UUID) ([]ListNewForYouTracksRow, error) {
|
|
rows, err := q.db.Query(ctx, listNewForYouTracks, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListNewForYouTracksRow
|
|
for rows.Next() {
|
|
var i ListNewForYouTracksRow
|
|
if err := rows.Scan(&i.ID, &i.AlbumID, &i.ArtistID); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listOnThisDayTracks = `-- name: ListOnThisDayTracks :many
|
|
WITH windowed AS (
|
|
SELECT track_id, COUNT(*) AS c
|
|
FROM play_events
|
|
WHERE user_id = $1 AND was_skipped = false
|
|
AND started_at < now() - interval '30 days'
|
|
AND ABS(EXTRACT(DOY FROM started_at) - EXTRACT(DOY FROM now())) <= 10
|
|
GROUP BY track_id
|
|
)
|
|
SELECT t.id, t.album_id, t.artist_id
|
|
FROM tracks t
|
|
JOIN windowed w ON w.track_id = t.id
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM lidarr_quarantine q
|
|
WHERE q.user_id = $1 AND q.track_id = t.id
|
|
)
|
|
ORDER BY w.c DESC, md5(t.id::text || $2::text)
|
|
LIMIT 200
|
|
`
|
|
|
|
type ListOnThisDayTracksParams struct {
|
|
UserID pgtype.UUID
|
|
Column2 string
|
|
}
|
|
|
|
type ListOnThisDayTracksRow struct {
|
|
ID pgtype.UUID
|
|
AlbumID pgtype.UUID
|
|
ArtistID pgtype.UUID
|
|
}
|
|
|
|
// #422 On This Day: tracks the user played around this calendar date
|
|
// (±10 day-of-year) in the past, excluding the very recent (>30 days
|
|
// ago) so it's nostalgic, not just "last week". Weighted by how much
|
|
// they were played in those windows. Floor relaxed from 60→30 days
|
|
// and window ±7→±10 so it surfaces on a months-old library instead
|
|
// of needing a full year of history; still skips cleanly (no rows →
|
|
// no playlist) when there's no qualifying history yet.
|
|
// $1 user_id, $2 date string.
|
|
func (q *Queries) ListOnThisDayTracks(ctx context.Context, arg ListOnThisDayTracksParams) ([]ListOnThisDayTracksRow, error) {
|
|
rows, err := q.db.Query(ctx, listOnThisDayTracks, arg.UserID, arg.Column2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListOnThisDayTracksRow
|
|
for rows.Next() {
|
|
var i ListOnThisDayTracksRow
|
|
if err := rows.Scan(&i.ID, &i.AlbumID, &i.ArtistID); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listRediscoverTracks = `-- name: ListRediscoverTracks :many
|
|
WITH stats AS (
|
|
SELECT pe.track_id, COUNT(*) AS c, MAX(pe.started_at) AS last_at
|
|
FROM play_events pe
|
|
WHERE pe.user_id = $1 AND pe.was_skipped = false
|
|
GROUP BY pe.track_id
|
|
)
|
|
SELECT t.id, t.album_id, t.artist_id
|
|
FROM tracks t
|
|
JOIN stats s ON s.track_id = t.id
|
|
WHERE s.c >= 3
|
|
AND s.last_at <= now() - interval '30 days'
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM lidarr_quarantine q
|
|
WHERE q.user_id = $1 AND q.track_id = t.id
|
|
)
|
|
ORDER BY (s.last_at <= now() - interval '6 months') DESC,
|
|
(s.c >= 5) DESC,
|
|
s.c DESC, t.id
|
|
LIMIT 200
|
|
`
|
|
|
|
type ListRediscoverTracksRow struct {
|
|
ID pgtype.UUID
|
|
AlbumID pgtype.UUID
|
|
ArtistID pgtype.UUID
|
|
}
|
|
|
|
// #420 Rediscover: tracks the user played a lot but has drifted away
|
|
// from. One blended pool (issue #1246: the old two-tier UNION was
|
|
// all-or-nothing — a single ">=6 months cold" row suppressed the whole
|
|
// ">=30 days" tier, which is how a one-song playlist shipped; the 6mo
|
|
// tier was also a strict subset of the 30d tier). Eligibility is >=3
|
|
// non-skip plays and >=30 days cold; the bar sits at 3 rather than 5
|
|
// so a weeks-old library still fields a pool — on young histories the
|
|
// >=5-play tracks are precisely the ones still in rotation. Ordering
|
|
// prefers true rediscoveries (>=6 months cold), then strong affection
|
|
// (>=5 plays), then raw play count; the producer's minimum floor
|
|
// decides whether the pool is big enough to ship at all. $1 user_id.
|
|
func (q *Queries) ListRediscoverTracks(ctx context.Context, userID pgtype.UUID) ([]ListRediscoverTracksRow, error) {
|
|
rows, err := q.db.Query(ctx, listRediscoverTracks, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListRediscoverTracksRow
|
|
for rows.Next() {
|
|
var i ListRediscoverTracksRow
|
|
if err := rows.Scan(&i.ID, &i.AlbumID, &i.ArtistID); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|