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).
This commit is contained in:
2026-05-06 12:28:18 -04:00
parent a86897b35e
commit 526a78b302
12 changed files with 550 additions and 88 deletions
+55 -14
View File
@@ -72,7 +72,7 @@ func (q *Queries) DeleteAlbumIfEmpty(ctx context.Context, id pgtype.UUID) (Delet
} }
const getAlbumByArtistAndTitle = `-- name: GetAlbumByArtistAndTitle :one 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 { type GetAlbumByArtistAndTitleParams struct {
@@ -95,12 +95,13 @@ func (q *Queries) GetAlbumByArtistAndTitle(ctx context.Context, arg GetAlbumByAr
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.CoverArtSource, &i.CoverArtSource,
&i.CoverArtSourcesVersion,
) )
return i, err return i, err
} }
const getAlbumByID = `-- name: GetAlbumByID :one 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) { 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.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.CoverArtSource, &i.CoverArtSource,
&i.CoverArtSourcesVersion,
) )
return i, err return i, err
} }
@@ -124,7 +126,7 @@ func (q *Queries) GetAlbumByID(ctx context.Context, id pgtype.UUID) (Album, erro
const getAlbumCoverageRollup = `-- name: GetAlbumCoverageRollup :one const getAlbumCoverageRollup = `-- name: GetAlbumCoverageRollup :one
SELECT SELECT
COUNT(*) AS total, 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 IS NULL) AS pending,
COUNT(*) FILTER (WHERE cover_art_source = 'none') AS settled, 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 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 // Invariant: with_art + pending + settled = total. (pending_no_mbid is
// not part of the sum — it's a subset of pending, surfaced separately.) // 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 // with the cover_art_source CHECK constraint in migration
// 0016_album_cover_source.up.sql. If a new source value is added there // 0016_album_cover_source.up.sql. If a new source value is added there
// without updating this query, with_art will silently undercount. // 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 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 FROM albums
JOIN artists ON artists.id = albums.artist_id JOIN artists ON artists.id = albums.artist_id
ORDER BY artists.sort_name, albums.sort_title 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.CreatedAt,
&i.Album.UpdatedAt, &i.Album.UpdatedAt,
&i.Album.CoverArtSource, &i.Album.CoverArtSource,
&i.Album.CoverArtSourcesVersion,
&i.ArtistSortName, &i.ArtistSortName,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -219,7 +222,7 @@ func (q *Queries) ListAlbumsAlphaByArtist(ctx context.Context, arg ListAlbumsAlp
} }
const listAlbumsAlphaByName = `-- name: ListAlbumsAlphaByName :many 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 { type ListAlbumsAlphaByNameParams struct {
@@ -247,6 +250,7 @@ func (q *Queries) ListAlbumsAlphaByName(ctx context.Context, arg ListAlbumsAlpha
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.CoverArtSource, &i.CoverArtSource,
&i.CoverArtSourcesVersion,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -259,7 +263,7 @@ func (q *Queries) ListAlbumsAlphaByName(ctx context.Context, arg ListAlbumsAlpha
} }
const listAlbumsAlphaWithArtist = `-- name: ListAlbumsAlphaWithArtist :many 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 FROM albums
JOIN artists ON artists.id = albums.artist_id JOIN artists ON artists.id = albums.artist_id
ORDER BY albums.sort_title, albums.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.CreatedAt,
&i.Album.UpdatedAt, &i.Album.UpdatedAt,
&i.Album.CoverArtSource, &i.Album.CoverArtSource,
&i.Album.CoverArtSourcesVersion,
&i.ArtistName, &i.ArtistName,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -311,7 +316,7 @@ func (q *Queries) ListAlbumsAlphaWithArtist(ctx context.Context, arg ListAlbumsA
} }
const listAlbumsByArtist = `-- name: ListAlbumsByArtist :many 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) { 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.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.CoverArtSource, &i.CoverArtSource,
&i.CoverArtSourcesVersion,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -346,7 +352,7 @@ func (q *Queries) ListAlbumsByArtist(ctx context.Context, artistID pgtype.UUID)
} }
const listAlbumsByGenre = `-- name: ListAlbumsByGenre :many 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 FROM albums
JOIN tracks ON tracks.album_id = albums.id JOIN tracks ON tracks.album_id = albums.id
WHERE tracks.genre = $1 WHERE tracks.genre = $1
@@ -381,6 +387,7 @@ func (q *Queries) ListAlbumsByGenre(ctx context.Context, arg ListAlbumsByGenrePa
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.CoverArtSource, &i.CoverArtSource,
&i.CoverArtSourcesVersion,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -446,7 +453,7 @@ func (q *Queries) ListAlbumsMissingMbidWithTrack(ctx context.Context, limit int3
} }
const listAlbumsNewest = `-- name: ListAlbumsNewest :many 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 { type ListAlbumsNewestParams struct {
@@ -474,6 +481,7 @@ func (q *Queries) ListAlbumsNewest(ctx context.Context, arg ListAlbumsNewestPara
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.CoverArtSource, &i.CoverArtSource,
&i.CoverArtSourcesVersion,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -486,7 +494,7 @@ func (q *Queries) ListAlbumsNewest(ctx context.Context, arg ListAlbumsNewestPara
} }
const listAlbumsRandom = `-- name: ListAlbumsRandom :many 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) { 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.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.CoverArtSource, &i.CoverArtSource,
&i.CoverArtSourcesVersion,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -521,7 +530,7 @@ func (q *Queries) ListAlbumsRandom(ctx context.Context, limit int32) ([]Album, e
} }
const listRecentlyAddedAlbumsWithArtist = `-- name: ListRecentlyAddedAlbumsWithArtist :many 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 FROM albums
JOIN artists ON artists.id = albums.artist_id JOIN artists ON artists.id = albums.artist_id
ORDER BY albums.created_at DESC, albums.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.CreatedAt,
&i.Album.UpdatedAt, &i.Album.UpdatedAt,
&i.Album.CoverArtSource, &i.Album.CoverArtSource,
&i.Album.CoverArtSourcesVersion,
&i.ArtistName, &i.ArtistName,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -570,7 +580,7 @@ func (q *Queries) ListRecentlyAddedAlbumsWithArtist(ctx context.Context, limit i
} }
const searchAlbums = `-- name: SearchAlbums :many 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 || '%' WHERE title ILIKE '%' || $1 || '%'
ORDER BY sort_title ORDER BY sort_title
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
@@ -602,6 +612,7 @@ func (q *Queries) SearchAlbums(ctx context.Context, arg SearchAlbumsParams) ([]A
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.CoverArtSource, &i.CoverArtSource,
&i.CoverArtSourcesVersion,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -613,6 +624,35 @@ func (q *Queries) SearchAlbums(ctx context.Context, arg SearchAlbumsParams) ([]A
return items, nil 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 const setAlbumMbidIfNull = `-- name: SetAlbumMbidIfNull :exec
UPDATE albums UPDATE albums
SET mbid = $2, SET mbid = $2,
@@ -643,7 +683,7 @@ DO UPDATE SET
release_date = EXCLUDED.release_date, release_date = EXCLUDED.release_date,
cover_art_path = EXCLUDED.cover_art_path, cover_art_path = EXCLUDED.cover_art_path,
updated_at = now() 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 { type UpsertAlbumParams struct {
@@ -676,6 +716,7 @@ func (q *Queries) UpsertAlbum(ctx context.Context, arg UpsertAlbumParams) (Album
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.CoverArtSource, &i.CoverArtSource,
&i.CoverArtSourcesVersion,
) )
return i, err return i, err
} }
+153 -13
View File
@@ -11,6 +11,21 @@ import (
"github.com/jackc/pgx/v5/pgtype" "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 const countArtists = `-- name: CountArtists :one
SELECT COUNT(*) FROM artists SELECT COUNT(*) FROM artists
` `
@@ -52,25 +67,46 @@ func (q *Queries) DeleteArtistIfEmpty(ctx context.Context, id pgtype.UUID) (pgty
} }
const getArtistByID = `-- name: GetArtistByID :one 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) row := q.db.QueryRow(ctx, getArtistByID, id)
var i Artist var i GetArtistByIDRow
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.Name, &i.Name,
&i.SortName, &i.SortName,
&i.Mbid, &i.Mbid,
&i.CreatedAt, &i.ArtistThumbPath,
&i.UpdatedAt, &i.ArtistFanartPath,
&i.ArtistArtSource,
&i.ArtistArtSourcesVersion,
) )
return i, err return i, err
} }
const getArtistByName = `-- name: GetArtistByName :one 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) { 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.Mbid,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.ArtistThumbPath,
&i.ArtistFanartPath,
&i.ArtistArtSource,
&i.ArtistArtSourcesVersion,
) )
return i, err return i, err
} }
const getArtistsByIDs = `-- name: GetArtistsByIDs :many 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 // 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.Mbid,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.ArtistThumbPath,
&i.ArtistFanartPath,
&i.ArtistArtSource,
&i.ArtistArtSourcesVersion,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -121,7 +165,7 @@ func (q *Queries) GetArtistsByIDs(ctx context.Context, dollar_1 []pgtype.UUID) (
} }
const listArtists = `-- name: ListArtists :many 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) { 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.Mbid,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.ArtistThumbPath,
&i.ArtistFanartPath,
&i.ArtistArtSource,
&i.ArtistArtSourcesVersion,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -152,7 +200,7 @@ func (q *Queries) ListArtists(ctx context.Context) ([]Artist, error) {
} }
const listArtistsAlpha = `-- name: ListArtistsAlpha :many 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 { type ListArtistsAlphaParams struct {
@@ -176,6 +224,10 @@ func (q *Queries) ListArtistsAlpha(ctx context.Context, arg ListArtistsAlphaPara
&i.Mbid, &i.Mbid,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.ArtistThumbPath,
&i.ArtistFanartPath,
&i.ArtistArtSource,
&i.ArtistArtSourcesVersion,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -188,7 +240,7 @@ func (q *Queries) ListArtistsAlpha(ctx context.Context, arg ListArtistsAlphaPara
} }
const listArtistsAlphaWithCovers = `-- name: ListArtistsAlphaWithCovers :many 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, cov.id AS cover_album_id,
cnt.album_count::bigint AS album_count cnt.album_count::bigint AS album_count
FROM artists FROM artists
@@ -237,6 +289,10 @@ func (q *Queries) ListArtistsAlphaWithCovers(ctx context.Context, arg ListArtist
&i.Artist.Mbid, &i.Artist.Mbid,
&i.Artist.CreatedAt, &i.Artist.CreatedAt,
&i.Artist.UpdatedAt, &i.Artist.UpdatedAt,
&i.Artist.ArtistThumbPath,
&i.Artist.ArtistFanartPath,
&i.Artist.ArtistArtSource,
&i.Artist.ArtistArtSourcesVersion,
&i.CoverAlbumID, &i.CoverAlbumID,
&i.AlbumCount, &i.AlbumCount,
); err != nil { ); err != nil {
@@ -250,8 +306,46 @@ func (q *Queries) ListArtistsAlphaWithCovers(ctx context.Context, arg ListArtist
return items, nil 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 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 { type ListArtistsNewestParams struct {
@@ -276,6 +370,10 @@ func (q *Queries) ListArtistsNewest(ctx context.Context, arg ListArtistsNewestPa
&i.Mbid, &i.Mbid,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.ArtistThumbPath,
&i.ArtistFanartPath,
&i.ArtistArtSource,
&i.ArtistArtSourcesVersion,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -288,7 +386,7 @@ func (q *Queries) ListArtistsNewest(ctx context.Context, arg ListArtistsNewestPa
} }
const searchArtists = `-- name: SearchArtists :many 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 || '%' WHERE name ILIKE '%' || $1 || '%'
ORDER BY sort_name ORDER BY sort_name
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
@@ -316,6 +414,10 @@ func (q *Queries) SearchArtists(ctx context.Context, arg SearchArtistsParams) ([
&i.Mbid, &i.Mbid,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.ArtistThumbPath,
&i.ArtistFanartPath,
&i.ArtistArtSource,
&i.ArtistArtSourcesVersion,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -327,6 +429,40 @@ func (q *Queries) SearchArtists(ctx context.Context, arg SearchArtistsParams) ([
return items, nil 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 const setArtistMbidIfNull = `-- name: SetArtistMbidIfNull :exec
UPDATE artists UPDATE artists
SET mbid = $2, SET mbid = $2,
@@ -355,7 +491,7 @@ DO UPDATE SET
name = EXCLUDED.name, name = EXCLUDED.name,
sort_name = EXCLUDED.sort_name, sort_name = EXCLUDED.sort_name,
updated_at = now() 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 { type UpsertArtistParams struct {
@@ -376,6 +512,10 @@ func (q *Queries) UpsertArtist(ctx context.Context, arg UpsertArtistParams) (Art
&i.Mbid, &i.Mbid,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.ArtistThumbPath,
&i.ArtistFanartPath,
&i.ArtistArtSource,
&i.ArtistArtSourcesVersion,
) )
return i, err return i, err
} }
+131
View File
@@ -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(&current_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(&current_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
}
+18 -21
View File
@@ -109,41 +109,38 @@ func (q *Queries) GetAlbumWithFirstTrackPath(ctx context.Context, id pgtype.UUID
const listAlbumsMissingCover = `-- name: ListAlbumsMissingCover :many const listAlbumsMissingCover = `-- name: ListAlbumsMissingCover :many
SELECT a.id, a.mbid, a.title, a.artist_id SELECT a.id
FROM albums a FROM albums a
WHERE a.cover_art_source IS NULL WHERE a.cover_art_source IS NULL
ORDER BY a.created_at ASC OR (a.cover_art_source = 'none' AND a.cover_art_sources_version != $1)
LIMIT $1 ORDER BY a.created_at
LIMIT $2
` `
type ListAlbumsMissingCoverRow struct { type ListAlbumsMissingCoverParams struct {
ID pgtype.UUID CoverArtSourcesVersion int32
Mbid *string Limit int32
Title string
ArtistID pgtype.UUID
} }
// M7 #353: album cover enrichment queries. // M7 #353: album cover enrichment queries.
// Albums where cover_art_source has never been set. Used by the boot // M7 cover-sources: returns rows the enricher should attempt. Eligible:
// backfill and post-scan enrichment passes. LIMIT supplied by caller. // cover_art_source IS NULL (never tried), OR cover_art_source = 'none'
func (q *Queries) ListAlbumsMissingCover(ctx context.Context, limit int32) ([]ListAlbumsMissingCoverRow, error) { // AND cover_art_sources_version != current (settled under an older
rows, err := q.db.Query(ctx, listAlbumsMissingCover, limit) // 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 { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []ListAlbumsMissingCoverRow var items []pgtype.UUID
for rows.Next() { for rows.Next() {
var i ListAlbumsMissingCoverRow var id pgtype.UUID
if err := rows.Scan( if err := rows.Scan(&id); err != nil {
&i.ID,
&i.Mbid,
&i.Title,
&i.ArtistID,
); err != nil {
return nil, err return nil, err
} }
items = append(items, i) items = append(items, id)
} }
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
return nil, err return nil, err
+7 -2
View File
@@ -120,7 +120,7 @@ func (q *Queries) ListLikedAlbumIDs(ctx context.Context, userID pgtype.UUID) ([]
} }
const listLikedAlbumRows = `-- name: ListLikedAlbumRows :many 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 JOIN general_likes_albums l ON l.album_id = a.id
WHERE l.user_id = $1 WHERE l.user_id = $1
ORDER BY l.liked_at DESC ORDER BY l.liked_at DESC
@@ -153,6 +153,7 @@ func (q *Queries) ListLikedAlbumRows(ctx context.Context, arg ListLikedAlbumRows
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.CoverArtSource, &i.CoverArtSource,
&i.CoverArtSourcesVersion,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -189,7 +190,7 @@ func (q *Queries) ListLikedArtistIDs(ctx context.Context, userID pgtype.UUID) ([
} }
const listLikedArtistRows = `-- name: ListLikedArtistRows :many 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 JOIN general_likes_artists l ON l.artist_id = a.id
WHERE l.user_id = $1 WHERE l.user_id = $1
ORDER BY l.liked_at DESC ORDER BY l.liked_at DESC
@@ -218,6 +219,10 @@ func (q *Queries) ListLikedArtistRows(ctx context.Context, arg ListLikedArtistRo
&i.Mbid, &i.Mbid,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.ArtistThumbPath,
&i.ArtistFanartPath,
&i.ArtistArtSource,
&i.ArtistArtSourcesVersion,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
+43 -23
View File
@@ -188,25 +188,30 @@ func (ns NullLidarrRequestStatus) Value() (driver.Value, error) {
} }
type Album struct { type Album struct {
ID pgtype.UUID ID pgtype.UUID
Title string Title string
SortTitle string SortTitle string
ArtistID pgtype.UUID ArtistID pgtype.UUID
ReleaseDate pgtype.Date ReleaseDate pgtype.Date
Mbid *string Mbid *string
CoverArtPath *string CoverArtPath *string
CreatedAt pgtype.Timestamptz CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz UpdatedAt pgtype.Timestamptz
CoverArtSource *string CoverArtSource *string
CoverArtSourcesVersion int32
} }
type Artist struct { type Artist struct {
ID pgtype.UUID ID pgtype.UUID
Name string Name string
SortName string SortName string
Mbid *string Mbid *string
CreatedAt pgtype.Timestamptz CreatedAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz UpdatedAt pgtype.Timestamptz
ArtistThumbPath *string
ArtistFanartPath *string
ArtistArtSource *string
ArtistArtSourcesVersion int32
} }
type ArtistSimilarity struct { type ArtistSimilarity struct {
@@ -236,6 +241,20 @@ type ContextualLike struct {
SessionID pgtype.UUID 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 { type GeneralLike struct {
UserID pgtype.UUID UserID pgtype.UUID
TrackID pgtype.UUID TrackID pgtype.UUID
@@ -364,13 +383,14 @@ type PlaylistTrack struct {
} }
type ScanRun struct { type ScanRun struct {
ID pgtype.UUID ID pgtype.UUID
StartedAt pgtype.Timestamptz StartedAt pgtype.Timestamptz
FinishedAt pgtype.Timestamptz FinishedAt pgtype.Timestamptz
Library []byte Library []byte
MbidBackfill []byte MbidBackfill []byte
CoverEnrich []byte CoverEnrich []byte
ErrorMessage *string ErrorMessage *string
ArtistArtEnrich []byte
} }
type ScrobbleQueue struct { type ScrobbleQueue struct {
+19 -5
View File
@@ -12,7 +12,7 @@ import (
) )
const listLastPlayedArtistsForUser = `-- name: ListLastPlayedArtistsForUser :many 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, cov.id AS cover_album_id,
cnt.album_count::bigint AS album_count, cnt.album_count::bigint AS album_count,
max_started.started_at::timestamptz AS last_played_at 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.Mbid,
&i.Artist.CreatedAt, &i.Artist.CreatedAt,
&i.Artist.UpdatedAt, &i.Artist.UpdatedAt,
&i.Artist.ArtistThumbPath,
&i.Artist.ArtistFanartPath,
&i.Artist.ArtistArtSource,
&i.Artist.ArtistArtSourcesVersion,
&i.CoverAlbumID, &i.CoverAlbumID,
&i.AlbumCount, &i.AlbumCount,
&i.LastPlayedAt, &i.LastPlayedAt,
@@ -157,7 +161,7 @@ func (q *Queries) ListMostPlayedTracksForUser(ctx context.Context, arg ListMostP
} }
const listRediscoverAlbumsFallbackForUser = `-- name: ListRediscoverAlbumsFallbackForUser :many 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 FROM general_likes_albums gla
JOIN albums ON albums.id = gla.album_id JOIN albums ON albums.id = gla.album_id
JOIN artists ON artists.id = albums.artist_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.CreatedAt,
&i.Album.UpdatedAt, &i.Album.UpdatedAt,
&i.Album.CoverArtSource, &i.Album.CoverArtSource,
&i.Album.CoverArtSourcesVersion,
&i.ArtistName, &i.ArtistName,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -226,7 +231,7 @@ WITH eligible AS (
HAVING COALESCE(max(pe.started_at), '1970-01-01'::timestamptz) HAVING COALESCE(max(pe.started_at), '1970-01-01'::timestamptz)
< now() - interval '14 days' < 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 FROM eligible e
JOIN albums ON albums.id = e.album_id JOIN albums ON albums.id = e.album_id
JOIN artists ON artists.id = albums.artist_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.CreatedAt,
&i.Album.UpdatedAt, &i.Album.UpdatedAt,
&i.Album.CoverArtSource, &i.Album.CoverArtSource,
&i.Album.CoverArtSourcesVersion,
&i.ArtistName, &i.ArtistName,
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -281,7 +287,7 @@ func (q *Queries) ListRediscoverAlbumsForUser(ctx context.Context, arg ListRedis
} }
const listRediscoverArtistsFallbackForUser = `-- name: ListRediscoverArtistsFallbackForUser :many 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, cov.id AS cover_album_id,
cnt.album_count::bigint AS album_count cnt.album_count::bigint AS album_count
FROM general_likes_artists gla FROM general_likes_artists gla
@@ -327,6 +333,10 @@ func (q *Queries) ListRediscoverArtistsFallbackForUser(ctx context.Context, arg
&i.Artist.Mbid, &i.Artist.Mbid,
&i.Artist.CreatedAt, &i.Artist.CreatedAt,
&i.Artist.UpdatedAt, &i.Artist.UpdatedAt,
&i.Artist.ArtistThumbPath,
&i.Artist.ArtistFanartPath,
&i.Artist.ArtistArtSource,
&i.Artist.ArtistArtSourcesVersion,
&i.CoverAlbumID, &i.CoverAlbumID,
&i.AlbumCount, &i.AlbumCount,
); err != nil { ); err != nil {
@@ -355,7 +365,7 @@ WITH eligible AS (
HAVING COALESCE(max(pe.started_at), '1970-01-01'::timestamptz) HAVING COALESCE(max(pe.started_at), '1970-01-01'::timestamptz)
< now() - interval '14 days' < 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, cov.id AS cover_album_id,
cnt.album_count::bigint AS album_count cnt.album_count::bigint AS album_count
FROM eligible e FROM eligible e
@@ -403,6 +413,10 @@ func (q *Queries) ListRediscoverArtistsForUser(ctx context.Context, arg ListRedi
&i.Artist.Mbid, &i.Artist.Mbid,
&i.Artist.CreatedAt, &i.Artist.CreatedAt,
&i.Artist.UpdatedAt, &i.Artist.UpdatedAt,
&i.Artist.ArtistThumbPath,
&i.Artist.ArtistFanartPath,
&i.Artist.ArtistArtSource,
&i.Artist.ArtistArtSourcesVersion,
&i.CoverAlbumID, &i.CoverAlbumID,
&i.AlbumCount, &i.AlbumCount,
); err != nil { ); err != nil {
+12 -2
View File
@@ -59,11 +59,21 @@ SELECT id, started_at, finished_at, library, mbid_backfill, cover_enrich, error_
LIMIT 1 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 // 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. // 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) row := q.db.QueryRow(ctx, getLatestScanRun)
var i ScanRun var i GetLatestScanRunRow
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.StartedAt, &i.StartedAt,
+13 -2
View File
@@ -136,15 +136,26 @@ SELECT a.id AS album_id,
-- Invariant: with_art + pending + settled = total. (pending_no_mbid is -- Invariant: with_art + pending + settled = total. (pending_no_mbid is
-- not part of the sum — it's a subset of pending, surfaced separately.) -- 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 -- with the cover_art_source CHECK constraint in migration
-- 0016_album_cover_source.up.sql. If a new source value is added there -- 0016_album_cover_source.up.sql. If a new source value is added there
-- without updating this query, with_art will silently undercount. -- without updating this query, with_art will silently undercount.
SELECT SELECT
COUNT(*) AS total, 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 IS NULL) AS pending,
COUNT(*) FILTER (WHERE cover_art_source = 'none') AS settled, 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 COUNT(*) FILTER (WHERE cover_art_source IS NULL AND mbid IS NULL) AS pending_no_mbid
FROM albums; 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;
+44 -1
View File
@@ -11,7 +11,15 @@ DO UPDATE SET
RETURNING *; RETURNING *;
-- name: GetArtistByID :one -- 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 -- name: GetArtistByName :one
SELECT * FROM artists WHERE name = $1 LIMIT 1; SELECT * FROM artists WHERE name = $1 LIMIT 1;
@@ -83,3 +91,38 @@ UPDATE artists
SET mbid = $2, SET mbid = $2,
updated_at = now() updated_at = now()
WHERE id = $1 AND mbid IS NULL; 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';
+46
View File
@@ -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;
+9 -5
View File
@@ -1,13 +1,17 @@
-- M7 #353: album cover enrichment queries. -- M7 #353: album cover enrichment queries.
-- name: ListAlbumsMissingCover :many -- name: ListAlbumsMissingCover :many
-- Albums where cover_art_source has never been set. Used by the boot -- M7 cover-sources: returns rows the enricher should attempt. Eligible:
-- backfill and post-scan enrichment passes. LIMIT supplied by caller. -- cover_art_source IS NULL (never tried), OR cover_art_source = 'none'
SELECT a.id, a.mbid, a.title, a.artist_id -- 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 FROM albums a
WHERE a.cover_art_source IS NULL WHERE a.cover_art_source IS NULL
ORDER BY a.created_at ASC OR (a.cover_art_source = 'none' AND a.cover_art_sources_version != $1)
LIMIT $1; ORDER BY a.created_at
LIMIT $2;
-- name: ListAlbumsRetryMissing :many -- name: ListAlbumsRetryMissing :many
-- Bulk-retry candidates: NULL (never tried) plus 'none' (tried, failed). -- Bulk-retry candidates: NULL (never tried) plus 'none' (tried, failed).