From 15170ed7c6446e21d1a3524668392a87b392e40f Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 4 May 2026 14:30:55 -0400 Subject: [PATCH] feat(db/m7-353): sqlc queries for album cover enrichment --- internal/db/dbq/albums.sql.go | 36 +++-- internal/db/dbq/covers.sql.go | 199 ++++++++++++++++++++++++ internal/db/dbq/likes.sql.go | 3 +- internal/db/dbq/models.go | 19 +-- internal/db/dbq/recommendation.sql.go | 6 +- internal/db/dbq/system_playlists.sql.go | 6 +- internal/db/queries/covers.sql | 56 +++++++ 7 files changed, 300 insertions(+), 25 deletions(-) create mode 100644 internal/db/dbq/covers.sql.go create mode 100644 internal/db/queries/covers.sql diff --git a/internal/db/dbq/albums.sql.go b/internal/db/dbq/albums.sql.go index 152a5842..0c72f61b 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 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 FROM albums WHERE artist_id = $1 AND title = $2 LIMIT 1 ` type GetAlbumByArtistAndTitleParams struct { @@ -94,12 +94,13 @@ func (q *Queries) GetAlbumByArtistAndTitle(ctx context.Context, arg GetAlbumByAr &i.CoverArtPath, &i.CreatedAt, &i.UpdatedAt, + &i.CoverArtSource, ) 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 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 FROM albums WHERE id = $1 ` func (q *Queries) GetAlbumByID(ctx context.Context, id pgtype.UUID) (Album, error) { @@ -115,12 +116,13 @@ func (q *Queries) GetAlbumByID(ctx context.Context, id pgtype.UUID) (Album, erro &i.CoverArtPath, &i.CreatedAt, &i.UpdatedAt, + &i.CoverArtSource, ) return i, err } 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, 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, 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 @@ -158,6 +160,7 @@ func (q *Queries) ListAlbumsAlphaByArtist(ctx context.Context, arg ListAlbumsAlp &i.Album.CoverArtPath, &i.Album.CreatedAt, &i.Album.UpdatedAt, + &i.Album.CoverArtSource, &i.ArtistSortName, ); err != nil { return nil, err @@ -171,7 +174,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 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 FROM albums ORDER BY sort_title LIMIT $1 OFFSET $2 ` type ListAlbumsAlphaByNameParams struct { @@ -198,6 +201,7 @@ func (q *Queries) ListAlbumsAlphaByName(ctx context.Context, arg ListAlbumsAlpha &i.CoverArtPath, &i.CreatedAt, &i.UpdatedAt, + &i.CoverArtSource, ); err != nil { return nil, err } @@ -210,7 +214,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, 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, artists.name AS artist_name FROM albums JOIN artists ON artists.id = albums.artist_id ORDER BY albums.sort_title, albums.id @@ -248,6 +252,7 @@ func (q *Queries) ListAlbumsAlphaWithArtist(ctx context.Context, arg ListAlbumsA &i.Album.CoverArtPath, &i.Album.CreatedAt, &i.Album.UpdatedAt, + &i.Album.CoverArtSource, &i.ArtistName, ); err != nil { return nil, err @@ -261,7 +266,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 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 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) { @@ -283,6 +288,7 @@ func (q *Queries) ListAlbumsByArtist(ctx context.Context, artistID pgtype.UUID) &i.CoverArtPath, &i.CreatedAt, &i.UpdatedAt, + &i.CoverArtSource, ); err != nil { return nil, err } @@ -295,7 +301,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 +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 FROM albums JOIN tracks ON tracks.album_id = albums.id WHERE tracks.genre = $1 @@ -329,6 +335,7 @@ func (q *Queries) ListAlbumsByGenre(ctx context.Context, arg ListAlbumsByGenrePa &i.CoverArtPath, &i.CreatedAt, &i.UpdatedAt, + &i.CoverArtSource, ); err != nil { return nil, err } @@ -341,7 +348,7 @@ func (q *Queries) ListAlbumsByGenre(ctx context.Context, arg ListAlbumsByGenrePa } const listAlbumsNewest = `-- name: ListAlbumsNewest :many -SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at 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 FROM albums ORDER BY created_at DESC LIMIT $1 OFFSET $2 ` type ListAlbumsNewestParams struct { @@ -368,6 +375,7 @@ func (q *Queries) ListAlbumsNewest(ctx context.Context, arg ListAlbumsNewestPara &i.CoverArtPath, &i.CreatedAt, &i.UpdatedAt, + &i.CoverArtSource, ); err != nil { return nil, err } @@ -380,7 +388,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 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 FROM albums ORDER BY random() LIMIT $1 ` func (q *Queries) ListAlbumsRandom(ctx context.Context, limit int32) ([]Album, error) { @@ -402,6 +410,7 @@ func (q *Queries) ListAlbumsRandom(ctx context.Context, limit int32) ([]Album, e &i.CoverArtPath, &i.CreatedAt, &i.UpdatedAt, + &i.CoverArtSource, ); err != nil { return nil, err } @@ -414,7 +423,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, 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, artists.name AS artist_name FROM albums JOIN artists ON artists.id = albums.artist_id ORDER BY albums.created_at DESC, albums.id @@ -449,6 +458,7 @@ func (q *Queries) ListRecentlyAddedAlbumsWithArtist(ctx context.Context, limit i &i.Album.CoverArtPath, &i.Album.CreatedAt, &i.Album.UpdatedAt, + &i.Album.CoverArtSource, &i.ArtistName, ); err != nil { return nil, err @@ -462,7 +472,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 FROM albums +SELECT id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source FROM albums WHERE title ILIKE '%' || $1 || '%' ORDER BY sort_title LIMIT $2 OFFSET $3 @@ -493,6 +503,7 @@ func (q *Queries) SearchAlbums(ctx context.Context, arg SearchAlbumsParams) ([]A &i.CoverArtPath, &i.CreatedAt, &i.UpdatedAt, + &i.CoverArtSource, ); err != nil { return nil, err } @@ -515,7 +526,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 +RETURNING id, title, sort_title, artist_id, release_date, mbid, cover_art_path, created_at, updated_at, cover_art_source ` type UpsertAlbumParams struct { @@ -547,6 +558,7 @@ func (q *Queries) UpsertAlbum(ctx context.Context, arg UpsertAlbumParams) (Album &i.CoverArtPath, &i.CreatedAt, &i.UpdatedAt, + &i.CoverArtSource, ) return i, err } diff --git a/internal/db/dbq/covers.sql.go b/internal/db/dbq/covers.sql.go new file mode 100644 index 00000000..f661984b --- /dev/null +++ b/internal/db/dbq/covers.sql.go @@ -0,0 +1,199 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: covers.sql + +package dbq + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const clearAlbumCover = `-- name: ClearAlbumCover :exec +UPDATE albums + SET cover_art_path = NULL, + cover_art_source = NULL + WHERE id = $1 +` + +// Used by the admin retry endpoint to wipe state before re-running the +// enricher. Sets both fields to NULL. +func (q *Queries) ClearAlbumCover(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, clearAlbumCover, id) + return err +} + +const countAlbumCoverSources = `-- name: CountAlbumCoverSources :one +SELECT + COUNT(*) FILTER (WHERE cover_art_source IS NULL) ::bigint AS untried, + COUNT(*) FILTER (WHERE cover_art_source = 'sidecar') ::bigint AS sidecar, + COUNT(*) FILTER (WHERE cover_art_source = 'embedded')::bigint AS embedded, + COUNT(*) FILTER (WHERE cover_art_source = 'mbcaa') ::bigint AS mbcaa, + COUNT(*) FILTER (WHERE cover_art_source = 'none') ::bigint AS none + FROM albums +` + +type CountAlbumCoverSourcesRow struct { + Untried int64 + Sidecar int64 + Embedded int64 + Mbcaa int64 + None int64 +} + +// Diagnostic for the admin overview: counts by source, including NULL. +// Returns one row with named tallies. +func (q *Queries) CountAlbumCoverSources(ctx context.Context) (CountAlbumCoverSourcesRow, error) { + row := q.db.QueryRow(ctx, countAlbumCoverSources) + var i CountAlbumCoverSourcesRow + err := row.Scan( + &i.Untried, + &i.Sidecar, + &i.Embedded, + &i.Mbcaa, + &i.None, + ) + return i, err +} + +const getAlbumWithFirstTrackPath = `-- name: GetAlbumWithFirstTrackPath :one +SELECT a.id, a.mbid, a.title, a.cover_art_path, a.cover_art_source, + t.file_path AS track_file_path + FROM albums a + LEFT JOIN tracks t ON t.album_id = a.id + WHERE a.id = $1 + ORDER BY t.disc_number NULLS LAST, t.track_number NULLS LAST, t.id + LIMIT 1 +` + +type GetAlbumWithFirstTrackPathRow struct { + ID pgtype.UUID + Mbid *string + Title string + CoverArtPath *string + CoverArtSource *string + TrackFilePath *string +} + +// Returns the album row and one of its track file paths (used to derive +// the album directory). Pick lowest position track for determinism. +func (q *Queries) GetAlbumWithFirstTrackPath(ctx context.Context, id pgtype.UUID) (GetAlbumWithFirstTrackPathRow, error) { + row := q.db.QueryRow(ctx, getAlbumWithFirstTrackPath, id) + var i GetAlbumWithFirstTrackPathRow + err := row.Scan( + &i.ID, + &i.Mbid, + &i.Title, + &i.CoverArtPath, + &i.CoverArtSource, + &i.TrackFilePath, + ) + return i, err +} + +const listAlbumsMissingCover = `-- name: ListAlbumsMissingCover :many + +SELECT a.id, a.mbid, a.title, a.artist_id + FROM albums a + WHERE a.cover_art_source IS NULL + ORDER BY a.created_at ASC + LIMIT $1 +` + +type ListAlbumsMissingCoverRow struct { + ID pgtype.UUID + Mbid *string + Title string + ArtistID pgtype.UUID +} + +// 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) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListAlbumsMissingCoverRow + for rows.Next() { + var i ListAlbumsMissingCoverRow + if err := rows.Scan( + &i.ID, + &i.Mbid, + &i.Title, + &i.ArtistID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listAlbumsRetryMissing = `-- name: ListAlbumsRetryMissing :many +SELECT a.id, a.mbid, a.title, a.artist_id + FROM albums a + WHERE a.cover_art_source IS NULL OR a.cover_art_source = 'none' + ORDER BY a.created_at ASC + LIMIT $1 +` + +type ListAlbumsRetryMissingRow struct { + ID pgtype.UUID + Mbid *string + Title string + ArtistID pgtype.UUID +} + +// Bulk-retry candidates: NULL (never tried) plus 'none' (tried, failed). +// Used by the admin "refetch missing covers" endpoint. LIMIT supplied. +func (q *Queries) ListAlbumsRetryMissing(ctx context.Context, limit int32) ([]ListAlbumsRetryMissingRow, error) { + rows, err := q.db.Query(ctx, listAlbumsRetryMissing, limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListAlbumsRetryMissingRow + for rows.Next() { + var i ListAlbumsRetryMissingRow + if err := rows.Scan( + &i.ID, + &i.Mbid, + &i.Title, + &i.ArtistID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const setAlbumCover = `-- name: SetAlbumCover :exec +UPDATE albums + SET cover_art_path = NULLIF($2::text, ''), + cover_art_source = $3 + WHERE id = $1 +` + +type SetAlbumCoverParams struct { + ID pgtype.UUID + Column2 string + CoverArtSource *string +} + +// Persist enrichment result. Pass cover_art_path = ” to clear (turns into NULL). +func (q *Queries) SetAlbumCover(ctx context.Context, arg SetAlbumCoverParams) error { + _, err := q.db.Exec(ctx, setAlbumCover, arg.ID, arg.Column2, arg.CoverArtSource) + return err +} diff --git a/internal/db/dbq/likes.sql.go b/internal/db/dbq/likes.sql.go index 5510829b..714a8e90 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 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 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 @@ -152,6 +152,7 @@ func (q *Queries) ListLikedAlbumRows(ctx context.Context, arg ListLikedAlbumRows &i.CoverArtPath, &i.CreatedAt, &i.UpdatedAt, + &i.CoverArtSource, ); err != nil { return nil, err } diff --git a/internal/db/dbq/models.go b/internal/db/dbq/models.go index 15ebe3a3..18d3eabc 100644 --- a/internal/db/dbq/models.go +++ b/internal/db/dbq/models.go @@ -188,15 +188,16 @@ 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 + 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 } type Artist struct { diff --git a/internal/db/dbq/recommendation.sql.go b/internal/db/dbq/recommendation.sql.go index a1ca1056..9db7c9ea 100644 --- a/internal/db/dbq/recommendation.sql.go +++ b/internal/db/dbq/recommendation.sql.go @@ -157,7 +157,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, 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, 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 @@ -198,6 +198,7 @@ func (q *Queries) ListRediscoverAlbumsFallbackForUser(ctx context.Context, arg L &i.Album.CoverArtPath, &i.Album.CreatedAt, &i.Album.UpdatedAt, + &i.Album.CoverArtSource, &i.ArtistName, ); err != nil { return nil, err @@ -225,7 +226,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, 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, artists.name AS artist_name FROM eligible e JOIN albums ON albums.id = e.album_id JOIN artists ON artists.id = albums.artist_id @@ -266,6 +267,7 @@ func (q *Queries) ListRediscoverAlbumsForUser(ctx context.Context, arg ListRedis &i.Album.CoverArtPath, &i.Album.CreatedAt, &i.Album.UpdatedAt, + &i.Album.CoverArtSource, &i.ArtistName, ); err != nil { return nil, err diff --git a/internal/db/dbq/system_playlists.sql.go b/internal/db/dbq/system_playlists.sql.go index 86088ccf..90be73f1 100644 --- a/internal/db/dbq/system_playlists.sql.go +++ b/internal/db/dbq/system_playlists.sql.go @@ -201,7 +201,11 @@ type ListPlaylistsByUserAndKindRow struct { UpdatedAt pgtype.Timestamptz } -// Powers GET /api/me/playlists?kind=. Sorted by updated_at DESC. +// Used in integration tests; production filter applied in Go +// (api.handleListPlaylists fetches via Service.List and filters by Kind in +// memory so it can also surface other users' public playlists alongside +// the caller's own kind-filtered ones). +// Sorted by updated_at DESC. func (q *Queries) ListPlaylistsByUserAndKind(ctx context.Context, arg ListPlaylistsByUserAndKindParams) ([]ListPlaylistsByUserAndKindRow, error) { rows, err := q.db.Query(ctx, listPlaylistsByUserAndKind, arg.UserID, arg.Column2) if err != nil { diff --git a/internal/db/queries/covers.sql b/internal/db/queries/covers.sql new file mode 100644 index 00000000..1fbc2ec3 --- /dev/null +++ b/internal/db/queries/covers.sql @@ -0,0 +1,56 @@ +-- 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 + FROM albums a + WHERE a.cover_art_source IS NULL + ORDER BY a.created_at ASC + LIMIT $1; + +-- name: ListAlbumsRetryMissing :many +-- Bulk-retry candidates: NULL (never tried) plus 'none' (tried, failed). +-- Used by the admin "refetch missing covers" endpoint. LIMIT supplied. +SELECT a.id, a.mbid, a.title, a.artist_id + FROM albums a + WHERE a.cover_art_source IS NULL OR a.cover_art_source = 'none' + ORDER BY a.created_at ASC + LIMIT $1; + +-- name: GetAlbumWithFirstTrackPath :one +-- Returns the album row and one of its track file paths (used to derive +-- the album directory). Pick lowest position track for determinism. +SELECT a.id, a.mbid, a.title, a.cover_art_path, a.cover_art_source, + t.file_path AS track_file_path + FROM albums a + LEFT JOIN tracks t ON t.album_id = a.id + WHERE a.id = $1 + ORDER BY t.disc_number NULLS LAST, t.track_number NULLS LAST, t.id + LIMIT 1; + +-- name: SetAlbumCover :exec +-- Persist enrichment result. Pass cover_art_path = '' to clear (turns into NULL). +UPDATE albums + SET cover_art_path = NULLIF($2::text, ''), + cover_art_source = $3 + WHERE id = $1; + +-- name: ClearAlbumCover :exec +-- Used by the admin retry endpoint to wipe state before re-running the +-- enricher. Sets both fields to NULL. +UPDATE albums + SET cover_art_path = NULL, + cover_art_source = NULL + WHERE id = $1; + +-- name: CountAlbumCoverSources :one +-- Diagnostic for the admin overview: counts by source, including NULL. +-- Returns one row with named tallies. +SELECT + COUNT(*) FILTER (WHERE cover_art_source IS NULL) ::bigint AS untried, + COUNT(*) FILTER (WHERE cover_art_source = 'sidecar') ::bigint AS sidecar, + COUNT(*) FILTER (WHERE cover_art_source = 'embedded')::bigint AS embedded, + COUNT(*) FILTER (WHERE cover_art_source = 'mbcaa') ::bigint AS mbcaa, + COUNT(*) FILTER (WHERE cover_art_source = 'none') ::bigint AS none + FROM albums;