1f0f7eee1a
The cross-user bucket query combined SELECT DISTINCT with ORDER BY md5(...), which Postgres rejects at plan time (SQLSTATE 42P10). buildDiscoverCandidates returned that error on first bucket failure, so the whole Discover playlist was skipped every nightly run — even though the random bucket pool was healthy. Switched to GROUP BY so the md5 ordering expression no longer needs to appear in the select list, and hardened the function so future single- bucket failures degrade gracefully via slot redistribution instead of taking out the whole playlist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
214 lines
6.5 KiB
Go
214 lines
6.5 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: discover.sql
|
|
|
|
package dbq
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const listCrossUserLikedTracksForDiscover = `-- name: ListCrossUserLikedTracksForDiscover :many
|
|
SELECT 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
|
|
)
|
|
GROUP BY t.id, t.album_id, t.artist_id
|
|
ORDER BY md5(t.id::text || $2::text)
|
|
LIMIT 60
|
|
`
|
|
|
|
type ListCrossUserLikedTracksForDiscoverParams struct {
|
|
UserID pgtype.UUID
|
|
Column2 string
|
|
}
|
|
|
|
type ListCrossUserLikedTracksForDiscoverRow struct {
|
|
ID pgtype.UUID
|
|
AlbumID pgtype.UUID
|
|
ArtistID pgtype.UUID
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// GROUP BY (not SELECT DISTINCT) so the md5 ORDER BY expression need
|
|
// not appear in the select list — Postgres rejects DISTINCT + ORDER BY
|
|
// by-expression at plan time (SQLSTATE 42P10), which previously caused
|
|
// the entire Discover build to fail silently.
|
|
func (q *Queries) ListCrossUserLikedTracksForDiscover(ctx context.Context, arg ListCrossUserLikedTracksForDiscoverParams) ([]ListCrossUserLikedTracksForDiscoverRow, error) {
|
|
rows, err := q.db.Query(ctx, listCrossUserLikedTracksForDiscover, arg.UserID, arg.Column2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListCrossUserLikedTracksForDiscoverRow
|
|
for rows.Next() {
|
|
var i ListCrossUserLikedTracksForDiscoverRow
|
|
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 listDormantArtistTracksForDiscover = `-- name: ListDormantArtistTracksForDiscover :many
|
|
|
|
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
|
|
`
|
|
|
|
type ListDormantArtistTracksForDiscoverParams struct {
|
|
UserID pgtype.UUID
|
|
Column2 string
|
|
}
|
|
|
|
type ListDormantArtistTracksForDiscoverRow struct {
|
|
ID pgtype.UUID
|
|
AlbumID pgtype.UUID
|
|
ArtistID pgtype.UUID
|
|
}
|
|
|
|
// 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.
|
|
// 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.
|
|
func (q *Queries) ListDormantArtistTracksForDiscover(ctx context.Context, arg ListDormantArtistTracksForDiscoverParams) ([]ListDormantArtistTracksForDiscoverRow, error) {
|
|
rows, err := q.db.Query(ctx, listDormantArtistTracksForDiscover, arg.UserID, arg.Column2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListDormantArtistTracksForDiscoverRow
|
|
for rows.Next() {
|
|
var i ListDormantArtistTracksForDiscoverRow
|
|
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 listRandomUnheardTracksForDiscover = `-- name: ListRandomUnheardTracksForDiscover :many
|
|
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
|
|
`
|
|
|
|
type ListRandomUnheardTracksForDiscoverParams struct {
|
|
UserID pgtype.UUID
|
|
Column2 string
|
|
}
|
|
|
|
type ListRandomUnheardTracksForDiscoverRow struct {
|
|
ID pgtype.UUID
|
|
AlbumID pgtype.UUID
|
|
ArtistID pgtype.UUID
|
|
}
|
|
|
|
// 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.
|
|
func (q *Queries) ListRandomUnheardTracksForDiscover(ctx context.Context, arg ListRandomUnheardTracksForDiscoverParams) ([]ListRandomUnheardTracksForDiscoverRow, error) {
|
|
rows, err := q.db.Query(ctx, listRandomUnheardTracksForDiscover, arg.UserID, arg.Column2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListRandomUnheardTracksForDiscoverRow
|
|
for rows.Next() {
|
|
var i ListRandomUnheardTracksForDiscoverRow
|
|
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
|
|
}
|