From 526a78b302f490bdb7f6efcd46efa1ff3abbe158 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 6 May 2026 12:28:18 -0400 Subject: [PATCH] feat(db/m7-cover-sources): SQL queries for provider settings + recheck - Extends GetAlbumCoverageRollup's with_art FILTER to include 'theaudiodb' so F's coverage gauge surfaces TheAudioDB-found rows as with_art rather than silently undercounting. - Modifies ListAlbumsMissingCover to take the current sources_version as a parameter; eligibility now includes stale-'none' rows. - Adds SetAlbumCoverWithVersion (atomic write of path + source + version stamp) and the parallel SetArtistArtWithVersion + ClearArtistArtNone + ListArtistsMissingArt for the artist-art enricher. - New coverart_settings.sql with ListProviderSettings, UpsertProviderSettings (boot reconciliation, preserves operator values on conflict), UpdateProviderSettings (admin PATCH; tri- state api_key handling), GetCurrentSourcesVersion, BumpSourcesVersion (RETURNING the new value). --- internal/db/dbq/albums.sql.go | 69 +++++++-- internal/db/dbq/artists.sql.go | 166 ++++++++++++++++++++-- internal/db/dbq/coverart_settings.sql.go | 131 +++++++++++++++++ internal/db/dbq/covers.sql.go | 39 +++-- internal/db/dbq/likes.sql.go | 9 +- internal/db/dbq/models.go | 66 ++++++--- internal/db/dbq/recommendation.sql.go | 24 +++- internal/db/dbq/scan_runs.sql.go | 14 +- internal/db/queries/albums.sql | 15 +- internal/db/queries/artists.sql | 45 +++++- internal/db/queries/coverart_settings.sql | 46 ++++++ internal/db/queries/covers.sql | 14 +- 12 files changed, 550 insertions(+), 88 deletions(-) create mode 100644 internal/db/dbq/coverart_settings.sql.go create mode 100644 internal/db/queries/coverart_settings.sql diff --git a/internal/db/dbq/albums.sql.go b/internal/db/dbq/albums.sql.go index e1772aa4..bbd72e86 100644 --- a/internal/db/dbq/albums.sql.go +++ b/internal/db/dbq/albums.sql.go @@ -72,7 +72,7 @@ func (q *Queries) DeleteAlbumIfEmpty(ctx context.Context, id pgtype.UUID) (Delet } const getAlbumByArtistAndTitle = `-- name: GetAlbumByArtistAndTitle :one -SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source FROM albums WHERE artist_id = $1 AND title = $2 LIMIT 1 +SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source, cover_art_sources_version FROM albums WHERE artist_id = $1 AND title = $2 LIMIT 1 ` type GetAlbumByArtistAndTitleParams struct { @@ -95,12 +95,13 @@ func (q *Queries) GetAlbumByArtistAndTitle(ctx context.Context, arg GetAlbumByAr &i.CreatedAt, &i.UpdatedAt, &i.CoverArtSource, + &i.CoverArtSourcesVersion, ) return i, err } const getAlbumByID = `-- name: GetAlbumByID :one -SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source FROM albums WHERE id = $1 +SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source, cover_art_sources_version FROM albums WHERE id = $1 ` func (q *Queries) GetAlbumByID(ctx context.Context, id pgtype.UUID) (Album, error) { @@ -117,6 +118,7 @@ func (q *Queries) GetAlbumByID(ctx context.Context, id pgtype.UUID) (Album, erro &i.CreatedAt, &i.UpdatedAt, &i.CoverArtSource, + &i.CoverArtSourcesVersion, ) return i, err } @@ -124,7 +126,7 @@ func (q *Queries) GetAlbumByID(ctx context.Context, id pgtype.UUID) (Album, erro const getAlbumCoverageRollup = `-- name: GetAlbumCoverageRollup :one SELECT COUNT(*) AS total, - COUNT(*) FILTER (WHERE cover_art_source IN ('sidecar','embedded','mbcaa')) AS with_art, + COUNT(*) FILTER (WHERE cover_art_source IN ('sidecar','embedded','mbcaa','theaudiodb')) AS with_art, COUNT(*) FILTER (WHERE cover_art_source IS NULL) AS pending, COUNT(*) FILTER (WHERE cover_art_source = 'none') AS settled, COUNT(*) FILTER (WHERE cover_art_source IS NULL AND mbid IS NULL) AS pending_no_mbid @@ -149,7 +151,7 @@ type GetAlbumCoverageRollupRow struct { // Invariant: with_art + pending + settled = total. (pending_no_mbid is // not part of the sum — it's a subset of pending, surfaced separately.) // -// The IN list below ('sidecar','embedded','mbcaa') must stay in sync +// The IN list below ('sidecar','embedded','mbcaa','theaudiodb') must stay in sync // with the cover_art_source CHECK constraint in migration // 0016_album_cover_source.up.sql. If a new source value is added there // without updating this query, with_art will silently undercount. @@ -167,7 +169,7 @@ func (q *Queries) GetAlbumCoverageRollup(ctx context.Context) (GetAlbumCoverageR } const listAlbumsAlphaByArtist = `-- name: ListAlbumsAlphaByArtist :many -SELECT albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, albums.cover_art_source, artists.sort_name AS artist_sort_name +SELECT albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, albums.cover_art_source, albums.cover_art_sources_version, artists.sort_name AS artist_sort_name FROM albums JOIN artists ON artists.id = albums.artist_id ORDER BY artists.sort_name, albums.sort_title @@ -206,6 +208,7 @@ func (q *Queries) ListAlbumsAlphaByArtist(ctx context.Context, arg ListAlbumsAlp &i.Album.CreatedAt, &i.Album.UpdatedAt, &i.Album.CoverArtSource, + &i.Album.CoverArtSourcesVersion, &i.ArtistSortName, ); err != nil { return nil, err @@ -219,7 +222,7 @@ func (q *Queries) ListAlbumsAlphaByArtist(ctx context.Context, arg ListAlbumsAlp } const listAlbumsAlphaByName = `-- name: ListAlbumsAlphaByName :many -SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source FROM albums ORDER BY sort_title LIMIT $1 OFFSET $2 +SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source, cover_art_sources_version FROM albums ORDER BY sort_title LIMIT $1 OFFSET $2 ` type ListAlbumsAlphaByNameParams struct { @@ -247,6 +250,7 @@ func (q *Queries) ListAlbumsAlphaByName(ctx context.Context, arg ListAlbumsAlpha &i.CreatedAt, &i.UpdatedAt, &i.CoverArtSource, + &i.CoverArtSourcesVersion, ); err != nil { return nil, err } @@ -259,7 +263,7 @@ func (q *Queries) ListAlbumsAlphaByName(ctx context.Context, arg ListAlbumsAlpha } const listAlbumsAlphaWithArtist = `-- name: ListAlbumsAlphaWithArtist :many -SELECT albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, albums.cover_art_source, artists.name AS artist_name +SELECT albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, albums.cover_art_source, albums.cover_art_sources_version, artists.name AS artist_name FROM albums JOIN artists ON artists.id = albums.artist_id ORDER BY albums.sort_title, albums.id @@ -298,6 +302,7 @@ func (q *Queries) ListAlbumsAlphaWithArtist(ctx context.Context, arg ListAlbumsA &i.Album.CreatedAt, &i.Album.UpdatedAt, &i.Album.CoverArtSource, + &i.Album.CoverArtSourcesVersion, &i.ArtistName, ); err != nil { return nil, err @@ -311,7 +316,7 @@ func (q *Queries) ListAlbumsAlphaWithArtist(ctx context.Context, arg ListAlbumsA } const listAlbumsByArtist = `-- name: ListAlbumsByArtist :many -SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source FROM albums WHERE artist_id = $1 ORDER BY release_date NULLS LAST, sort_title +SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source, cover_art_sources_version FROM albums WHERE artist_id = $1 ORDER BY release_date NULLS LAST, sort_title ` func (q *Queries) ListAlbumsByArtist(ctx context.Context, artistID pgtype.UUID) ([]Album, error) { @@ -334,6 +339,7 @@ func (q *Queries) ListAlbumsByArtist(ctx context.Context, artistID pgtype.UUID) &i.CreatedAt, &i.UpdatedAt, &i.CoverArtSource, + &i.CoverArtSourcesVersion, ); err != nil { return nil, err } @@ -346,7 +352,7 @@ func (q *Queries) ListAlbumsByArtist(ctx context.Context, artistID pgtype.UUID) } const listAlbumsByGenre = `-- name: ListAlbumsByGenre :many -SELECT DISTINCT ON (albums.id) albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, albums.cover_art_source +SELECT DISTINCT ON (albums.id) albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, albums.cover_art_source, albums.cover_art_sources_version FROM albums JOIN tracks ON tracks.album_id = albums.id WHERE tracks.genre = $1 @@ -381,6 +387,7 @@ func (q *Queries) ListAlbumsByGenre(ctx context.Context, arg ListAlbumsByGenrePa &i.CreatedAt, &i.UpdatedAt, &i.CoverArtSource, + &i.CoverArtSourcesVersion, ); err != nil { return nil, err } @@ -446,7 +453,7 @@ func (q *Queries) ListAlbumsMissingMbidWithTrack(ctx context.Context, limit int3 } const listAlbumsNewest = `-- name: ListAlbumsNewest :many -SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source FROM albums ORDER BY created_at DESC LIMIT $1 OFFSET $2 +SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source, cover_art_sources_version FROM albums ORDER BY created_at DESC LIMIT $1 OFFSET $2 ` type ListAlbumsNewestParams struct { @@ -474,6 +481,7 @@ func (q *Queries) ListAlbumsNewest(ctx context.Context, arg ListAlbumsNewestPara &i.CreatedAt, &i.UpdatedAt, &i.CoverArtSource, + &i.CoverArtSourcesVersion, ); err != nil { return nil, err } @@ -486,7 +494,7 @@ func (q *Queries) ListAlbumsNewest(ctx context.Context, arg ListAlbumsNewestPara } const listAlbumsRandom = `-- name: ListAlbumsRandom :many -SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source FROM albums ORDER BY random() LIMIT $1 +SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source, cover_art_sources_version FROM albums ORDER BY random() LIMIT $1 ` func (q *Queries) ListAlbumsRandom(ctx context.Context, limit int32) ([]Album, error) { @@ -509,6 +517,7 @@ func (q *Queries) ListAlbumsRandom(ctx context.Context, limit int32) ([]Album, e &i.CreatedAt, &i.UpdatedAt, &i.CoverArtSource, + &i.CoverArtSourcesVersion, ); err != nil { return nil, err } @@ -521,7 +530,7 @@ func (q *Queries) ListAlbumsRandom(ctx context.Context, limit int32) ([]Album, e } const listRecentlyAddedAlbumsWithArtist = `-- name: ListRecentlyAddedAlbumsWithArtist :many -SELECT albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, albums.cover_art_source, artists.name AS artist_name +SELECT albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, albums.cover_art_source, albums.cover_art_sources_version, artists.name AS artist_name FROM albums JOIN artists ON artists.id = albums.artist_id ORDER BY albums.created_at DESC, albums.id @@ -557,6 +566,7 @@ func (q *Queries) ListRecentlyAddedAlbumsWithArtist(ctx context.Context, limit i &i.Album.CreatedAt, &i.Album.UpdatedAt, &i.Album.CoverArtSource, + &i.Album.CoverArtSourcesVersion, &i.ArtistName, ); err != nil { return nil, err @@ -570,7 +580,7 @@ func (q *Queries) ListRecentlyAddedAlbumsWithArtist(ctx context.Context, limit i } const searchAlbums = `-- name: SearchAlbums :many -SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source FROM albums +SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source, cover_art_sources_version FROM albums WHERE title ILIKE '%' || $1 || '%' ORDER BY sort_title LIMIT $2 OFFSET $3 @@ -602,6 +612,7 @@ func (q *Queries) SearchAlbums(ctx context.Context, arg SearchAlbumsParams) ([]A &i.CreatedAt, &i.UpdatedAt, &i.CoverArtSource, + &i.CoverArtSourcesVersion, ); err != nil { return nil, err } @@ -613,6 +624,35 @@ func (q *Queries) SearchAlbums(ctx context.Context, arg SearchAlbumsParams) ([]A return items, nil } +const setAlbumCoverWithVersion = `-- name: SetAlbumCoverWithVersion :exec +UPDATE albums + SET cover_art_path = $2, + cover_art_source = $3, + cover_art_sources_version = $4, + updated_at = now() + WHERE id = $1 +` + +type SetAlbumCoverWithVersionParams struct { + ID pgtype.UUID + CoverArtPath *string + CoverArtSource *string + CoverArtSourcesVersion int32 +} + +// M7 cover-sources: records a successful or settled enrichment with the +// current sources_version stamp. cover_art_path is NULL when source is +// 'none' (no file written). Atomic; called per album per pass. +func (q *Queries) SetAlbumCoverWithVersion(ctx context.Context, arg SetAlbumCoverWithVersionParams) error { + _, err := q.db.Exec(ctx, setAlbumCoverWithVersion, + arg.ID, + arg.CoverArtPath, + arg.CoverArtSource, + arg.CoverArtSourcesVersion, + ) + return err +} + const setAlbumMbidIfNull = `-- name: SetAlbumMbidIfNull :exec UPDATE albums SET mbid = $2, @@ -643,7 +683,7 @@ DO UPDATE SET release_date = EXCLUDED.release_date, cover_art_path = EXCLUDED.cover_art_path, updated_at = now() -RETURNING id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source +RETURNING id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source, cover_art_sources_version ` type UpsertAlbumParams struct { @@ -676,6 +716,7 @@ func (q *Queries) UpsertAlbum(ctx context.Context, arg UpsertAlbumParams) (Album &i.CreatedAt, &i.UpdatedAt, &i.CoverArtSource, + &i.CoverArtSourcesVersion, ) return i, err } diff --git a/internal/db/dbq/artists.sql.go b/internal/db/dbq/artists.sql.go index 2df9bf12..8272c1d1 100644 --- a/internal/db/dbq/artists.sql.go +++ b/internal/db/dbq/artists.sql.go @@ -11,6 +11,21 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const clearArtistArtNone = `-- name: ClearArtistArtNone :exec +UPDATE artists + SET artist_art_source = NULL, + updated_at = now() + WHERE id = $1 + AND artist_art_source = 'none' +` + +// M7 cover-sources: parallel of ClearAlbumCoverNone. Used by the +// eligibility loop to flip stale 'none' rows back to NULL. +func (q *Queries) ClearArtistArtNone(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, clearArtistArtNone, id) + return err +} + const countArtists = `-- name: CountArtists :one SELECT COUNT(*) FROM artists ` @@ -52,25 +67,46 @@ func (q *Queries) DeleteArtistIfEmpty(ctx context.Context, id pgtype.UUID) (pgty } const getArtistByID = `-- name: GetArtistByID :one -SELECT id, name, sort_name, mbid, created_at, updated_at FROM artists WHERE id = $1 +SELECT a.id, a.name, a.sort_name, a.mbid, + a.artist_thumb_path, a.artist_fanart_path, + a.artist_art_source, a.artist_art_sources_version + FROM artists a + WHERE a.id = $1 ` -func (q *Queries) GetArtistByID(ctx context.Context, id pgtype.UUID) (Artist, error) { +type GetArtistByIDRow struct { + ID pgtype.UUID + Name string + SortName string + Mbid *string + ArtistThumbPath *string + ArtistFanartPath *string + ArtistArtSource *string + ArtistArtSourcesVersion int32 +} + +// M7 cover-sources: enricher reads the artist's mbid + current art +// columns to drive eligibility check. (Modified from SELECT * to +// explicitly include the new artist_art_* columns added in migration +// 0018.) +func (q *Queries) GetArtistByID(ctx context.Context, id pgtype.UUID) (GetArtistByIDRow, error) { row := q.db.QueryRow(ctx, getArtistByID, id) - var i Artist + var i GetArtistByIDRow err := row.Scan( &i.ID, &i.Name, &i.SortName, &i.Mbid, - &i.CreatedAt, - &i.UpdatedAt, + &i.ArtistThumbPath, + &i.ArtistFanartPath, + &i.ArtistArtSource, + &i.ArtistArtSourcesVersion, ) return i, err } const getArtistByName = `-- name: GetArtistByName :one -SELECT id, name, sort_name, mbid, created_at, updated_at FROM artists WHERE name = $1 LIMIT 1 +SELECT id, name, sort_name, mbid, created_at, updated_at, artist_thumb_path, artist_fanart_path, artist_art_source, artist_art_sources_version FROM artists WHERE name = $1 LIMIT 1 ` func (q *Queries) GetArtistByName(ctx context.Context, name string) (Artist, error) { @@ -83,12 +119,16 @@ func (q *Queries) GetArtistByName(ctx context.Context, name string) (Artist, err &i.Mbid, &i.CreatedAt, &i.UpdatedAt, + &i.ArtistThumbPath, + &i.ArtistFanartPath, + &i.ArtistArtSource, + &i.ArtistArtSourcesVersion, ) return i, err } const getArtistsByIDs = `-- name: GetArtistsByIDs :many -SELECT id, name, sort_name, mbid, created_at, updated_at FROM artists WHERE id = ANY($1::uuid[]) +SELECT id, name, sort_name, mbid, created_at, updated_at, artist_thumb_path, artist_fanart_path, artist_art_source, artist_art_sources_version FROM artists WHERE id = ANY($1::uuid[]) ` // Batched lookup used by M5c suggestion attribution to resolve top-3 @@ -109,6 +149,10 @@ func (q *Queries) GetArtistsByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ( &i.Mbid, &i.CreatedAt, &i.UpdatedAt, + &i.ArtistThumbPath, + &i.ArtistFanartPath, + &i.ArtistArtSource, + &i.ArtistArtSourcesVersion, ); err != nil { return nil, err } @@ -121,7 +165,7 @@ func (q *Queries) GetArtistsByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ( } const listArtists = `-- name: ListArtists :many -SELECT id, name, sort_name, mbid, created_at, updated_at FROM artists ORDER BY sort_name +SELECT id, name, sort_name, mbid, created_at, updated_at, artist_thumb_path, artist_fanart_path, artist_art_source, artist_art_sources_version FROM artists ORDER BY sort_name ` func (q *Queries) ListArtists(ctx context.Context) ([]Artist, error) { @@ -140,6 +184,10 @@ func (q *Queries) ListArtists(ctx context.Context) ([]Artist, error) { &i.Mbid, &i.CreatedAt, &i.UpdatedAt, + &i.ArtistThumbPath, + &i.ArtistFanartPath, + &i.ArtistArtSource, + &i.ArtistArtSourcesVersion, ); err != nil { return nil, err } @@ -152,7 +200,7 @@ func (q *Queries) ListArtists(ctx context.Context) ([]Artist, error) { } const listArtistsAlpha = `-- name: ListArtistsAlpha :many -SELECT id, name, sort_name, mbid, created_at, updated_at FROM artists ORDER BY sort_name, name, id LIMIT $1 OFFSET $2 +SELECT id, name, sort_name, mbid, created_at, updated_at, artist_thumb_path, artist_fanart_path, artist_art_source, artist_art_sources_version FROM artists ORDER BY sort_name, name, id LIMIT $1 OFFSET $2 ` type ListArtistsAlphaParams struct { @@ -176,6 +224,10 @@ func (q *Queries) ListArtistsAlpha(ctx context.Context, arg ListArtistsAlphaPara &i.Mbid, &i.CreatedAt, &i.UpdatedAt, + &i.ArtistThumbPath, + &i.ArtistFanartPath, + &i.ArtistArtSource, + &i.ArtistArtSourcesVersion, ); err != nil { return nil, err } @@ -188,7 +240,7 @@ func (q *Queries) ListArtistsAlpha(ctx context.Context, arg ListArtistsAlphaPara } const listArtistsAlphaWithCovers = `-- name: ListArtistsAlphaWithCovers :many -SELECT artists.id, artists.name, artists.sort_name, artists.mbid, artists.created_at, artists.updated_at, +SELECT artists.id, artists.name, artists.sort_name, artists.mbid, artists.created_at, artists.updated_at, artists.artist_thumb_path, artists.artist_fanart_path, artists.artist_art_source, artists.artist_art_sources_version, cov.id AS cover_album_id, cnt.album_count::bigint AS album_count FROM artists @@ -237,6 +289,10 @@ func (q *Queries) ListArtistsAlphaWithCovers(ctx context.Context, arg ListArtist &i.Artist.Mbid, &i.Artist.CreatedAt, &i.Artist.UpdatedAt, + &i.Artist.ArtistThumbPath, + &i.Artist.ArtistFanartPath, + &i.Artist.ArtistArtSource, + &i.Artist.ArtistArtSourcesVersion, &i.CoverAlbumID, &i.AlbumCount, ); err != nil { @@ -250,8 +306,46 @@ func (q *Queries) ListArtistsAlphaWithCovers(ctx context.Context, arg ListArtist return items, nil } +const listArtistsMissingArt = `-- name: ListArtistsMissingArt :many +SELECT a.id + FROM artists a + WHERE a.artist_art_source IS NULL + OR (a.artist_art_source = 'none' AND a.artist_art_sources_version != $1) + ORDER BY a.created_at + LIMIT $2 +` + +type ListArtistsMissingArtParams struct { + ArtistArtSourcesVersion int32 + Limit int32 +} + +// M7 cover-sources: returns artist rows the artist-art enricher should +// attempt. Eligible: artist_art_source IS NULL OR (= 'none' AND stale +// artist_art_sources_version). MBID-less artists are returned but the +// enricher skips them at the row.mbid == nil guard inside the chain. +func (q *Queries) ListArtistsMissingArt(ctx context.Context, arg ListArtistsMissingArtParams) ([]pgtype.UUID, error) { + rows, err := q.db.Query(ctx, listArtistsMissingArt, arg.ArtistArtSourcesVersion, arg.Limit) + 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 listArtistsNewest = `-- name: ListArtistsNewest :many -SELECT id, name, sort_name, mbid, created_at, updated_at FROM artists ORDER BY created_at DESC, id DESC LIMIT $1 OFFSET $2 +SELECT id, name, sort_name, mbid, created_at, updated_at, artist_thumb_path, artist_fanart_path, artist_art_source, artist_art_sources_version FROM artists ORDER BY created_at DESC, id DESC LIMIT $1 OFFSET $2 ` type ListArtistsNewestParams struct { @@ -276,6 +370,10 @@ func (q *Queries) ListArtistsNewest(ctx context.Context, arg ListArtistsNewestPa &i.Mbid, &i.CreatedAt, &i.UpdatedAt, + &i.ArtistThumbPath, + &i.ArtistFanartPath, + &i.ArtistArtSource, + &i.ArtistArtSourcesVersion, ); err != nil { return nil, err } @@ -288,7 +386,7 @@ func (q *Queries) ListArtistsNewest(ctx context.Context, arg ListArtistsNewestPa } const searchArtists = `-- name: SearchArtists :many -SELECT id, name, sort_name, mbid, created_at, updated_at FROM artists +SELECT id, name, sort_name, mbid, created_at, updated_at, artist_thumb_path, artist_fanart_path, artist_art_source, artist_art_sources_version FROM artists WHERE name ILIKE '%' || $1 || '%' ORDER BY sort_name LIMIT $2 OFFSET $3 @@ -316,6 +414,10 @@ func (q *Queries) SearchArtists(ctx context.Context, arg SearchArtistsParams) ([ &i.Mbid, &i.CreatedAt, &i.UpdatedAt, + &i.ArtistThumbPath, + &i.ArtistFanartPath, + &i.ArtistArtSource, + &i.ArtistArtSourcesVersion, ); err != nil { return nil, err } @@ -327,6 +429,40 @@ func (q *Queries) SearchArtists(ctx context.Context, arg SearchArtistsParams) ([ return items, nil } +const setArtistArtWithVersion = `-- name: SetArtistArtWithVersion :exec +UPDATE artists + SET artist_thumb_path = $2, + artist_fanart_path = $3, + artist_art_source = $4, + artist_art_sources_version = $5, + updated_at = now() + WHERE id = $1 +` + +type SetArtistArtWithVersionParams struct { + ID pgtype.UUID + ArtistThumbPath *string + ArtistFanartPath *string + ArtistArtSource *string + ArtistArtSourcesVersion int32 +} + +// M7 cover-sources: records a successful or settled artist-art +// enrichment with the current sources_version stamp. thumb_path / +// fanart_path are NULL on a 'none' result, or independently NULL if +// the provider returned only one of the two image types (e.g. +// TheAudioDB had a thumb but no fanart for this artist). +func (q *Queries) SetArtistArtWithVersion(ctx context.Context, arg SetArtistArtWithVersionParams) error { + _, err := q.db.Exec(ctx, setArtistArtWithVersion, + arg.ID, + arg.ArtistThumbPath, + arg.ArtistFanartPath, + arg.ArtistArtSource, + arg.ArtistArtSourcesVersion, + ) + return err +} + const setArtistMbidIfNull = `-- name: SetArtistMbidIfNull :exec UPDATE artists SET mbid = $2, @@ -355,7 +491,7 @@ DO UPDATE SET name = EXCLUDED.name, sort_name = EXCLUDED.sort_name, updated_at = now() -RETURNING id, name, sort_name, mbid, created_at, updated_at +RETURNING id, name, sort_name, mbid, created_at, updated_at, artist_thumb_path, artist_fanart_path, artist_art_source, artist_art_sources_version ` type UpsertArtistParams struct { @@ -376,6 +512,10 @@ func (q *Queries) UpsertArtist(ctx context.Context, arg UpsertArtistParams) (Art &i.Mbid, &i.CreatedAt, &i.UpdatedAt, + &i.ArtistThumbPath, + &i.ArtistFanartPath, + &i.ArtistArtSource, + &i.ArtistArtSourcesVersion, ) return i, err } diff --git a/internal/db/dbq/coverart_settings.sql.go b/internal/db/dbq/coverart_settings.sql.go new file mode 100644 index 00000000..4ef1bb98 --- /dev/null +++ b/internal/db/dbq/coverart_settings.sql.go @@ -0,0 +1,131 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: coverart_settings.sql + +package dbq + +import ( + "context" +) + +const bumpSourcesVersion = `-- name: BumpSourcesVersion :one +UPDATE cover_art_sources_meta + SET current_version = current_version + 1 + WHERE id = true +RETURNING current_version +` + +// Increments and returns the new version. Called by SettingsService +// only when the *enabled set* changes (not on key/display_order +// changes). RETURNING gives the new value back so the handler can +// include it in the response without a follow-up SELECT. +func (q *Queries) BumpSourcesVersion(ctx context.Context) (int32, error) { + row := q.db.QueryRow(ctx, bumpSourcesVersion) + var current_version int32 + err := row.Scan(¤t_version) + return current_version, err +} + +const getCurrentSourcesVersion = `-- name: GetCurrentSourcesVersion :one +SELECT current_version FROM cover_art_sources_meta WHERE id = true +` + +func (q *Queries) GetCurrentSourcesVersion(ctx context.Context) (int32, error) { + row := q.db.QueryRow(ctx, getCurrentSourcesVersion) + var current_version int32 + err := row.Scan(¤t_version) + return current_version, err +} + +const listProviderSettings = `-- name: ListProviderSettings :many + +SELECT provider_id, enabled, api_key, display_order, created_at, updated_at + FROM cover_art_provider_settings + ORDER BY display_order, provider_id +` + +// M7 cover-sources: queries for the cover_art_provider_settings table +// and the cover_art_sources_meta singleton. The SettingsService +// consumes these. +// Returns all rows from cover_art_provider_settings. Used by the +// SettingsService at boot for reconciliation and by the admin GET +// handler. +func (q *Queries) ListProviderSettings(ctx context.Context) ([]CoverArtProviderSetting, error) { + rows, err := q.db.Query(ctx, listProviderSettings) + if err != nil { + return nil, err + } + defer rows.Close() + var items []CoverArtProviderSetting + for rows.Next() { + var i CoverArtProviderSetting + if err := rows.Scan( + &i.ProviderID, + &i.Enabled, + &i.ApiKey, + &i.DisplayOrder, + &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 updateProviderSettings = `-- name: UpdateProviderSettings :exec +UPDATE cover_art_provider_settings + SET enabled = COALESCE($2, enabled), + api_key = CASE WHEN $3::boolean THEN $4 ELSE api_key END, + updated_at = now() + WHERE provider_id = $1 +` + +type UpdateProviderSettingsParams struct { + ProviderID string + Enabled bool + Column3 bool + ApiKey *string +} + +// Admin PATCH applies one or both fields. Pass NULL for enabled to +// leave it unchanged. Pass FALSE for the apiKeyChanged flag to leave +// the api_key unchanged regardless of apiKey value; pass TRUE with +// empty string to clear, or TRUE with non-empty to set. +func (q *Queries) UpdateProviderSettings(ctx context.Context, arg UpdateProviderSettingsParams) error { + _, err := q.db.Exec(ctx, updateProviderSettings, + arg.ProviderID, + arg.Enabled, + arg.Column3, + arg.ApiKey, + ) + return err +} + +const upsertProviderSettings = `-- name: UpsertProviderSettings :exec +INSERT INTO cover_art_provider_settings (provider_id, enabled, display_order) +VALUES ($1, $2, $3) +ON CONFLICT (provider_id) DO UPDATE + SET display_order = EXCLUDED.display_order, + updated_at = now() +` + +type UpsertProviderSettingsParams struct { + ProviderID string + Enabled bool + DisplayOrder int32 +} + +// INSERT a default row for a newly-registered provider, or update an +// existing row's display_order on boot reconciliation. Does NOT +// overwrite enabled / api_key on conflict — the operator's settings +// persist across restarts. +func (q *Queries) UpsertProviderSettings(ctx context.Context, arg UpsertProviderSettingsParams) error { + _, err := q.db.Exec(ctx, upsertProviderSettings, arg.ProviderID, arg.Enabled, arg.DisplayOrder) + return err +} diff --git a/internal/db/dbq/covers.sql.go b/internal/db/dbq/covers.sql.go index f726cc03..068ce10c 100644 --- a/internal/db/dbq/covers.sql.go +++ b/internal/db/dbq/covers.sql.go @@ -109,41 +109,38 @@ func (q *Queries) GetAlbumWithFirstTrackPath(ctx context.Context, id pgtype.UUID const listAlbumsMissingCover = `-- name: ListAlbumsMissingCover :many -SELECT a.id, a.mbid, a.title, a.artist_id +SELECT a.id FROM albums a WHERE a.cover_art_source IS NULL - ORDER BY a.created_at ASC - LIMIT $1 + OR (a.cover_art_source = 'none' AND a.cover_art_sources_version != $1) + ORDER BY a.created_at + LIMIT $2 ` -type ListAlbumsMissingCoverRow struct { - ID pgtype.UUID - Mbid *string - Title string - ArtistID pgtype.UUID +type ListAlbumsMissingCoverParams struct { + CoverArtSourcesVersion int32 + Limit int32 } // M7 #353: album cover enrichment queries. -// Albums where cover_art_source has never been set. Used by the boot -// backfill and post-scan enrichment passes. LIMIT supplied by caller. -func (q *Queries) ListAlbumsMissingCover(ctx context.Context, limit int32) ([]ListAlbumsMissingCoverRow, error) { - rows, err := q.db.Query(ctx, listAlbumsMissingCover, limit) +// M7 cover-sources: returns rows the enricher should attempt. Eligible: +// cover_art_source IS NULL (never tried), OR cover_art_source = 'none' +// AND cover_art_sources_version != current (settled under an older +// source set; recheck against the current chain). LIMIT supplied by +// caller for batching. +func (q *Queries) ListAlbumsMissingCover(ctx context.Context, arg ListAlbumsMissingCoverParams) ([]pgtype.UUID, error) { + rows, err := q.db.Query(ctx, listAlbumsMissingCover, arg.CoverArtSourcesVersion, arg.Limit) if err != nil { return nil, err } defer rows.Close() - var items []ListAlbumsMissingCoverRow + var items []pgtype.UUID for rows.Next() { - var i ListAlbumsMissingCoverRow - if err := rows.Scan( - &i.ID, - &i.Mbid, - &i.Title, - &i.ArtistID, - ); err != nil { + var id pgtype.UUID + if err := rows.Scan(&id); err != nil { return nil, err } - items = append(items, i) + items = append(items, id) } if err := rows.Err(); err != nil { return nil, err diff --git a/internal/db/dbq/likes.sql.go b/internal/db/dbq/likes.sql.go index 714a8e90..3475c710 100644 --- a/internal/db/dbq/likes.sql.go +++ b/internal/db/dbq/likes.sql.go @@ -120,7 +120,7 @@ func (q *Queries) ListLikedAlbumIDs(ctx context.Context, userID pgtype.UUID) ([] } const listLikedAlbumRows = `-- name: ListLikedAlbumRows :many -SELECT a.id, a.title, a.sort_title, a.artist_id, a.release_date, a.mbid, a.cover_art_path, a.created_at, a.updated_at, a.cover_art_source FROM albums a +SELECT a.id, a.title, a.sort_title, a.artist_id, a.release_date, a.mbid, a.cover_art_path, a.created_at, a.updated_at, a.cover_art_source, a.cover_art_sources_version FROM albums a JOIN general_likes_albums l ON l.album_id = a.id WHERE l.user_id = $1 ORDER BY l.liked_at DESC @@ -153,6 +153,7 @@ func (q *Queries) ListLikedAlbumRows(ctx context.Context, arg ListLikedAlbumRows &i.CreatedAt, &i.UpdatedAt, &i.CoverArtSource, + &i.CoverArtSourcesVersion, ); err != nil { return nil, err } @@ -189,7 +190,7 @@ func (q *Queries) ListLikedArtistIDs(ctx context.Context, userID pgtype.UUID) ([ } const listLikedArtistRows = `-- name: ListLikedArtistRows :many -SELECT a.id, a.name, a.sort_name, a.mbid, a.created_at, a.updated_at FROM artists a +SELECT a.id, a.name, a.sort_name, a.mbid, a.created_at, a.updated_at, a.artist_thumb_path, a.artist_fanart_path, a.artist_art_source, a.artist_art_sources_version FROM artists a JOIN general_likes_artists l ON l.artist_id = a.id WHERE l.user_id = $1 ORDER BY l.liked_at DESC @@ -218,6 +219,10 @@ func (q *Queries) ListLikedArtistRows(ctx context.Context, arg ListLikedArtistRo &i.Mbid, &i.CreatedAt, &i.UpdatedAt, + &i.ArtistThumbPath, + &i.ArtistFanartPath, + &i.ArtistArtSource, + &i.ArtistArtSourcesVersion, ); err != nil { return nil, err } diff --git a/internal/db/dbq/models.go b/internal/db/dbq/models.go index b98c0eb2..1f583a88 100644 --- a/internal/db/dbq/models.go +++ b/internal/db/dbq/models.go @@ -188,25 +188,30 @@ func (ns NullLidarrRequestStatus) Value() (driver.Value, error) { } type Album struct { - ID pgtype.UUID - Title string - SortTitle string - ArtistID pgtype.UUID - ReleaseDate pgtype.Date - Mbid *string - CoverArtPath *string - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz - CoverArtSource *string + ID pgtype.UUID + Title string + SortTitle string + ArtistID pgtype.UUID + ReleaseDate pgtype.Date + Mbid *string + CoverArtPath *string + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz + CoverArtSource *string + CoverArtSourcesVersion int32 } type Artist struct { - ID pgtype.UUID - Name string - SortName string - Mbid *string - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz + ID pgtype.UUID + Name string + SortName string + Mbid *string + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz + ArtistThumbPath *string + ArtistFanartPath *string + ArtistArtSource *string + ArtistArtSourcesVersion int32 } type ArtistSimilarity struct { @@ -236,6 +241,20 @@ type ContextualLike struct { SessionID pgtype.UUID } +type CoverArtProviderSetting struct { + ProviderID string + Enabled bool + ApiKey *string + DisplayOrder int32 + CreatedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} + +type CoverArtSourcesMetum struct { + ID bool + CurrentVersion int32 +} + type GeneralLike struct { UserID pgtype.UUID TrackID pgtype.UUID @@ -364,13 +383,14 @@ type PlaylistTrack struct { } type ScanRun struct { - ID pgtype.UUID - StartedAt pgtype.Timestamptz - FinishedAt pgtype.Timestamptz - Library []byte - MbidBackfill []byte - CoverEnrich []byte - ErrorMessage *string + ID pgtype.UUID + StartedAt pgtype.Timestamptz + FinishedAt pgtype.Timestamptz + Library []byte + MbidBackfill []byte + CoverEnrich []byte + ErrorMessage *string + ArtistArtEnrich []byte } type ScrobbleQueue struct { diff --git a/internal/db/dbq/recommendation.sql.go b/internal/db/dbq/recommendation.sql.go index 9db7c9ea..e5ca6ba6 100644 --- a/internal/db/dbq/recommendation.sql.go +++ b/internal/db/dbq/recommendation.sql.go @@ -12,7 +12,7 @@ import ( ) const listLastPlayedArtistsForUser = `-- name: ListLastPlayedArtistsForUser :many -SELECT a.id, a.name, a.sort_name, a.mbid, a.created_at, a.updated_at, +SELECT a.id, a.name, a.sort_name, a.mbid, a.created_at, a.updated_at, a.artist_thumb_path, a.artist_fanart_path, a.artist_art_source, a.artist_art_sources_version, cov.id AS cover_album_id, cnt.album_count::bigint AS album_count, max_started.started_at::timestamptz AS last_played_at @@ -68,6 +68,10 @@ func (q *Queries) ListLastPlayedArtistsForUser(ctx context.Context, arg ListLast &i.Artist.Mbid, &i.Artist.CreatedAt, &i.Artist.UpdatedAt, + &i.Artist.ArtistThumbPath, + &i.Artist.ArtistFanartPath, + &i.Artist.ArtistArtSource, + &i.Artist.ArtistArtSourcesVersion, &i.CoverAlbumID, &i.AlbumCount, &i.LastPlayedAt, @@ -157,7 +161,7 @@ func (q *Queries) ListMostPlayedTracksForUser(ctx context.Context, arg ListMostP } const listRediscoverAlbumsFallbackForUser = `-- name: ListRediscoverAlbumsFallbackForUser :many -SELECT albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, albums.cover_art_source, artists.name AS artist_name +SELECT albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, albums.cover_art_source, albums.cover_art_sources_version, artists.name AS artist_name FROM general_likes_albums gla JOIN albums ON albums.id = gla.album_id JOIN artists ON artists.id = albums.artist_id @@ -199,6 +203,7 @@ func (q *Queries) ListRediscoverAlbumsFallbackForUser(ctx context.Context, arg L &i.Album.CreatedAt, &i.Album.UpdatedAt, &i.Album.CoverArtSource, + &i.Album.CoverArtSourcesVersion, &i.ArtistName, ); err != nil { return nil, err @@ -226,7 +231,7 @@ WITH eligible AS ( HAVING COALESCE(max(pe.started_at), '1970-01-01'::timestamptz) < now() - interval '14 days' ) -SELECT albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, albums.cover_art_source, artists.name AS artist_name +SELECT albums.id, albums.title, albums.sort_title, albums.artist_id, albums.release_date, albums.mbid, albums.cover_art_path, albums.created_at, albums.updated_at, albums.cover_art_source, albums.cover_art_sources_version, artists.name AS artist_name FROM eligible e JOIN albums ON albums.id = e.album_id JOIN artists ON artists.id = albums.artist_id @@ -268,6 +273,7 @@ func (q *Queries) ListRediscoverAlbumsForUser(ctx context.Context, arg ListRedis &i.Album.CreatedAt, &i.Album.UpdatedAt, &i.Album.CoverArtSource, + &i.Album.CoverArtSourcesVersion, &i.ArtistName, ); err != nil { return nil, err @@ -281,7 +287,7 @@ func (q *Queries) ListRediscoverAlbumsForUser(ctx context.Context, arg ListRedis } const listRediscoverArtistsFallbackForUser = `-- name: ListRediscoverArtistsFallbackForUser :many -SELECT artists.id, artists.name, artists.sort_name, artists.mbid, artists.created_at, artists.updated_at, +SELECT artists.id, artists.name, artists.sort_name, artists.mbid, artists.created_at, artists.updated_at, artists.artist_thumb_path, artists.artist_fanart_path, artists.artist_art_source, artists.artist_art_sources_version, cov.id AS cover_album_id, cnt.album_count::bigint AS album_count FROM general_likes_artists gla @@ -327,6 +333,10 @@ func (q *Queries) ListRediscoverArtistsFallbackForUser(ctx context.Context, arg &i.Artist.Mbid, &i.Artist.CreatedAt, &i.Artist.UpdatedAt, + &i.Artist.ArtistThumbPath, + &i.Artist.ArtistFanartPath, + &i.Artist.ArtistArtSource, + &i.Artist.ArtistArtSourcesVersion, &i.CoverAlbumID, &i.AlbumCount, ); err != nil { @@ -355,7 +365,7 @@ WITH eligible AS ( HAVING COALESCE(max(pe.started_at), '1970-01-01'::timestamptz) < now() - interval '14 days' ) -SELECT artists.id, artists.name, artists.sort_name, artists.mbid, artists.created_at, artists.updated_at, +SELECT artists.id, artists.name, artists.sort_name, artists.mbid, artists.created_at, artists.updated_at, artists.artist_thumb_path, artists.artist_fanart_path, artists.artist_art_source, artists.artist_art_sources_version, cov.id AS cover_album_id, cnt.album_count::bigint AS album_count FROM eligible e @@ -403,6 +413,10 @@ func (q *Queries) ListRediscoverArtistsForUser(ctx context.Context, arg ListRedi &i.Artist.Mbid, &i.Artist.CreatedAt, &i.Artist.UpdatedAt, + &i.Artist.ArtistThumbPath, + &i.Artist.ArtistFanartPath, + &i.Artist.ArtistArtSource, + &i.Artist.ArtistArtSourcesVersion, &i.CoverAlbumID, &i.AlbumCount, ); err != nil { diff --git a/internal/db/dbq/scan_runs.sql.go b/internal/db/dbq/scan_runs.sql.go index 5a3928e3..a21e0ad2 100644 --- a/internal/db/dbq/scan_runs.sql.go +++ b/internal/db/dbq/scan_runs.sql.go @@ -59,11 +59,21 @@ SELECT id, started_at, finished_at, library, mbid_backfill, cover_enrich, error_ LIMIT 1 ` +type GetLatestScanRunRow struct { + ID pgtype.UUID + StartedAt pgtype.Timestamptz + FinishedAt pgtype.Timestamptz + Library []byte + MbidBackfill []byte + CoverEnrich []byte + ErrorMessage *string +} + // Powers GET /api/admin/scan/status. Returns NoRows when no scan has // ever run; the API layer maps that to a 200 with empty payload. -func (q *Queries) GetLatestScanRun(ctx context.Context) (ScanRun, error) { +func (q *Queries) GetLatestScanRun(ctx context.Context) (GetLatestScanRunRow, error) { row := q.db.QueryRow(ctx, getLatestScanRun) - var i ScanRun + var i GetLatestScanRunRow err := row.Scan( &i.ID, &i.StartedAt, diff --git a/internal/db/queries/albums.sql b/internal/db/queries/albums.sql index 27930f3c..375f219f 100644 --- a/internal/db/queries/albums.sql +++ b/internal/db/queries/albums.sql @@ -136,15 +136,26 @@ SELECT a.id AS album_id, -- Invariant: with_art + pending + settled = total. (pending_no_mbid is -- not part of the sum — it's a subset of pending, surfaced separately.) -- --- The IN list below ('sidecar','embedded','mbcaa') must stay in sync +-- The IN list below ('sidecar','embedded','mbcaa','theaudiodb') must stay in sync -- with the cover_art_source CHECK constraint in migration -- 0016_album_cover_source.up.sql. If a new source value is added there -- without updating this query, with_art will silently undercount. SELECT COUNT(*) AS total, - COUNT(*) FILTER (WHERE cover_art_source IN ('sidecar','embedded','mbcaa')) AS with_art, + COUNT(*) FILTER (WHERE cover_art_source IN ('sidecar','embedded','mbcaa','theaudiodb')) AS with_art, COUNT(*) FILTER (WHERE cover_art_source IS NULL) AS pending, COUNT(*) FILTER (WHERE cover_art_source = 'none') AS settled, COUNT(*) FILTER (WHERE cover_art_source IS NULL AND mbid IS NULL) AS pending_no_mbid FROM albums; +-- name: SetAlbumCoverWithVersion :exec +-- M7 cover-sources: records a successful or settled enrichment with the +-- current sources_version stamp. cover_art_path is NULL when source is +-- 'none' (no file written). Atomic; called per album per pass. +UPDATE albums + SET cover_art_path = $2, + cover_art_source = $3, + cover_art_sources_version = $4, + updated_at = now() + WHERE id = $1; + diff --git a/internal/db/queries/artists.sql b/internal/db/queries/artists.sql index b0efef49..0de71efd 100644 --- a/internal/db/queries/artists.sql +++ b/internal/db/queries/artists.sql @@ -11,7 +11,15 @@ DO UPDATE SET RETURNING *; -- name: GetArtistByID :one -SELECT * FROM artists WHERE id = $1; +-- M7 cover-sources: enricher reads the artist's mbid + current art +-- columns to drive eligibility check. (Modified from SELECT * to +-- explicitly include the new artist_art_* columns added in migration +-- 0018.) +SELECT a.id, a.name, a.sort_name, a.mbid, + a.artist_thumb_path, a.artist_fanart_path, + a.artist_art_source, a.artist_art_sources_version + FROM artists a + WHERE a.id = $1; -- name: GetArtistByName :one SELECT * FROM artists WHERE name = $1 LIMIT 1; @@ -83,3 +91,38 @@ UPDATE artists SET mbid = $2, updated_at = now() WHERE id = $1 AND mbid IS NULL; + +-- name: ListArtistsMissingArt :many +-- M7 cover-sources: returns artist rows the artist-art enricher should +-- attempt. Eligible: artist_art_source IS NULL OR (= 'none' AND stale +-- artist_art_sources_version). MBID-less artists are returned but the +-- enricher skips them at the row.mbid == nil guard inside the chain. +SELECT a.id + FROM artists a + WHERE a.artist_art_source IS NULL + OR (a.artist_art_source = 'none' AND a.artist_art_sources_version != $1) + ORDER BY a.created_at + LIMIT $2; + +-- name: SetArtistArtWithVersion :exec +-- M7 cover-sources: records a successful or settled artist-art +-- enrichment with the current sources_version stamp. thumb_path / +-- fanart_path are NULL on a 'none' result, or independently NULL if +-- the provider returned only one of the two image types (e.g. +-- TheAudioDB had a thumb but no fanart for this artist). +UPDATE artists + SET artist_thumb_path = $2, + artist_fanart_path = $3, + artist_art_source = $4, + artist_art_sources_version = $5, + updated_at = now() + WHERE id = $1; + +-- name: ClearArtistArtNone :exec +-- M7 cover-sources: parallel of ClearAlbumCoverNone. Used by the +-- eligibility loop to flip stale 'none' rows back to NULL. +UPDATE artists + SET artist_art_source = NULL, + updated_at = now() + WHERE id = $1 + AND artist_art_source = 'none'; diff --git a/internal/db/queries/coverart_settings.sql b/internal/db/queries/coverart_settings.sql new file mode 100644 index 00000000..edb6b54b --- /dev/null +++ b/internal/db/queries/coverart_settings.sql @@ -0,0 +1,46 @@ +-- M7 cover-sources: queries for the cover_art_provider_settings table +-- and the cover_art_sources_meta singleton. The SettingsService +-- consumes these. + +-- name: ListProviderSettings :many +-- Returns all rows from cover_art_provider_settings. Used by the +-- SettingsService at boot for reconciliation and by the admin GET +-- handler. +SELECT provider_id, enabled, api_key, display_order, created_at, updated_at + FROM cover_art_provider_settings + ORDER BY display_order, provider_id; + +-- name: UpsertProviderSettings :exec +-- INSERT a default row for a newly-registered provider, or update an +-- existing row's display_order on boot reconciliation. Does NOT +-- overwrite enabled / api_key on conflict — the operator's settings +-- persist across restarts. +INSERT INTO cover_art_provider_settings (provider_id, enabled, display_order) +VALUES ($1, $2, $3) +ON CONFLICT (provider_id) DO UPDATE + SET display_order = EXCLUDED.display_order, + updated_at = now(); + +-- name: UpdateProviderSettings :exec +-- Admin PATCH applies one or both fields. Pass NULL for enabled to +-- leave it unchanged. Pass FALSE for the apiKeyChanged flag to leave +-- the api_key unchanged regardless of apiKey value; pass TRUE with +-- empty string to clear, or TRUE with non-empty to set. +UPDATE cover_art_provider_settings + SET enabled = COALESCE($2, enabled), + api_key = CASE WHEN $3::boolean THEN $4 ELSE api_key END, + updated_at = now() + WHERE provider_id = $1; + +-- name: GetCurrentSourcesVersion :one +SELECT current_version FROM cover_art_sources_meta WHERE id = true; + +-- name: BumpSourcesVersion :one +-- Increments and returns the new version. Called by SettingsService +-- only when the *enabled set* changes (not on key/display_order +-- changes). RETURNING gives the new value back so the handler can +-- include it in the response without a follow-up SELECT. +UPDATE cover_art_sources_meta + SET current_version = current_version + 1 + WHERE id = true +RETURNING current_version; diff --git a/internal/db/queries/covers.sql b/internal/db/queries/covers.sql index 10a55410..7d17d98a 100644 --- a/internal/db/queries/covers.sql +++ b/internal/db/queries/covers.sql @@ -1,13 +1,17 @@ -- M7 #353: album cover enrichment queries. -- name: ListAlbumsMissingCover :many --- Albums where cover_art_source has never been set. Used by the boot --- backfill and post-scan enrichment passes. LIMIT supplied by caller. -SELECT a.id, a.mbid, a.title, a.artist_id +-- M7 cover-sources: returns rows the enricher should attempt. Eligible: +-- cover_art_source IS NULL (never tried), OR cover_art_source = 'none' +-- AND cover_art_sources_version != current (settled under an older +-- source set; recheck against the current chain). LIMIT supplied by +-- caller for batching. +SELECT a.id FROM albums a WHERE a.cover_art_source IS NULL - ORDER BY a.created_at ASC - LIMIT $1; + OR (a.cover_art_source = 'none' AND a.cover_art_sources_version != $1) + ORDER BY a.created_at + LIMIT $2; -- name: ListAlbumsRetryMissing :many -- Bulk-retry candidates: NULL (never tried) plus 'none' (tried, failed).