feat(playlists): #411 R3 — five discovery mixes (#419-423)
Each is one candidate query + one registry entry; zero client work (R2 made tiles/refresh/shuffle generic, all are singleton kinds so web's server `refreshable` flag and Flutter's derived getter both light up automatically). - deep_cuts (#419): <=2-play tracks from liked / heavily-played artists; diversity-capped. - rediscover (#420): >=5-play tracks not heard in 6 months, by historical affection. - new_for_you (#421): tracks from albums added <=30d whose artist the user likes/plays; album-coherent (no cap). - on_this_day (#422): tracks played within ±7 day-of-year in prior windows (>60d ago), weighted by play count. - first_listens (#423): never-played albums, tiered liked-artist → played-artist → rest; album-coherent. system_mixes.go producers mirror the Discover model (SQL gives the ranking; finishMix caps+truncates to 100 to match For-You/Discover shuffle depth; album-coherent mixes skip the cap). Builder query failure is non-fatal (logged, yields no playlist) like Discover. Existing system_test existence checks are unaffected. Closes the #411 system-playlists-v2 umbrella's new-types thread. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
// 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 '60 days'
|
||||
AND ABS(EXTRACT(DOY FROM started_at) - EXTRACT(DOY FROM now())) <= 7
|
||||
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
|
||||
// (±7 day-of-year) in the past, excluding the very recent (>60 days
|
||||
// ago) so it's nostalgic, not just "last week". Weighted by how much
|
||||
// they were played in those windows.
|
||||
// $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 track_id, COUNT(*) AS c, MAX(started_at) AS last_at
|
||||
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 stats s ON s.track_id = t.id
|
||||
WHERE s.c >= 5
|
||||
AND s.last_at <= now() - interval '6 months'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM lidarr_quarantine q
|
||||
WHERE q.user_id = $1 AND q.track_id = t.id
|
||||
)
|
||||
ORDER BY 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 (>=5 non-skip) but
|
||||
// not in the last 6 months. Ordered by historical affection.
|
||||
// $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
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
-- 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.
|
||||
|
||||
-- name: ListDeepCutsTracks :many
|
||||
-- #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.
|
||||
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;
|
||||
|
||||
-- name: ListRediscoverTracks :many
|
||||
-- #420 Rediscover: tracks the user played a lot (>=5 non-skip) but
|
||||
-- not in the last 6 months. Ordered by historical affection.
|
||||
-- $1 user_id.
|
||||
WITH stats AS (
|
||||
SELECT track_id, COUNT(*) AS c, MAX(started_at) AS last_at
|
||||
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 stats s ON s.track_id = t.id
|
||||
WHERE s.c >= 5
|
||||
AND s.last_at <= now() - interval '6 months'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM lidarr_quarantine q
|
||||
WHERE q.user_id = $1 AND q.track_id = t.id
|
||||
)
|
||||
ORDER BY s.c DESC, t.id
|
||||
LIMIT 200;
|
||||
|
||||
-- name: ListNewForYouTracks :many
|
||||
-- #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.
|
||||
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;
|
||||
|
||||
-- name: ListOnThisDayTracks :many
|
||||
-- #422 On This Day: tracks the user played around this calendar date
|
||||
-- (±7 day-of-year) in the past, excluding the very recent (>60 days
|
||||
-- ago) so it's nostalgic, not just "last week". Weighted by how much
|
||||
-- they were played in those windows.
|
||||
-- $1 user_id, $2 date string.
|
||||
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 '60 days'
|
||||
AND ABS(EXTRACT(DOY FROM started_at) - EXTRACT(DOY FROM now())) <= 7
|
||||
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;
|
||||
|
||||
-- name: ListFirstListensTracks :many
|
||||
-- #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.
|
||||
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;
|
||||
Reference in New Issue
Block a user