Files
minstrel/internal/db/dbq/system_playlists.sql.go
T
bvandeusen 0ba31c7816 feat(server/playlists): manual Discover refresh endpoint
POST /api/playlists/system/discover/refresh re-runs the system
playlist build synchronously for the calling user, then returns
their newly-built Discover playlist's UUID and track count.

Used by the frontend's "Refresh" affordances (next task) on the
Discover detail page header and the home Playlists row tile kebab.
Operator's escape hatch when they want a different randomness
without waiting for tomorrow's cron tick (e.g., after pasting a
Last.fm key, after the daily cover-art enrichment expanded their
playable library, or just for the heck of it).

Authenticated user only; the authed sub-router's RequireUser
middleware handles 401. Each user refreshes only their own
Discover — no admin route, no cross-user impersonation.

Adds GetSystemPlaylistByVariantForUser sqlc query for the response
lookup. Returns playlist_id=null and track_count=0 if the build
succeeded but the user's library yielded no eligible tracks
(degenerate empty-library).
2026-05-07 08:39:26 -04:00

455 lines
13 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: system_playlists.sql
package dbq
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const clearStaleSystemPlaylistInFlight = `-- name: ClearStaleSystemPlaylistInFlight :exec
UPDATE system_playlist_runs SET in_flight = false WHERE in_flight = true
`
// Cron startup recovery: clear any in_flight=true rows from a previously
// crashed process. Safe because at startup, by definition, nothing is
// still building.
func (q *Queries) ClearStaleSystemPlaylistInFlight(ctx context.Context) error {
_, err := q.db.Exec(ctx, clearStaleSystemPlaylistInFlight)
return err
}
const createSystemPlaylist = `-- name: CreateSystemPlaylist :one
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 id, user_id, name, description, is_public, cover_path, track_count, duration_sec, created_at, updated_at, kind, system_variant, seed_artist_id
`
type CreateSystemPlaylistParams struct {
UserID pgtype.UUID
Name string
SystemVariant *string
SeedArtistID pgtype.UUID
CoverPath *string
}
// 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).
func (q *Queries) CreateSystemPlaylist(ctx context.Context, arg CreateSystemPlaylistParams) (Playlist, error) {
row := q.db.QueryRow(ctx, createSystemPlaylist,
arg.UserID,
arg.Name,
arg.SystemVariant,
arg.SeedArtistID,
arg.CoverPath,
)
var i Playlist
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.IsPublic,
&i.CoverPath,
&i.TrackCount,
&i.DurationSec,
&i.CreatedAt,
&i.UpdatedAt,
&i.Kind,
&i.SystemVariant,
&i.SeedArtistID,
)
return i, err
}
const deleteSystemPlaylistsForUser = `-- name: DeleteSystemPlaylistsForUser :exec
DELETE FROM playlists WHERE user_id = $1 AND kind = 'system'
`
// Atomic-replace step. CASCADE on playlist_tracks.playlist_id handles
// the join rows.
func (q *Queries) DeleteSystemPlaylistsForUser(ctx context.Context, userID pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteSystemPlaylistsForUser, userID)
return err
}
const failSystemPlaylistRun = `-- name: FailSystemPlaylistRun :exec
UPDATE system_playlist_runs
SET in_flight = false,
last_error = $2
WHERE user_id = $1
`
type FailSystemPlaylistRunParams struct {
UserID pgtype.UUID
LastError *string
}
// Mark a failed build complete (in_flight=false, error captured).
func (q *Queries) FailSystemPlaylistRun(ctx context.Context, arg FailSystemPlaylistRunParams) error {
_, err := q.db.Exec(ctx, failSystemPlaylistRun, arg.UserID, arg.LastError)
return err
}
const finishSystemPlaylistRun = `-- name: FinishSystemPlaylistRun :exec
UPDATE system_playlist_runs
SET last_run_at = $2,
last_run_date = $3,
in_flight = false,
last_error = NULL
WHERE user_id = $1
`
type FinishSystemPlaylistRunParams struct {
UserID pgtype.UUID
LastRunAt pgtype.Timestamptz
LastRunDate pgtype.Date
}
// Mark a successful build complete.
func (q *Queries) FinishSystemPlaylistRun(ctx context.Context, arg FinishSystemPlaylistRunParams) error {
_, err := q.db.Exec(ctx, finishSystemPlaylistRun, arg.UserID, arg.LastRunAt, arg.LastRunDate)
return err
}
const getSystemPlaylistByVariantForUser = `-- name: GetSystemPlaylistByVariantForUser :one
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
`
type GetSystemPlaylistByVariantForUserParams struct {
UserID pgtype.UUID
SystemVariant *string
}
type GetSystemPlaylistByVariantForUserRow struct {
ID pgtype.UUID
UserID pgtype.UUID
Name string
Kind string
SystemVariant *string
SeedArtistID pgtype.UUID
CoverPath *string
TrackCount int32
}
// 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.
func (q *Queries) GetSystemPlaylistByVariantForUser(ctx context.Context, arg GetSystemPlaylistByVariantForUserParams) (GetSystemPlaylistByVariantForUserRow, error) {
row := q.db.QueryRow(ctx, getSystemPlaylistByVariantForUser, arg.UserID, arg.SystemVariant)
var i GetSystemPlaylistByVariantForUserRow
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Kind,
&i.SystemVariant,
&i.SeedArtistID,
&i.CoverPath,
&i.TrackCount,
)
return i, err
}
const getSystemPlaylistRun = `-- name: GetSystemPlaylistRun :one
SELECT user_id, last_run_at, last_run_date, in_flight, last_error
FROM system_playlist_runs
WHERE user_id = $1
`
func (q *Queries) GetSystemPlaylistRun(ctx context.Context, userID pgtype.UUID) (SystemPlaylistRun, error) {
row := q.db.QueryRow(ctx, getSystemPlaylistRun, userID)
var i SystemPlaylistRun
err := row.Scan(
&i.UserID,
&i.LastRunAt,
&i.LastRunDate,
&i.InFlight,
&i.LastError,
)
return i, err
}
const listActiveUsersForSystemPlaylists = `-- name: ListActiveUsersForSystemPlaylists :many
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'
)
`
// M7 #352 slice 2: system-generated playlist queries.
// Active = had a play in the last 7 days. The cron iterates this list.
func (q *Queries) ListActiveUsersForSystemPlaylists(ctx context.Context) ([]pgtype.UUID, error) {
rows, err := q.db.Query(ctx, listActiveUsersForSystemPlaylists)
if err != nil {
return nil, err
}
defer rows.Close()
var items []pgtype.UUID
for rows.Next() {
var id pgtype.UUID
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listPlaylistsByUserAndKind = `-- name: ListPlaylistsByUserAndKind :many
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
`
type ListPlaylistsByUserAndKindParams struct {
UserID pgtype.UUID
Column2 string
}
type ListPlaylistsByUserAndKindRow struct {
ID pgtype.UUID
UserID pgtype.UUID
Name string
Description string
IsPublic bool
Kind string
SystemVariant *string
SeedArtistID pgtype.UUID
CoverPath *string
TrackCount int32
DurationSec int32
CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
}
// 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.
func (q *Queries) ListPlaylistsByUserAndKind(ctx context.Context, arg ListPlaylistsByUserAndKindParams) ([]ListPlaylistsByUserAndKindRow, error) {
rows, err := q.db.Query(ctx, listPlaylistsByUserAndKind, arg.UserID, arg.Column2)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListPlaylistsByUserAndKindRow
for rows.Next() {
var i ListPlaylistsByUserAndKindRow
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.IsPublic,
&i.Kind,
&i.SystemVariant,
&i.SeedArtistID,
&i.CoverPath,
&i.TrackCount,
&i.DurationSec,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const pickSeedArtists = `-- name: PickSeedArtists :many
WITH plays AS (
SELECT t.artist_id,
COUNT(*) FILTER (WHERE pe.was_skipped = false) AS play_count
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
),
liked AS (
SELECT artist_id FROM general_likes_artists WHERE user_id = $1
)
SELECT p.artist_id,
(p.play_count + CASE WHEN l.artist_id IS NOT NULL THEN 5 ELSE 0 END)::bigint AS score
FROM plays p
LEFT JOIN liked l ON l.artist_id = p.artist_id
ORDER BY score DESC, p.artist_id
LIMIT 3
`
type PickSeedArtistsRow struct {
ArtistID pgtype.UUID
Score int64
}
// Top-3 most-engaged distinct artists in the user's last 7 days.
// Score = unskipped-play count + 5 if user has liked the artist.
func (q *Queries) PickSeedArtists(ctx context.Context, userID pgtype.UUID) ([]PickSeedArtistsRow, error) {
rows, err := q.db.Query(ctx, pickSeedArtists, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []PickSeedArtistsRow
for rows.Next() {
var i PickSeedArtistsRow
if err := rows.Scan(&i.ArtistID, &i.Score); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const pickTopAlbumCoverForArtistByUser = `-- name: PickTopAlbumCoverForArtistByUser :one
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
`
type PickTopAlbumCoverForArtistByUserParams struct {
UserID pgtype.UUID
ArtistID pgtype.UUID
}
// "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).
func (q *Queries) PickTopAlbumCoverForArtistByUser(ctx context.Context, arg PickTopAlbumCoverForArtistByUserParams) (*string, error) {
row := q.db.QueryRow(ctx, pickTopAlbumCoverForArtistByUser, arg.UserID, arg.ArtistID)
var cover_art_path *string
err := row.Scan(&cover_art_path)
return cover_art_path, err
}
const pickTopPlayedTrackForArtistByUser = `-- name: PickTopPlayedTrackForArtistByUser :one
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
`
type PickTopPlayedTrackForArtistByUserParams struct {
UserID pgtype.UUID
ArtistID pgtype.UUID
}
// "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{}.
func (q *Queries) PickTopPlayedTrackForArtistByUser(ctx context.Context, arg PickTopPlayedTrackForArtistByUserParams) (pgtype.UUID, error) {
row := q.db.QueryRow(ctx, pickTopPlayedTrackForArtistByUser, arg.UserID, arg.ArtistID)
var id pgtype.UUID
err := row.Scan(&id)
return id, err
}
const pickTopPlayedTrackForUser = `-- name: PickTopPlayedTrackForUser :one
SELECT t.id
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 pe.was_skipped = false
GROUP BY t.id
ORDER BY COUNT(*) DESC, t.id
LIMIT 1
`
// For-You seed selection. Returns the user's most-played non-skipped
// track in the last 7 days; tie-break by track_id for determinism.
func (q *Queries) PickTopPlayedTrackForUser(ctx context.Context, userID pgtype.UUID) (pgtype.UUID, error) {
row := q.db.QueryRow(ctx, pickTopPlayedTrackForUser, userID)
var id pgtype.UUID
err := row.Scan(&id)
return id, err
}
const tryClaimSystemPlaylistRun = `-- name: TryClaimSystemPlaylistRun :one
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
`
// Atomic test-and-set on in_flight. Returns the row if we won the claim,
// or pgx.ErrNoRows if someone else is already building.
func (q *Queries) TryClaimSystemPlaylistRun(ctx context.Context, userID pgtype.UUID) (SystemPlaylistRun, error) {
row := q.db.QueryRow(ctx, tryClaimSystemPlaylistRun, userID)
var i SystemPlaylistRun
err := row.Scan(
&i.UserID,
&i.LastRunAt,
&i.LastRunDate,
&i.InFlight,
&i.LastError,
)
return i, err
}