feat(database): shape the library like files, and shrink the catalog
CI / check (push) Successful in 3m7s
CI / e2e (push) Canceled after 1m45s

Plans 013 and 014, the album page that prompted them, and the smaller
fixes they turned up. Changelog, largest first.

## The local library is shaped like files, not like MusicBrainz

`audio_files` carries its own tags and points at `albums` and
`artists`; `file_genres` is the one real many-to-many. `recordings`,
`release_group_recordings`, `artist_credit`, `artist_credit_artist`,
`recording_genres`, `release_groups` and `release_to_rg` are gone from
the local side, and with them a six-way join in every read, a
`MIN(release_group_id)` subquery in eleven queries and a
first-credited-artist subquery in nine. Measured on a real 25,966-file
library, every many-to-many that model expressed was 1:1 in the data.

- Ownership is a file. `GetFilePathsByRecordingMBIDs`,
  `LibraryMBIDIndex.CheckMBIDs`, `collectLibraryEntities` and
  `pruneStaleLocalCrossReferences` all join `audio_files`, so the 812
  orphaned recordings, 216 release groups and 260 artists that library
  carried are now structurally impossible.
- One projection: every track query selects from the `track_metadata`
  view, one row type, one mapper. Nine hand-rolled copies had drifted
  far enough to report different years on different screens.
- `library_id = 0` means every library, so each list query exists once
  instead of scoped and unscoped with a branch at every call site.
- No migration chain. `sql/schemas/` is the one description of the
  shape; `sql/migrations/`, `applyMigrations` and `schema_migrations`
  are squashed away, along with the drift between them that had sqlc
  generating against a stale schema.
- `database.InsertTestTrack` is the one test seeder; twenty test files
  had been assembling the old FK chain each in its own order.

## The catalog stores its ids as bytes

`explore_index`'s three 36-char MBID columns and its entity-type text
are 16 raw bytes and a small integer. The table and its six indexes go
780 MB to 405 MB on a real 2,052,200-row catalog, which is why a fresh
install is ~0.6 GB rather than ~1.0 GB.

- `backend/explore/mbid.go` is the only place the encoding is known;
  everything above it speaks dashed strings.
- `CHECK(length(mbid) = 16)` makes a stringly write fail at the insert
  rather than silently returning no rows, since SQLite does not coerce
  between TEXT and BLOB.
- The importer asks the artifact what encoding it carries and converts
  on the way in, so the artifact already published keeps working and no
  format bump is needed.
- `indexRowColumns`/`scanIndexRow` replace four copies of a 22-column
  list, and `TestStoredEncodingRoundTrips` sweeps every read path.

## An album page that says how much of the album is yours

- One question, asked once: is there a file. `filePaths` is filled by a
  single batched lookup when the tracklist settles, and the badge, the
  Play count, the dimmed rows and every menu item read it — replacing
  four claims of decreasing confidence that could show a green tick on
  an album whose every action did nothing.
- Play, Play 7 of 12, or no play button at all.
- `total_tracks` on `explore_index` (~2 bytes over 400,677 release
  groups) and on `audio_files` from tags that have always carried it:
  a complete MBID-matched album now makes no catalog call at all, where
  it used to spend the most expensive request the app makes.
- A merged cluster shows the running order the most releases agree on,
  and the version list marks the release you own rather than standing a
  synthetic entry in for it.
- `AlbumReleasesFailed`: a slow fetch is no longer reported as a failed
  one by a 12-second timer.
- Rows not in the library are dimmed in place (with `aria-disabled`)
  instead of the owned ones wearing a green tick and a legend.

## Caches and cover art get ceilings

- Only the three tiers of a cover are stored; the full-resolution copy
  nothing rendered was 1,134 MB of a 1.4 GB covers directory.
- One artist portrait is downloaded and the rest are remembered as
  URLs — 4.1 GB of a 5.3 GB cache was candidates no code path reads.
- `browsedArtBudget` and `httpCacheBudget` bound what an age cannot:
  the same install held art for 5,770 artists in a 1,301-artist
  library.
- `OrphanedArtistImagesJob` joined a bare MBID onto a sharded
  directory, so it deleted the rows that were the only record of the
  files it left behind. `explore.ArtistImageDir` is that layout's one
  definition now.

## The autotag queue asks whether there is work

`tagging_items` was a row per album folder, not a queue, and no query
read the `tag_status` column that held the answer. The four queue
queries ask the files, which matters most where it is least visible:
`startPrefetch` was scoring every album in a tagged library against
MusicBrainz.

## Phantom playlist tracks resolve in place

An M3U8 imported before its files leaves phantom rows; they now match
by path and fall back to position, keep their place in the playlist
when resolved, and pair best-first so two phantoms cannot claim the
same file.

## Playing a track plays the list it is in

Double-click, and Play on a single row's menu, queue the list as
displayed with `startIndex` on that row — the album page and the track
list used to queue one track and discard the album around it. A
multi-row selection still plays exactly itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfVYUVExXsx1nSWrXN8mAh
This commit is contained in:
2026-08-16 13:58:15 -04:00
co-authored by Claude Opus 5
parent 1128881e8d
commit e7748f1fd5
208 changed files with 10944 additions and 12104 deletions
@@ -1,10 +0,0 @@
-- Adds SplitMixedFolder's synthetic-group bookkeeping to an
-- existing tagging_items table. A fresh database never runs this
-- file: sql/schemas/tagging_items.sql already declares these
-- columns, so applySchema's isFreshDatabase check stamps this
-- version as applied without executing it.
ALTER TABLE tagging_items ADD COLUMN synthetic INTEGER NOT NULL DEFAULT 0;
ALTER TABLE tagging_items ADD COLUMN parent_group_key TEXT NOT NULL DEFAULT '';
CREATE INDEX IF NOT EXISTS idx_tagging_items_parent_group_key
ON tagging_items(parent_group_key) WHERE parent_group_key != '';
@@ -1,24 +0,0 @@
-- Repairs tagging_items rows left behind by a library-scan bug: the
-- rescan's orphan-cleanup phase deleted audio_files rows for files
-- removed from disk without decrementing/clearing their tagging
-- group, so a folder whose contents were fully replaced kept a
-- phantom entry (stale track_count, no matching audio_files) in the
-- autotag queue forever. The library scan code no longer has this
-- gap, but a database written before the fix still carries the
-- damage — this is a one-time repair, not ongoing bookkeeping.
--
-- Drop groups with no audio_files left at all.
DELETE FROM tagging_items
WHERE group_key NOT IN (
SELECT DISTINCT group_key FROM audio_files WHERE group_key != ''
);
-- Reconcile track_count for groups that are still alive but drifted
-- (some, not all, of their tracks were removed without decrementing).
UPDATE tagging_items
SET track_count = (
SELECT COUNT(*) FROM audio_files WHERE audio_files.group_key = tagging_items.group_key
)
WHERE track_count != (
SELECT COUNT(*) FROM audio_files WHERE audio_files.group_key = tagging_items.group_key
);
@@ -1 +0,0 @@
ALTER TABLE tagging_items ADD COLUMN album_artist_conflict INTEGER NOT NULL DEFAULT 0;
@@ -1,3 +0,0 @@
ALTER TABLE queue ADD COLUMN source_type TEXT NOT NULL DEFAULT '';
ALTER TABLE queue ADD COLUMN source_id INTEGER NOT NULL DEFAULT 0;
ALTER TABLE queue ADD COLUMN source_label TEXT NOT NULL DEFAULT '';
@@ -1 +0,0 @@
ALTER TABLE release_groups ADD COLUMN pending_release_mbid TEXT;
@@ -1 +0,0 @@
ALTER TABLE release_group_recordings ADD COLUMN total_tracks INTEGER;
+137
View File
@@ -0,0 +1,137 @@
-- Queries over albums (formerly release_groups).
--
-- The two-copy pattern is gone here too: one query answers both the
-- whole-library and the single-library case. The `fallback_ac`
-- subquery every album read used to carry -- "if the album has no album
-- artist credit, borrow one from any of its recordings" -- is gone with
-- it, because the album carries its own credit text now.
-- name: UpsertAlbum :one
INSERT INTO albums (name, artist_credit, artist_id, year, cover_art_id)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(name, artist_credit) DO UPDATE SET
artist_id = COALESCE(excluded.artist_id, albums.artist_id),
year = COALESCE(excluded.year, albums.year),
cover_art_id = COALESCE(excluded.cover_art_id, albums.cover_art_id)
RETURNING *;
-- name: GetAlbum :one
SELECT * FROM albums WHERE id = ? LIMIT 1;
-- name: SetAlbumMBID :exec
UPDATE albums SET mbid = ? WHERE id = ?;
-- name: SetAlbumOriginalYear :exec
UPDATE albums SET original_year = ? WHERE id = ?;
-- name: SetAlbumCoverArt :exec
UPDATE albums SET cover_art_id = ? WHERE id = ?;
-- name: SetAlbumPendingReleaseMBID :exec
UPDATE albums SET pending_release_mbid = ? WHERE id = ?;
-- name: ResolveAlbumPendingReleaseMBID :exec
-- Clears the pending marker once the release-group MBID it stood in for
-- has been resolved. Guarded so a real MBID is never overwritten.
UPDATE albums
SET mbid = ?, pending_release_mbid = NULL
WHERE id = ? AND (mbid IS NULL OR mbid = '');
-- name: GetAlbumsWithPendingReleaseMBID :many
SELECT id, pending_release_mbid FROM albums
WHERE pending_release_mbid IS NOT NULL AND pending_release_mbid != ''
AND (mbid IS NULL OR mbid = '');
-- name: DeleteAlbum :exec
DELETE FROM albums WHERE id = ?;
-- name: DeleteAllAlbums :exec
DELETE FROM albums;
-- name: GetEmptyAlbumIDs :many
-- Albums with no file left behind them. Under the old schema this was
-- one of three orphan sweeps that had to run by hand and did not;
-- audio_files is the only thing that can leave an album empty now, so
-- this is the whole of it.
SELECT id FROM albums al
WHERE NOT EXISTS (
SELECT 1 FROM audio_files af WHERE af.album_id = al.id
);
-- name: GetAlbums :many
SELECT
al.id,
al.name,
COALESCE(al.original_year, al.year) AS year,
COALESCE(al.year, 0) AS release_year,
al.mbid,
al.artist_credit AS artist_name,
CAST(COALESCE(ar.mbid, '') AS TEXT) AS artist_mbid,
COALESCE(ca.file_path, '') AS cover_art_path
FROM albums al
LEFT JOIN artists ar ON ar.id = al.artist_id
LEFT JOIN cover_art ca ON ca.id = al.cover_art_id
WHERE EXISTS (
SELECT 1 FROM audio_files af
WHERE af.album_id = al.id
AND af.library_id = COALESCE(NULLIF(CAST(sqlc.arg(library_id) AS INTEGER), 0), af.library_id)
)
ORDER BY al.name;
-- name: GetAlbumsByArtistName :many
SELECT
al.id,
al.name,
COALESCE(al.original_year, al.year) AS year,
COALESCE(al.year, 0) AS release_year,
al.mbid,
al.artist_credit AS artist_name,
CAST(COALESCE(ar.mbid, '') AS TEXT) AS artist_mbid,
COALESCE(ca.file_path, '') AS cover_art_path
FROM albums al
LEFT JOIN artists ar ON ar.id = al.artist_id
LEFT JOIN cover_art ca ON ca.id = al.cover_art_id
WHERE (al.artist_credit = sqlc.arg(artist) OR ar.name = sqlc.arg(artist))
AND EXISTS (
SELECT 1 FROM audio_files af
WHERE af.album_id = al.id
AND af.library_id = COALESCE(NULLIF(CAST(sqlc.arg(library_id) AS INTEGER), 0), af.library_id)
)
ORDER BY year, al.name;
-- name: GetAlbumCompleteness :one
-- "Do I have all of this album", answered from the tags on disk.
--
-- The expectation is a **sum over discs**, not one number: totals are
-- declared per disc ("5/12" on disc 2 means 12 tracks on disc 2), so a
-- multi-disc album's expectation is the sum of each disc's declared
-- total. A disc whose files declared nothing leaves the whole album
-- unknowable rather than being covered by the discs that did -- which is
-- what `known` reports.
--
-- Owned counts DISTINCT track numbers: this app detects duplicates, and
-- counting two files of track 3 twice would report a short album as
-- complete.
SELECT
-- Distinct (disc, track) pairs: this app detects duplicates, and
-- counting two files of track 3 twice would report a short album as
-- complete. A file with no track number falls back to its own id,
-- because three untagged files are three tracks, not one.
CAST(COUNT(DISTINCT CAST(COALESCE(a.disc_number, 1) AS TEXT) || ':' ||
COALESCE(CAST(a.track_number AS TEXT), 'f' || a.id)
) AS INTEGER) AS owned,
CAST(COALESCE((
SELECT SUM(per_disc.total)
FROM (
SELECT MAX(b.total_tracks) AS total
FROM audio_files b
WHERE b.album_id = sqlc.arg(album_id) AND b.total_tracks IS NOT NULL
GROUP BY COALESCE(b.disc_number, 1)
) per_disc
), 0) AS INTEGER) AS expected,
CAST((
SELECT COUNT(*) = 0 FROM audio_files c
WHERE c.album_id = sqlc.arg(album_id) AND c.total_tracks IS NULL
) AS INTEGER) AS known
FROM audio_files a
WHERE a.album_id = sqlc.arg(album_id);
@@ -1,42 +0,0 @@
-- name: CreateArtistCredit :one
INSERT INTO artist_credit (text) VALUES (?)
RETURNING *;
-- name: GetArtistCredit :one
SELECT * FROM artist_credit
WHERE id = ? LIMIT 1;
-- name: GetArtistCreditByText :one
SELECT * FROM artist_credit
WHERE text = ? LIMIT 1;
-- name: UpsertArtistCredit :one
INSERT INTO artist_credit (text) VALUES (?)
ON CONFLICT(text) DO UPDATE SET text = excluded.text
RETURNING *;
-- name: UpdateArtistCredit :exec
UPDATE artist_credit
SET text = ?
WHERE id = ?;
-- name: DeleteArtistCredit :exec
DELETE FROM artist_credit
WHERE id = ?;
-- name: DeleteAllArtistCredits :exec
DELETE FROM artist_credit;
-- name: CountArtistCreditReferences :one
SELECT
(SELECT COUNT(*) FROM recordings WHERE artist_credit_id = ?1) +
(SELECT COUNT(*) FROM release_groups WHERE album_artist_credit_id = ?1)
AS total;
-- name: GetOrphanedArtistCreditIDs :many
-- Artist credits no longer used by any recording or release group - run
-- after orphaned recordings/release groups are deleted, so a credit
-- that only existed for now-removed tracks is cleaned up too.
SELECT ac.id FROM artist_credit ac
WHERE NOT EXISTS (SELECT 1 FROM recordings r WHERE r.artist_credit_id = ac.id)
AND NOT EXISTS (SELECT 1 FROM release_groups rg WHERE rg.album_artist_credit_id = ac.id);
@@ -1,24 +0,0 @@
-- name: CreateArtistCreditArtist :one
INSERT INTO artist_credit_artist (artist_id, credit_id) VALUES (?, ?)
RETURNING *;
-- name: GetArtistCreditArtist :one
SELECT * FROM artist_credit_artist
WHERE id = ? LIMIT 1;
-- name: UpdateArtistCreditArtist :exec
UPDATE artist_credit_artist
SET artist_id = ?, credit_id = ?
WHERE id =?;
-- name: DeleteArtistCreditArtist :exec
DELETE FROM artist_credit_artist
WHERE id =?;
-- name: DeleteAllArtistCreditArtists :exec
DELETE FROM artist_credit_artist;
-- name: DeleteArtistCreditArtistByCredit :exec
DELETE FROM artist_credit_artist
WHERE credit_id = ?;
+36 -48
View File
@@ -1,67 +1,55 @@
-- name: CreateArtist :one
INSERT INTO artists (name) VALUES (?)
-- Queries over artists.
--
-- An artist row is reachable two ways: as a file's primary artist
-- (audio_files.artist_id) and as an album's artist (albums.artist_id).
-- Both used to route through artist_credit + artist_credit_artist,
-- which is how "which album artists are in library 2" came to be a
-- five-join subquery inside a three-join query.
-- name: UpsertArtist :one
INSERT INTO artists (name, mbid) VALUES (?, ?)
ON CONFLICT(name) DO UPDATE SET
mbid = COALESCE(excluded.mbid, artists.mbid)
RETURNING *;
-- name: GetArtist :one
SELECT * FROM artists
WHERE id = ? LIMIT 1;
SELECT * FROM artists WHERE id = ? LIMIT 1;
-- name: GetArtistByName :one
SELECT * FROM artists
WHERE name = ? LIMIT 1;
SELECT * FROM artists WHERE name = ? LIMIT 1;
-- name: UpsertArtist :one
INSERT INTO artists (name) VALUES (?)
ON CONFLICT(name) DO UPDATE SET name = excluded.name
RETURNING *;
-- name: UpdateArtist :exec
UPDATE artists
SET name = ?
WHERE id = ?;
-- name: SetArtistMBID :exec
UPDATE artists SET mbid = ? WHERE id = ?;
-- name: DeleteArtist :exec
DELETE FROM artists
WHERE id = ?;
DELETE FROM artists WHERE id = ?;
-- name: DeleteAllArtists :exec
DELETE FROM artists;
-- name: GetUnreferencedArtistIDs :many
-- Artists no file and no album points at any more.
SELECT id FROM artists a
WHERE NOT EXISTS (SELECT 1 FROM audio_files af WHERE af.artist_id = a.id)
AND NOT EXISTS (SELECT 1 FROM albums al WHERE al.artist_id = a.id);
-- name: GetAllArtists :many
SELECT * FROM artists
ORDER BY name;
SELECT * FROM artists ORDER BY name;
-- name: GetAlbumArtists :many
SELECT DISTINCT a.id, a.name, a.mbid
FROM artists a
JOIN artist_credit_artist aca ON aca.artist_id = a.id
JOIN artist_credit ac ON ac.id = aca.credit_id
JOIN release_groups rg ON rg.album_artist_credit_id = ac.id
ORDER BY a.name;
-- name: GetOrphanedArtistIDs :many
-- Artists no longer credited on any recording or release group - left
-- behind when a scan's orphan cleanup removes the audio_files that used
-- to justify them, since deleting an audio_files row doesn't cascade.
SELECT a.id FROM artists a
WHERE NOT EXISTS (
SELECT 1 FROM artist_credit_artist aca WHERE aca.artist_id = a.id
);
-- name: GetAlbumArtistsByLibrary :many
SELECT DISTINCT a.id, a.name, a.mbid
FROM artists a
JOIN artist_credit_artist aca ON aca.artist_id = a.id
JOIN artist_credit ac ON ac.id = aca.credit_id
JOIN release_groups rg ON rg.album_artist_credit_id = ac.id
WHERE a.id IN (
SELECT DISTINCT aca2.artist_id
FROM artist_credit_artist aca2
JOIN artist_credit ac2 ON ac2.id = aca2.credit_id
JOIN release_groups rg2 ON rg2.album_artist_credit_id = ac2.id
JOIN release_group_recordings rgr2 ON rgr2.release_group_id = rg2.id
JOIN recordings r2 ON r2.id = rgr2.recording_id
JOIN audio_files af2 ON af2.recording_id = r2.id
WHERE af2.library_id = ?
JOIN albums al ON al.artist_id = a.id
WHERE EXISTS (
SELECT 1 FROM audio_files af
WHERE af.album_id = al.id
AND af.library_id = COALESCE(NULLIF(CAST(sqlc.arg(library_id) AS INTEGER), 0), af.library_id)
)
ORDER BY a.name;
-- name: GetArtistByFilePath :one
SELECT COALESCE(a.name, '') AS artist_name, COALESCE(a.mbid, '') AS artist_mbid
FROM audio_files af
LEFT JOIN artists a ON a.id = af.artist_id
WHERE af.file_path = ?
LIMIT 1;
+170 -310
View File
@@ -1,39 +1,60 @@
-- Queries over audio_files and the track_metadata view above it.
--
-- Every query that returns "a track" selects from `track_metadata`,
-- which is the one place the projection is defined. The scoped and
-- unscoped variants that used to be written twice are one query now:
-- library_id 0 means "every library", and `(:id = 0 OR library_id = :id)`
-- costs nothing measurable (23 ms vs 21 ms over 26k rows) because these
-- queries scan either way.
-- ---------------------------------------------------------------------
-- Writes
-- ---------------------------------------------------------------------
-- name: CreateAudioFile :one
INSERT INTO audio_files (file_path, length_milliseconds, file_type_id, recording_id, sample_rate, bit_depth, channels, bitrate, file_size, basename, library_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING *;
-- name: CreateAudioFileWithGroupKey :one
INSERT INTO audio_files (
file_path, length_milliseconds, file_type_id, recording_id,
sample_rate, bit_depth, channels, bitrate, file_size, basename,
library_id, group_key, tag_status, modified_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
file_path, library_id, file_type_id,
length_milliseconds, sample_rate, bit_depth, channels, bitrate, file_size,
title, artist_credit, artist_id, album_id,
track_number, disc_number, total_tracks, year, composer, comment,
recording_mbid, basename, group_key, modified_at, tag_status
) VALUES (
?, ?, ?,
?, ?, ?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?
)
RETURNING *;
-- name: GetAudioFileGroupKey :one
SELECT group_key FROM audio_files
WHERE id = ? LIMIT 1;
-- name: UpdateAudioFileTags :exec
-- A rescan of a file whose mtime moved: the tags are re-read and
-- written over the same row. Under the old schema this created a
-- *new* recording and repointed the file at it, abandoning the old one
-- -- which is where 812 orphaned rows and every phantom "you own this"
-- came from. There is nothing to orphan now.
UPDATE audio_files
SET title = ?, artist_credit = ?, artist_id = ?, album_id = ?,
track_number = ?, disc_number = ?, total_tracks = ?, year = ?,
composer = ?, comment = ?, recording_mbid = ?,
sample_rate = ?, bit_depth = ?, channels = ?, bitrate = ?,
file_size = ?, length_milliseconds = ?, modified_at = ?
WHERE id = ?;
-- name: SetAudioFileGroupKey :exec
UPDATE audio_files SET group_key = ? WHERE id = ?;
-- name: GetAudioFile :one
SELECT * FROM audio_files
WHERE id = ? LIMIT 1;
-- name: GetAudioFileByPath :one
SELECT * FROM audio_files
WHERE file_path = ? LIMIT 1;
-- name: UpdateAudioFile :exec
UPDATE audio_files
SET file_path = ?, length_milliseconds = ?, file_type_id = ?, recording_id = ?, sample_rate = ?, bit_depth = ?, channels = ?, bitrate = ?, file_size = ?, basename = ?
WHERE id = ?;
-- name: UpdateAudioFileRecording :exec
-- name: PromoteAudioFileTagStatusIfUntagged :exec
-- A rescan re-reads the tags of a file whose mtime moved, so a file
-- another tagger stamped with MBIDs since import arrives here still
-- carrying the 'untagged' status it was created with (only the insert
-- path sets it). Promote it the same way saveAudioFile does.
-- Guarded on 'untagged' so it cannot overwrite a deliberate
-- 'user_skipped_permanent', and so a file losing its MBIDs is left
-- alone -- demotion is the scan's judgement, not this statement's.
UPDATE audio_files
SET recording_id = ?, sample_rate = ?, bit_depth = ?, channels = ?, bitrate = ?, file_size = ?, length_milliseconds = ?, modified_at = ?
WHERE id = ?;
SET tag_status = 'user_confirmed'
WHERE id = ? AND tag_status = 'untagged';
-- name: UpdateAudioFileStat :exec
-- Records the on-disk mtime/size without re-reading tags. Used to
@@ -43,307 +64,146 @@ UPDATE audio_files
SET modified_at = ?, file_size = ?
WHERE id = ?;
-- name: SetAudioFileRecordingMBID :exec
UPDATE audio_files SET recording_mbid = ? WHERE id = ?;
-- name: DeleteAudioFile :exec
DELETE FROM audio_files WHERE id = ?;
-- name: DeleteAllAudioFiles :exec
DELETE FROM audio_files;
-- ---------------------------------------------------------------------
-- Reads: the file row itself
-- ---------------------------------------------------------------------
-- name: GetAudioFile :one
SELECT * FROM audio_files WHERE id = ? LIMIT 1;
-- name: GetAudioFileByPath :one
SELECT * FROM audio_files WHERE file_path = ? LIMIT 1;
-- name: GetAudioFileGroupKey :one
SELECT group_key FROM audio_files WHERE id = ? LIMIT 1;
-- name: GetAllAudioFilePaths :many
SELECT id, file_path FROM audio_files;
-- name: GetAudioFilesByPaths :many
SELECT id, library_id, file_path, group_key FROM audio_files
WHERE file_path IN (sqlc.slice('paths'));
-- name: GetRandomAudioFilePath :one
SELECT file_path FROM audio_files ORDER BY RANDOM() LIMIT 1;
-- name: CountAudioFiles :one
SELECT COUNT(*) AS count FROM audio_files
WHERE library_id = COALESCE(NULLIF(CAST(sqlc.arg(library_id) AS INTEGER), 0), library_id);
-- name: GetLibraryMaxModifiedAt :one
-- Newest recorded mtime in a library, for the startup soft scan. 0 when
-- the library is empty or no row has a baseline yet.
SELECT CAST(COALESCE(MAX(modified_at), 0) AS INTEGER) FROM audio_files
WHERE library_id = ?;
-- name: DeleteAudioFile :exec
DELETE FROM audio_files
WHERE id = ?;
-- ---------------------------------------------------------------------
-- Reads: tracks
-- ---------------------------------------------------------------------
-- name: CountAudioFiles :one
SELECT count(*) FROM audio_files;
-- name: GetTracks :many
SELECT * FROM track_metadata
WHERE library_id = COALESCE(NULLIF(CAST(sqlc.arg(library_id) AS INTEGER), 0), library_id);
-- name: GetRandomAudioFilePath :one
SELECT file_path FROM audio_files
ORDER BY RANDOM()
LIMIT 1;
-- name: GetTrackByPath :one
SELECT * FROM track_metadata WHERE file_path = ? LIMIT 1;
-- name: GetAllAudioFiles :many
SELECT * FROM audio_files;
-- name: GetTracksByAlbum :many
SELECT * FROM track_metadata
WHERE album_id = sqlc.arg(album_id)
AND library_id = COALESCE(NULLIF(CAST(sqlc.arg(library_id) AS INTEGER), 0), library_id)
ORDER BY disc_number, track_number;
-- name: GetAllAudioFilePaths :many
SELECT id, file_path FROM audio_files;
-- name: GetAudioFilesNeedingMetadata :many
SELECT * FROM audio_files
WHERE recording_id = 0;
-- name: GetAllAudioFilesWithArtist :many
SELECT
af.id,
af.file_path,
af.length_milliseconds,
af.file_type_id,
af.recording_id,
COALESCE(ac.text, '') AS artist_name,
COALESCE(r.name, '') AS title
FROM audio_files af
JOIN recordings r ON af.recording_id = r.id
JOIN artist_credit ac ON r.artist_credit_id = ac.id;
-- name: GetTrackMetadataByPath :one
SELECT
af.file_path,
af.length_milliseconds,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist,
COALESCE(rg.name, '') AS album,
COALESCE(ca.file_path, '') AS cover_art_path,
COALESCE(a.mbid, '') AS artist_mbid,
COALESCE(rg.mbid, '') AS release_group_mbid,
COALESCE(r.mbid, '') AS recording_mbid
FROM audio_files af
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN artist_credit_artist aca ON aca.credit_id = ac.id
LEFT JOIN artists a ON a.id = aca.artist_id
LEFT JOIN release_group_recordings rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
WHERE af.file_path = ?
LIMIT 1;
-- name: GetAllTracksWithFullMetadata :many
SELECT
af.file_path,
af.length_milliseconds,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist_name,
r.track_number,
r.disc_number,
COALESCE(rg.name, '') AS album,
CAST(COALESCE(
(SELECT GROUP_CONCAT(g.name, '||')
FROM recording_genres rg_sub
JOIN genres g ON rg_sub.genre_id = g.id
WHERE rg_sub.recording_id = r.id),
''
) AS TEXT) AS genre,
COALESCE(r.year, 0) AS year,
COALESCE(r.composer, '') AS composer,
COALESCE(ft.extension, '') AS file_type,
af.sample_rate,
af.bit_depth,
af.channels,
af.bitrate,
af.file_size,
af.play_count,
af.last_played,
COALESCE(ca.file_path, '') AS cover_art_path,
COALESCE(a.mbid, '') AS artist_mbid,
COALESCE(rg.mbid, '') AS release_group_mbid,
COALESCE(r.mbid, '') AS recording_mbid
FROM audio_files af
JOIN recordings r ON af.recording_id = r.id
JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN artist_credit_artist aca ON aca.credit_id = ac.id
LEFT JOIN artists a ON a.id = aca.artist_id
LEFT JOIN release_group_recordings rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
LEFT JOIN file_types ft ON af.file_type_id = ft.id;
-- name: SearchAudioFilesByBasename :many
SELECT
af.file_path,
af.length_milliseconds,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist,
COALESCE(rg.name, '') AS album
FROM audio_files af
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN (
SELECT recording_id, MIN(release_group_id) AS release_group_id
FROM release_group_recordings
GROUP BY recording_id
) rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
WHERE af.basename = ?
LIMIT ?;
-- name: GetTracksByGenre :many
SELECT tm.* FROM track_metadata tm
JOIN file_genres fg ON fg.audio_file_id = tm.id
JOIN genres g ON g.id = fg.genre_id
WHERE g.name = sqlc.arg(genre)
AND tm.library_id = COALESCE(NULLIF(CAST(sqlc.arg(library_id) AS INTEGER), 0), tm.library_id);
-- name: LookupTrackMetaByPaths :many
SELECT id, file_path, title, artist_name, album, cover_art_path, artist_mbid, release_group_mbid, recording_mbid
SELECT id, file_path, title, artist_name, album, cover_art_path,
artist_mbid, release_group_mbid, recording_mbid
FROM track_metadata
WHERE file_path IN (sqlc.slice('paths'));
-- name: GetAudioFilesByLibrary :many
SELECT * FROM audio_files WHERE library_id = ?;
-- name: SearchTracksByBasename :many
SELECT id, file_path, length_milliseconds, title, artist_name, album
FROM track_metadata
WHERE file_path IN (
SELECT file_path FROM audio_files WHERE basename = sqlc.arg(basename)
)
LIMIT sqlc.arg(lim);
-- name: CountAudioFilesByLibrary :one
SELECT COUNT(*) AS count FROM audio_files WHERE library_id = ?;
-- ---------------------------------------------------------------------
-- Reads: file paths, grouped by whatever the caller asked about
-- ---------------------------------------------------------------------
-- These answer "what can I play" and they all ask audio_files, because
-- that is the only table whose rows are files. Grouped rather than
-- flattened because the caller owns the order.
-- name: DeleteAllAudioFiles :exec
DELETE FROM audio_files;
-- name: GetAllTracksWithFullMetadataByLibrary :many
SELECT
af.file_path,
af.length_milliseconds,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist_name,
r.track_number,
r.disc_number,
COALESCE(rg.name, '') AS album,
CAST(COALESCE(
(SELECT GROUP_CONCAT(g.name, '||')
FROM recording_genres rg_sub
JOIN genres g ON rg_sub.genre_id = g.id
WHERE rg_sub.recording_id = r.id),
''
) AS TEXT) AS genre,
COALESCE(r.year, 0) AS year,
COALESCE(r.composer, '') AS composer,
COALESCE(ft.extension, '') AS file_type,
af.sample_rate,
af.bit_depth,
af.channels,
af.bitrate,
af.file_size,
af.play_count,
af.last_played,
COALESCE(ca.file_path, '') AS cover_art_path,
COALESCE(a.mbid, '') AS artist_mbid,
COALESCE(rg.mbid, '') AS release_group_mbid,
COALESCE(r.mbid, '') AS recording_mbid
FROM audio_files af
JOIN recordings r ON af.recording_id = r.id
JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN artist_credit_artist aca ON aca.credit_id = ac.id
LEFT JOIN artists a ON a.id = aca.artist_id
LEFT JOIN release_group_recordings rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
LEFT JOIN file_types ft ON af.file_type_id = ft.id
WHERE af.library_id = ?;
-- name: GetAudioFilesByReleaseGroup :many
SELECT
af.file_path,
af.length_milliseconds,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist_name,
rgr.track_number,
rgr.disc_number,
COALESCE(rg.name, '') AS album,
CAST(COALESCE(
(SELECT GROUP_CONCAT(g.name, '||')
FROM recording_genres rg_sub
JOIN genres g ON rg_sub.genre_id = g.id
WHERE rg_sub.recording_id = r.id),
''
) AS TEXT) AS genre,
COALESCE(r.year, 0) AS year,
COALESCE(r.composer, '') AS composer,
COALESCE(ft.extension, '') AS file_type,
af.sample_rate,
af.bit_depth,
af.channels,
af.bitrate,
af.file_size,
COALESCE(a.mbid, '') AS artist_mbid,
COALESCE(rg.mbid, '') AS release_group_mbid,
COALESCE(r.mbid, '') AS recording_mbid
FROM release_group_recordings rgr
JOIN recordings r ON rgr.recording_id = r.id
JOIN audio_files af ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN artist_credit_artist aca ON aca.credit_id = ac.id
LEFT JOIN artists a ON a.id = aca.artist_id
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
LEFT JOIN file_types ft ON af.file_type_id = ft.id
WHERE rgr.release_group_id = ?
ORDER BY rgr.disc_number, rgr.track_number;
-- name: GetAudioFilesByReleaseGroupByLibrary :many
SELECT
af.file_path,
af.length_milliseconds,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist_name,
rgr.track_number,
rgr.disc_number,
COALESCE(rg.name, '') AS album,
CAST(COALESCE(
(SELECT GROUP_CONCAT(g.name, '||')
FROM recording_genres rg_sub
JOIN genres g ON rg_sub.genre_id = g.id
WHERE rg_sub.recording_id = r.id),
''
) AS TEXT) AS genre,
COALESCE(r.year, 0) AS year,
COALESCE(r.composer, '') AS composer,
COALESCE(ft.extension, '') AS file_type,
af.sample_rate,
af.bit_depth,
af.channels,
af.bitrate,
af.file_size,
COALESCE(a.mbid, '') AS artist_mbid,
COALESCE(rg.mbid, '') AS release_group_mbid,
COALESCE(r.mbid, '') AS recording_mbid
FROM release_group_recordings rgr
JOIN recordings r ON rgr.recording_id = r.id
JOIN audio_files af ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN artist_credit_artist aca ON aca.credit_id = ac.id
LEFT JOIN artists a ON a.id = aca.artist_id
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
LEFT JOIN file_types ft ON af.file_type_id = ft.id
WHERE rgr.release_group_id = ? AND af.library_id = ?
ORDER BY rgr.disc_number, rgr.track_number;
-- "Play this artist" and "play these albums" wanted file paths and asked
-- for whole track rows to get them, one round trip per album (perf.m2).
-- These answer the same question in one query and carry only what the
-- caller uses; the release group id comes back so the caller can keep
-- its own album ordering.
-- name: GetFilePathsByReleaseGroups :many
SELECT rgr.release_group_id, af.file_path
FROM release_group_recordings rgr
JOIN recordings r ON rgr.recording_id = r.id
JOIN audio_files af ON af.recording_id = r.id
WHERE rgr.release_group_id IN (sqlc.slice('release_group_ids'))
ORDER BY rgr.disc_number, rgr.track_number;
-- name: GetFilePathsByReleaseGroupsByLibrary :many
SELECT rgr.release_group_id, af.file_path
FROM release_group_recordings rgr
JOIN recordings r ON rgr.recording_id = r.id
JOIN audio_files af ON af.recording_id = r.id
WHERE rgr.release_group_id IN (sqlc.slice('release_group_ids'))
AND af.library_id = ?
ORDER BY rgr.disc_number, rgr.track_number;
-- Same shape again, keyed on recording MBID, for the catalog side.
-- An Explore album page knows which of its tracks the user owns only
-- as a set of recording MBIDs -- that is exactly how the backend
-- decides `inLibrary` (markReleasesInLibrary -> CheckMBIDs) -- and
-- MBTrack.LocalID is declared but never written by anything, so there
-- is no id to ask by. Grouped by MBID because a recording can have
-- more than one file (the duplicate fixtures are precisely that) and
-- because the caller owns the order: the tracklist's, not the
-- database's.
-- name: GetFilePathsByAlbums :many
-- The library filter is applied in Go rather than here: sqlc numbers a
-- named parameter (?2) but expands a slice into N placeholders, so the
-- two together bind the wrong values - GetFilePathsByAlbums([1,2], 0)
-- read album id 2 as the library id. Returning library_id and
-- filtering the (small) result is the version that cannot be wrong.
SELECT album_id, library_id, file_path FROM audio_files
WHERE album_id IN (sqlc.slice('album_ids'))
ORDER BY disc_number, track_number;
-- name: GetFilePathsByRecordingMBIDs :many
SELECT r.mbid AS recording_mbid, af.file_path
FROM recordings r
JOIN audio_files af ON af.recording_id = r.id
WHERE r.mbid IN (sqlc.slice('mbids'))
ORDER BY af.file_path;
-- The ownership question in its only honest form: which of these
-- catalog recordings has a *file* behind it. Asked of audio_files, so
-- a metadata row with no file cannot answer yes.
SELECT recording_mbid, library_id, file_path FROM audio_files
WHERE recording_mbid IN (sqlc.slice('mbids'))
ORDER BY file_path;
-- name: GetFilePathsByRecordingMBIDsByLibrary :many
SELECT r.mbid AS recording_mbid, af.file_path
FROM recordings r
JOIN audio_files af ON af.recording_id = r.id
WHERE r.mbid IN (sqlc.slice('mbids'))
AND af.library_id = ?
ORDER BY af.file_path;
-- name: GetFilePathsByGenres :many
SELECT g.name AS genre, af.library_id, af.file_path
FROM audio_files af
JOIN file_genres fg ON fg.audio_file_id = af.id
JOIN genres g ON g.id = fg.genre_id
WHERE g.name IN (sqlc.slice('genres'))
ORDER BY af.disc_number, af.track_number;
-- name: GetAudioFilesByPaths :many
SELECT id, library_id, file_path, group_key FROM audio_files
WHERE file_path IN (sqlc.slice('paths'));
-- name: GetFilePathsByArtistMBID :many
SELECT DISTINCT af.file_path
FROM audio_files af
JOIN artists a ON a.id = af.artist_id
WHERE a.mbid = ?;
-- ---------------------------------------------------------------------
-- Ownership, asked in bulk
-- ---------------------------------------------------------------------
-- name: OwnedRecordingMBIDs :many
-- Which of these recording MBIDs are actually in the library. This is
-- what marks a catalog tracklist owned; it used to be
-- `SELECT mbid FROM recordings`, which answered yes for 129 tracks in a
-- real library that had no file at all.
SELECT DISTINCT recording_mbid FROM audio_files
WHERE recording_mbid IN (sqlc.slice('mbids'));
-- name: OwnedAlbumMBIDs :many
SELECT DISTINCT al.mbid FROM albums al
JOIN audio_files af ON af.album_id = al.id
WHERE al.mbid IN (sqlc.slice('mbids'));
-- name: OwnedArtistMBIDs :many
SELECT DISTINCT a.mbid FROM artists a
JOIN audio_files af ON af.artist_id = a.id
WHERE a.mbid IN (sqlc.slice('mbids'));
-- name: GetAudioFilesInLibrary :many
SELECT * FROM audio_files WHERE library_id = ?;
+35 -135
View File
@@ -1,150 +1,50 @@
-- Queries over genres and file_genres.
--
-- The track-returning ones live in audio_files.sql with the rest of the
-- track_metadata reads; what is left here is the genre list itself and
-- the link table's writes.
-- name: UpsertGenre :one
INSERT INTO genres (name) VALUES (?)
ON CONFLICT(name) DO UPDATE SET name = name
ON CONFLICT(name) DO UPDATE SET name = excluded.name
RETURNING *;
-- name: CreateRecordingGenre :exec
INSERT OR IGNORE INTO recording_genres (recording_id, genre_id)
VALUES (?, ?);
-- name: LinkFileGenre :exec
INSERT OR IGNORE INTO file_genres (audio_file_id, genre_id) VALUES (?, ?);
-- name: DeleteRecordingGenres :exec
DELETE FROM recording_genres
WHERE recording_id = ?;
-- name: DeleteFileGenres :exec
DELETE FROM file_genres WHERE audio_file_id = ?;
-- name: GetGenresByRecordingID :many
SELECT g.*
FROM genres g
JOIN recording_genres rg ON g.id = rg.genre_id
WHERE rg.recording_id = ?;
-- name: GetGenreNamesByFile :many
SELECT g.name FROM genres g
JOIN file_genres fg ON fg.genre_id = g.id
WHERE fg.audio_file_id = ?;
-- name: DeleteAllRecordingGenres :exec
DELETE FROM recording_genres;
-- name: DeleteAllGenres :exec
DELETE FROM genres;
-- name: GetTracksByGenre :many
SELECT
af.file_path,
af.length_milliseconds,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist_name,
r.track_number,
r.disc_number,
COALESCE(rlg.name, '') AS album,
CAST(COALESCE(
(SELECT GROUP_CONCAT(g2.name, '||')
FROM recording_genres rg2
JOIN genres g2 ON rg2.genre_id = g2.id
WHERE rg2.recording_id = r.id),
''
) AS TEXT) AS genre,
COALESCE(r.year, 0) AS year,
COALESCE(r.composer, '') AS composer,
COALESCE(ft.extension, '') AS file_type,
af.sample_rate,
af.bit_depth,
af.channels,
af.bitrate,
af.file_size
FROM genres g
JOIN recording_genres rg ON g.id = rg.genre_id
JOIN recordings r ON rg.recording_id = r.id
JOIN audio_files af ON af.recording_id = r.id
JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN (
SELECT recording_id,
MIN(release_group_id) AS release_group_id
FROM release_group_recordings
GROUP BY recording_id
) rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rlg ON rgr.release_group_id = rlg.id
LEFT JOIN file_types ft ON af.file_type_id = ft.id
WHERE g.name = ?
ORDER BY r.name;
-- name: GetTracksByGenreByLibrary :many
SELECT
af.file_path,
af.length_milliseconds,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist_name,
r.track_number,
r.disc_number,
COALESCE(rlg.name, '') AS album,
CAST(COALESCE(
(SELECT GROUP_CONCAT(g2.name, '||')
FROM recording_genres rg2
JOIN genres g2 ON rg2.genre_id = g2.id
WHERE rg2.recording_id = r.id),
''
) AS TEXT) AS genre,
COALESCE(r.year, 0) AS year,
COALESCE(r.composer, '') AS composer,
COALESCE(ft.extension, '') AS file_type,
af.sample_rate,
af.bit_depth,
af.channels,
af.bitrate,
af.file_size
FROM genres g
JOIN recording_genres rg ON g.id = rg.genre_id
JOIN recordings r ON rg.recording_id = r.id
JOIN audio_files af ON af.recording_id = r.id
JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN (
SELECT recording_id,
MIN(release_group_id) AS release_group_id
FROM release_group_recordings
GROUP BY recording_id
) rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rlg ON rgr.release_group_id = rlg.id
LEFT JOIN file_types ft ON af.file_type_id = ft.id
WHERE g.name = ? AND af.library_id = ?
ORDER BY r.name;
-- name: CountGenreReferences :one
SELECT COUNT(*) FROM recording_genres WHERE genre_id = ?;
-- name: GetGenreNamesByFilePaths :many
-- Genres for many files at once. The mix builder asked this one file
-- at a time, inside two nested loops -- twelve thousand single-row
-- queries to assemble one mix.
SELECT af.file_path, g.name
FROM audio_files af
JOIN file_genres fg ON fg.audio_file_id = af.id
JOIN genres g ON g.id = fg.genre_id
WHERE af.file_path IN (sqlc.slice('paths'));
-- name: DeleteGenre :exec
DELETE FROM genres WHERE id = ?;
-- name: DeleteAllGenres :exec
DELETE FROM genres;
-- name: GetUnusedGenreIDs :many
SELECT id FROM genres g
WHERE NOT EXISTS (SELECT 1 FROM file_genres fg WHERE fg.genre_id = g.id);
-- name: GetAllGenresWithCounts :many
SELECT g.name, COUNT(rg.recording_id) AS track_count
SELECT g.name, COUNT(fg.audio_file_id) AS track_count
FROM genres g
JOIN recording_genres rg ON g.id = rg.genre_id
JOIN file_genres fg ON fg.genre_id = g.id
JOIN audio_files af ON af.id = fg.audio_file_id
WHERE af.library_id = COALESCE(NULLIF(CAST(sqlc.arg(library_id) AS INTEGER), 0), af.library_id)
GROUP BY g.id, g.name
ORDER BY g.name;
-- name: GetAllGenresWithCountsByLibrary :many
SELECT g.name, COUNT(rg.recording_id) AS track_count
FROM genres g
JOIN recording_genres rg ON g.id = rg.genre_id
JOIN recordings r ON rg.recording_id = r.id
JOIN audio_files af ON af.recording_id = r.id
WHERE af.library_id = ?
GROUP BY g.id, g.name
ORDER BY g.name;
-- Same as GetFilePathsByReleaseGroups, for "play these genres" (perf.m2):
-- one query instead of one per genre, and file paths instead of whole
-- track rows, which was 6 MB over the IPC for five genres.
-- name: GetFilePathsByGenres :many
SELECT g.name AS genre_name, af.file_path
FROM genres g
JOIN recording_genres rg ON g.id = rg.genre_id
JOIN recordings r ON rg.recording_id = r.id
JOIN audio_files af ON af.recording_id = r.id
WHERE g.name IN (sqlc.slice('genre_names'))
ORDER BY r.name;
-- name: GetFilePathsByGenresByLibrary :many
SELECT g.name AS genre_name, af.file_path
FROM genres g
JOIN recording_genres rg ON g.id = rg.genre_id
JOIN recordings r ON rg.recording_id = r.id
JOIN audio_files af ON af.recording_id = r.id
WHERE g.name IN (sqlc.slice('genre_names'))
AND af.library_id = ?
ORDER BY r.name;
+25 -33
View File
@@ -2,16 +2,15 @@
--
-- Every one of these returns album ids and nothing else. The display
-- columns (cover art, artist credit, year) already have exactly one
-- correct expression of them, in GetAllAlbumsWithDetails, and a second
-- correct expression of them, in GetAlbums, and a second
-- copy per shelf would be six more places for that to drift. The home
-- service joins the ids back to that one album list in Go.
-- name: HomeRecentlyPlayedAlbums :many
-- Albums with the most recent play, newest first.
SELECT rg.id AS album_id
FROM release_groups rg
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN audio_files af ON af.recording_id = rgr.recording_id
FROM albums rg
JOIN audio_files af ON af.album_id = rg.id
WHERE af.last_played IS NOT NULL
GROUP BY rg.id
ORDER BY MAX(af.last_played) DESC
@@ -22,9 +21,8 @@ LIMIT ?;
-- stands in for one: it is monotonic and assigned at import, which is
-- the same ordering an added_at column would give.
SELECT rg.id AS album_id
FROM release_groups rg
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN audio_files af ON af.recording_id = rgr.recording_id
FROM albums rg
JOIN audio_files af ON af.album_id = rg.id
GROUP BY rg.id
ORDER BY MAX(af.id) DESC
LIMIT ?;
@@ -32,9 +30,8 @@ LIMIT ?;
-- name: HomeMostPlayedAlbums :many
-- Albums by total plays across their tracks.
SELECT rg.id AS album_id
FROM release_groups rg
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN audio_files af ON af.recording_id = rgr.recording_id
FROM albums rg
JOIN audio_files af ON af.album_id = rg.id
GROUP BY rg.id
HAVING SUM(af.play_count) > 0
ORDER BY SUM(af.play_count) DESC
@@ -45,9 +42,8 @@ LIMIT ?;
-- shelf is a different suggestion each time rather than the same
-- alphabetical head of the list forever.
SELECT rg.id AS album_id
FROM release_groups rg
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN audio_files af ON af.recording_id = rgr.recording_id
FROM albums rg
JOIN audio_files af ON af.album_id = rg.id
GROUP BY rg.id
HAVING SUM(af.play_count) = 0
ORDER BY RANDOM()
@@ -56,9 +52,8 @@ LIMIT ?;
-- name: HomeStaleAlbums :many
-- Played before, but not for a long while.
SELECT rg.id AS album_id
FROM release_groups rg
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN audio_files af ON af.recording_id = rgr.recording_id
FROM albums rg
JOIN audio_files af ON af.album_id = rg.id
WHERE af.last_played IS NOT NULL
GROUP BY rg.id
HAVING MAX(af.last_played) < datetime('now', ?)
@@ -67,9 +62,8 @@ LIMIT ?;
-- name: HomeRandomAlbums :many
SELECT rg.id AS album_id
FROM release_groups rg
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN audio_files af ON af.recording_id = rgr.recording_id
FROM albums rg
JOIN audio_files af ON af.album_id = rg.id
GROUP BY rg.id
ORDER BY RANDOM()
LIMIT ?;
@@ -78,10 +72,10 @@ LIMIT ?;
-- A random sample of albums carrying a genre, so the same genre shelf
-- is not the same ten albums every time the page opens.
SELECT rg.id AS album_id
FROM release_groups rg
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN recording_genres rgen ON rgen.recording_id = rgr.recording_id
JOIN genres g ON g.id = rgen.genre_id
FROM albums rg
JOIN audio_files af ON af.album_id = rg.id
JOIN file_genres fg ON fg.audio_file_id = af.id
JOIN genres g ON g.id = fg.genre_id
WHERE g.name = ?
GROUP BY rg.id
ORDER BY RANDOM()
@@ -93,10 +87,10 @@ LIMIT ?;
-- album carries is a shelf about that one album.
SELECT
g.name AS genre,
COUNT(DISTINCT rgr.release_group_id) AS album_count
COUNT(DISTINCT af.album_id) AS album_count
FROM genres g
JOIN recording_genres rgen ON rgen.genre_id = g.id
JOIN release_group_recordings rgr ON rgr.recording_id = rgen.recording_id
JOIN file_genres fg ON fg.genre_id = g.id
JOIN audio_files af ON af.id = fg.audio_file_id
GROUP BY g.id
HAVING album_count >= 3
ORDER BY album_count DESC
@@ -106,14 +100,12 @@ LIMIT ?;
-- Artists by total plays, as the album-artist credit text the album
-- list already displays.
SELECT
COALESCE(ac.text, '') AS artist_name,
rg.artist_credit AS artist_name,
SUM(af.play_count) AS plays
FROM release_groups rg
JOIN artist_credit ac ON ac.id = rg.album_artist_credit_id
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN audio_files af ON af.recording_id = rgr.recording_id
WHERE ac.text <> ''
GROUP BY ac.text
FROM albums rg
JOIN audio_files af ON af.album_id = rg.id
WHERE rg.artist_credit <> ''
GROUP BY rg.artist_credit
HAVING plays > 0
ORDER BY plays DESC
LIMIT ?;
-30
View File
@@ -1,30 +0,0 @@
-- Queries backing the dynamic-mix queue fallback (backend/explore/mix.go):
-- expanding a seed selection into a candidate pool by artist similarity
-- and genre overlap, restricted to what is actually in the library.
-- name: GetFilePathsByArtistMBID :many
SELECT DISTINCT af.file_path
FROM audio_files af
JOIN recordings r ON af.recording_id = r.id
JOIN artist_credit ac ON r.artist_credit_id = ac.id
JOIN artist_credit_artist aca ON aca.credit_id = ac.id
JOIN artists a ON a.id = aca.artist_id
WHERE a.mbid = ?;
-- name: GetGenreNamesByFilePath :many
SELECT DISTINCT g.name
FROM genres g
JOIN recording_genres rg ON g.id = rg.genre_id
JOIN recordings r ON rg.recording_id = r.id
JOIN audio_files af ON af.recording_id = r.id
WHERE af.file_path = ?;
-- name: GetArtistByFilePath :one
SELECT COALESCE(a.name, '') AS artist_name, COALESCE(a.mbid, '') AS artist_mbid
FROM audio_files af
JOIN recordings r ON af.recording_id = r.id
JOIN artist_credit ac ON r.artist_credit_id = ac.id
JOIN artist_credit_artist aca ON aca.credit_id = ac.id
JOIN artists a ON a.id = aca.artist_id
WHERE af.file_path = ?
LIMIT 1;
+25 -66
View File
@@ -47,29 +47,18 @@ SELECT
pt.playlist_id,
pt.audio_file_id,
pt.position,
COALESCE(af.file_path, '') AS file_path,
COALESCE(af.length_milliseconds, 0) AS length_milliseconds,
COALESCE(r.name, pt.phantom_title, '') AS title,
COALESCE(ac.text, pt.phantom_artist, '') AS artist,
COALESCE(rg.name, pt.phantom_album, '') AS album,
COALESCE(ca.file_path, pt.phantom_cover_art_path, '') AS cover_art_path,
COALESCE(tm.file_path, '') AS file_path,
COALESCE(tm.length_milliseconds, 0) AS length_milliseconds,
COALESCE(tm.title, pt.phantom_title, '') AS title,
COALESCE(tm.artist_name, pt.phantom_artist, '') AS artist,
COALESCE(tm.album, pt.phantom_album, '') AS album,
COALESCE(NULLIF(tm.cover_art_path, ''), pt.phantom_cover_art_path, '') AS cover_art_path,
CASE WHEN pt.audio_file_id IS NULL THEN 1 ELSE 0 END AS is_phantom,
COALESCE(a.mbid, '') AS artist_mbid,
COALESCE(rg.mbid, '') AS release_group_mbid,
COALESCE(r.mbid, '') AS recording_mbid
CAST(COALESCE(tm.artist_mbid, '') AS TEXT) AS artist_mbid,
COALESCE(tm.release_group_mbid, '') AS release_group_mbid,
COALESCE(tm.recording_mbid, '') AS recording_mbid
FROM playlist_tracks pt
LEFT JOIN audio_files af ON pt.audio_file_id = af.id
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN artist_credit_artist aca ON aca.credit_id = ac.id
LEFT JOIN artists a ON a.id = aca.artist_id
LEFT JOIN (
SELECT recording_id, MIN(release_group_id) AS release_group_id
FROM release_group_recordings
GROUP BY recording_id
) rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
LEFT JOIN track_metadata tm ON tm.id = pt.audio_file_id
WHERE pt.playlist_id = ?
ORDER BY pt.position;
@@ -79,29 +68,18 @@ SELECT
pt.playlist_id,
pt.audio_file_id,
pt.position,
COALESCE(af.file_path, '') AS file_path,
COALESCE(af.length_milliseconds, 0) AS length_milliseconds,
COALESCE(r.name, pt.phantom_title, '') AS title,
COALESCE(ac.text, pt.phantom_artist, '') AS artist,
COALESCE(rg.name, pt.phantom_album, '') AS album,
COALESCE(ca.file_path, pt.phantom_cover_art_path, '') AS cover_art_path,
COALESCE(tm.file_path, '') AS file_path,
COALESCE(tm.length_milliseconds, 0) AS length_milliseconds,
COALESCE(tm.title, pt.phantom_title, '') AS title,
COALESCE(tm.artist_name, pt.phantom_artist, '') AS artist,
COALESCE(tm.album, pt.phantom_album, '') AS album,
COALESCE(NULLIF(tm.cover_art_path, ''), pt.phantom_cover_art_path, '') AS cover_art_path,
CASE WHEN pt.audio_file_id IS NULL THEN 1 ELSE 0 END AS is_phantom,
COALESCE(a.mbid, '') AS artist_mbid,
COALESCE(rg.mbid, '') AS release_group_mbid,
COALESCE(r.mbid, '') AS recording_mbid
CAST(COALESCE(tm.artist_mbid, '') AS TEXT) AS artist_mbid,
COALESCE(tm.release_group_mbid, '') AS release_group_mbid,
COALESCE(tm.recording_mbid, '') AS recording_mbid
FROM playlist_tracks pt
LEFT JOIN audio_files af ON pt.audio_file_id = af.id
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN artist_credit_artist aca ON aca.credit_id = ac.id
LEFT JOIN artists a ON a.id = aca.artist_id
LEFT JOIN (
SELECT recording_id, MIN(release_group_id) AS release_group_id
FROM release_group_recordings
GROUP BY recording_id
) rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
LEFT JOIN track_metadata tm ON tm.id = pt.audio_file_id
ORDER BY pt.playlist_id, pt.position;
-- name: DeleteAllPlaylistTracks :exec
@@ -132,27 +110,8 @@ WHERE playlist_id = ? AND audio_file_id = (
);
-- name: GetTrackPhantomMetadata :one
SELECT
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist,
COALESCE(rg.name, '') AS album,
af.length_milliseconds AS duration_ms,
CAST(COALESCE(
(SELECT GROUP_CONCAT(g.name, '||')
FROM recording_genres rg_sub
JOIN genres g ON rg_sub.genre_id = g.id
WHERE rg_sub.recording_id = r.id),
''
) AS TEXT) AS genre,
COALESCE(ca.file_path, '') AS cover_art_path
FROM audio_files af
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN (
SELECT recording_id, MIN(release_group_id) AS release_group_id
FROM release_group_recordings
GROUP BY recording_id
) rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
WHERE af.id = ?;
-- The display fields a playlist row keeps after its file goes away.
SELECT title, artist_name AS artist, album,
length_milliseconds AS duration_ms, genre, cover_art_path
FROM track_metadata
WHERE id = ?;
+5 -20
View File
@@ -13,27 +13,12 @@ SET current_position = ?
WHERE id = 1;
-- name: GetQueueTracks :many
SELECT qt.id, qt.audio_file_id, qt.position, af.file_path,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist,
COALESCE(rg.name, '') AS album,
COALESCE(ca.file_path, '') AS cover_art_path,
COALESCE(a.mbid, '') AS artist_mbid,
COALESCE(rg.mbid, '') AS release_group_mbid,
COALESCE(r.mbid, '') AS recording_mbid
-- The queue's rows, joined to the one track projection.
SELECT qt.id, qt.audio_file_id, qt.position, tm.file_path,
tm.title, tm.artist_name AS artist, tm.album, tm.cover_art_path,
tm.artist_mbid, tm.release_group_mbid, tm.recording_mbid
FROM queue_tracks qt
JOIN audio_files af ON qt.audio_file_id = af.id
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN artist_credit_artist aca ON aca.credit_id = ac.id
LEFT JOIN artists a ON a.id = aca.artist_id
LEFT JOIN (
SELECT recording_id, MIN(release_group_id) AS release_group_id
FROM release_group_recordings
GROUP BY recording_id
) rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
JOIN track_metadata tm ON tm.id = qt.audio_file_id
ORDER BY qt.position;
-- name: GetQueueTrackCount :one
@@ -1,47 +0,0 @@
-- name: CreateRecording :one
INSERT INTO recordings (name, artist_credit_id) VALUES (?, ?)
RETURNING *;
-- name: CreateRecordingFull :one
INSERT INTO recordings (
name, artist_credit_id, track_number, disc_number,
year, genre, composer, lyrics, comment
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING *;
-- name: GetRecording :one
SELECT * FROM recordings
WHERE id = ? LIMIT 1;
-- name: UpdateRecording :exec
UPDATE recordings
SET name = ?, artist_credit_id = ?
WHERE id = ?;
-- name: UpdateRecordingFull :exec
UPDATE recordings
SET name = ?, artist_credit_id = ?, track_number = ?, disc_number = ?,
year = ?, genre = ?, composer = ?, lyrics = ?, comment = ?
WHERE id = ?;
-- name: DeleteRecording :exec
DELETE FROM recordings
WHERE id = ?;
-- name: DeleteAllRecordings :exec
DELETE FROM recordings;
-- name: GetAllRecordings :many
SELECT * FROM recordings
ORDER BY name;
-- name: CountRecordingsByArtistCredit :one
SELECT COUNT(*) FROM recordings WHERE artist_credit_id = ?;
-- name: GetOrphanedRecordingIDs :many
-- Recordings no longer backed by any audio_files row - left behind
-- when a scan's orphan cleanup deletes the file that used to own them,
-- since deleting audio_files doesn't cascade to recordings.
SELECT r.id FROM recordings r
LEFT JOIN audio_files af ON af.recording_id = r.id
WHERE af.id IS NULL;
@@ -1,50 +0,0 @@
-- name: CreateReleaseGroupRecording :one
INSERT INTO release_group_recordings (
release_group_id, recording_id, track_number, disc_number, total_tracks
)
VALUES (?, ?, ?, ?, ?)
RETURNING *;
-- name: GetAlbumCompleteness :one
WITH discs AS (
SELECT
COALESCE(rgr.disc_number, 1) AS disc,
MAX(COALESCE(rgr.total_tracks, 0)) AS declared,
COUNT(DISTINCT COALESCE(rgr.track_number, -rgr.recording_id)) AS owned
FROM release_group_recordings rgr
WHERE rgr.release_group_id = ?
GROUP BY COALESCE(rgr.disc_number, 1)
)
SELECT
CAST(COALESCE(SUM(owned), 0) AS INTEGER) AS owned,
CAST(COALESCE(SUM(declared), 0) AS INTEGER) AS expected,
CAST(COALESCE(SUM(CASE WHEN declared = 0 THEN 1 ELSE 0 END), 0) AS INTEGER) AS discs_untotalled
FROM discs;
-- name: GetReleaseGroupRecording :one
SELECT * FROM release_group_recordings
WHERE id = ? LIMIT 1;
-- name: GetReleaseGroupRecordings :many
SELECT * FROM release_group_recordings
WHERE release_group_id = ?
ORDER BY disc_number, track_number;
-- name: GetRecordingReleaseGroups :many
SELECT * FROM release_group_recordings
WHERE recording_id = ?;
-- name: DeleteReleaseGroupRecording :exec
DELETE FROM release_group_recordings
WHERE id = ?;
-- name: DeleteReleaseGroupRecordingByFK :exec
DELETE FROM release_group_recordings
WHERE release_group_id = ? AND recording_id = ?;
-- name: DeleteAllReleaseGroupRecordings :exec
DELETE FROM release_group_recordings;
-- name: DeleteReleaseGroupRecordingsByRecording :exec
DELETE FROM release_group_recordings
WHERE recording_id = ?;
@@ -1,216 +0,0 @@
-- name: CreateReleaseGroup :one
INSERT INTO release_groups (name) VALUES (?)
RETURNING *;
-- name: CreateReleaseGroupFull :one
INSERT INTO release_groups (
name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs
) VALUES (?, ?, ?, ?, ?, ?)
RETURNING *;
-- name: GetReleaseGroup :one
SELECT * FROM release_groups
WHERE id = ? LIMIT 1;
-- name: GetReleaseGroupByNameAndArtist :one
SELECT * FROM release_groups
WHERE name = ? AND album_artist_credit_id = ? LIMIT 1;
-- name: UpsertReleaseGroup :one
INSERT INTO release_groups (name, album_artist_credit_id, year)
VALUES (?, ?, ?)
ON CONFLICT(name, album_artist_credit_id) DO UPDATE SET
album_artist_credit_id = COALESCE(excluded.album_artist_credit_id, release_groups.album_artist_credit_id),
year = COALESCE(excluded.year, release_groups.year)
RETURNING *;
-- name: SetReleaseGroupOriginalYear :exec
-- Set the release group's original-release-year (release-group's
-- first-release-date from MusicBrainz). Called from autotag apply
-- when the user confirms a candidate; the file-tag year stays in
-- the year column.
UPDATE release_groups SET original_year = ? WHERE id = ?;
-- name: UpdateReleaseGroup :exec
UPDATE release_groups
SET name = ?
WHERE id = ?;
-- name: UpdateReleaseGroupCoverArt :exec
UPDATE release_groups
SET cover_art_id = ?
WHERE id = ?;
-- name: DeleteReleaseGroup :exec
DELETE FROM release_groups
WHERE id = ?;
-- name: DeleteAllReleaseGroups :exec
DELETE FROM release_groups;
-- name: GetAllReleaseGroups :many
SELECT * FROM release_groups
ORDER BY name;
-- name: GetAllAlbumsWithDetails :many
SELECT
rg.id,
rg.name,
-- year prefers original release year (MB first-release-date)
-- over the file-tag year so the UI surfaces the album's
-- original year by default. release_year keeps the file-tag
-- year accessible.
COALESCE(rg.original_year, rg.year) AS year,
COALESCE(rg.year, 0) AS release_year,
rg.mbid,
COALESCE(ac.text, fallback_ac.text, '') as artist_name,
-- primary (first-credited) album artist's MBID, for linking the
-- artist name to its detail page. Empty when the album has no
-- MB-tagged album-artist credit.
CAST(COALESCE((
SELECT a.mbid
FROM artist_credit_artist aca_p
JOIN artists a ON a.id = aca_p.artist_id
WHERE aca_p.credit_id = rg.album_artist_credit_id
ORDER BY aca_p.id
LIMIT 1
), '') AS TEXT) as artist_mbid,
COALESCE(ca.file_path, '') as cover_art_path
FROM release_groups rg
LEFT JOIN artist_credit ac ON rg.album_artist_credit_id = ac.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
LEFT JOIN (
SELECT rgr.release_group_id, ac2.text
FROM release_group_recordings rgr
JOIN recordings rec ON rec.id = rgr.recording_id
JOIN artist_credit ac2 ON ac2.id = rec.artist_credit_id
GROUP BY rgr.release_group_id
) fallback_ac ON fallback_ac.release_group_id = rg.id
ORDER BY rg.name;
-- name: GetAllAlbumsWithDetailsByLibrary :many
SELECT
rg.id,
rg.name,
-- year prefers original release year (MB first-release-date)
-- over the file-tag year so the UI surfaces the album's
-- original year by default. release_year keeps the file-tag
-- year accessible.
COALESCE(rg.original_year, rg.year) AS year,
COALESCE(rg.year, 0) AS release_year,
rg.mbid,
COALESCE(ac.text, fallback_ac.text, '') as artist_name,
-- primary (first-credited) album artist's MBID, for linking the
-- artist name to its detail page. Empty when the album has no
-- MB-tagged album-artist credit.
CAST(COALESCE((
SELECT a.mbid
FROM artist_credit_artist aca_p
JOIN artists a ON a.id = aca_p.artist_id
WHERE aca_p.credit_id = rg.album_artist_credit_id
ORDER BY aca_p.id
LIMIT 1
), '') AS TEXT) as artist_mbid,
COALESCE(ca.file_path, '') as cover_art_path
FROM release_groups rg
LEFT JOIN artist_credit ac ON rg.album_artist_credit_id = ac.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
LEFT JOIN (
SELECT rgr.release_group_id, ac2.text
FROM release_group_recordings rgr
JOIN recordings rec ON rec.id = rgr.recording_id
JOIN artist_credit ac2 ON ac2.id = rec.artist_credit_id
GROUP BY rgr.release_group_id
) fallback_ac ON fallback_ac.release_group_id = rg.id
WHERE rg.id IN (
SELECT DISTINCT rgr2.release_group_id
FROM release_group_recordings rgr2
JOIN recordings r2 ON r2.id = rgr2.recording_id
JOIN audio_files af2 ON af2.recording_id = r2.id
WHERE af2.library_id = ?
)
ORDER BY rg.name;
-- name: GetAlbumsByArtist :many
SELECT
rg.id,
rg.name,
COALESCE(rg.original_year, rg.year) AS year,
COALESCE(rg.year, 0) AS release_year,
COALESCE(ac.text, fallback_ac.text, '') as artist_name,
-- primary (first-credited) album artist's MBID, for linking the
-- artist name to its detail page. Empty when the album has no
-- MB-tagged album-artist credit.
CAST(COALESCE((
SELECT a.mbid
FROM artist_credit_artist aca_p
JOIN artists a ON a.id = aca_p.artist_id
WHERE aca_p.credit_id = rg.album_artist_credit_id
ORDER BY aca_p.id
LIMIT 1
), '') AS TEXT) as artist_mbid,
COALESCE(ca.file_path, '') as cover_art_path
FROM release_groups rg
JOIN artist_credit ac ON rg.album_artist_credit_id = ac.id
JOIN artist_credit_artist aca ON aca.credit_id = ac.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
LEFT JOIN (
SELECT rgr.release_group_id, ac2.text
FROM release_group_recordings rgr
JOIN recordings rec ON rec.id = rgr.recording_id
JOIN artist_credit ac2 ON ac2.id = rec.artist_credit_id
GROUP BY rgr.release_group_id
) fallback_ac ON fallback_ac.release_group_id = rg.id
WHERE aca.artist_id = ?
ORDER BY rg.name;
-- name: CountReleaseGroupRecordings :one
SELECT COUNT(*) FROM release_group_recordings WHERE release_group_id = ?;
-- name: GetOrphanedReleaseGroupIDs :many
-- Release groups with no recordings left in them - run after orphaned
-- recordings (and their release_group_recordings rows) are deleted, so
-- a release group whose last owned track was removed is cleaned up too.
SELECT rg.id FROM release_groups rg
LEFT JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
WHERE rgr.id IS NULL;
-- name: GetAlbumsByArtistByLibrary :many
SELECT
rg.id,
rg.name,
COALESCE(rg.original_year, rg.year) AS year,
COALESCE(rg.year, 0) AS release_year,
COALESCE(ac.text, fallback_ac.text, '') as artist_name,
-- primary (first-credited) album artist's MBID, for linking the
-- artist name to its detail page. Empty when the album has no
-- MB-tagged album-artist credit.
CAST(COALESCE((
SELECT a.mbid
FROM artist_credit_artist aca_p
JOIN artists a ON a.id = aca_p.artist_id
WHERE aca_p.credit_id = rg.album_artist_credit_id
ORDER BY aca_p.id
LIMIT 1
), '') AS TEXT) as artist_mbid,
COALESCE(ca.file_path, '') as cover_art_path
FROM release_groups rg
JOIN artist_credit ac ON rg.album_artist_credit_id = ac.id
JOIN artist_credit_artist aca ON aca.credit_id = ac.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
LEFT JOIN (
SELECT rgr.release_group_id, ac2.text
FROM release_group_recordings rgr
JOIN recordings rec ON rec.id = rgr.recording_id
JOIN artist_credit ac2 ON ac2.id = rec.artist_credit_id
GROUP BY rgr.release_group_id
) fallback_ac ON fallback_ac.release_group_id = rg.id
WHERE aca.artist_id = ?
AND rg.id IN (
SELECT DISTINCT rgr2.release_group_id
FROM release_group_recordings rgr2
JOIN recordings r2 ON r2.id = rgr2.recording_id
JOIN audio_files af2 ON af2.recording_id = r2.id
WHERE af2.library_id = ?
)
ORDER BY rg.name;
+87 -62
View File
@@ -87,10 +87,7 @@ LIMIT 1;
SELECT ti.group_key
FROM tagging_items ti
JOIN audio_files af ON af.group_key = ti.group_key
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN release_group_recordings rgr ON rgr.recording_id = r.id
LEFT JOIN release_groups rg ON rg.id = rgr.release_group_id
LEFT JOIN albums rg ON rg.id = af.album_id
WHERE ti.synthetic = 0
AND ti.track_count >= 4
AND (
@@ -98,13 +95,23 @@ WHERE ti.synthetic = 0
OR LOWER(TRIM(ti.album_artist)) IN ('various artists', 'various', 'va', 'v.a.', 'v a', 'unknown')
)
GROUP BY ti.group_key
HAVING COUNT(DISTINCT CASE WHEN ac.text != '' THEN LOWER(TRIM(ac.text)) END) > 1
HAVING COUNT(DISTINCT CASE WHEN af.artist_credit != '' THEN LOWER(TRIM(af.artist_credit)) END) > 1
AND COUNT(DISTINCT CASE WHEN rg.name != '' THEN LOWER(TRIM(rg.name)) END) > 1;
-- name: CountPendingTaggingItems :one
SELECT COUNT(*) FROM tagging_items
WHERE status = 'pending'
AND (CAST(@library_id AS INTEGER) = 0 OR library_id = @library_id);
-- "Needs tagging" is a question about the files, not about the row:
-- every scanned folder gets a tagging_items row (see
-- UpsertTaggingItemOnTrackAdd), including one whose files all arrived
-- carrying a recording MBID. Without the EXISTS a fully MB-tagged
-- library reports its entire album count as pending work. See the
-- same predicate on the three list queries below.
SELECT COUNT(*) FROM tagging_items ti
WHERE ti.status = 'pending'
AND (CAST(@library_id AS INTEGER) = 0 OR ti.library_id = @library_id)
AND EXISTS (
SELECT 1 FROM audio_files af
WHERE af.group_key = ti.group_key AND af.tag_status = 'untagged'
);
-- name: ListPendingTaggingItemsAlphabetical :many
SELECT
@@ -125,6 +132,18 @@ LEFT JOIN libraries lb ON lb.id = ti.library_id
WHERE (CAST(@library_id AS INTEGER) = 0 OR ti.library_id = @library_id)
AND (CAST(@status_filter AS TEXT) = 'all' OR ti.status = @status_filter)
AND ti.cleared_at IS NULL
-- Actionable rows must have something to act on: see
-- CountPendingTaggingItems. Reviewed rows (confirmed/skipped) are
-- exempt because they are history, not work -- an applied folder is
-- fully tagged by definition and would otherwise vanish from the
-- sidebar's Completed section the instant it succeeded.
AND (
ti.status IN ('confirmed', 'skipped')
OR EXISTS (
SELECT 1 FROM audio_files af
WHERE af.group_key = ti.group_key AND af.tag_status = 'untagged'
)
)
ORDER BY LOWER(ti.album_artist), LOWER(ti.album_name), ti.disc_number
LIMIT @row_limit OFFSET @row_offset;
@@ -150,6 +169,14 @@ LEFT JOIN libraries lb ON lb.id = ti.library_id
WHERE (CAST(@library_id AS INTEGER) = 0 OR ti.library_id = @library_id)
AND (CAST(@status_filter AS TEXT) = 'all' OR ti.status = @status_filter)
AND ti.cleared_at IS NULL
-- See ListPendingTaggingItemsAlphabetical.
AND (
ti.status IN ('confirmed', 'skipped')
OR EXISTS (
SELECT 1 FROM audio_files af
WHERE af.group_key = ti.group_key AND af.tag_status = 'untagged'
)
)
ORDER BY ti.score IS NULL, ti.score DESC, LOWER(ti.album_artist), LOWER(ti.album_name)
LIMIT @row_limit OFFSET @row_offset;
@@ -204,61 +231,53 @@ ORDER BY ti.created_at DESC, ti.group_key
LIMIT @row_limit OFFSET @row_offset;
-- name: ListAudioFilesInTaggingGroup :many
-- album_name/album_artist are the PER-TRACK tags (via each track's
-- own release_group link), not the folder-level tagging_items
-- values. SplitMixedFolder clusters on these to find sub-albums
-- hiding inside a folder full of unrelated tracks.
-- album_name/album_artist are the PER-TRACK tags (each file's own
-- album link), not the folder-level tagging_items values.
-- SplitMixedFolder clusters on these to find sub-albums hiding inside
-- a folder full of unrelated tracks.
SELECT
af.id,
af.file_path,
af.basename,
af.length_milliseconds,
af.tag_status,
COALESCE(r.track_number, 0) AS track_number,
COALESCE(r.disc_number, 0) AS disc_number,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist_name,
COALESCE(r.mbid, '') AS recording_mbid,
COALESCE(rg.name, '') AS album_name,
COALESCE(rgac.text, '') AS album_artist
COALESCE(af.track_number, 0) AS track_number,
COALESCE(af.disc_number, 0) AS disc_number,
af.title,
af.artist_credit AS artist_name,
COALESCE(af.recording_mbid, '') AS recording_mbid,
COALESCE(al.name, '') AS album_name,
COALESCE(al.artist_credit, '') AS album_artist
FROM audio_files af
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN release_group_recordings rgr ON rgr.recording_id = r.id
LEFT JOIN release_groups rg ON rg.id = rgr.release_group_id
LEFT JOIN artist_credit rgac ON rg.album_artist_credit_id = rgac.id
LEFT JOIN albums al ON al.id = af.album_id
WHERE af.group_key = ?
ORDER BY COALESCE(r.disc_number, 0),
COALESCE(r.track_number, 0),
ORDER BY COALESCE(af.disc_number, 0),
COALESCE(af.track_number, 0),
af.file_path;
-- name: ListLocalReleaseGroupCandidates :many
-- Returns one row per (release_group, track) combination for any
-- local release_group that has an MBID. Callers group these in Go
-- and filter by normalized album-name match. Joined case-insensitive
-- on name to pre-filter cheaply; Go does the real normalization.
-- name: ListLocalAlbumCandidates :many
-- One row per (album, track) for any local album carrying an MBID.
-- Callers group these in Go and filter by normalized album-name match;
-- the join is case-insensitive on name to pre-filter cheaply.
SELECT
rg.id AS release_group_id,
rg.mbid AS release_group_mbid,
rg.name AS album_name,
COALESCE(rg.year, 0) AS year,
COALESCE(ac.text, '') AS artist_credit,
COALESCE(rgr.track_number, 0) AS track_number,
COALESCE(rgr.disc_number, 0) AS disc_number,
COALESCE(r.name, '') AS track_title,
COALESCE(r.mbid, '') AS recording_mbid,
COALESCE(local_af.length_milliseconds, 0) AS length_milliseconds
FROM release_groups rg
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN recordings r ON r.id = rgr.recording_id
LEFT JOIN artist_credit ac ON rg.album_artist_credit_id = ac.id
LEFT JOIN audio_files local_af ON local_af.recording_id = r.id
WHERE rg.mbid IS NOT NULL
AND rg.mbid != ''
AND r.mbid IS NOT NULL
AND r.mbid != ''
AND rg.name = ? COLLATE NOCASE
ORDER BY rg.id, rgr.disc_number, rgr.track_number;
al.id AS album_id,
al.mbid AS album_mbid,
al.name AS album_name,
COALESCE(al.year, 0) AS year,
al.artist_credit,
COALESCE(af.track_number, 0) AS track_number,
COALESCE(af.disc_number, 0) AS disc_number,
af.title AS track_title,
COALESCE(af.recording_mbid, '') AS recording_mbid,
af.length_milliseconds
FROM albums al
JOIN audio_files af ON af.album_id = al.id
WHERE al.mbid IS NOT NULL
AND al.mbid != ''
AND af.recording_mbid IS NOT NULL
AND af.recording_mbid != ''
AND al.name = ? COLLATE NOCASE
ORDER BY al.id, af.disc_number, af.track_number;
-- name: SetTaggingItemBestMatch :exec
UPDATE tagging_items
@@ -284,17 +303,16 @@ WHERE group_key = ?;
-- name: SetAudioFileTagStatus :exec
UPDATE audio_files SET tag_status = ? WHERE id = ?;
-- name: SetRecordingMBID :exec
UPDATE recordings SET mbid = ? WHERE id = ?;
-- name: SetFileRecordingMBID :exec
UPDATE audio_files SET recording_mbid = ? WHERE id = ?;
-- name: SetReleaseGroupMBID :exec
UPDATE release_groups SET mbid = ? WHERE id = ?;
-- name: GetRecordingReleaseGroupID :one
SELECT COALESCE(rgr.release_group_id, 0) AS release_group_id
FROM release_group_recordings rgr
WHERE rgr.recording_id = ?
LIMIT 1;
-- name: SetFileAlbumMBID :exec
-- The album MBID for the album a file belongs to. Keyed by file
-- because that is what the autotag apply path holds; under the old
-- schema it had to look the release group up through two join tables
-- first (GetRecordingReleaseGroupID), which is gone.
UPDATE albums SET mbid = ?
WHERE albums.id = (SELECT af.album_id FROM audio_files af WHERE af.id = ?);
-- name: GetNextPendingTaggingItem :one
SELECT
@@ -315,5 +333,12 @@ LEFT JOIN libraries lb ON lb.id = ti.library_id
WHERE ti.status = 'pending'
AND (CAST(@library_id AS INTEGER) = 0 OR ti.library_id = @library_id)
AND ti.group_key > @after_group_key
-- See CountPendingTaggingItems: the cursor must not stop on a
-- folder the list query no longer shows, or "next" walks folders
-- that are not in the sidebar.
AND EXISTS (
SELECT 1 FROM audio_files af
WHERE af.group_key = ti.group_key AND af.tag_status = 'untagged'
)
ORDER BY ti.group_key
LIMIT 1;
+43
View File
@@ -0,0 +1,43 @@
-- One row per album in the library.
--
-- This is `release_groups` renamed, and the rename is the point: a
-- release group is a *MusicBrainz* concept and the catalog still has
-- them (`explore_index.entity_type = 'release_group'`). What this
-- table holds is the local thing — the album some files on disk belong
-- to — which may or may not have a catalog counterpart. Calling both
-- of them "release group" is most of why "is this album mine" was a
-- question three different subsystems answered three different ways.
--
-- `artist_credit` is the album artist as tagged ("Various Artists",
-- "A & B"); `artist_id` is the primary artist it resolves to. Album
-- identity is (name, artist_credit), which is what the old
-- UNIQUE(name, album_artist_credit_id) meant with a join in the way.
CREATE TABLE IF NOT EXISTS albums (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
artist_credit TEXT NOT NULL DEFAULT '',
artist_id INTEGER,
mbid TEXT,
-- year is the tagged year of the copy on disk; original_year is
-- MusicBrainz's first-release date when known. For a 2010 remaster
-- of a 1973 album: original_year 1973, year 2010.
year INTEGER,
original_year INTEGER,
cover_art_id INTEGER,
-- Set when the files carried a release MBID but no release-group
-- MBID; a background pass resolves it and clears this.
pending_release_mbid TEXT,
FOREIGN KEY(cover_art_id) REFERENCES cover_art(id),
FOREIGN KEY(artist_id) REFERENCES artists(id),
UNIQUE(name, artist_credit)
);
CREATE INDEX IF NOT EXISTS idx_albums_artist_id
ON albums(artist_id);
CREATE INDEX IF NOT EXISTS idx_albums_cover_art_id
ON albums(cover_art_id);
CREATE INDEX IF NOT EXISTS idx_albums_mbid
ON albums(mbid) WHERE mbid IS NOT NULL;
@@ -1,4 +0,0 @@
CREATE TABLE IF NOT EXISTS artist_credit (
id INTEGER PRIMARY KEY,
text TEXT NOT NULL UNIQUE
);
@@ -1,16 +0,0 @@
CREATE TABLE IF NOT EXISTS artist_credit_artist (
id integer PRIMARY KEY,
artist_id int NOT NULL,
credit_id int NOT NULL,
FOREIGN KEY(artist_id) REFERENCES artists(id),
FOREIGN KEY(credit_id) REFERENCES artist_credit(id)
);
CREATE INDEX IF NOT EXISTS idx_artist_credit_artist_artist_id
ON artist_credit_artist(artist_id);
CREATE INDEX IF NOT EXISTS idx_artist_credit_artist_credit_id
ON artist_credit_artist(credit_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_artist_credit_artist_unique
ON artist_credit_artist(artist_id, credit_id);
@@ -12,8 +12,8 @@ CREATE TABLE IF NOT EXISTS artist_images (
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_artist_images_mbid
ON artist_images(artist_mbid);
-- No index on artist_mbid alone: the UNIQUE index below has it as its
-- leftmost column.
CREATE UNIQUE INDEX IF NOT EXISTS idx_artist_images_source
ON artist_images(artist_mbid, source, source_url);
@@ -12,4 +12,6 @@ CREATE TABLE IF NOT EXISTS artist_metadata (
PRIMARY KEY (mbid, source)
);
CREATE INDEX IF NOT EXISTS idx_artist_metadata_mbid ON artist_metadata(mbid);
-- No index on mbid alone: PRIMARY KEY (mbid, source) already has it as
-- its leftmost column, so a second one costs a write per row and serves
-- no read.
+92 -27
View File
@@ -1,34 +1,92 @@
-- One row per audio file, and the file's tags live on it.
--
-- This table used to be a stub — path, format, a foreign key — with
-- every tag-derived field one join away in `recordings`, which was in
-- turn linked to an album through `release_group_recordings` and to an
-- artist through `artist_credit` + `artist_credit_artist`. That is
-- MusicBrainz's data model, and it is the right model for MusicBrainz:
-- a recording really can appear on many releases and a credit really
-- can list many artists.
--
-- It was the wrong model here, and the library said so. Measured on a
-- real 25,966-file library: **no** recording had more than one file,
-- **no** recording belonged to more than one release group, and 3 of
-- 2,823 credits listed more than one artist. Every many-to-many the
-- schema modelled was 1:1 in the data, and the cost of modelling it
-- anyway was a six-way join in every read, a `MIN(release_group_id)`
-- subquery in eleven queries to collapse a fan-out that never happened,
-- a first-credited-artist subquery in nine more to collapse the other
-- one, and — the reason this changed — a whole class of bugs where a
-- `recordings` row **outlived the file that created it**. Retagging a
-- file created a new recording and abandoned the old one, so the same
-- library carried 812 recordings, 216 release groups and 260 artists
-- with no file behind them, and everything that asked "do I own this"
-- by looking for a metadata row got 129 confident yeses for tracks
-- that could not be played.
--
-- With the tags on the file, ownership is not a rule anyone can forget:
-- the row *is* the file.
CREATE TABLE IF NOT EXISTS audio_files (
id integer PRIMARY KEY,
file_path text NOT NULL UNIQUE,
length_milliseconds int NOT NULL,
file_type_id int NOT NULL,
recording_id int NOT NULL,
sample_rate int NOT NULL DEFAULT 0,
bit_depth int NOT NULL DEFAULT 0,
channels int NOT NULL DEFAULT 0,
bitrate int NOT NULL DEFAULT 0,
file_size int NOT NULL DEFAULT 0,
basename text NOT NULL DEFAULT '',
library_id int NOT NULL DEFAULT 0,
play_count int NOT NULL DEFAULT 0,
last_played datetime,
tag_status TEXT NOT NULL DEFAULT 'untagged'
id INTEGER PRIMARY KEY,
file_path TEXT NOT NULL UNIQUE,
library_id INTEGER NOT NULL DEFAULT 0,
file_type_id INTEGER NOT NULL,
-- Audio properties, read from the file itself.
length_milliseconds INTEGER NOT NULL,
sample_rate INTEGER NOT NULL DEFAULT 0,
bit_depth INTEGER NOT NULL DEFAULT 0,
channels INTEGER NOT NULL DEFAULT 0,
bitrate INTEGER NOT NULL DEFAULT 0,
file_size INTEGER NOT NULL DEFAULT 0,
-- Tags. `artist_credit` is the credit as tagged ("A feat. B") and is
-- for display; `artist_id` is the primary artist it resolves to, and
-- is what grouping, browsing and the artist page use. Keeping both
-- is what makes the credit table unnecessary: the string is the only
-- thing that was ever read off it.
title TEXT NOT NULL DEFAULT '',
artist_credit TEXT NOT NULL DEFAULT '',
artist_id INTEGER,
album_id INTEGER,
track_number INTEGER,
disc_number INTEGER,
-- The denominator the tag declared: the 12 in "5/12", per disc. It
-- is what lets "do I have all of this album" be answered from disk
-- instead of from MusicBrainz. NULL means the tag did not say, which
-- is a third state and not the same as zero.
total_tracks INTEGER,
year INTEGER,
composer TEXT NOT NULL DEFAULT '',
comment TEXT NOT NULL DEFAULT '',
recording_mbid TEXT,
-- Library bookkeeping.
basename TEXT NOT NULL DEFAULT '',
group_key TEXT NOT NULL DEFAULT '',
-- File mtime as a Unix timestamp in seconds, captured at import and
-- compared against the on-disk mtime during a scan to detect files
-- another application retagged in place.
modified_at INTEGER NOT NULL DEFAULT 0,
play_count INTEGER NOT NULL DEFAULT 0,
last_played DATETIME,
tag_status TEXT NOT NULL DEFAULT 'untagged'
CHECK(tag_status IN (
'untagged', 'auto_matched', 'user_confirmed', 'user_skipped_permanent'
)),
group_key TEXT NOT NULL DEFAULT '',
-- File mtime as a Unix timestamp in seconds, captured at import.
-- Compared against the on-disk mtime during a scan to detect files
-- another application retagged in place. 0 means "never recorded"
-- (rows predating migration 47) and is treated as not-stale so an
-- upgrade does not re-import the whole library.
modified_at int NOT NULL DEFAULT 0,
FOREIGN KEY(file_type_id) REFERENCES file_types(id),
FOREIGN KEY(recording_id) REFERENCES recordings(id),
FOREIGN KEY(library_id) REFERENCES libraries(id)
FOREIGN KEY(library_id) REFERENCES libraries(id),
FOREIGN KEY(artist_id) REFERENCES artists(id),
FOREIGN KEY(album_id) REFERENCES albums(id)
);
CREATE INDEX IF NOT EXISTS idx_audio_files_album_id
ON audio_files(album_id);
CREATE INDEX IF NOT EXISTS idx_audio_files_artist_id
ON audio_files(artist_id);
CREATE INDEX IF NOT EXISTS idx_audio_files_basename
ON audio_files(basename);
@@ -36,10 +94,17 @@ CREATE INDEX IF NOT EXISTS idx_audio_files_group_key
ON audio_files(group_key) WHERE group_key != '';
CREATE INDEX IF NOT EXISTS idx_audio_files_library_id
ON audio_files(library_id);
ON audio_files(library_id);
CREATE INDEX IF NOT EXISTS idx_audio_files_recording_id
ON audio_files(recording_id);
-- The ownership question, asked by MBID: "is there a *file* with this
-- recording MBID". Nothing may answer it from a metadata table again.
CREATE INDEX IF NOT EXISTS idx_audio_files_recording_mbid
ON audio_files(recording_mbid) WHERE recording_mbid IS NOT NULL;
-- Answers "does this tagging group still contain untagged files" in one
-- seek per group. The autotag queue asks it once per row.
CREATE INDEX IF NOT EXISTS idx_audio_files_untagged_group_key
ON audio_files(group_key) WHERE tag_status = 'untagged';
CREATE INDEX IF NOT EXISTS idx_audio_files_tag_status_untagged
ON audio_files(library_id) WHERE tag_status = 'untagged';
+39 -7
View File
@@ -1,10 +1,25 @@
-- The downloaded MusicBrainz/ListenBrainz catalog.
--
-- MusicBrainz ids are stored as their 16 raw bytes and entity types as
-- small integers, which is a size decision: on a real 2,052,200-row
-- catalog those four columns were 220 MB of a 383 MB table and were
-- carried again in every index keyed on them, and the conversion took
-- the table and its four indexes from 677 MB to 389 MB. See
-- backend/explore/mbid.go, which is the only place that encoding is
-- known -- everything above it speaks dashed strings and entity names.
--
-- The CHECK constraints are what make a mistake loud. SQLite does not
-- coerce between TEXT and BLOB, so a query comparing this column
-- against a 36-character string returns no rows rather than an error;
-- a *write* of one fails here instead, at the insert that made it.
CREATE TABLE IF NOT EXISTS explore_index (
id INTEGER PRIMARY KEY AUTOINCREMENT,
entity_type TEXT NOT NULL,
mbid TEXT NOT NULL,
entity_type INTEGER NOT NULL,
mbid BLOB NOT NULL CHECK(length(mbid) = 16),
title TEXT NOT NULL,
artist_name TEXT NOT NULL,
artist_mbid TEXT NOT NULL,
artist_mbid BLOB NOT NULL
CHECK(length(artist_mbid) IN (0, 16)),
aliases TEXT NOT NULL DEFAULT '',
-- Popularity signals, derived from the ListenBrainz listens dump.
@@ -13,7 +28,8 @@ CREATE TABLE IF NOT EXISTS explore_index (
-- Recording-specific fields.
duration INTEGER NOT NULL DEFAULT 0,
caa_release_mbid TEXT NOT NULL DEFAULT '',
caa_release_mbid BLOB NOT NULL DEFAULT x''
CHECK(length(caa_release_mbid) IN (0, 16)),
release_name TEXT NOT NULL DEFAULT '',
-- Release-group-specific fields.
@@ -21,6 +37,13 @@ CREATE TABLE IF NOT EXISTS explore_index (
secondary_types TEXT NOT NULL DEFAULT '',
release_date TEXT NOT NULL DEFAULT '',
-- How many tracks the release group's canonical release has, so
-- "do I have all of this" is answerable offline for an album the
-- library holds no tags for. Zero means the catalog does not say,
-- which is the same third state the local answer has -- and is what
-- every row carries until a central dump build fills it.
total_tracks INTEGER NOT NULL DEFAULT 0,
-- Artist-specific fields.
artist_type TEXT NOT NULL DEFAULT '',
country TEXT NOT NULL DEFAULT '',
@@ -46,13 +69,22 @@ CREATE TABLE IF NOT EXISTS explore_index (
UNIQUE(mbid)
);
-- The exact-match tier's two indexes.
--
-- Their predicate is the champion set - the popular rows plus whatever
-- the user owns - and matching it to `ExactMatches`' own WHERE clause is
-- what makes them small. They used to say `popularity > 0`, which on a
-- real 2,052,200-row catalog covered 2,046,645 of them: a full index
-- wearing a partial index's clothes, 101 MB for the pair. Narrowed to
-- the set the tier can actually return, they are 3 MB and the query
-- plan is unchanged (measured, on that catalog).
CREATE INDEX IF NOT EXISTS idx_explore_artist_lower
ON explore_index(LOWER(artist_name))
WHERE popularity > 0;
WHERE popularity >= 10000 OR in_library = 1;
CREATE INDEX IF NOT EXISTS idx_explore_caa_release
ON explore_index(caa_release_mbid)
WHERE entity_type = 'release_group' AND caa_release_mbid != '';
WHERE entity_type = 2 AND caa_release_mbid != x'';
CREATE INDEX IF NOT EXISTS idx_explore_index_artist_mbid
ON explore_index(artist_mbid, entity_type, popularity DESC);
@@ -62,4 +94,4 @@ CREATE INDEX IF NOT EXISTS idx_explore_index_entity_pop
CREATE INDEX IF NOT EXISTS idx_explore_title_lower
ON explore_index(LOWER(title))
WHERE popularity > 0;
WHERE popularity >= 10000 OR in_library = 1;
@@ -0,0 +1,18 @@
-- Genres per file. This is `recording_genres` with the recording taken
-- out of the middle: it is the one many-to-many in the local library
-- that is actually many-to-many (a real library runs about four genre
-- rows per file), which is why it stays a join table when the others
-- did not.
CREATE TABLE IF NOT EXISTS file_genres (
audio_file_id INTEGER NOT NULL,
genre_id INTEGER NOT NULL,
PRIMARY KEY (audio_file_id, genre_id),
FOREIGN KEY(audio_file_id) REFERENCES audio_files(id) ON DELETE CASCADE,
FOREIGN KEY(genre_id) REFERENCES genres(id)
) WITHOUT ROWID;
-- The reverse direction ("which files are in this genre"). The
-- forward direction is served by the primary key, so — unlike the
-- table this replaces — there is no third index restating it.
CREATE INDEX IF NOT EXISTS idx_file_genres_genre_id
ON file_genres(genre_id);
+6 -10
View File
@@ -1,15 +1,11 @@
-- One row per "go find me this", from the moment the user asks until
-- the files are in the library or the attempt is abandoned.
-- One row per folder the user has added as a music library.
--
-- release_mbid / release_group_mbid are the anchor: a request that
-- carries one can be matched against a known tracklist at import time,
-- which is what makes unattended completion safe. Free-text requests
-- (both NULL) are always presented to the user for confirmation.
-- Everything else keyed by library_id means "which of these folders did
-- this come from"; a library_id of 0 in a query means "all of them".
--
-- `expected` caches the anchor's tracklist as JSON so ranking and
-- import do not have to re-resolve it, and so a request survives the
-- explore index being rebuilt underneath it.
-- autotag_warning_acked records that the user has been told what
-- autotagging will do to the files in this folder, which is a decision
-- they made and not something a rescan can rediscover.
CREATE TABLE IF NOT EXISTS libraries (
id INTEGER PRIMARY KEY,
+25
View File
@@ -0,0 +1,25 @@
-- Lyrics for a file, and where they came from.
--
-- These used to be a column on `recordings`, in a table classified
-- `Owned` — data a rescan can rebuild from the files. That was true of
-- lyrics read out of a USLT frame and false of lyrics fetched from
-- LRCLIB, and nothing recorded which was which, so a library with
-- 24,294 of them could not answer how many were free to rebuild and how
-- many were network traffic waiting to happen. `source` answers it.
--
-- `recording_mbid` is carried alongside the file id so a future
-- re-import can re-adopt fetched lyrics without asking LRCLIB again;
-- the file id is the key because untagged files have no MBID and are
-- exactly the ones whose lyrics had to be fetched.
CREATE TABLE IF NOT EXISTS lyrics (
audio_file_id INTEGER PRIMARY KEY,
text TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'tag'
CHECK(source IN ('tag', 'lrclib')),
recording_mbid TEXT,
fetched_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(audio_file_id) REFERENCES audio_files(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_lyrics_recording_mbid
ON lyrics(recording_mbid) WHERE recording_mbid IS NOT NULL;
@@ -1,14 +0,0 @@
CREATE TABLE IF NOT EXISTS recording_genres (
id INTEGER PRIMARY KEY,
recording_id INTEGER NOT NULL,
genre_id INTEGER NOT NULL,
FOREIGN KEY(recording_id) REFERENCES recordings(id),
FOREIGN KEY(genre_id) REFERENCES genres(id),
UNIQUE(recording_id, genre_id)
);
CREATE INDEX IF NOT EXISTS idx_recording_genres_genre_id
ON recording_genres(genre_id);
CREATE INDEX IF NOT EXISTS idx_recording_genres_recording_id
ON recording_genres(recording_id);
@@ -1,19 +0,0 @@
CREATE TABLE IF NOT EXISTS recordings (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
artist_credit_id INTEGER NOT NULL,
track_number INTEGER,
disc_number INTEGER,
year INTEGER,
genre TEXT,
composer TEXT,
lyrics TEXT,
comment TEXT,
mbid TEXT,
FOREIGN KEY(artist_credit_id) REFERENCES artist_credit(id)
);
CREATE INDEX IF NOT EXISTS idx_recordings_artist_credit_id
ON recordings(artist_credit_id);
CREATE INDEX IF NOT EXISTS idx_recordings_mbid ON recordings(mbid) WHERE mbid IS NOT NULL;
@@ -1,21 +0,0 @@
CREATE TABLE IF NOT EXISTS release_group_recordings (
id INTEGER PRIMARY KEY,
release_group_id INTEGER NOT NULL,
recording_id INTEGER NOT NULL,
track_number INTEGER,
disc_number INTEGER,
-- The denominator the file's own tag declared: the 12 in "5/12", per
-- disc. Read off every file at scan and, until now, discarded — so
-- "do I have all of this album" had no local answer and the album
-- page asked MusicBrainz. NULL means the tag did not say, which is
-- a third state and not the same as zero.
total_tracks INTEGER,
FOREIGN KEY(release_group_id) REFERENCES release_groups(id),
FOREIGN KEY(recording_id) REFERENCES recordings(id)
);
CREATE INDEX IF NOT EXISTS idx_release_group_recordings_recording_id
ON release_group_recordings(recording_id);
CREATE INDEX IF NOT EXISTS idx_release_group_recordings_release_group_id
ON release_group_recordings(release_group_id);
@@ -1,20 +0,0 @@
CREATE TABLE IF NOT EXISTS "release_groups" (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
cover_art_id INTEGER,
album_artist_credit_id INTEGER,
year INTEGER,
total_tracks INTEGER,
total_discs INTEGER, mbid TEXT, original_year INTEGER, pending_release_mbid TEXT,
FOREIGN KEY(cover_art_id) REFERENCES cover_art(id),
FOREIGN KEY(album_artist_credit_id) REFERENCES artist_credit(id),
UNIQUE(name, album_artist_credit_id)
);
CREATE INDEX IF NOT EXISTS idx_release_groups_album_artist_credit_id
ON release_groups(album_artist_credit_id);
CREATE INDEX IF NOT EXISTS idx_release_groups_cover_art_id
ON release_groups(cover_art_id);
CREATE INDEX IF NOT EXISTS idx_release_groups_mbid ON release_groups(mbid) WHERE mbid IS NOT NULL;
@@ -1,4 +1,11 @@
-- Release MBID -> release-group MBID, captured during a dump import.
--
-- It is empty on an ordinary install and looks droppable for that
-- reason: only a local dump build (`indexbuild`) fills it. The daily
-- incremental refresh reads it to roll per-release listen counts up to
-- the release group they belong to, so an install that has built its
-- own index does need it.
CREATE TABLE IF NOT EXISTS release_to_rg (
release_mbid TEXT PRIMARY KEY,
rg_mbid TEXT NOT NULL
) WITHOUT ROWID;
) WITHOUT ROWID;
@@ -6,5 +6,5 @@ CREATE TABLE IF NOT EXISTS similar_artist_map (
PRIMARY KEY (source_artist_mbid, similar_artist_mbid)
);
CREATE INDEX IF NOT EXISTS idx_similar_artist_map_source
ON similar_artist_map(source_artist_mbid);
-- No index on source_artist_mbid alone: the PRIMARY KEY has it as its
-- leftmost column.
+40 -28
View File
@@ -1,23 +1,42 @@
CREATE VIEW IF NOT EXISTS track_metadata AS
-- The one definition of "a track, with everything a list needs".
--
-- A view is a definition, not data, so it is dropped and recreated on
-- every open rather than carrying a migration alongside it: CREATE VIEW
-- IF NOT EXISTS silently keeps an older database on the old definition,
-- and a migration file restating it would be the second description of
-- the schema the migration rules exist to prevent.
--
-- This projection used to exist **nine times** — four copies in
-- audio_files.sql, two in playlists.sql, two in genres.sql, one in
-- queue.sql — plus this view, which only the raw-SQL search paths used.
-- They had already drifted: this view preferred the album's
-- original_year for `year` and GetAllTracksWithFullMetadata used the
-- track's own, so the same library reported different years on
-- different screens. Every query that wants a track row now selects
-- from here, which is also why there is one row type and one mapper on
-- the Go side instead of nine and a twenty-two-argument function.
DROP VIEW IF EXISTS track_metadata;
CREATE VIEW track_metadata AS
SELECT
af.id,
af.file_path,
af.length_milliseconds,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist_name,
r.track_number,
r.disc_number,
COALESCE(rg.name, '') AS album,
af.title,
af.artist_credit AS artist_name,
af.track_number,
af.disc_number,
COALESCE(al.name, '') AS album,
CAST(COALESCE(
(SELECT GROUP_CONCAT(g.name, '||')
FROM recording_genres rg_sub
JOIN genres g ON rg_sub.genre_id = g.id
WHERE rg_sub.recording_id = r.id),
FROM file_genres fg
JOIN genres g ON g.id = fg.genre_id
WHERE fg.audio_file_id = af.id),
''
) AS TEXT) AS genre,
COALESCE(rg.original_year, rg.year, r.year, 0) AS year,
COALESCE(rg.year, r.year, 0) AS release_year,
COALESCE(r.composer, '') AS composer,
COALESCE(al.original_year, al.year, af.year, 0) AS year,
COALESCE(al.year, af.year, 0) AS release_year,
af.composer,
COALESCE(ft.extension, '') AS file_type,
af.sample_rate,
af.bit_depth,
@@ -28,20 +47,13 @@ CREATE VIEW IF NOT EXISTS track_metadata AS
af.play_count,
af.last_played,
COALESCE(ca.file_path, '') AS cover_art_path,
COALESCE(a.mbid, '') AS artist_mbid,
COALESCE(rg.mbid, '') AS release_group_mbid,
COALESCE(r.mbid, '') AS recording_mbid
COALESCE(ar.mbid, '') AS artist_mbid,
COALESCE(al.mbid, '') AS release_group_mbid,
COALESCE(af.recording_mbid, '') AS recording_mbid,
af.album_id,
af.artist_id
FROM audio_files af
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN artist_credit_artist aca ON aca.credit_id = ac.id
LEFT JOIN artists a ON a.id = aca.artist_id
LEFT JOIN (
SELECT recording_id,
MIN(release_group_id) AS release_group_id
FROM release_group_recordings
GROUP BY recording_id
) rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
LEFT JOIN file_types ft ON af.file_type_id = ft.id;
LEFT JOIN albums al ON al.id = af.album_id
LEFT JOIN artists ar ON ar.id = af.artist_id
LEFT JOIN cover_art ca ON ca.id = al.cover_art_id
LEFT JOIN file_types ft ON ft.id = af.file_type_id;
+426
View File
@@ -0,0 +1,426 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: albums.sql
package sqlcgen
import (
"context"
"database/sql"
)
const deleteAlbum = `-- name: DeleteAlbum :exec
DELETE FROM albums WHERE id = ?
`
func (q *Queries) DeleteAlbum(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, deleteAlbum, id)
return err
}
const deleteAllAlbums = `-- name: DeleteAllAlbums :exec
DELETE FROM albums
`
func (q *Queries) DeleteAllAlbums(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteAllAlbums)
return err
}
const getAlbum = `-- name: GetAlbum :one
SELECT id, name, artist_credit, artist_id, mbid, year, original_year, cover_art_id, pending_release_mbid FROM albums WHERE id = ? LIMIT 1
`
func (q *Queries) GetAlbum(ctx context.Context, id int64) (Album, error) {
row := q.db.QueryRowContext(ctx, getAlbum, id)
var i Album
err := row.Scan(
&i.ID,
&i.Name,
&i.ArtistCredit,
&i.ArtistID,
&i.Mbid,
&i.Year,
&i.OriginalYear,
&i.CoverArtID,
&i.PendingReleaseMbid,
)
return i, err
}
const getAlbumCompleteness = `-- name: GetAlbumCompleteness :one
SELECT
-- Distinct (disc, track) pairs: this app detects duplicates, and
-- counting two files of track 3 twice would report a short album as
-- complete. A file with no track number falls back to its own id,
-- because three untagged files are three tracks, not one.
CAST(COUNT(DISTINCT CAST(COALESCE(a.disc_number, 1) AS TEXT) || ':' ||
COALESCE(CAST(a.track_number AS TEXT), 'f' || a.id)
) AS INTEGER) AS owned,
CAST(COALESCE((
SELECT SUM(per_disc.total)
FROM (
SELECT MAX(b.total_tracks) AS total
FROM audio_files b
WHERE b.album_id = ?1 AND b.total_tracks IS NOT NULL
GROUP BY COALESCE(b.disc_number, 1)
) per_disc
), 0) AS INTEGER) AS expected,
CAST((
SELECT COUNT(*) = 0 FROM audio_files c
WHERE c.album_id = ?1 AND c.total_tracks IS NULL
) AS INTEGER) AS known
FROM audio_files a
WHERE a.album_id = ?1
`
type GetAlbumCompletenessRow struct {
Owned int64
Expected int64
Known int64
}
// "Do I have all of this album", answered from the tags on disk.
//
// The expectation is a **sum over discs**, not one number: totals are
// declared per disc ("5/12" on disc 2 means 12 tracks on disc 2), so a
// multi-disc album's expectation is the sum of each disc's declared
// total. A disc whose files declared nothing leaves the whole album
// unknowable rather than being covered by the discs that did -- which is
// what `known` reports.
//
// Owned counts DISTINCT track numbers: this app detects duplicates, and
// counting two files of track 3 twice would report a short album as
// complete.
func (q *Queries) GetAlbumCompleteness(ctx context.Context, albumID sql.NullInt64) (GetAlbumCompletenessRow, error) {
row := q.db.QueryRowContext(ctx, getAlbumCompleteness, albumID)
var i GetAlbumCompletenessRow
err := row.Scan(&i.Owned, &i.Expected, &i.Known)
return i, err
}
const getAlbums = `-- name: GetAlbums :many
SELECT
al.id,
al.name,
COALESCE(al.original_year, al.year) AS year,
COALESCE(al.year, 0) AS release_year,
al.mbid,
al.artist_credit AS artist_name,
CAST(COALESCE(ar.mbid, '') AS TEXT) AS artist_mbid,
COALESCE(ca.file_path, '') AS cover_art_path
FROM albums al
LEFT JOIN artists ar ON ar.id = al.artist_id
LEFT JOIN cover_art ca ON ca.id = al.cover_art_id
WHERE EXISTS (
SELECT 1 FROM audio_files af
WHERE af.album_id = al.id
AND af.library_id = COALESCE(NULLIF(CAST(?1 AS INTEGER), 0), af.library_id)
)
ORDER BY al.name
`
type GetAlbumsRow struct {
ID int64
Name string
Year sql.NullInt64
ReleaseYear int64
Mbid sql.NullString
ArtistName string
ArtistMbid string
CoverArtPath string
}
func (q *Queries) GetAlbums(ctx context.Context, libraryID int64) ([]GetAlbumsRow, error) {
rows, err := q.db.QueryContext(ctx, getAlbums, libraryID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAlbumsRow
for rows.Next() {
var i GetAlbumsRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Year,
&i.ReleaseYear,
&i.Mbid,
&i.ArtistName,
&i.ArtistMbid,
&i.CoverArtPath,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAlbumsByArtistName = `-- name: GetAlbumsByArtistName :many
SELECT
al.id,
al.name,
COALESCE(al.original_year, al.year) AS year,
COALESCE(al.year, 0) AS release_year,
al.mbid,
al.artist_credit AS artist_name,
CAST(COALESCE(ar.mbid, '') AS TEXT) AS artist_mbid,
COALESCE(ca.file_path, '') AS cover_art_path
FROM albums al
LEFT JOIN artists ar ON ar.id = al.artist_id
LEFT JOIN cover_art ca ON ca.id = al.cover_art_id
WHERE (al.artist_credit = ?1 OR ar.name = ?1)
AND EXISTS (
SELECT 1 FROM audio_files af
WHERE af.album_id = al.id
AND af.library_id = COALESCE(NULLIF(CAST(?2 AS INTEGER), 0), af.library_id)
)
ORDER BY year, al.name
`
type GetAlbumsByArtistNameParams struct {
Artist string
LibraryID int64
}
type GetAlbumsByArtistNameRow struct {
ID int64
Name string
Year sql.NullInt64
ReleaseYear int64
Mbid sql.NullString
ArtistName string
ArtistMbid string
CoverArtPath string
}
func (q *Queries) GetAlbumsByArtistName(ctx context.Context, arg GetAlbumsByArtistNameParams) ([]GetAlbumsByArtistNameRow, error) {
rows, err := q.db.QueryContext(ctx, getAlbumsByArtistName, arg.Artist, arg.LibraryID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAlbumsByArtistNameRow
for rows.Next() {
var i GetAlbumsByArtistNameRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Year,
&i.ReleaseYear,
&i.Mbid,
&i.ArtistName,
&i.ArtistMbid,
&i.CoverArtPath,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAlbumsWithPendingReleaseMBID = `-- name: GetAlbumsWithPendingReleaseMBID :many
SELECT id, pending_release_mbid FROM albums
WHERE pending_release_mbid IS NOT NULL AND pending_release_mbid != ''
AND (mbid IS NULL OR mbid = '')
`
type GetAlbumsWithPendingReleaseMBIDRow struct {
ID int64
PendingReleaseMbid sql.NullString
}
func (q *Queries) GetAlbumsWithPendingReleaseMBID(ctx context.Context) ([]GetAlbumsWithPendingReleaseMBIDRow, error) {
rows, err := q.db.QueryContext(ctx, getAlbumsWithPendingReleaseMBID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAlbumsWithPendingReleaseMBIDRow
for rows.Next() {
var i GetAlbumsWithPendingReleaseMBIDRow
if err := rows.Scan(&i.ID, &i.PendingReleaseMbid); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getEmptyAlbumIDs = `-- name: GetEmptyAlbumIDs :many
SELECT id FROM albums al
WHERE NOT EXISTS (
SELECT 1 FROM audio_files af WHERE af.album_id = al.id
)
`
// Albums with no file left behind them. Under the old schema this was
// one of three orphan sweeps that had to run by hand and did not;
// audio_files is the only thing that can leave an album empty now, so
// this is the whole of it.
func (q *Queries) GetEmptyAlbumIDs(ctx context.Context) ([]int64, error) {
rows, err := q.db.QueryContext(ctx, getEmptyAlbumIDs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const resolveAlbumPendingReleaseMBID = `-- name: ResolveAlbumPendingReleaseMBID :exec
UPDATE albums
SET mbid = ?, pending_release_mbid = NULL
WHERE id = ? AND (mbid IS NULL OR mbid = '')
`
type ResolveAlbumPendingReleaseMBIDParams struct {
Mbid sql.NullString
ID int64
}
// Clears the pending marker once the release-group MBID it stood in for
// has been resolved. Guarded so a real MBID is never overwritten.
func (q *Queries) ResolveAlbumPendingReleaseMBID(ctx context.Context, arg ResolveAlbumPendingReleaseMBIDParams) error {
_, err := q.db.ExecContext(ctx, resolveAlbumPendingReleaseMBID, arg.Mbid, arg.ID)
return err
}
const setAlbumCoverArt = `-- name: SetAlbumCoverArt :exec
UPDATE albums SET cover_art_id = ? WHERE id = ?
`
type SetAlbumCoverArtParams struct {
CoverArtID sql.NullInt64
ID int64
}
func (q *Queries) SetAlbumCoverArt(ctx context.Context, arg SetAlbumCoverArtParams) error {
_, err := q.db.ExecContext(ctx, setAlbumCoverArt, arg.CoverArtID, arg.ID)
return err
}
const setAlbumMBID = `-- name: SetAlbumMBID :exec
UPDATE albums SET mbid = ? WHERE id = ?
`
type SetAlbumMBIDParams struct {
Mbid sql.NullString
ID int64
}
func (q *Queries) SetAlbumMBID(ctx context.Context, arg SetAlbumMBIDParams) error {
_, err := q.db.ExecContext(ctx, setAlbumMBID, arg.Mbid, arg.ID)
return err
}
const setAlbumOriginalYear = `-- name: SetAlbumOriginalYear :exec
UPDATE albums SET original_year = ? WHERE id = ?
`
type SetAlbumOriginalYearParams struct {
OriginalYear sql.NullInt64
ID int64
}
func (q *Queries) SetAlbumOriginalYear(ctx context.Context, arg SetAlbumOriginalYearParams) error {
_, err := q.db.ExecContext(ctx, setAlbumOriginalYear, arg.OriginalYear, arg.ID)
return err
}
const setAlbumPendingReleaseMBID = `-- name: SetAlbumPendingReleaseMBID :exec
UPDATE albums SET pending_release_mbid = ? WHERE id = ?
`
type SetAlbumPendingReleaseMBIDParams struct {
PendingReleaseMbid sql.NullString
ID int64
}
func (q *Queries) SetAlbumPendingReleaseMBID(ctx context.Context, arg SetAlbumPendingReleaseMBIDParams) error {
_, err := q.db.ExecContext(ctx, setAlbumPendingReleaseMBID, arg.PendingReleaseMbid, arg.ID)
return err
}
const upsertAlbum = `-- name: UpsertAlbum :one
INSERT INTO albums (name, artist_credit, artist_id, year, cover_art_id)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(name, artist_credit) DO UPDATE SET
artist_id = COALESCE(excluded.artist_id, albums.artist_id),
year = COALESCE(excluded.year, albums.year),
cover_art_id = COALESCE(excluded.cover_art_id, albums.cover_art_id)
RETURNING id, name, artist_credit, artist_id, mbid, year, original_year, cover_art_id, pending_release_mbid
`
type UpsertAlbumParams struct {
Name string
ArtistCredit string
ArtistID sql.NullInt64
Year sql.NullInt64
CoverArtID sql.NullInt64
}
// Queries over albums (formerly release_groups).
//
// The two-copy pattern is gone here too: one query answers both the
// whole-library and the single-library case. The `fallback_ac`
// subquery every album read used to carry -- "if the album has no album
// artist credit, borrow one from any of its recordings" -- is gone with
// it, because the album carries its own credit text now.
func (q *Queries) UpsertAlbum(ctx context.Context, arg UpsertAlbumParams) (Album, error) {
row := q.db.QueryRowContext(ctx, upsertAlbum,
arg.Name,
arg.ArtistCredit,
arg.ArtistID,
arg.Year,
arg.CoverArtID,
)
var i Album
err := row.Scan(
&i.ID,
&i.Name,
&i.ArtistCredit,
&i.ArtistID,
&i.Mbid,
&i.Year,
&i.OriginalYear,
&i.CoverArtID,
&i.PendingReleaseMbid,
)
return i, err
}
@@ -1,140 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: artist_credit.sql
package sqlcgen
import (
"context"
)
const countArtistCreditReferences = `-- name: CountArtistCreditReferences :one
SELECT
(SELECT COUNT(*) FROM recordings WHERE artist_credit_id = ?1) +
(SELECT COUNT(*) FROM release_groups WHERE album_artist_credit_id = ?1)
AS total
`
func (q *Queries) CountArtistCreditReferences(ctx context.Context, artistCreditID int64) (int64, error) {
row := q.db.QueryRowContext(ctx, countArtistCreditReferences, artistCreditID)
var total int64
err := row.Scan(&total)
return total, err
}
const createArtistCredit = `-- name: CreateArtistCredit :one
INSERT INTO artist_credit (text) VALUES (?)
RETURNING id, text
`
func (q *Queries) CreateArtistCredit(ctx context.Context, text string) (ArtistCredit, error) {
row := q.db.QueryRowContext(ctx, createArtistCredit, text)
var i ArtistCredit
err := row.Scan(&i.ID, &i.Text)
return i, err
}
const deleteAllArtistCredits = `-- name: DeleteAllArtistCredits :exec
DELETE FROM artist_credit
`
func (q *Queries) DeleteAllArtistCredits(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteAllArtistCredits)
return err
}
const deleteArtistCredit = `-- name: DeleteArtistCredit :exec
DELETE FROM artist_credit
WHERE id = ?
`
func (q *Queries) DeleteArtistCredit(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, deleteArtistCredit, id)
return err
}
const getArtistCredit = `-- name: GetArtistCredit :one
SELECT id, text FROM artist_credit
WHERE id = ? LIMIT 1
`
func (q *Queries) GetArtistCredit(ctx context.Context, id int64) (ArtistCredit, error) {
row := q.db.QueryRowContext(ctx, getArtistCredit, id)
var i ArtistCredit
err := row.Scan(&i.ID, &i.Text)
return i, err
}
const getArtistCreditByText = `-- name: GetArtistCreditByText :one
SELECT id, text FROM artist_credit
WHERE text = ? LIMIT 1
`
func (q *Queries) GetArtistCreditByText(ctx context.Context, text string) (ArtistCredit, error) {
row := q.db.QueryRowContext(ctx, getArtistCreditByText, text)
var i ArtistCredit
err := row.Scan(&i.ID, &i.Text)
return i, err
}
const getOrphanedArtistCreditIDs = `-- name: GetOrphanedArtistCreditIDs :many
SELECT ac.id FROM artist_credit ac
WHERE NOT EXISTS (SELECT 1 FROM recordings r WHERE r.artist_credit_id = ac.id)
AND NOT EXISTS (SELECT 1 FROM release_groups rg WHERE rg.album_artist_credit_id = ac.id)
`
// Artist credits no longer used by any recording or release group - run
// after orphaned recordings/release groups are deleted, so a credit
// that only existed for now-removed tracks is cleaned up too.
func (q *Queries) GetOrphanedArtistCreditIDs(ctx context.Context) ([]int64, error) {
rows, err := q.db.QueryContext(ctx, getOrphanedArtistCreditIDs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateArtistCredit = `-- name: UpdateArtistCredit :exec
UPDATE artist_credit
SET text = ?
WHERE id = ?
`
type UpdateArtistCreditParams struct {
Text string
ID int64
}
func (q *Queries) UpdateArtistCredit(ctx context.Context, arg UpdateArtistCreditParams) error {
_, err := q.db.ExecContext(ctx, updateArtistCredit, arg.Text, arg.ID)
return err
}
const upsertArtistCredit = `-- name: UpsertArtistCredit :one
INSERT INTO artist_credit (text) VALUES (?)
ON CONFLICT(text) DO UPDATE SET text = excluded.text
RETURNING id, text
`
func (q *Queries) UpsertArtistCredit(ctx context.Context, text string) (ArtistCredit, error) {
row := q.db.QueryRowContext(ctx, upsertArtistCredit, text)
var i ArtistCredit
err := row.Scan(&i.ID, &i.Text)
return i, err
}
@@ -1,85 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: artist_credit_artists.sql
package sqlcgen
import (
"context"
)
const createArtistCreditArtist = `-- name: CreateArtistCreditArtist :one
INSERT INTO artist_credit_artist (artist_id, credit_id) VALUES (?, ?)
RETURNING id, artist_id, credit_id
`
type CreateArtistCreditArtistParams struct {
ArtistID int64
CreditID int64
}
func (q *Queries) CreateArtistCreditArtist(ctx context.Context, arg CreateArtistCreditArtistParams) (ArtistCreditArtist, error) {
row := q.db.QueryRowContext(ctx, createArtistCreditArtist, arg.ArtistID, arg.CreditID)
var i ArtistCreditArtist
err := row.Scan(&i.ID, &i.ArtistID, &i.CreditID)
return i, err
}
const deleteAllArtistCreditArtists = `-- name: DeleteAllArtistCreditArtists :exec
DELETE FROM artist_credit_artist
`
func (q *Queries) DeleteAllArtistCreditArtists(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteAllArtistCreditArtists)
return err
}
const deleteArtistCreditArtist = `-- name: DeleteArtistCreditArtist :exec
DELETE FROM artist_credit_artist
WHERE id =?
`
func (q *Queries) DeleteArtistCreditArtist(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, deleteArtistCreditArtist, id)
return err
}
const deleteArtistCreditArtistByCredit = `-- name: DeleteArtistCreditArtistByCredit :exec
DELETE FROM artist_credit_artist
WHERE credit_id = ?
`
func (q *Queries) DeleteArtistCreditArtistByCredit(ctx context.Context, creditID int64) error {
_, err := q.db.ExecContext(ctx, deleteArtistCreditArtistByCredit, creditID)
return err
}
const getArtistCreditArtist = `-- name: GetArtistCreditArtist :one
SELECT id, artist_id, credit_id FROM artist_credit_artist
WHERE id = ? LIMIT 1
`
func (q *Queries) GetArtistCreditArtist(ctx context.Context, id int64) (ArtistCreditArtist, error) {
row := q.db.QueryRowContext(ctx, getArtistCreditArtist, id)
var i ArtistCreditArtist
err := row.Scan(&i.ID, &i.ArtistID, &i.CreditID)
return i, err
}
const updateArtistCreditArtist = `-- name: UpdateArtistCreditArtist :exec
UPDATE artist_credit_artist
SET artist_id = ?, credit_id = ?
WHERE id =?
`
type UpdateArtistCreditArtistParams struct {
ArtistID int64
CreditID int64
ID int64
}
func (q *Queries) UpdateArtistCreditArtist(ctx context.Context, arg UpdateArtistCreditArtistParams) error {
_, err := q.db.ExecContext(ctx, updateArtistCreditArtist, arg.ArtistID, arg.CreditID, arg.ID)
return err
}
+63 -88
View File
@@ -7,20 +7,9 @@ package sqlcgen
import (
"context"
"database/sql"
)
const createArtist = `-- name: CreateArtist :one
INSERT INTO artists (name) VALUES (?)
RETURNING id, name, mbid
`
func (q *Queries) CreateArtist(ctx context.Context, name string) (Artist, error) {
row := q.db.QueryRowContext(ctx, createArtist, name)
var i Artist
err := row.Scan(&i.ID, &i.Name, &i.Mbid)
return i, err
}
const deleteAllArtists = `-- name: DeleteAllArtists :exec
DELETE FROM artists
`
@@ -31,8 +20,7 @@ func (q *Queries) DeleteAllArtists(ctx context.Context) error {
}
const deleteArtist = `-- name: DeleteArtist :exec
DELETE FROM artists
WHERE id = ?
DELETE FROM artists WHERE id = ?
`
func (q *Queries) DeleteArtist(ctx context.Context, id int64) error {
@@ -43,56 +31,17 @@ func (q *Queries) DeleteArtist(ctx context.Context, id int64) error {
const getAlbumArtists = `-- name: GetAlbumArtists :many
SELECT DISTINCT a.id, a.name, a.mbid
FROM artists a
JOIN artist_credit_artist aca ON aca.artist_id = a.id
JOIN artist_credit ac ON ac.id = aca.credit_id
JOIN release_groups rg ON rg.album_artist_credit_id = ac.id
ORDER BY a.name
`
func (q *Queries) GetAlbumArtists(ctx context.Context) ([]Artist, error) {
rows, err := q.db.QueryContext(ctx, getAlbumArtists)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Artist
for rows.Next() {
var i Artist
if err := rows.Scan(&i.ID, &i.Name, &i.Mbid); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAlbumArtistsByLibrary = `-- name: GetAlbumArtistsByLibrary :many
SELECT DISTINCT a.id, a.name, a.mbid
FROM artists a
JOIN artist_credit_artist aca ON aca.artist_id = a.id
JOIN artist_credit ac ON ac.id = aca.credit_id
JOIN release_groups rg ON rg.album_artist_credit_id = ac.id
WHERE a.id IN (
SELECT DISTINCT aca2.artist_id
FROM artist_credit_artist aca2
JOIN artist_credit ac2 ON ac2.id = aca2.credit_id
JOIN release_groups rg2 ON rg2.album_artist_credit_id = ac2.id
JOIN release_group_recordings rgr2 ON rgr2.release_group_id = rg2.id
JOIN recordings r2 ON r2.id = rgr2.recording_id
JOIN audio_files af2 ON af2.recording_id = r2.id
WHERE af2.library_id = ?
JOIN albums al ON al.artist_id = a.id
WHERE EXISTS (
SELECT 1 FROM audio_files af
WHERE af.album_id = al.id
AND af.library_id = COALESCE(NULLIF(CAST(?1 AS INTEGER), 0), af.library_id)
)
ORDER BY a.name
`
func (q *Queries) GetAlbumArtistsByLibrary(ctx context.Context, libraryID int64) ([]Artist, error) {
rows, err := q.db.QueryContext(ctx, getAlbumArtistsByLibrary, libraryID)
func (q *Queries) GetAlbumArtists(ctx context.Context, libraryID int64) ([]Artist, error) {
rows, err := q.db.QueryContext(ctx, getAlbumArtists, libraryID)
if err != nil {
return nil, err
}
@@ -115,8 +64,7 @@ func (q *Queries) GetAlbumArtistsByLibrary(ctx context.Context, libraryID int64)
}
const getAllArtists = `-- name: GetAllArtists :many
SELECT id, name, mbid FROM artists
ORDER BY name
SELECT id, name, mbid FROM artists ORDER BY name
`
func (q *Queries) GetAllArtists(ctx context.Context) ([]Artist, error) {
@@ -143,8 +91,7 @@ func (q *Queries) GetAllArtists(ctx context.Context) ([]Artist, error) {
}
const getArtist = `-- name: GetArtist :one
SELECT id, name, mbid FROM artists
WHERE id = ? LIMIT 1
SELECT id, name, mbid FROM artists WHERE id = ? LIMIT 1
`
func (q *Queries) GetArtist(ctx context.Context, id int64) (Artist, error) {
@@ -154,9 +101,28 @@ func (q *Queries) GetArtist(ctx context.Context, id int64) (Artist, error) {
return i, err
}
const getArtistByFilePath = `-- name: GetArtistByFilePath :one
SELECT COALESCE(a.name, '') AS artist_name, COALESCE(a.mbid, '') AS artist_mbid
FROM audio_files af
LEFT JOIN artists a ON a.id = af.artist_id
WHERE af.file_path = ?
LIMIT 1
`
type GetArtistByFilePathRow struct {
ArtistName string
ArtistMbid string
}
func (q *Queries) GetArtistByFilePath(ctx context.Context, filePath string) (GetArtistByFilePathRow, error) {
row := q.db.QueryRowContext(ctx, getArtistByFilePath, filePath)
var i GetArtistByFilePathRow
err := row.Scan(&i.ArtistName, &i.ArtistMbid)
return i, err
}
const getArtistByName = `-- name: GetArtistByName :one
SELECT id, name, mbid FROM artists
WHERE name = ? LIMIT 1
SELECT id, name, mbid FROM artists WHERE name = ? LIMIT 1
`
func (q *Queries) GetArtistByName(ctx context.Context, name string) (Artist, error) {
@@ -166,18 +132,15 @@ func (q *Queries) GetArtistByName(ctx context.Context, name string) (Artist, err
return i, err
}
const getOrphanedArtistIDs = `-- name: GetOrphanedArtistIDs :many
SELECT a.id FROM artists a
WHERE NOT EXISTS (
SELECT 1 FROM artist_credit_artist aca WHERE aca.artist_id = a.id
)
const getUnreferencedArtistIDs = `-- name: GetUnreferencedArtistIDs :many
SELECT id FROM artists a
WHERE NOT EXISTS (SELECT 1 FROM audio_files af WHERE af.artist_id = a.id)
AND NOT EXISTS (SELECT 1 FROM albums al WHERE al.artist_id = a.id)
`
// Artists no longer credited on any recording or release group - left
// behind when a scan's orphan cleanup removes the audio_files that used
// to justify them, since deleting an audio_files row doesn't cascade.
func (q *Queries) GetOrphanedArtistIDs(ctx context.Context) ([]int64, error) {
rows, err := q.db.QueryContext(ctx, getOrphanedArtistIDs)
// Artists no file and no album points at any more.
func (q *Queries) GetUnreferencedArtistIDs(ctx context.Context) ([]int64, error) {
rows, err := q.db.QueryContext(ctx, getUnreferencedArtistIDs)
if err != nil {
return nil, err
}
@@ -199,30 +162,42 @@ func (q *Queries) GetOrphanedArtistIDs(ctx context.Context) ([]int64, error) {
return items, nil
}
const updateArtist = `-- name: UpdateArtist :exec
UPDATE artists
SET name = ?
WHERE id = ?
const setArtistMBID = `-- name: SetArtistMBID :exec
UPDATE artists SET mbid = ? WHERE id = ?
`
type UpdateArtistParams struct {
Name string
type SetArtistMBIDParams struct {
Mbid sql.NullString
ID int64
}
func (q *Queries) UpdateArtist(ctx context.Context, arg UpdateArtistParams) error {
_, err := q.db.ExecContext(ctx, updateArtist, arg.Name, arg.ID)
func (q *Queries) SetArtistMBID(ctx context.Context, arg SetArtistMBIDParams) error {
_, err := q.db.ExecContext(ctx, setArtistMBID, arg.Mbid, arg.ID)
return err
}
const upsertArtist = `-- name: UpsertArtist :one
INSERT INTO artists (name) VALUES (?)
ON CONFLICT(name) DO UPDATE SET name = excluded.name
INSERT INTO artists (name, mbid) VALUES (?, ?)
ON CONFLICT(name) DO UPDATE SET
mbid = COALESCE(excluded.mbid, artists.mbid)
RETURNING id, name, mbid
`
func (q *Queries) UpsertArtist(ctx context.Context, name string) (Artist, error) {
row := q.db.QueryRowContext(ctx, upsertArtist, name)
type UpsertArtistParams struct {
Name string
Mbid sql.NullString
}
// Queries over artists.
//
// An artist row is reachable two ways: as a file's primary artist
// (audio_files.artist_id) and as an album's artist (albums.artist_id).
// Both used to route through artist_credit + artist_credit_artist,
// which is how "which album artists are in library 2" came to be a
// five-join subquery inside a three-join query.
func (q *Queries) UpsertArtist(ctx context.Context, arg UpsertArtistParams) (Artist, error) {
row := q.db.QueryRowContext(ctx, upsertArtist, arg.Name, arg.Mbid)
var i Artist
err := row.Scan(&i.ID, &i.Name, &i.Mbid)
return i, err
File diff suppressed because it is too large Load Diff
+65 -355
View File
@@ -7,36 +7,9 @@ package sqlcgen
import (
"context"
"database/sql"
"strings"
)
const countGenreReferences = `-- name: CountGenreReferences :one
SELECT COUNT(*) FROM recording_genres WHERE genre_id = ?
`
func (q *Queries) CountGenreReferences(ctx context.Context, genreID int64) (int64, error) {
row := q.db.QueryRowContext(ctx, countGenreReferences, genreID)
var count int64
err := row.Scan(&count)
return count, err
}
const createRecordingGenre = `-- name: CreateRecordingGenre :exec
INSERT OR IGNORE INTO recording_genres (recording_id, genre_id)
VALUES (?, ?)
`
type CreateRecordingGenreParams struct {
RecordingID int64
GenreID int64
}
func (q *Queries) CreateRecordingGenre(ctx context.Context, arg CreateRecordingGenreParams) error {
_, err := q.db.ExecContext(ctx, createRecordingGenre, arg.RecordingID, arg.GenreID)
return err
}
const deleteAllGenres = `-- name: DeleteAllGenres :exec
DELETE FROM genres
`
@@ -46,12 +19,12 @@ func (q *Queries) DeleteAllGenres(ctx context.Context) error {
return err
}
const deleteAllRecordingGenres = `-- name: DeleteAllRecordingGenres :exec
DELETE FROM recording_genres
const deleteFileGenres = `-- name: DeleteFileGenres :exec
DELETE FROM file_genres WHERE audio_file_id = ?
`
func (q *Queries) DeleteAllRecordingGenres(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteAllRecordingGenres)
func (q *Queries) DeleteFileGenres(ctx context.Context, audioFileID int64) error {
_, err := q.db.ExecContext(ctx, deleteFileGenres, audioFileID)
return err
}
@@ -64,20 +37,12 @@ func (q *Queries) DeleteGenre(ctx context.Context, id int64) error {
return err
}
const deleteRecordingGenres = `-- name: DeleteRecordingGenres :exec
DELETE FROM recording_genres
WHERE recording_id = ?
`
func (q *Queries) DeleteRecordingGenres(ctx context.Context, recordingID int64) error {
_, err := q.db.ExecContext(ctx, deleteRecordingGenres, recordingID)
return err
}
const getAllGenresWithCounts = `-- name: GetAllGenresWithCounts :many
SELECT g.name, COUNT(rg.recording_id) AS track_count
SELECT g.name, COUNT(fg.audio_file_id) AS track_count
FROM genres g
JOIN recording_genres rg ON g.id = rg.genre_id
JOIN file_genres fg ON fg.genre_id = g.id
JOIN audio_files af ON af.id = fg.audio_file_id
WHERE af.library_id = COALESCE(NULLIF(CAST(?1 AS INTEGER), 0), af.library_id)
GROUP BY g.id, g.name
ORDER BY g.name
`
@@ -87,8 +52,8 @@ type GetAllGenresWithCountsRow struct {
TrackCount int64
}
func (q *Queries) GetAllGenresWithCounts(ctx context.Context) ([]GetAllGenresWithCountsRow, error) {
rows, err := q.db.QueryContext(ctx, getAllGenresWithCounts)
func (q *Queries) GetAllGenresWithCounts(ctx context.Context, libraryID int64) ([]GetAllGenresWithCountsRow, error) {
rows, err := q.db.QueryContext(ctx, getAllGenresWithCounts, libraryID)
if err != nil {
return nil, err
}
@@ -110,35 +75,25 @@ func (q *Queries) GetAllGenresWithCounts(ctx context.Context) ([]GetAllGenresWit
return items, nil
}
const getAllGenresWithCountsByLibrary = `-- name: GetAllGenresWithCountsByLibrary :many
SELECT g.name, COUNT(rg.recording_id) AS track_count
FROM genres g
JOIN recording_genres rg ON g.id = rg.genre_id
JOIN recordings r ON rg.recording_id = r.id
JOIN audio_files af ON af.recording_id = r.id
WHERE af.library_id = ?
GROUP BY g.id, g.name
ORDER BY g.name
const getGenreNamesByFile = `-- name: GetGenreNamesByFile :many
SELECT g.name FROM genres g
JOIN file_genres fg ON fg.genre_id = g.id
WHERE fg.audio_file_id = ?
`
type GetAllGenresWithCountsByLibraryRow struct {
Name string
TrackCount int64
}
func (q *Queries) GetAllGenresWithCountsByLibrary(ctx context.Context, libraryID int64) ([]GetAllGenresWithCountsByLibraryRow, error) {
rows, err := q.db.QueryContext(ctx, getAllGenresWithCountsByLibrary, libraryID)
func (q *Queries) GetGenreNamesByFile(ctx context.Context, audioFileID int64) ([]string, error) {
rows, err := q.db.QueryContext(ctx, getGenreNamesByFile, audioFileID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllGenresWithCountsByLibraryRow
var items []string
for rows.Next() {
var i GetAllGenresWithCountsByLibraryRow
if err := rows.Scan(&i.Name, &i.TrackCount); err != nil {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
items = append(items, i)
items = append(items, name)
}
if err := rows.Close(); err != nil {
return nil, err
@@ -149,45 +104,42 @@ func (q *Queries) GetAllGenresWithCountsByLibrary(ctx context.Context, libraryID
return items, nil
}
const getFilePathsByGenres = `-- name: GetFilePathsByGenres :many
SELECT g.name AS genre_name, af.file_path
FROM genres g
JOIN recording_genres rg ON g.id = rg.genre_id
JOIN recordings r ON rg.recording_id = r.id
JOIN audio_files af ON af.recording_id = r.id
WHERE g.name IN (/*SLICE:genre_names*/?)
ORDER BY r.name
const getGenreNamesByFilePaths = `-- name: GetGenreNamesByFilePaths :many
SELECT af.file_path, g.name
FROM audio_files af
JOIN file_genres fg ON fg.audio_file_id = af.id
JOIN genres g ON g.id = fg.genre_id
WHERE af.file_path IN (/*SLICE:paths*/?)
`
type GetFilePathsByGenresRow struct {
GenreName string
FilePath string
type GetGenreNamesByFilePathsRow struct {
FilePath string
Name string
}
// Same as GetFilePathsByReleaseGroups, for "play these genres" (perf.m2):
// one query instead of one per genre, and file paths instead of whole
// track rows, which was 6 MB over the IPC for five genres.
func (q *Queries) GetFilePathsByGenres(ctx context.Context, genreNames []string) ([]GetFilePathsByGenresRow, error) {
query := getFilePathsByGenres
// Genres for many files at once. The mix builder asked this one file
// at a time, inside two nested loops -- twelve thousand single-row
// queries to assemble one mix.
func (q *Queries) GetGenreNamesByFilePaths(ctx context.Context, paths []string) ([]GetGenreNamesByFilePathsRow, error) {
query := getGenreNamesByFilePaths
var queryParams []interface{}
if len(genreNames) > 0 {
for _, v := range genreNames {
if len(paths) > 0 {
for _, v := range paths {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:genre_names*/?", strings.Repeat(",?", len(genreNames))[1:], 1)
query = strings.Replace(query, "/*SLICE:paths*/?", strings.Repeat(",?", len(paths))[1:], 1)
} else {
query = strings.Replace(query, "/*SLICE:genre_names*/?", "NULL", 1)
query = strings.Replace(query, "/*SLICE:paths*/?", "NULL", 1)
}
rows, err := q.db.QueryContext(ctx, query, queryParams...)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetFilePathsByGenresRow
var items []GetGenreNamesByFilePathsRow
for rows.Next() {
var i GetFilePathsByGenresRow
if err := rows.Scan(&i.GenreName, &i.FilePath); err != nil {
var i GetGenreNamesByFilePathsRow
if err := rows.Scan(&i.FilePath, &i.Name); err != nil {
return nil, err
}
items = append(items, i)
@@ -201,51 +153,24 @@ func (q *Queries) GetFilePathsByGenres(ctx context.Context, genreNames []string)
return items, nil
}
const getFilePathsByGenresByLibrary = `-- name: GetFilePathsByGenresByLibrary :many
SELECT g.name AS genre_name, af.file_path
FROM genres g
JOIN recording_genres rg ON g.id = rg.genre_id
JOIN recordings r ON rg.recording_id = r.id
JOIN audio_files af ON af.recording_id = r.id
WHERE g.name IN (/*SLICE:genre_names*/?)
AND af.library_id = ?
ORDER BY r.name
const getUnusedGenreIDs = `-- name: GetUnusedGenreIDs :many
SELECT id FROM genres g
WHERE NOT EXISTS (SELECT 1 FROM file_genres fg WHERE fg.genre_id = g.id)
`
type GetFilePathsByGenresByLibraryParams struct {
GenreNames []string
LibraryID int64
}
type GetFilePathsByGenresByLibraryRow struct {
GenreName string
FilePath string
}
func (q *Queries) GetFilePathsByGenresByLibrary(ctx context.Context, arg GetFilePathsByGenresByLibraryParams) ([]GetFilePathsByGenresByLibraryRow, error) {
query := getFilePathsByGenresByLibrary
var queryParams []interface{}
if len(arg.GenreNames) > 0 {
for _, v := range arg.GenreNames {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:genre_names*/?", strings.Repeat(",?", len(arg.GenreNames))[1:], 1)
} else {
query = strings.Replace(query, "/*SLICE:genre_names*/?", "NULL", 1)
}
queryParams = append(queryParams, arg.LibraryID)
rows, err := q.db.QueryContext(ctx, query, queryParams...)
func (q *Queries) GetUnusedGenreIDs(ctx context.Context) ([]int64, error) {
rows, err := q.db.QueryContext(ctx, getUnusedGenreIDs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetFilePathsByGenresByLibraryRow
var items []int64
for rows.Next() {
var i GetFilePathsByGenresByLibraryRow
if err := rows.Scan(&i.GenreName, &i.FilePath); err != nil {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, i)
items = append(items, id)
}
if err := rows.Close(); err != nil {
return nil, err
@@ -256,247 +181,32 @@ func (q *Queries) GetFilePathsByGenresByLibrary(ctx context.Context, arg GetFile
return items, nil
}
const getGenresByRecordingID = `-- name: GetGenresByRecordingID :many
SELECT g.id, g.name
FROM genres g
JOIN recording_genres rg ON g.id = rg.genre_id
WHERE rg.recording_id = ?
const linkFileGenre = `-- name: LinkFileGenre :exec
INSERT OR IGNORE INTO file_genres (audio_file_id, genre_id) VALUES (?, ?)
`
func (q *Queries) GetGenresByRecordingID(ctx context.Context, recordingID int64) ([]Genre, error) {
rows, err := q.db.QueryContext(ctx, getGenresByRecordingID, recordingID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Genre
for rows.Next() {
var i Genre
if err := rows.Scan(&i.ID, &i.Name); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
type LinkFileGenreParams struct {
AudioFileID int64
GenreID int64
}
const getTracksByGenre = `-- name: GetTracksByGenre :many
SELECT
af.file_path,
af.length_milliseconds,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist_name,
r.track_number,
r.disc_number,
COALESCE(rlg.name, '') AS album,
CAST(COALESCE(
(SELECT GROUP_CONCAT(g2.name, '||')
FROM recording_genres rg2
JOIN genres g2 ON rg2.genre_id = g2.id
WHERE rg2.recording_id = r.id),
''
) AS TEXT) AS genre,
COALESCE(r.year, 0) AS year,
COALESCE(r.composer, '') AS composer,
COALESCE(ft.extension, '') AS file_type,
af.sample_rate,
af.bit_depth,
af.channels,
af.bitrate,
af.file_size
FROM genres g
JOIN recording_genres rg ON g.id = rg.genre_id
JOIN recordings r ON rg.recording_id = r.id
JOIN audio_files af ON af.recording_id = r.id
JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN (
SELECT recording_id,
MIN(release_group_id) AS release_group_id
FROM release_group_recordings
GROUP BY recording_id
) rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rlg ON rgr.release_group_id = rlg.id
LEFT JOIN file_types ft ON af.file_type_id = ft.id
WHERE g.name = ?
ORDER BY r.name
`
type GetTracksByGenreRow struct {
FilePath string
LengthMilliseconds int64
Title string
ArtistName string
TrackNumber sql.NullInt64
DiscNumber sql.NullInt64
Album string
Genre string
Year int64
Composer string
FileType string
SampleRate int64
BitDepth int64
Channels int64
Bitrate int64
FileSize int64
}
func (q *Queries) GetTracksByGenre(ctx context.Context, name string) ([]GetTracksByGenreRow, error) {
rows, err := q.db.QueryContext(ctx, getTracksByGenre, name)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTracksByGenreRow
for rows.Next() {
var i GetTracksByGenreRow
if err := rows.Scan(
&i.FilePath,
&i.LengthMilliseconds,
&i.Title,
&i.ArtistName,
&i.TrackNumber,
&i.DiscNumber,
&i.Album,
&i.Genre,
&i.Year,
&i.Composer,
&i.FileType,
&i.SampleRate,
&i.BitDepth,
&i.Channels,
&i.Bitrate,
&i.FileSize,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getTracksByGenreByLibrary = `-- name: GetTracksByGenreByLibrary :many
SELECT
af.file_path,
af.length_milliseconds,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist_name,
r.track_number,
r.disc_number,
COALESCE(rlg.name, '') AS album,
CAST(COALESCE(
(SELECT GROUP_CONCAT(g2.name, '||')
FROM recording_genres rg2
JOIN genres g2 ON rg2.genre_id = g2.id
WHERE rg2.recording_id = r.id),
''
) AS TEXT) AS genre,
COALESCE(r.year, 0) AS year,
COALESCE(r.composer, '') AS composer,
COALESCE(ft.extension, '') AS file_type,
af.sample_rate,
af.bit_depth,
af.channels,
af.bitrate,
af.file_size
FROM genres g
JOIN recording_genres rg ON g.id = rg.genre_id
JOIN recordings r ON rg.recording_id = r.id
JOIN audio_files af ON af.recording_id = r.id
JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN (
SELECT recording_id,
MIN(release_group_id) AS release_group_id
FROM release_group_recordings
GROUP BY recording_id
) rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rlg ON rgr.release_group_id = rlg.id
LEFT JOIN file_types ft ON af.file_type_id = ft.id
WHERE g.name = ? AND af.library_id = ?
ORDER BY r.name
`
type GetTracksByGenreByLibraryParams struct {
Name string
LibraryID int64
}
type GetTracksByGenreByLibraryRow struct {
FilePath string
LengthMilliseconds int64
Title string
ArtistName string
TrackNumber sql.NullInt64
DiscNumber sql.NullInt64
Album string
Genre string
Year int64
Composer string
FileType string
SampleRate int64
BitDepth int64
Channels int64
Bitrate int64
FileSize int64
}
func (q *Queries) GetTracksByGenreByLibrary(ctx context.Context, arg GetTracksByGenreByLibraryParams) ([]GetTracksByGenreByLibraryRow, error) {
rows, err := q.db.QueryContext(ctx, getTracksByGenreByLibrary, arg.Name, arg.LibraryID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTracksByGenreByLibraryRow
for rows.Next() {
var i GetTracksByGenreByLibraryRow
if err := rows.Scan(
&i.FilePath,
&i.LengthMilliseconds,
&i.Title,
&i.ArtistName,
&i.TrackNumber,
&i.DiscNumber,
&i.Album,
&i.Genre,
&i.Year,
&i.Composer,
&i.FileType,
&i.SampleRate,
&i.BitDepth,
&i.Channels,
&i.Bitrate,
&i.FileSize,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
func (q *Queries) LinkFileGenre(ctx context.Context, arg LinkFileGenreParams) error {
_, err := q.db.ExecContext(ctx, linkFileGenre, arg.AudioFileID, arg.GenreID)
return err
}
const upsertGenre = `-- name: UpsertGenre :one
INSERT INTO genres (name) VALUES (?)
ON CONFLICT(name) DO UPDATE SET name = name
ON CONFLICT(name) DO UPDATE SET name = excluded.name
RETURNING id, name
`
// Queries over genres and file_genres.
//
// The track-returning ones live in audio_files.sql with the rest of the
// track_metadata reads; what is left here is the genre list itself and
// the link table's writes.
func (q *Queries) UpsertGenre(ctx context.Context, name string) (Genre, error) {
row := q.db.QueryRowContext(ctx, upsertGenre, name)
var i Genre
+25 -33
View File
@@ -12,10 +12,10 @@ import (
const homeAlbumsByGenre = `-- name: HomeAlbumsByGenre :many
SELECT rg.id AS album_id
FROM release_groups rg
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN recording_genres rgen ON rgen.recording_id = rgr.recording_id
JOIN genres g ON g.id = rgen.genre_id
FROM albums rg
JOIN audio_files af ON af.album_id = rg.id
JOIN file_genres fg ON fg.audio_file_id = af.id
JOIN genres g ON g.id = fg.genre_id
WHERE g.name = ?
GROUP BY rg.id
ORDER BY RANDOM()
@@ -54,9 +54,8 @@ func (q *Queries) HomeAlbumsByGenre(ctx context.Context, arg HomeAlbumsByGenrePa
const homeMostPlayedAlbums = `-- name: HomeMostPlayedAlbums :many
SELECT rg.id AS album_id
FROM release_groups rg
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN audio_files af ON af.recording_id = rgr.recording_id
FROM albums rg
JOIN audio_files af ON af.album_id = rg.id
GROUP BY rg.id
HAVING SUM(af.play_count) > 0
ORDER BY SUM(af.play_count) DESC
@@ -89,9 +88,8 @@ func (q *Queries) HomeMostPlayedAlbums(ctx context.Context, limit int64) ([]int6
const homeRandomAlbums = `-- name: HomeRandomAlbums :many
SELECT rg.id AS album_id
FROM release_groups rg
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN audio_files af ON af.recording_id = rgr.recording_id
FROM albums rg
JOIN audio_files af ON af.album_id = rg.id
GROUP BY rg.id
ORDER BY RANDOM()
LIMIT ?
@@ -122,9 +120,8 @@ func (q *Queries) HomeRandomAlbums(ctx context.Context, limit int64) ([]int64, e
const homeRecentlyAddedAlbums = `-- name: HomeRecentlyAddedAlbums :many
SELECT rg.id AS album_id
FROM release_groups rg
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN audio_files af ON af.recording_id = rgr.recording_id
FROM albums rg
JOIN audio_files af ON af.album_id = rg.id
GROUP BY rg.id
ORDER BY MAX(af.id) DESC
LIMIT ?
@@ -159,9 +156,8 @@ func (q *Queries) HomeRecentlyAddedAlbums(ctx context.Context, limit int64) ([]i
const homeRecentlyPlayedAlbums = `-- name: HomeRecentlyPlayedAlbums :many
SELECT rg.id AS album_id
FROM release_groups rg
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN audio_files af ON af.recording_id = rgr.recording_id
FROM albums rg
JOIN audio_files af ON af.album_id = rg.id
WHERE af.last_played IS NOT NULL
GROUP BY rg.id
ORDER BY MAX(af.last_played) DESC
@@ -172,7 +168,7 @@ LIMIT ?
//
// Every one of these returns album ids and nothing else. The display
// columns (cover art, artist credit, year) already have exactly one
// correct expression of them, in GetAllAlbumsWithDetails, and a second
// correct expression of them, in GetAlbums, and a second
// copy per shelf would be six more places for that to drift. The home
// service joins the ids back to that one album list in Go.
// Albums with the most recent play, newest first.
@@ -201,9 +197,8 @@ func (q *Queries) HomeRecentlyPlayedAlbums(ctx context.Context, limit int64) ([]
const homeStaleAlbums = `-- name: HomeStaleAlbums :many
SELECT rg.id AS album_id
FROM release_groups rg
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN audio_files af ON af.recording_id = rgr.recording_id
FROM albums rg
JOIN audio_files af ON af.album_id = rg.id
WHERE af.last_played IS NOT NULL
GROUP BY rg.id
HAVING MAX(af.last_played) < datetime('now', ?)
@@ -242,14 +237,12 @@ func (q *Queries) HomeStaleAlbums(ctx context.Context, arg HomeStaleAlbumsParams
const homeTopArtists = `-- name: HomeTopArtists :many
SELECT
COALESCE(ac.text, '') AS artist_name,
rg.artist_credit AS artist_name,
SUM(af.play_count) AS plays
FROM release_groups rg
JOIN artist_credit ac ON ac.id = rg.album_artist_credit_id
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN audio_files af ON af.recording_id = rgr.recording_id
WHERE ac.text <> ''
GROUP BY ac.text
FROM albums rg
JOIN audio_files af ON af.album_id = rg.id
WHERE rg.artist_credit <> ''
GROUP BY rg.artist_credit
HAVING plays > 0
ORDER BY plays DESC
LIMIT ?
@@ -288,10 +281,10 @@ func (q *Queries) HomeTopArtists(ctx context.Context, limit int64) ([]HomeTopArt
const homeTopGenres = `-- name: HomeTopGenres :many
SELECT
g.name AS genre,
COUNT(DISTINCT rgr.release_group_id) AS album_count
COUNT(DISTINCT af.album_id) AS album_count
FROM genres g
JOIN recording_genres rgen ON rgen.genre_id = g.id
JOIN release_group_recordings rgr ON rgr.recording_id = rgen.recording_id
JOIN file_genres fg ON fg.genre_id = g.id
JOIN audio_files af ON af.id = fg.audio_file_id
GROUP BY g.id
HAVING album_count >= 3
ORDER BY album_count DESC
@@ -331,9 +324,8 @@ func (q *Queries) HomeTopGenres(ctx context.Context, limit int64) ([]HomeTopGenr
const homeUnplayedAlbums = `-- name: HomeUnplayedAlbums :many
SELECT rg.id AS album_id
FROM release_groups rg
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN audio_files af ON af.recording_id = rgr.recording_id
FROM albums rg
JOIN audio_files af ON af.album_id = rg.id
GROUP BY rg.id
HAVING SUM(af.play_count) = 0
ORDER BY RANDOM()
-103
View File
@@ -1,103 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: mix.sql
package sqlcgen
import (
"context"
"database/sql"
)
const getArtistByFilePath = `-- name: GetArtistByFilePath :one
SELECT COALESCE(a.name, '') AS artist_name, COALESCE(a.mbid, '') AS artist_mbid
FROM audio_files af
JOIN recordings r ON af.recording_id = r.id
JOIN artist_credit ac ON r.artist_credit_id = ac.id
JOIN artist_credit_artist aca ON aca.credit_id = ac.id
JOIN artists a ON a.id = aca.artist_id
WHERE af.file_path = ?
LIMIT 1
`
type GetArtistByFilePathRow struct {
ArtistName string
ArtistMbid string
}
func (q *Queries) GetArtistByFilePath(ctx context.Context, filePath string) (GetArtistByFilePathRow, error) {
row := q.db.QueryRowContext(ctx, getArtistByFilePath, filePath)
var i GetArtistByFilePathRow
err := row.Scan(&i.ArtistName, &i.ArtistMbid)
return i, err
}
const getFilePathsByArtistMBID = `-- name: GetFilePathsByArtistMBID :many
SELECT DISTINCT af.file_path
FROM audio_files af
JOIN recordings r ON af.recording_id = r.id
JOIN artist_credit ac ON r.artist_credit_id = ac.id
JOIN artist_credit_artist aca ON aca.credit_id = ac.id
JOIN artists a ON a.id = aca.artist_id
WHERE a.mbid = ?
`
// Queries backing the dynamic-mix queue fallback (backend/explore/mix.go):
// expanding a seed selection into a candidate pool by artist similarity
// and genre overlap, restricted to what is actually in the library.
func (q *Queries) GetFilePathsByArtistMBID(ctx context.Context, mbid sql.NullString) ([]string, error) {
rows, err := q.db.QueryContext(ctx, getFilePathsByArtistMBID, mbid)
if err != nil {
return nil, err
}
defer rows.Close()
var items []string
for rows.Next() {
var file_path string
if err := rows.Scan(&file_path); err != nil {
return nil, err
}
items = append(items, file_path)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getGenreNamesByFilePath = `-- name: GetGenreNamesByFilePath :many
SELECT DISTINCT g.name
FROM genres g
JOIN recording_genres rg ON g.id = rg.genre_id
JOIN recordings r ON rg.recording_id = r.id
JOIN audio_files af ON af.recording_id = r.id
WHERE af.file_path = ?
`
func (q *Queries) GetGenreNamesByFilePath(ctx context.Context, filePath string) ([]string, error) {
rows, err := q.db.QueryContext(ctx, getGenreNamesByFilePath, filePath)
if err != nil {
return nil, err
}
defer rows.Close()
var items []string
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
items = append(items, name)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
+47 -62
View File
@@ -9,23 +9,24 @@ import (
"time"
)
type Album struct {
ID int64
Name string
ArtistCredit string
ArtistID sql.NullInt64
Mbid sql.NullString
Year sql.NullInt64
OriginalYear sql.NullInt64
CoverArtID sql.NullInt64
PendingReleaseMbid sql.NullString
}
type Artist struct {
ID int64
Name string
Mbid sql.NullString
}
type ArtistCredit struct {
ID int64
Text string
}
type ArtistCreditArtist struct {
ID int64
ArtistID int64
CreditID int64
}
type ArtistEnrichment struct {
ArtistMbid string
BrowsedAt sql.NullTime
@@ -56,21 +57,31 @@ type ArtistMetadatum struct {
type AudioFile struct {
ID int64
FilePath string
LengthMilliseconds int64
LibraryID int64
FileTypeID int64
RecordingID int64
LengthMilliseconds int64
SampleRate int64
BitDepth int64
Channels int64
Bitrate int64
FileSize int64
Title string
ArtistCredit string
ArtistID sql.NullInt64
AlbumID sql.NullInt64
TrackNumber sql.NullInt64
DiscNumber sql.NullInt64
TotalTracks sql.NullInt64
Year sql.NullInt64
Composer string
Comment string
RecordingMbid sql.NullString
Basename string
LibraryID int64
GroupKey string
ModifiedAt int64
PlayCount int64
LastPlayed sql.NullTime
TagStatus string
GroupKey string
ModifiedAt int64
}
type CoverArt struct {
@@ -160,20 +171,21 @@ type ExploreChampionFt struct {
type ExploreIndex struct {
ID int64
EntityType string
Mbid string
EntityType int64
Mbid []byte
Title string
ArtistName string
ArtistMbid string
ArtistMbid []byte
Aliases string
Popularity int64
ListenerCount int64
Duration int64
CaaReleaseMbid string
CaaReleaseMbid []byte
ReleaseName string
PrimaryType string
SecondaryTypes string
ReleaseDate string
TotalTracks int64
ArtistType string
Country string
Disambiguation string
@@ -197,6 +209,11 @@ type ExploreIndexMetum struct {
Value string
}
type FileGenre struct {
AudioFileID int64
GenreID int64
}
type FileType struct {
ID int64
Extension string
@@ -231,6 +248,14 @@ type Library struct {
AutotagWarningAcked int64
}
type Lyric struct {
AudioFileID int64
Text string
Source string
RecordingMbid sql.NullString
FetchedAt time.Time
}
type LyricsIndex struct {
Lyrics string
}
@@ -291,48 +316,6 @@ type QueueTrack struct {
Position int64
}
type Recording struct {
ID int64
Name string
ArtistCreditID int64
TrackNumber sql.NullInt64
DiscNumber sql.NullInt64
Year sql.NullInt64
Genre sql.NullString
Composer sql.NullString
Lyrics sql.NullString
Comment sql.NullString
Mbid sql.NullString
}
type RecordingGenre struct {
ID int64
RecordingID int64
GenreID int64
}
type ReleaseGroup struct {
ID int64
Name string
CoverArtID sql.NullInt64
AlbumArtistCreditID sql.NullInt64
Year sql.NullInt64
TotalTracks sql.NullInt64
TotalDiscs sql.NullInt64
Mbid sql.NullString
OriginalYear sql.NullInt64
PendingReleaseMbid sql.NullString
}
type ReleaseGroupRecording struct {
ID int64
ReleaseGroupID int64
RecordingID int64
TrackNumber sql.NullInt64
DiscNumber sql.NullInt64
TotalTracks sql.NullInt64
}
type ReleaseToRg struct {
ReleaseMbid string
RgMbid string
@@ -410,4 +393,6 @@ type TrackMetadatum struct {
ArtistMbid string
ReleaseGroupMbid string
RecordingMbid string
AlbumID sql.NullInt64
ArtistID sql.NullInt64
}
+25 -66
View File
@@ -124,29 +124,18 @@ SELECT
pt.playlist_id,
pt.audio_file_id,
pt.position,
COALESCE(af.file_path, '') AS file_path,
COALESCE(af.length_milliseconds, 0) AS length_milliseconds,
COALESCE(r.name, pt.phantom_title, '') AS title,
COALESCE(ac.text, pt.phantom_artist, '') AS artist,
COALESCE(rg.name, pt.phantom_album, '') AS album,
COALESCE(ca.file_path, pt.phantom_cover_art_path, '') AS cover_art_path,
COALESCE(tm.file_path, '') AS file_path,
COALESCE(tm.length_milliseconds, 0) AS length_milliseconds,
COALESCE(tm.title, pt.phantom_title, '') AS title,
COALESCE(tm.artist_name, pt.phantom_artist, '') AS artist,
COALESCE(tm.album, pt.phantom_album, '') AS album,
COALESCE(NULLIF(tm.cover_art_path, ''), pt.phantom_cover_art_path, '') AS cover_art_path,
CASE WHEN pt.audio_file_id IS NULL THEN 1 ELSE 0 END AS is_phantom,
COALESCE(a.mbid, '') AS artist_mbid,
COALESCE(rg.mbid, '') AS release_group_mbid,
COALESCE(r.mbid, '') AS recording_mbid
CAST(COALESCE(tm.artist_mbid, '') AS TEXT) AS artist_mbid,
COALESCE(tm.release_group_mbid, '') AS release_group_mbid,
COALESCE(tm.recording_mbid, '') AS recording_mbid
FROM playlist_tracks pt
LEFT JOIN audio_files af ON pt.audio_file_id = af.id
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN artist_credit_artist aca ON aca.credit_id = ac.id
LEFT JOIN artists a ON a.id = aca.artist_id
LEFT JOIN (
SELECT recording_id, MIN(release_group_id) AS release_group_id
FROM release_group_recordings
GROUP BY recording_id
) rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
LEFT JOIN track_metadata tm ON tm.id = pt.audio_file_id
ORDER BY pt.playlist_id, pt.position
`
@@ -368,29 +357,18 @@ SELECT
pt.playlist_id,
pt.audio_file_id,
pt.position,
COALESCE(af.file_path, '') AS file_path,
COALESCE(af.length_milliseconds, 0) AS length_milliseconds,
COALESCE(r.name, pt.phantom_title, '') AS title,
COALESCE(ac.text, pt.phantom_artist, '') AS artist,
COALESCE(rg.name, pt.phantom_album, '') AS album,
COALESCE(ca.file_path, pt.phantom_cover_art_path, '') AS cover_art_path,
COALESCE(tm.file_path, '') AS file_path,
COALESCE(tm.length_milliseconds, 0) AS length_milliseconds,
COALESCE(tm.title, pt.phantom_title, '') AS title,
COALESCE(tm.artist_name, pt.phantom_artist, '') AS artist,
COALESCE(tm.album, pt.phantom_album, '') AS album,
COALESCE(NULLIF(tm.cover_art_path, ''), pt.phantom_cover_art_path, '') AS cover_art_path,
CASE WHEN pt.audio_file_id IS NULL THEN 1 ELSE 0 END AS is_phantom,
COALESCE(a.mbid, '') AS artist_mbid,
COALESCE(rg.mbid, '') AS release_group_mbid,
COALESCE(r.mbid, '') AS recording_mbid
CAST(COALESCE(tm.artist_mbid, '') AS TEXT) AS artist_mbid,
COALESCE(tm.release_group_mbid, '') AS release_group_mbid,
COALESCE(tm.recording_mbid, '') AS recording_mbid
FROM playlist_tracks pt
LEFT JOIN audio_files af ON pt.audio_file_id = af.id
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN artist_credit_artist aca ON aca.credit_id = ac.id
LEFT JOIN artists a ON a.id = aca.artist_id
LEFT JOIN (
SELECT recording_id, MIN(release_group_id) AS release_group_id
FROM release_group_recordings
GROUP BY recording_id
) rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
LEFT JOIN track_metadata tm ON tm.id = pt.audio_file_id
WHERE pt.playlist_id = ?
ORDER BY pt.position
`
@@ -451,30 +429,10 @@ func (q *Queries) GetPlaylistTracksWithMetadata(ctx context.Context, playlistID
}
const getTrackPhantomMetadata = `-- name: GetTrackPhantomMetadata :one
SELECT
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist,
COALESCE(rg.name, '') AS album,
af.length_milliseconds AS duration_ms,
CAST(COALESCE(
(SELECT GROUP_CONCAT(g.name, '||')
FROM recording_genres rg_sub
JOIN genres g ON rg_sub.genre_id = g.id
WHERE rg_sub.recording_id = r.id),
''
) AS TEXT) AS genre,
COALESCE(ca.file_path, '') AS cover_art_path
FROM audio_files af
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN (
SELECT recording_id, MIN(release_group_id) AS release_group_id
FROM release_group_recordings
GROUP BY recording_id
) rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
WHERE af.id = ?
SELECT title, artist_name AS artist, album,
length_milliseconds AS duration_ms, genre, cover_art_path
FROM track_metadata
WHERE id = ?
`
type GetTrackPhantomMetadataRow struct {
@@ -486,6 +444,7 @@ type GetTrackPhantomMetadataRow struct {
CoverArtPath string
}
// The display fields a playlist row keeps after its file goes away.
func (q *Queries) GetTrackPhantomMetadata(ctx context.Context, id int64) (GetTrackPhantomMetadataRow, error) {
row := q.db.QueryRowContext(ctx, getTrackPhantomMetadata, id)
var i GetTrackPhantomMetadataRow
+5 -20
View File
@@ -61,27 +61,11 @@ func (q *Queries) GetQueueTrackCount(ctx context.Context) (int64, error) {
}
const getQueueTracks = `-- name: GetQueueTracks :many
SELECT qt.id, qt.audio_file_id, qt.position, af.file_path,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist,
COALESCE(rg.name, '') AS album,
COALESCE(ca.file_path, '') AS cover_art_path,
COALESCE(a.mbid, '') AS artist_mbid,
COALESCE(rg.mbid, '') AS release_group_mbid,
COALESCE(r.mbid, '') AS recording_mbid
SELECT qt.id, qt.audio_file_id, qt.position, tm.file_path,
tm.title, tm.artist_name AS artist, tm.album, tm.cover_art_path,
tm.artist_mbid, tm.release_group_mbid, tm.recording_mbid
FROM queue_tracks qt
JOIN audio_files af ON qt.audio_file_id = af.id
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN artist_credit_artist aca ON aca.credit_id = ac.id
LEFT JOIN artists a ON a.id = aca.artist_id
LEFT JOIN (
SELECT recording_id, MIN(release_group_id) AS release_group_id
FROM release_group_recordings
GROUP BY recording_id
) rgr ON r.id = rgr.recording_id
LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
JOIN track_metadata tm ON tm.id = qt.audio_file_id
ORDER BY qt.position
`
@@ -99,6 +83,7 @@ type GetQueueTracksRow struct {
RecordingMbid string
}
// The queue's rows, joined to the one track projection.
func (q *Queries) GetQueueTracks(ctx context.Context) ([]GetQueueTracksRow, error) {
rows, err := q.db.QueryContext(ctx, getQueueTracks)
if err != nil {
@@ -1,268 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: recordings.sql
package sqlcgen
import (
"context"
"database/sql"
)
const countRecordingsByArtistCredit = `-- name: CountRecordingsByArtistCredit :one
SELECT COUNT(*) FROM recordings WHERE artist_credit_id = ?
`
func (q *Queries) CountRecordingsByArtistCredit(ctx context.Context, artistCreditID int64) (int64, error) {
row := q.db.QueryRowContext(ctx, countRecordingsByArtistCredit, artistCreditID)
var count int64
err := row.Scan(&count)
return count, err
}
const createRecording = `-- name: CreateRecording :one
INSERT INTO recordings (name, artist_credit_id) VALUES (?, ?)
RETURNING id, name, artist_credit_id, track_number, disc_number, year, genre, composer, lyrics, comment, mbid
`
type CreateRecordingParams struct {
Name string
ArtistCreditID int64
}
func (q *Queries) CreateRecording(ctx context.Context, arg CreateRecordingParams) (Recording, error) {
row := q.db.QueryRowContext(ctx, createRecording, arg.Name, arg.ArtistCreditID)
var i Recording
err := row.Scan(
&i.ID,
&i.Name,
&i.ArtistCreditID,
&i.TrackNumber,
&i.DiscNumber,
&i.Year,
&i.Genre,
&i.Composer,
&i.Lyrics,
&i.Comment,
&i.Mbid,
)
return i, err
}
const createRecordingFull = `-- name: CreateRecordingFull :one
INSERT INTO recordings (
name, artist_credit_id, track_number, disc_number,
year, genre, composer, lyrics, comment
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id, name, artist_credit_id, track_number, disc_number, year, genre, composer, lyrics, comment, mbid
`
type CreateRecordingFullParams struct {
Name string
ArtistCreditID int64
TrackNumber sql.NullInt64
DiscNumber sql.NullInt64
Year sql.NullInt64
Genre sql.NullString
Composer sql.NullString
Lyrics sql.NullString
Comment sql.NullString
}
func (q *Queries) CreateRecordingFull(ctx context.Context, arg CreateRecordingFullParams) (Recording, error) {
row := q.db.QueryRowContext(ctx, createRecordingFull,
arg.Name,
arg.ArtistCreditID,
arg.TrackNumber,
arg.DiscNumber,
arg.Year,
arg.Genre,
arg.Composer,
arg.Lyrics,
arg.Comment,
)
var i Recording
err := row.Scan(
&i.ID,
&i.Name,
&i.ArtistCreditID,
&i.TrackNumber,
&i.DiscNumber,
&i.Year,
&i.Genre,
&i.Composer,
&i.Lyrics,
&i.Comment,
&i.Mbid,
)
return i, err
}
const deleteAllRecordings = `-- name: DeleteAllRecordings :exec
DELETE FROM recordings
`
func (q *Queries) DeleteAllRecordings(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteAllRecordings)
return err
}
const deleteRecording = `-- name: DeleteRecording :exec
DELETE FROM recordings
WHERE id = ?
`
func (q *Queries) DeleteRecording(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, deleteRecording, id)
return err
}
const getAllRecordings = `-- name: GetAllRecordings :many
SELECT id, name, artist_credit_id, track_number, disc_number, year, genre, composer, lyrics, comment, mbid FROM recordings
ORDER BY name
`
func (q *Queries) GetAllRecordings(ctx context.Context) ([]Recording, error) {
rows, err := q.db.QueryContext(ctx, getAllRecordings)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Recording
for rows.Next() {
var i Recording
if err := rows.Scan(
&i.ID,
&i.Name,
&i.ArtistCreditID,
&i.TrackNumber,
&i.DiscNumber,
&i.Year,
&i.Genre,
&i.Composer,
&i.Lyrics,
&i.Comment,
&i.Mbid,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getOrphanedRecordingIDs = `-- name: GetOrphanedRecordingIDs :many
SELECT r.id FROM recordings r
LEFT JOIN audio_files af ON af.recording_id = r.id
WHERE af.id IS NULL
`
// Recordings no longer backed by any audio_files row - left behind
// when a scan's orphan cleanup deletes the file that used to own them,
// since deleting audio_files doesn't cascade to recordings.
func (q *Queries) GetOrphanedRecordingIDs(ctx context.Context) ([]int64, error) {
rows, err := q.db.QueryContext(ctx, getOrphanedRecordingIDs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getRecording = `-- name: GetRecording :one
SELECT id, name, artist_credit_id, track_number, disc_number, year, genre, composer, lyrics, comment, mbid FROM recordings
WHERE id = ? LIMIT 1
`
func (q *Queries) GetRecording(ctx context.Context, id int64) (Recording, error) {
row := q.db.QueryRowContext(ctx, getRecording, id)
var i Recording
err := row.Scan(
&i.ID,
&i.Name,
&i.ArtistCreditID,
&i.TrackNumber,
&i.DiscNumber,
&i.Year,
&i.Genre,
&i.Composer,
&i.Lyrics,
&i.Comment,
&i.Mbid,
)
return i, err
}
const updateRecording = `-- name: UpdateRecording :exec
UPDATE recordings
SET name = ?, artist_credit_id = ?
WHERE id = ?
`
type UpdateRecordingParams struct {
Name string
ArtistCreditID int64
ID int64
}
func (q *Queries) UpdateRecording(ctx context.Context, arg UpdateRecordingParams) error {
_, err := q.db.ExecContext(ctx, updateRecording, arg.Name, arg.ArtistCreditID, arg.ID)
return err
}
const updateRecordingFull = `-- name: UpdateRecordingFull :exec
UPDATE recordings
SET name = ?, artist_credit_id = ?, track_number = ?, disc_number = ?,
year = ?, genre = ?, composer = ?, lyrics = ?, comment = ?
WHERE id = ?
`
type UpdateRecordingFullParams struct {
Name string
ArtistCreditID int64
TrackNumber sql.NullInt64
DiscNumber sql.NullInt64
Year sql.NullInt64
Genre sql.NullString
Composer sql.NullString
Lyrics sql.NullString
Comment sql.NullString
ID int64
}
func (q *Queries) UpdateRecordingFull(ctx context.Context, arg UpdateRecordingFullParams) error {
_, err := q.db.ExecContext(ctx, updateRecordingFull,
arg.Name,
arg.ArtistCreditID,
arg.TrackNumber,
arg.DiscNumber,
arg.Year,
arg.Genre,
arg.Composer,
arg.Lyrics,
arg.Comment,
arg.ID,
)
return err
}
@@ -1,211 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: release_group_recordings.sql
package sqlcgen
import (
"context"
"database/sql"
)
const createReleaseGroupRecording = `-- name: CreateReleaseGroupRecording :one
INSERT INTO release_group_recordings (
release_group_id, recording_id, track_number, disc_number, total_tracks
)
VALUES (?, ?, ?, ?, ?)
RETURNING id, release_group_id, recording_id, track_number, disc_number, total_tracks
`
type CreateReleaseGroupRecordingParams struct {
ReleaseGroupID int64
RecordingID int64
TrackNumber sql.NullInt64
DiscNumber sql.NullInt64
TotalTracks sql.NullInt64
}
func (q *Queries) CreateReleaseGroupRecording(ctx context.Context, arg CreateReleaseGroupRecordingParams) (ReleaseGroupRecording, error) {
row := q.db.QueryRowContext(ctx, createReleaseGroupRecording,
arg.ReleaseGroupID,
arg.RecordingID,
arg.TrackNumber,
arg.DiscNumber,
arg.TotalTracks,
)
var i ReleaseGroupRecording
err := row.Scan(
&i.ID,
&i.ReleaseGroupID,
&i.RecordingID,
&i.TrackNumber,
&i.DiscNumber,
&i.TotalTracks,
)
return i, err
}
const deleteAllReleaseGroupRecordings = `-- name: DeleteAllReleaseGroupRecordings :exec
DELETE FROM release_group_recordings
`
func (q *Queries) DeleteAllReleaseGroupRecordings(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteAllReleaseGroupRecordings)
return err
}
const deleteReleaseGroupRecording = `-- name: DeleteReleaseGroupRecording :exec
DELETE FROM release_group_recordings
WHERE id = ?
`
func (q *Queries) DeleteReleaseGroupRecording(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, deleteReleaseGroupRecording, id)
return err
}
const deleteReleaseGroupRecordingByFK = `-- name: DeleteReleaseGroupRecordingByFK :exec
DELETE FROM release_group_recordings
WHERE release_group_id = ? AND recording_id = ?
`
type DeleteReleaseGroupRecordingByFKParams struct {
ReleaseGroupID int64
RecordingID int64
}
func (q *Queries) DeleteReleaseGroupRecordingByFK(ctx context.Context, arg DeleteReleaseGroupRecordingByFKParams) error {
_, err := q.db.ExecContext(ctx, deleteReleaseGroupRecordingByFK, arg.ReleaseGroupID, arg.RecordingID)
return err
}
const deleteReleaseGroupRecordingsByRecording = `-- name: DeleteReleaseGroupRecordingsByRecording :exec
DELETE FROM release_group_recordings
WHERE recording_id = ?
`
func (q *Queries) DeleteReleaseGroupRecordingsByRecording(ctx context.Context, recordingID int64) error {
_, err := q.db.ExecContext(ctx, deleteReleaseGroupRecordingsByRecording, recordingID)
return err
}
const getAlbumCompleteness = `-- name: GetAlbumCompleteness :one
WITH discs AS (
SELECT
COALESCE(rgr.disc_number, 1) AS disc,
MAX(COALESCE(rgr.total_tracks, 0)) AS declared,
COUNT(DISTINCT COALESCE(rgr.track_number, -rgr.recording_id)) AS owned
FROM release_group_recordings rgr
WHERE rgr.release_group_id = ?
GROUP BY COALESCE(rgr.disc_number, 1)
)
SELECT
CAST(COALESCE(SUM(owned), 0) AS INTEGER) AS owned,
CAST(COALESCE(SUM(declared), 0) AS INTEGER) AS expected,
CAST(COALESCE(SUM(CASE WHEN declared = 0 THEN 1 ELSE 0 END), 0) AS INTEGER) AS discs_untotalled
FROM discs
`
type GetAlbumCompletenessRow struct {
Owned int64
Expected int64
DiscsUntotalled int64
}
func (q *Queries) GetAlbumCompleteness(ctx context.Context, releaseGroupID int64) (GetAlbumCompletenessRow, error) {
row := q.db.QueryRowContext(ctx, getAlbumCompleteness, releaseGroupID)
var i GetAlbumCompletenessRow
err := row.Scan(&i.Owned, &i.Expected, &i.DiscsUntotalled)
return i, err
}
const getRecordingReleaseGroups = `-- name: GetRecordingReleaseGroups :many
SELECT id, release_group_id, recording_id, track_number, disc_number, total_tracks FROM release_group_recordings
WHERE recording_id = ?
`
func (q *Queries) GetRecordingReleaseGroups(ctx context.Context, recordingID int64) ([]ReleaseGroupRecording, error) {
rows, err := q.db.QueryContext(ctx, getRecordingReleaseGroups, recordingID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ReleaseGroupRecording
for rows.Next() {
var i ReleaseGroupRecording
if err := rows.Scan(
&i.ID,
&i.ReleaseGroupID,
&i.RecordingID,
&i.TrackNumber,
&i.DiscNumber,
&i.TotalTracks,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getReleaseGroupRecording = `-- name: GetReleaseGroupRecording :one
SELECT id, release_group_id, recording_id, track_number, disc_number, total_tracks FROM release_group_recordings
WHERE id = ? LIMIT 1
`
func (q *Queries) GetReleaseGroupRecording(ctx context.Context, id int64) (ReleaseGroupRecording, error) {
row := q.db.QueryRowContext(ctx, getReleaseGroupRecording, id)
var i ReleaseGroupRecording
err := row.Scan(
&i.ID,
&i.ReleaseGroupID,
&i.RecordingID,
&i.TrackNumber,
&i.DiscNumber,
&i.TotalTracks,
)
return i, err
}
const getReleaseGroupRecordings = `-- name: GetReleaseGroupRecordings :many
SELECT id, release_group_id, recording_id, track_number, disc_number, total_tracks FROM release_group_recordings
WHERE release_group_id = ?
ORDER BY disc_number, track_number
`
func (q *Queries) GetReleaseGroupRecordings(ctx context.Context, releaseGroupID int64) ([]ReleaseGroupRecording, error) {
rows, err := q.db.QueryContext(ctx, getReleaseGroupRecordings, releaseGroupID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ReleaseGroupRecording
for rows.Next() {
var i ReleaseGroupRecording
if err := rows.Scan(
&i.ID,
&i.ReleaseGroupID,
&i.RecordingID,
&i.TrackNumber,
&i.DiscNumber,
&i.TotalTracks,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
@@ -1,639 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: release_groups.sql
package sqlcgen
import (
"context"
"database/sql"
)
const countReleaseGroupRecordings = `-- name: CountReleaseGroupRecordings :one
SELECT COUNT(*) FROM release_group_recordings WHERE release_group_id = ?
`
func (q *Queries) CountReleaseGroupRecordings(ctx context.Context, releaseGroupID int64) (int64, error) {
row := q.db.QueryRowContext(ctx, countReleaseGroupRecordings, releaseGroupID)
var count int64
err := row.Scan(&count)
return count, err
}
const createReleaseGroup = `-- name: CreateReleaseGroup :one
INSERT INTO release_groups (name) VALUES (?)
RETURNING id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid, original_year, pending_release_mbid
`
func (q *Queries) CreateReleaseGroup(ctx context.Context, name string) (ReleaseGroup, error) {
row := q.db.QueryRowContext(ctx, createReleaseGroup, name)
var i ReleaseGroup
err := row.Scan(
&i.ID,
&i.Name,
&i.CoverArtID,
&i.AlbumArtistCreditID,
&i.Year,
&i.TotalTracks,
&i.TotalDiscs,
&i.Mbid,
&i.OriginalYear,
&i.PendingReleaseMbid,
)
return i, err
}
const createReleaseGroupFull = `-- name: CreateReleaseGroupFull :one
INSERT INTO release_groups (
name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs
) VALUES (?, ?, ?, ?, ?, ?)
RETURNING id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid, original_year, pending_release_mbid
`
type CreateReleaseGroupFullParams struct {
Name string
CoverArtID sql.NullInt64
AlbumArtistCreditID sql.NullInt64
Year sql.NullInt64
TotalTracks sql.NullInt64
TotalDiscs sql.NullInt64
}
func (q *Queries) CreateReleaseGroupFull(ctx context.Context, arg CreateReleaseGroupFullParams) (ReleaseGroup, error) {
row := q.db.QueryRowContext(ctx, createReleaseGroupFull,
arg.Name,
arg.CoverArtID,
arg.AlbumArtistCreditID,
arg.Year,
arg.TotalTracks,
arg.TotalDiscs,
)
var i ReleaseGroup
err := row.Scan(
&i.ID,
&i.Name,
&i.CoverArtID,
&i.AlbumArtistCreditID,
&i.Year,
&i.TotalTracks,
&i.TotalDiscs,
&i.Mbid,
&i.OriginalYear,
&i.PendingReleaseMbid,
)
return i, err
}
const deleteAllReleaseGroups = `-- name: DeleteAllReleaseGroups :exec
DELETE FROM release_groups
`
func (q *Queries) DeleteAllReleaseGroups(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteAllReleaseGroups)
return err
}
const deleteReleaseGroup = `-- name: DeleteReleaseGroup :exec
DELETE FROM release_groups
WHERE id = ?
`
func (q *Queries) DeleteReleaseGroup(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, deleteReleaseGroup, id)
return err
}
const getAlbumsByArtist = `-- name: GetAlbumsByArtist :many
SELECT
rg.id,
rg.name,
COALESCE(rg.original_year, rg.year) AS year,
COALESCE(rg.year, 0) AS release_year,
COALESCE(ac.text, fallback_ac.text, '') as artist_name,
-- primary (first-credited) album artist's MBID, for linking the
-- artist name to its detail page. Empty when the album has no
-- MB-tagged album-artist credit.
CAST(COALESCE((
SELECT a.mbid
FROM artist_credit_artist aca_p
JOIN artists a ON a.id = aca_p.artist_id
WHERE aca_p.credit_id = rg.album_artist_credit_id
ORDER BY aca_p.id
LIMIT 1
), '') AS TEXT) as artist_mbid,
COALESCE(ca.file_path, '') as cover_art_path
FROM release_groups rg
JOIN artist_credit ac ON rg.album_artist_credit_id = ac.id
JOIN artist_credit_artist aca ON aca.credit_id = ac.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
LEFT JOIN (
SELECT rgr.release_group_id, ac2.text
FROM release_group_recordings rgr
JOIN recordings rec ON rec.id = rgr.recording_id
JOIN artist_credit ac2 ON ac2.id = rec.artist_credit_id
GROUP BY rgr.release_group_id
) fallback_ac ON fallback_ac.release_group_id = rg.id
WHERE aca.artist_id = ?
ORDER BY rg.name
`
type GetAlbumsByArtistRow struct {
ID int64
Name string
Year sql.NullInt64
ReleaseYear int64
ArtistName string
ArtistMbid string
CoverArtPath string
}
func (q *Queries) GetAlbumsByArtist(ctx context.Context, artistID int64) ([]GetAlbumsByArtistRow, error) {
rows, err := q.db.QueryContext(ctx, getAlbumsByArtist, artistID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAlbumsByArtistRow
for rows.Next() {
var i GetAlbumsByArtistRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Year,
&i.ReleaseYear,
&i.ArtistName,
&i.ArtistMbid,
&i.CoverArtPath,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAlbumsByArtistByLibrary = `-- name: GetAlbumsByArtistByLibrary :many
SELECT
rg.id,
rg.name,
COALESCE(rg.original_year, rg.year) AS year,
COALESCE(rg.year, 0) AS release_year,
COALESCE(ac.text, fallback_ac.text, '') as artist_name,
-- primary (first-credited) album artist's MBID, for linking the
-- artist name to its detail page. Empty when the album has no
-- MB-tagged album-artist credit.
CAST(COALESCE((
SELECT a.mbid
FROM artist_credit_artist aca_p
JOIN artists a ON a.id = aca_p.artist_id
WHERE aca_p.credit_id = rg.album_artist_credit_id
ORDER BY aca_p.id
LIMIT 1
), '') AS TEXT) as artist_mbid,
COALESCE(ca.file_path, '') as cover_art_path
FROM release_groups rg
JOIN artist_credit ac ON rg.album_artist_credit_id = ac.id
JOIN artist_credit_artist aca ON aca.credit_id = ac.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
LEFT JOIN (
SELECT rgr.release_group_id, ac2.text
FROM release_group_recordings rgr
JOIN recordings rec ON rec.id = rgr.recording_id
JOIN artist_credit ac2 ON ac2.id = rec.artist_credit_id
GROUP BY rgr.release_group_id
) fallback_ac ON fallback_ac.release_group_id = rg.id
WHERE aca.artist_id = ?
AND rg.id IN (
SELECT DISTINCT rgr2.release_group_id
FROM release_group_recordings rgr2
JOIN recordings r2 ON r2.id = rgr2.recording_id
JOIN audio_files af2 ON af2.recording_id = r2.id
WHERE af2.library_id = ?
)
ORDER BY rg.name
`
type GetAlbumsByArtistByLibraryParams struct {
ArtistID int64
LibraryID int64
}
type GetAlbumsByArtistByLibraryRow struct {
ID int64
Name string
Year sql.NullInt64
ReleaseYear int64
ArtistName string
ArtistMbid string
CoverArtPath string
}
func (q *Queries) GetAlbumsByArtistByLibrary(ctx context.Context, arg GetAlbumsByArtistByLibraryParams) ([]GetAlbumsByArtistByLibraryRow, error) {
rows, err := q.db.QueryContext(ctx, getAlbumsByArtistByLibrary, arg.ArtistID, arg.LibraryID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAlbumsByArtistByLibraryRow
for rows.Next() {
var i GetAlbumsByArtistByLibraryRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Year,
&i.ReleaseYear,
&i.ArtistName,
&i.ArtistMbid,
&i.CoverArtPath,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAllAlbumsWithDetails = `-- name: GetAllAlbumsWithDetails :many
SELECT
rg.id,
rg.name,
-- year prefers original release year (MB first-release-date)
-- over the file-tag year so the UI surfaces the album's
-- original year by default. release_year keeps the file-tag
-- year accessible.
COALESCE(rg.original_year, rg.year) AS year,
COALESCE(rg.year, 0) AS release_year,
rg.mbid,
COALESCE(ac.text, fallback_ac.text, '') as artist_name,
-- primary (first-credited) album artist's MBID, for linking the
-- artist name to its detail page. Empty when the album has no
-- MB-tagged album-artist credit.
CAST(COALESCE((
SELECT a.mbid
FROM artist_credit_artist aca_p
JOIN artists a ON a.id = aca_p.artist_id
WHERE aca_p.credit_id = rg.album_artist_credit_id
ORDER BY aca_p.id
LIMIT 1
), '') AS TEXT) as artist_mbid,
COALESCE(ca.file_path, '') as cover_art_path
FROM release_groups rg
LEFT JOIN artist_credit ac ON rg.album_artist_credit_id = ac.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
LEFT JOIN (
SELECT rgr.release_group_id, ac2.text
FROM release_group_recordings rgr
JOIN recordings rec ON rec.id = rgr.recording_id
JOIN artist_credit ac2 ON ac2.id = rec.artist_credit_id
GROUP BY rgr.release_group_id
) fallback_ac ON fallback_ac.release_group_id = rg.id
ORDER BY rg.name
`
type GetAllAlbumsWithDetailsRow struct {
ID int64
Name string
Year sql.NullInt64
ReleaseYear int64
Mbid sql.NullString
ArtistName string
ArtistMbid string
CoverArtPath string
}
func (q *Queries) GetAllAlbumsWithDetails(ctx context.Context) ([]GetAllAlbumsWithDetailsRow, error) {
rows, err := q.db.QueryContext(ctx, getAllAlbumsWithDetails)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllAlbumsWithDetailsRow
for rows.Next() {
var i GetAllAlbumsWithDetailsRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Year,
&i.ReleaseYear,
&i.Mbid,
&i.ArtistName,
&i.ArtistMbid,
&i.CoverArtPath,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAllAlbumsWithDetailsByLibrary = `-- name: GetAllAlbumsWithDetailsByLibrary :many
SELECT
rg.id,
rg.name,
-- year prefers original release year (MB first-release-date)
-- over the file-tag year so the UI surfaces the album's
-- original year by default. release_year keeps the file-tag
-- year accessible.
COALESCE(rg.original_year, rg.year) AS year,
COALESCE(rg.year, 0) AS release_year,
rg.mbid,
COALESCE(ac.text, fallback_ac.text, '') as artist_name,
-- primary (first-credited) album artist's MBID, for linking the
-- artist name to its detail page. Empty when the album has no
-- MB-tagged album-artist credit.
CAST(COALESCE((
SELECT a.mbid
FROM artist_credit_artist aca_p
JOIN artists a ON a.id = aca_p.artist_id
WHERE aca_p.credit_id = rg.album_artist_credit_id
ORDER BY aca_p.id
LIMIT 1
), '') AS TEXT) as artist_mbid,
COALESCE(ca.file_path, '') as cover_art_path
FROM release_groups rg
LEFT JOIN artist_credit ac ON rg.album_artist_credit_id = ac.id
LEFT JOIN cover_art ca ON rg.cover_art_id = ca.id
LEFT JOIN (
SELECT rgr.release_group_id, ac2.text
FROM release_group_recordings rgr
JOIN recordings rec ON rec.id = rgr.recording_id
JOIN artist_credit ac2 ON ac2.id = rec.artist_credit_id
GROUP BY rgr.release_group_id
) fallback_ac ON fallback_ac.release_group_id = rg.id
WHERE rg.id IN (
SELECT DISTINCT rgr2.release_group_id
FROM release_group_recordings rgr2
JOIN recordings r2 ON r2.id = rgr2.recording_id
JOIN audio_files af2 ON af2.recording_id = r2.id
WHERE af2.library_id = ?
)
ORDER BY rg.name
`
type GetAllAlbumsWithDetailsByLibraryRow struct {
ID int64
Name string
Year sql.NullInt64
ReleaseYear int64
Mbid sql.NullString
ArtistName string
ArtistMbid string
CoverArtPath string
}
func (q *Queries) GetAllAlbumsWithDetailsByLibrary(ctx context.Context, libraryID int64) ([]GetAllAlbumsWithDetailsByLibraryRow, error) {
rows, err := q.db.QueryContext(ctx, getAllAlbumsWithDetailsByLibrary, libraryID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllAlbumsWithDetailsByLibraryRow
for rows.Next() {
var i GetAllAlbumsWithDetailsByLibraryRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Year,
&i.ReleaseYear,
&i.Mbid,
&i.ArtistName,
&i.ArtistMbid,
&i.CoverArtPath,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getAllReleaseGroups = `-- name: GetAllReleaseGroups :many
SELECT id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid, original_year, pending_release_mbid FROM release_groups
ORDER BY name
`
func (q *Queries) GetAllReleaseGroups(ctx context.Context) ([]ReleaseGroup, error) {
rows, err := q.db.QueryContext(ctx, getAllReleaseGroups)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ReleaseGroup
for rows.Next() {
var i ReleaseGroup
if err := rows.Scan(
&i.ID,
&i.Name,
&i.CoverArtID,
&i.AlbumArtistCreditID,
&i.Year,
&i.TotalTracks,
&i.TotalDiscs,
&i.Mbid,
&i.OriginalYear,
&i.PendingReleaseMbid,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getOrphanedReleaseGroupIDs = `-- name: GetOrphanedReleaseGroupIDs :many
SELECT rg.id FROM release_groups rg
LEFT JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
WHERE rgr.id IS NULL
`
// Release groups with no recordings left in them - run after orphaned
// recordings (and their release_group_recordings rows) are deleted, so
// a release group whose last owned track was removed is cleaned up too.
func (q *Queries) GetOrphanedReleaseGroupIDs(ctx context.Context) ([]int64, error) {
rows, err := q.db.QueryContext(ctx, getOrphanedReleaseGroupIDs)
if err != nil {
return nil, err
}
defer rows.Close()
var items []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getReleaseGroup = `-- name: GetReleaseGroup :one
SELECT id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid, original_year, pending_release_mbid FROM release_groups
WHERE id = ? LIMIT 1
`
func (q *Queries) GetReleaseGroup(ctx context.Context, id int64) (ReleaseGroup, error) {
row := q.db.QueryRowContext(ctx, getReleaseGroup, id)
var i ReleaseGroup
err := row.Scan(
&i.ID,
&i.Name,
&i.CoverArtID,
&i.AlbumArtistCreditID,
&i.Year,
&i.TotalTracks,
&i.TotalDiscs,
&i.Mbid,
&i.OriginalYear,
&i.PendingReleaseMbid,
)
return i, err
}
const getReleaseGroupByNameAndArtist = `-- name: GetReleaseGroupByNameAndArtist :one
SELECT id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid, original_year, pending_release_mbid FROM release_groups
WHERE name = ? AND album_artist_credit_id = ? LIMIT 1
`
type GetReleaseGroupByNameAndArtistParams struct {
Name string
AlbumArtistCreditID sql.NullInt64
}
func (q *Queries) GetReleaseGroupByNameAndArtist(ctx context.Context, arg GetReleaseGroupByNameAndArtistParams) (ReleaseGroup, error) {
row := q.db.QueryRowContext(ctx, getReleaseGroupByNameAndArtist, arg.Name, arg.AlbumArtistCreditID)
var i ReleaseGroup
err := row.Scan(
&i.ID,
&i.Name,
&i.CoverArtID,
&i.AlbumArtistCreditID,
&i.Year,
&i.TotalTracks,
&i.TotalDiscs,
&i.Mbid,
&i.OriginalYear,
&i.PendingReleaseMbid,
)
return i, err
}
const setReleaseGroupOriginalYear = `-- name: SetReleaseGroupOriginalYear :exec
UPDATE release_groups SET original_year = ? WHERE id = ?
`
type SetReleaseGroupOriginalYearParams struct {
OriginalYear sql.NullInt64
ID int64
}
// Set the release group's original-release-year (release-group's
// first-release-date from MusicBrainz). Called from autotag apply
// when the user confirms a candidate; the file-tag year stays in
// the year column.
func (q *Queries) SetReleaseGroupOriginalYear(ctx context.Context, arg SetReleaseGroupOriginalYearParams) error {
_, err := q.db.ExecContext(ctx, setReleaseGroupOriginalYear, arg.OriginalYear, arg.ID)
return err
}
const updateReleaseGroup = `-- name: UpdateReleaseGroup :exec
UPDATE release_groups
SET name = ?
WHERE id = ?
`
type UpdateReleaseGroupParams struct {
Name string
ID int64
}
func (q *Queries) UpdateReleaseGroup(ctx context.Context, arg UpdateReleaseGroupParams) error {
_, err := q.db.ExecContext(ctx, updateReleaseGroup, arg.Name, arg.ID)
return err
}
const updateReleaseGroupCoverArt = `-- name: UpdateReleaseGroupCoverArt :exec
UPDATE release_groups
SET cover_art_id = ?
WHERE id = ?
`
type UpdateReleaseGroupCoverArtParams struct {
CoverArtID sql.NullInt64
ID int64
}
func (q *Queries) UpdateReleaseGroupCoverArt(ctx context.Context, arg UpdateReleaseGroupCoverArtParams) error {
_, err := q.db.ExecContext(ctx, updateReleaseGroupCoverArt, arg.CoverArtID, arg.ID)
return err
}
const upsertReleaseGroup = `-- name: UpsertReleaseGroup :one
INSERT INTO release_groups (name, album_artist_credit_id, year)
VALUES (?, ?, ?)
ON CONFLICT(name, album_artist_credit_id) DO UPDATE SET
album_artist_credit_id = COALESCE(excluded.album_artist_credit_id, release_groups.album_artist_credit_id),
year = COALESCE(excluded.year, release_groups.year)
RETURNING id, name, cover_art_id, album_artist_credit_id, year, total_tracks, total_discs, mbid, original_year, pending_release_mbid
`
type UpsertReleaseGroupParams struct {
Name string
AlbumArtistCreditID sql.NullInt64
Year sql.NullInt64
}
func (q *Queries) UpsertReleaseGroup(ctx context.Context, arg UpsertReleaseGroupParams) (ReleaseGroup, error) {
row := q.db.QueryRowContext(ctx, upsertReleaseGroup, arg.Name, arg.AlbumArtistCreditID, arg.Year)
var i ReleaseGroup
err := row.Scan(
&i.ID,
&i.Name,
&i.CoverArtID,
&i.AlbumArtistCreditID,
&i.Year,
&i.TotalTracks,
&i.TotalDiscs,
&i.Mbid,
&i.OriginalYear,
&i.PendingReleaseMbid,
)
return i, err
}
+104 -87
View File
@@ -25,11 +25,21 @@ func (q *Queries) ClearCompletedTaggingItems(ctx context.Context, libraryID int6
}
const countPendingTaggingItems = `-- name: CountPendingTaggingItems :one
SELECT COUNT(*) FROM tagging_items
WHERE status = 'pending'
AND (CAST(?1 AS INTEGER) = 0 OR library_id = ?1)
SELECT COUNT(*) FROM tagging_items ti
WHERE ti.status = 'pending'
AND (CAST(?1 AS INTEGER) = 0 OR ti.library_id = ?1)
AND EXISTS (
SELECT 1 FROM audio_files af
WHERE af.group_key = ti.group_key AND af.tag_status = 'untagged'
)
`
// "Needs tagging" is a question about the files, not about the row:
// every scanned folder gets a tagging_items row (see
// UpsertTaggingItemOnTrackAdd), including one whose files all arrived
// carrying a recording MBID. Without the EXISTS a fully MB-tagged
// library reports its entire album count as pending work. See the
// same predicate on the three list queries below.
func (q *Queries) CountPendingTaggingItems(ctx context.Context, libraryID int64) (int64, error) {
row := q.db.QueryRowContext(ctx, countPendingTaggingItems, libraryID)
var count int64
@@ -77,6 +87,13 @@ LEFT JOIN libraries lb ON lb.id = ti.library_id
WHERE ti.status = 'pending'
AND (CAST(?1 AS INTEGER) = 0 OR ti.library_id = ?1)
AND ti.group_key > ?2
-- See CountPendingTaggingItems: the cursor must not stop on a
-- folder the list query no longer shows, or "next" walks folders
-- that are not in the sidebar.
AND EXISTS (
SELECT 1 FROM audio_files af
WHERE af.group_key = ti.group_key AND af.tag_status = 'untagged'
)
ORDER BY ti.group_key
LIMIT 1
`
@@ -185,20 +202,6 @@ func (q *Queries) GetPendingFolderDetail(ctx context.Context, groupKey string) (
return i, err
}
const getRecordingReleaseGroupID = `-- name: GetRecordingReleaseGroupID :one
SELECT COALESCE(rgr.release_group_id, 0) AS release_group_id
FROM release_group_recordings rgr
WHERE rgr.recording_id = ?
LIMIT 1
`
func (q *Queries) GetRecordingReleaseGroupID(ctx context.Context, recordingID int64) (int64, error) {
row := q.db.QueryRowContext(ctx, getRecordingReleaseGroupID, recordingID)
var release_group_id int64
err := row.Scan(&release_group_id)
return release_group_id, err
}
const getTaggingItem = `-- name: GetTaggingItem :one
SELECT group_key, library_id, track_count, album_name, album_artist, disc_number, best_match_release_mbid, score, last_checked_at, status, cleared_at, created_at, synthetic, parent_group_key, album_artist_conflict FROM tagging_items
WHERE group_key = ?
@@ -235,22 +238,18 @@ SELECT
af.basename,
af.length_milliseconds,
af.tag_status,
COALESCE(r.track_number, 0) AS track_number,
COALESCE(r.disc_number, 0) AS disc_number,
COALESCE(r.name, '') AS title,
COALESCE(ac.text, '') AS artist_name,
COALESCE(r.mbid, '') AS recording_mbid,
COALESCE(rg.name, '') AS album_name,
COALESCE(rgac.text, '') AS album_artist
COALESCE(af.track_number, 0) AS track_number,
COALESCE(af.disc_number, 0) AS disc_number,
af.title,
af.artist_credit AS artist_name,
COALESCE(af.recording_mbid, '') AS recording_mbid,
COALESCE(al.name, '') AS album_name,
COALESCE(al.artist_credit, '') AS album_artist
FROM audio_files af
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN release_group_recordings rgr ON rgr.recording_id = r.id
LEFT JOIN release_groups rg ON rg.id = rgr.release_group_id
LEFT JOIN artist_credit rgac ON rg.album_artist_credit_id = rgac.id
LEFT JOIN albums al ON al.id = af.album_id
WHERE af.group_key = ?
ORDER BY COALESCE(r.disc_number, 0),
COALESCE(r.track_number, 0),
ORDER BY COALESCE(af.disc_number, 0),
COALESCE(af.track_number, 0),
af.file_path
`
@@ -269,10 +268,10 @@ type ListAudioFilesInTaggingGroupRow struct {
AlbumArtist string
}
// album_name/album_artist are the PER-TRACK tags (via each track's
// own release_group link), not the folder-level tagging_items
// values. SplitMixedFolder clusters on these to find sub-albums
// hiding inside a folder full of unrelated tracks.
// album_name/album_artist are the PER-TRACK tags (each file's own
// album link), not the folder-level tagging_items values.
// SplitMixedFolder clusters on these to find sub-albums hiding inside
// a folder full of unrelated tracks.
func (q *Queries) ListAudioFilesInTaggingGroup(ctx context.Context, groupKey string) ([]ListAudioFilesInTaggingGroupRow, error) {
rows, err := q.db.QueryContext(ctx, listAudioFilesInTaggingGroup, groupKey)
if err != nil {
@@ -313,10 +312,7 @@ const listLikelyMixedBagGroupKeys = `-- name: ListLikelyMixedBagGroupKeys :many
SELECT ti.group_key
FROM tagging_items ti
JOIN audio_files af ON af.group_key = ti.group_key
LEFT JOIN recordings r ON af.recording_id = r.id
LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id
LEFT JOIN release_group_recordings rgr ON rgr.recording_id = r.id
LEFT JOIN release_groups rg ON rg.id = rgr.release_group_id
LEFT JOIN albums rg ON rg.id = af.album_id
WHERE ti.synthetic = 0
AND ti.track_count >= 4
AND (
@@ -324,7 +320,7 @@ WHERE ti.synthetic = 0
OR LOWER(TRIM(ti.album_artist)) IN ('various artists', 'various', 'va', 'v.a.', 'v a', 'unknown')
)
GROUP BY ti.group_key
HAVING COUNT(DISTINCT CASE WHEN ac.text != '' THEN LOWER(TRIM(ac.text)) END) > 1
HAVING COUNT(DISTINCT CASE WHEN af.artist_credit != '' THEN LOWER(TRIM(af.artist_credit)) END) > 1
AND COUNT(DISTINCT CASE WHEN rg.name != '' THEN LOWER(TRIM(rg.name)) END) > 1
`
@@ -359,34 +355,31 @@ func (q *Queries) ListLikelyMixedBagGroupKeys(ctx context.Context) ([]string, er
return items, nil
}
const listLocalReleaseGroupCandidates = `-- name: ListLocalReleaseGroupCandidates :many
const listLocalAlbumCandidates = `-- name: ListLocalAlbumCandidates :many
SELECT
rg.id AS release_group_id,
rg.mbid AS release_group_mbid,
rg.name AS album_name,
COALESCE(rg.year, 0) AS year,
COALESCE(ac.text, '') AS artist_credit,
COALESCE(rgr.track_number, 0) AS track_number,
COALESCE(rgr.disc_number, 0) AS disc_number,
COALESCE(r.name, '') AS track_title,
COALESCE(r.mbid, '') AS recording_mbid,
COALESCE(local_af.length_milliseconds, 0) AS length_milliseconds
FROM release_groups rg
JOIN release_group_recordings rgr ON rgr.release_group_id = rg.id
JOIN recordings r ON r.id = rgr.recording_id
LEFT JOIN artist_credit ac ON rg.album_artist_credit_id = ac.id
LEFT JOIN audio_files local_af ON local_af.recording_id = r.id
WHERE rg.mbid IS NOT NULL
AND rg.mbid != ''
AND r.mbid IS NOT NULL
AND r.mbid != ''
AND rg.name = ? COLLATE NOCASE
ORDER BY rg.id, rgr.disc_number, rgr.track_number
al.id AS album_id,
al.mbid AS album_mbid,
al.name AS album_name,
COALESCE(al.year, 0) AS year,
al.artist_credit,
COALESCE(af.track_number, 0) AS track_number,
COALESCE(af.disc_number, 0) AS disc_number,
af.title AS track_title,
COALESCE(af.recording_mbid, '') AS recording_mbid,
af.length_milliseconds
FROM albums al
JOIN audio_files af ON af.album_id = al.id
WHERE al.mbid IS NOT NULL
AND al.mbid != ''
AND af.recording_mbid IS NOT NULL
AND af.recording_mbid != ''
AND al.name = ? COLLATE NOCASE
ORDER BY al.id, af.disc_number, af.track_number
`
type ListLocalReleaseGroupCandidatesRow struct {
ReleaseGroupID int64
ReleaseGroupMbid sql.NullString
type ListLocalAlbumCandidatesRow struct {
AlbumID int64
AlbumMbid sql.NullString
AlbumName string
Year int64
ArtistCredit string
@@ -397,22 +390,21 @@ type ListLocalReleaseGroupCandidatesRow struct {
LengthMilliseconds int64
}
// Returns one row per (release_group, track) combination for any
// local release_group that has an MBID. Callers group these in Go
// and filter by normalized album-name match. Joined case-insensitive
// on name to pre-filter cheaply; Go does the real normalization.
func (q *Queries) ListLocalReleaseGroupCandidates(ctx context.Context, name string) ([]ListLocalReleaseGroupCandidatesRow, error) {
rows, err := q.db.QueryContext(ctx, listLocalReleaseGroupCandidates, name)
// One row per (album, track) for any local album carrying an MBID.
// Callers group these in Go and filter by normalized album-name match;
// the join is case-insensitive on name to pre-filter cheaply.
func (q *Queries) ListLocalAlbumCandidates(ctx context.Context, name string) ([]ListLocalAlbumCandidatesRow, error) {
rows, err := q.db.QueryContext(ctx, listLocalAlbumCandidates, name)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListLocalReleaseGroupCandidatesRow
var items []ListLocalAlbumCandidatesRow
for rows.Next() {
var i ListLocalReleaseGroupCandidatesRow
var i ListLocalAlbumCandidatesRow
if err := rows.Scan(
&i.ReleaseGroupID,
&i.ReleaseGroupMbid,
&i.AlbumID,
&i.AlbumMbid,
&i.AlbumName,
&i.Year,
&i.ArtistCredit,
@@ -454,6 +446,18 @@ LEFT JOIN libraries lb ON lb.id = ti.library_id
WHERE (CAST(?1 AS INTEGER) = 0 OR ti.library_id = ?1)
AND (CAST(?2 AS TEXT) = 'all' OR ti.status = ?2)
AND ti.cleared_at IS NULL
-- Actionable rows must have something to act on: see
-- CountPendingTaggingItems. Reviewed rows (confirmed/skipped) are
-- exempt because they are history, not work -- an applied folder is
-- fully tagged by definition and would otherwise vanish from the
-- sidebar's Completed section the instant it succeeded.
AND (
ti.status IN ('confirmed', 'skipped')
OR EXISTS (
SELECT 1 FROM audio_files af
WHERE af.group_key = ti.group_key AND af.tag_status = 'untagged'
)
)
ORDER BY LOWER(ti.album_artist), LOWER(ti.album_name), ti.disc_number
LIMIT ?4 OFFSET ?3
`
@@ -628,6 +632,14 @@ LEFT JOIN libraries lb ON lb.id = ti.library_id
WHERE (CAST(?1 AS INTEGER) = 0 OR ti.library_id = ?1)
AND (CAST(?2 AS TEXT) = 'all' OR ti.status = ?2)
AND ti.cleared_at IS NULL
-- See ListPendingTaggingItemsAlphabetical.
AND (
ti.status IN ('confirmed', 'skipped')
OR EXISTS (
SELECT 1 FROM audio_files af
WHERE af.group_key = ti.group_key AND af.tag_status = 'untagged'
)
)
ORDER BY ti.score IS NULL, ti.score DESC, LOWER(ti.album_artist), LOWER(ti.album_name)
LIMIT ?4 OFFSET ?3
`
@@ -758,31 +770,36 @@ func (q *Queries) SetAudioFileTagStatus(ctx context.Context, arg SetAudioFileTag
return err
}
const setRecordingMBID = `-- name: SetRecordingMBID :exec
UPDATE recordings SET mbid = ? WHERE id = ?
const setFileAlbumMBID = `-- name: SetFileAlbumMBID :exec
UPDATE albums SET mbid = ?
WHERE albums.id = (SELECT af.album_id FROM audio_files af WHERE af.id = ?)
`
type SetRecordingMBIDParams struct {
type SetFileAlbumMBIDParams struct {
Mbid sql.NullString
ID int64
}
func (q *Queries) SetRecordingMBID(ctx context.Context, arg SetRecordingMBIDParams) error {
_, err := q.db.ExecContext(ctx, setRecordingMBID, arg.Mbid, arg.ID)
// The album MBID for the album a file belongs to. Keyed by file
// because that is what the autotag apply path holds; under the old
// schema it had to look the release group up through two join tables
// first (GetRecordingReleaseGroupID), which is gone.
func (q *Queries) SetFileAlbumMBID(ctx context.Context, arg SetFileAlbumMBIDParams) error {
_, err := q.db.ExecContext(ctx, setFileAlbumMBID, arg.Mbid, arg.ID)
return err
}
const setReleaseGroupMBID = `-- name: SetReleaseGroupMBID :exec
UPDATE release_groups SET mbid = ? WHERE id = ?
const setFileRecordingMBID = `-- name: SetFileRecordingMBID :exec
UPDATE audio_files SET recording_mbid = ? WHERE id = ?
`
type SetReleaseGroupMBIDParams struct {
Mbid sql.NullString
ID int64
type SetFileRecordingMBIDParams struct {
RecordingMbid sql.NullString
ID int64
}
func (q *Queries) SetReleaseGroupMBID(ctx context.Context, arg SetReleaseGroupMBIDParams) error {
_, err := q.db.ExecContext(ctx, setReleaseGroupMBID, arg.Mbid, arg.ID)
func (q *Queries) SetFileRecordingMBID(ctx context.Context, arg SetFileRecordingMBIDParams) error {
_, err := q.db.ExecContext(ctx, setFileRecordingMBID, arg.RecordingMbid, arg.ID)
return err
}