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).
This commit is contained in:
2026-05-07 08:39:26 -04:00
parent f77a0699ec
commit 0ba31c7816
5 changed files with 200 additions and 0 deletions
+49
View File
@@ -119,6 +119,55 @@ func (q *Queries) FinishSystemPlaylistRun(ctx context.Context, arg FinishSystemP
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
+16
View File
@@ -154,3 +154,19 @@ INSERT INTO playlists (
kind, system_variant, seed_artist_id, cover_path
) VALUES ($1, $2, '', false, 'system', $3, $4, $5)
RETURNING *;
-- name: GetSystemPlaylistByVariantForUser :one
-- 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.
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;