Files
yellowjacket/backend/database/sql/queries/audio_files.sql
T
yonlu 4a9fe1d207 fix(library): open track details by path, not from the whole library
Track details from the queue, a playlist or a smart playlist looked
the track up in libraryStore's whole-library array, and returned
silently when it had not loaded, so the menu item did nothing. That
dependency is also why every track was fetched eagerly at startup
(20.5 MB at 26k tracks).

GetTracksByPaths answers for the paths in hand, and trackCache holds
the answers: bounded, coalesced into one call per task, and forgetting
what the retag, removal and scan events name. Every opener, and
track-details' own refresh after a save, go through it. A source sweep
pins the whole-library array to the store and the Tracks view.

Closes #279
2026-10-05 23:01:37 -04:00

213 lines
8.1 KiB
SQL

-- 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, 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: 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: 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 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
-- backfill the staleness baseline for files the scan skipped, and to
-- re-baseline after YellowJacket's own tag writer rewrites a file.
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 = ?;
-- ---------------------------------------------------------------------
-- Reads: tracks
-- ---------------------------------------------------------------------
-- name: GetTracks :many
SELECT * FROM track_metadata
WHERE library_id = COALESCE(NULLIF(CAST(sqlc.arg(library_id) AS INTEGER), 0), library_id);
-- name: GetTrackByPath :one
SELECT * FROM track_metadata WHERE file_path = ? LIMIT 1;
-- name: GetTracksByPaths :many
SELECT * FROM track_metadata WHERE file_path IN (sqlc.slice('paths'));
-- 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: 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
FROM track_metadata
WHERE file_path IN (sqlc.slice('paths'));
-- 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);
-- ---------------------------------------------------------------------
-- 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: 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
-- 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: 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: 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 = ?;