Rewrites the internal/db/queries comments to rule 32 and regenerates dbq, whose doc comments and embedded SQL carry them. The bugs behind the tiered mixes, the md5 order seed, the score bands and the round-robin arms are restated as the constraint each one enforces; counts from one library and stale cross-references are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
246 lines
9.9 KiB
SQL
246 lines
9.9 KiB
SQL
-- name: UpsertAlbum :one
|
|
INSERT INTO albums (title, sort_title, artist_id, release_date, mbid, cover_art_path, release_group_mbid)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
ON CONFLICT (mbid) WHERE mbid IS NOT NULL
|
|
DO UPDATE SET
|
|
title = EXCLUDED.title,
|
|
sort_title = EXCLUDED.sort_title,
|
|
artist_id = EXCLUDED.artist_id,
|
|
release_date = EXCLUDED.release_date,
|
|
cover_art_path = EXCLUDED.cover_art_path,
|
|
release_group_mbid = COALESCE(EXCLUDED.release_group_mbid, albums.release_group_mbid),
|
|
updated_at = now()
|
|
RETURNING *;
|
|
|
|
-- name: GetAlbumByID :one
|
|
SELECT * FROM albums WHERE id = $1;
|
|
|
|
-- name: GetAlbumWithArtist :one
|
|
-- Combined fetch for /api/albums/{id}: returns the album row + the
|
|
-- joined artist name in a single round trip. Replaces a sequential
|
|
-- GetAlbumByID + GetArtistByID pair on the hot detail-page path.
|
|
SELECT sqlc.embed(albums), artists.name AS artist_name
|
|
FROM albums
|
|
JOIN artists ON artists.id = albums.artist_id
|
|
WHERE albums.id = $1;
|
|
|
|
-- name: ListAlbumsByArtistWithTrackCount :many
|
|
-- Replaces the N+1 pattern in handleGetArtist (1 + N CountTracksByAlbum
|
|
-- per album). Returns each album joined with its track count via a
|
|
-- correlated subquery — single round trip regardless of album count.
|
|
SELECT sqlc.embed(albums),
|
|
(SELECT count(*) FROM tracks t WHERE t.album_id = albums.id)::bigint
|
|
AS track_count
|
|
FROM albums
|
|
WHERE albums.artist_id = $1
|
|
ORDER BY release_date NULLS LAST, sort_title;
|
|
|
|
-- name: GetAlbumByArtistAndTitle :one
|
|
-- Scanner uses this for the no-mbid dedupe path: resolve-or-create. An album
|
|
-- of the same name in another release group is a different album (one
|
|
-- artist's self-titled albums), so a row whose
|
|
-- group is known and differs is not returned. A row of the file's own group
|
|
-- comes first, then one with no group yet, then the one holding the most
|
|
-- tracks from the file's folder: a file tagged with no group (a live bonus
|
|
-- track beside its album) belongs with its folder, not the oldest
|
|
-- row, and a few strays already misfiled do not outvote the folder.
|
|
SELECT albums.* FROM albums
|
|
WHERE albums.artist_id = sqlc.arg(artist_id) AND albums.title = sqlc.arg(title)
|
|
AND (sqlc.narg(release_group_mbid)::text IS NULL OR albums.release_group_mbid IS NULL
|
|
OR albums.release_group_mbid = sqlc.narg(release_group_mbid))
|
|
ORDER BY (albums.release_group_mbid IS NOT DISTINCT FROM sqlc.narg(release_group_mbid)) DESC,
|
|
(SELECT count(*) FROM tracks t
|
|
WHERE t.album_id = albums.id AND starts_with(t.file_path, sqlc.arg(dir)::text || '/')) DESC,
|
|
albums.id
|
|
LIMIT 1;
|
|
|
|
-- name: ListAlbumsByArtist :many
|
|
SELECT * FROM albums WHERE artist_id = $1 ORDER BY release_date NULLS LAST, sort_title;
|
|
|
|
-- name: ListAlbumsAlphaByName :many
|
|
SELECT * FROM albums ORDER BY sort_title LIMIT $1 OFFSET $2;
|
|
|
|
-- name: ListAlbumsAlphaByArtist :many
|
|
-- Sorted by the owning artist's sort_name. Needed by Subsonic's
|
|
-- alphabeticalByArtist album list type.
|
|
SELECT sqlc.embed(albums), 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
|
|
LIMIT $1 OFFSET $2;
|
|
|
|
-- name: ListAlbumsNewest :many
|
|
SELECT * FROM albums ORDER BY created_at DESC LIMIT $1 OFFSET $2;
|
|
|
|
-- name: ListAlbumsRandom :many
|
|
SELECT * FROM albums ORDER BY random() LIMIT $1;
|
|
|
|
-- name: ListAlbumsByGenre :many
|
|
-- Album "belongs to" a genre if any of its tracks carry that genre.
|
|
-- Serves Subsonic getAlbumList?type=byGenre.
|
|
--
|
|
-- Splits tracks.genre on [;,], so a track tagged "Rock;Pop" is reachable from
|
|
-- both "Rock" and "Pop"; comparing the whole column would hide every
|
|
-- multi-genre track from a Subsonic client asking for a genre. The same split
|
|
-- as recommendation.sql, discover.sql and the genre browse index.
|
|
--
|
|
-- EXISTS rather than JOIN + DISTINCT ON: the lateral split emits one row per
|
|
-- (track, genre-fragment), so a join would multiply rows per album and lean
|
|
-- on DISTINCT to undo it. EXISTS asks the question directly.
|
|
SELECT albums.*
|
|
FROM albums
|
|
WHERE EXISTS (
|
|
SELECT 1
|
|
FROM tracks
|
|
JOIN LATERAL regexp_split_to_table(coalesce(tracks.genre, ''), '[;,]') AS g(genre) ON true
|
|
WHERE tracks.album_id = albums.id
|
|
AND trim(g.genre) = trim(sqlc.arg(genre)::text)
|
|
)
|
|
ORDER BY albums.sort_title, albums.id
|
|
LIMIT sqlc.arg(lim) OFFSET sqlc.arg(off);
|
|
|
|
-- name: SearchAlbums :many
|
|
SELECT * FROM albums
|
|
WHERE title ILIKE '%' || $1 || '%'
|
|
ORDER BY sort_title
|
|
LIMIT $2 OFFSET $3;
|
|
|
|
-- name: CountAlbumsMatching :one
|
|
SELECT COUNT(*) FROM albums WHERE title ILIKE '%' || $1::text || '%';
|
|
|
|
-- name: ListRecentlyAddedAlbumsWithArtist :many
|
|
-- Recently-added albums joined with artist_name + artist_id for the
|
|
-- home-page section. created_at is the row-insert timestamp from the
|
|
-- scanner — the closest proxy to "added to library". Stable secondary
|
|
-- ordering by id keeps pagination tie-break deterministic.
|
|
SELECT sqlc.embed(albums), artists.name AS artist_name
|
|
FROM albums
|
|
JOIN artists ON artists.id = albums.artist_id
|
|
ORDER BY albums.created_at DESC, albums.id
|
|
LIMIT $1;
|
|
|
|
-- name: ListAlbumsAlphaWithArtist :many
|
|
-- Alpha-sorted album list joined with artist_name. Used by
|
|
-- /api/library/albums for the wrapping-grid page. Stable id-tiebreak.
|
|
SELECT sqlc.embed(albums), artists.name AS artist_name
|
|
FROM albums
|
|
JOIN artists ON artists.id = albums.artist_id
|
|
ORDER BY albums.sort_title, albums.id
|
|
LIMIT $1 OFFSET $2;
|
|
|
|
-- name: CountAlbums :one
|
|
-- Total album count for the /api/library/albums envelope.
|
|
SELECT COUNT(*) FROM albums;
|
|
|
|
-- name: CountAlbumsByArtist :one
|
|
-- Used by request-progress reporting to count how many albums have been
|
|
-- ingested for a matched artist while a request is still in flight.
|
|
SELECT COUNT(*) FROM albums WHERE artist_id = $1;
|
|
|
|
-- name: DeleteAlbumIfEmpty :one
|
|
-- Deletes the album row only when it has no remaining tracks.
|
|
-- Used after a track delete to tidy up orphaned albums. RETURNING gives
|
|
-- us the artist_id so the service can chain into the artist-empty check.
|
|
-- Returns no rows when the album still has tracks; the service treats
|
|
-- pgx.ErrNoRows as "album not orphaned, skip cascade".
|
|
DELETE FROM albums a
|
|
WHERE a.id = $1
|
|
AND NOT EXISTS (SELECT 1 FROM tracks t WHERE t.album_id = a.id)
|
|
RETURNING a.id, a.artist_id;
|
|
|
|
-- name: SetAlbumMbidIfNull :exec
|
|
-- Heal MBID on existing album rows during rescan. Idempotent —
|
|
-- only updates when the existing mbid is NULL.
|
|
UPDATE albums
|
|
SET mbid = $2,
|
|
updated_at = now()
|
|
WHERE id = $1 AND mbid IS NULL;
|
|
|
|
-- name: SetAlbumReleaseGroupMbidIfNull :exec
|
|
-- Heal the release-group id on an existing album row, from a rescan's
|
|
-- tags or from the re-acquisition sweeper's MusicBrainz lookup. Only fills a
|
|
-- NULL, so a tag never gets overwritten by a later guess or vice versa.
|
|
UPDATE albums
|
|
SET release_group_mbid = $2,
|
|
updated_at = now()
|
|
WHERE id = $1 AND release_group_mbid IS NULL;
|
|
|
|
-- name: GetReleaseGroupForReleaseMbid :one
|
|
-- The release group the library already knows for a release id, so a
|
|
-- request carrying a release id can be repaired without asking MusicBrainz.
|
|
SELECT release_group_mbid::text
|
|
FROM albums
|
|
WHERE mbid = $1 AND release_group_mbid IS NOT NULL
|
|
LIMIT 1;
|
|
|
|
-- name: SetReleaseGroupForReleaseMbidIfNull :exec
|
|
-- Cache a MusicBrainz-resolved release group onto the album row that
|
|
-- holds the release, filling only a NULL.
|
|
UPDATE albums
|
|
SET release_group_mbid = sqlc.arg(release_group_mbid)::text,
|
|
updated_at = now()
|
|
WHERE mbid = sqlc.arg(release_mbid)::text AND release_group_mbid IS NULL;
|
|
|
|
-- name: ListAlbumsMissingMbidWithTrack :many
|
|
-- One-shot MBID backfill: returns each album where mbid IS NULL alongside
|
|
-- one of its tracks' file_path so the worker can re-read tags. LIMIT
|
|
-- supplied by caller for batching/progress purposes. The track read is
|
|
-- one still on disk; an album with none left is skipped until a scan
|
|
-- finds its files again.
|
|
SELECT a.id AS album_id,
|
|
a.artist_id AS artist_id,
|
|
a.title AS title,
|
|
t.file_path AS track_file_path
|
|
FROM albums a
|
|
JOIN LATERAL (
|
|
SELECT file_path
|
|
FROM tracks
|
|
WHERE album_id = a.id
|
|
AND missing_since IS NULL
|
|
ORDER BY disc_number NULLS LAST, track_number NULLS LAST, id
|
|
LIMIT 1
|
|
) t ON true
|
|
WHERE a.mbid IS NULL
|
|
ORDER BY a.created_at ASC
|
|
LIMIT $1;
|
|
|
|
-- name: GetAlbumCoverageRollup :one
|
|
-- Library-wide cover-art coverage rollup for the
|
|
-- admin gauge. Single sequential scan with FILTER aggregates —
|
|
-- sub-millisecond on realistic libraries. pending_no_mbid is a SUBSET
|
|
-- of pending; the UI surfaces it separately so operators can see how
|
|
-- many "pending" albums are blocked on missing MBID and won't be
|
|
-- helped by another scan.
|
|
--
|
|
-- 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 must name every source the enrichers write (the
|
|
-- cover_art_source CHECK accepts any non-empty string since migration
|
|
-- 0030). A source missing here makes with_art silently undercount.
|
|
SELECT
|
|
COUNT(*) AS total,
|
|
COUNT(*) FILTER (WHERE cover_art_source IN
|
|
('sidecar','embedded','mbcaa','theaudiodb','deezer','lastfm')) 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
|
|
-- 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;
|
|
|
|
|
|
-- name: GetAlbumsByIDs :many
|
|
-- Batched lookup used by /api/library/sync to hydrate upsert payloads
|
|
--. Mirror of GetArtistsByIDs.
|
|
SELECT * FROM albums WHERE id = ANY($1::uuid[]);
|