Files
yellowjacket/backend/database/sql/sqlcgen/audio_files.sql.go
T
yonluandClaude Opus 5 e7748f1fd5
CI / check (push) Successful in 3m7s
CI / e2e (push) Canceled after 1m45s
feat(database): shape the library like files, and shrink the catalog
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
2026-08-16 13:58:15 -04:00

1191 lines
32 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: audio_files.sql
package sqlcgen
import (
"context"
"database/sql"
"strings"
)
const countAudioFiles = `-- name: CountAudioFiles :one
SELECT COUNT(*) AS count FROM audio_files
WHERE library_id = COALESCE(NULLIF(CAST(?1 AS INTEGER), 0), library_id)
`
func (q *Queries) CountAudioFiles(ctx context.Context, libraryID int64) (int64, error) {
row := q.db.QueryRowContext(ctx, countAudioFiles, libraryID)
var count int64
err := row.Scan(&count)
return count, err
}
const createAudioFile = `-- 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 id, 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, play_count, last_played, tag_status
`
type CreateAudioFileParams struct {
FilePath string
LibraryID int64
FileTypeID 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
GroupKey string
ModifiedAt int64
TagStatus string
}
// 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
// ---------------------------------------------------------------------
func (q *Queries) CreateAudioFile(ctx context.Context, arg CreateAudioFileParams) (AudioFile, error) {
row := q.db.QueryRowContext(ctx, createAudioFile,
arg.FilePath,
arg.LibraryID,
arg.FileTypeID,
arg.LengthMilliseconds,
arg.SampleRate,
arg.BitDepth,
arg.Channels,
arg.Bitrate,
arg.FileSize,
arg.Title,
arg.ArtistCredit,
arg.ArtistID,
arg.AlbumID,
arg.TrackNumber,
arg.DiscNumber,
arg.TotalTracks,
arg.Year,
arg.Composer,
arg.Comment,
arg.RecordingMbid,
arg.Basename,
arg.GroupKey,
arg.ModifiedAt,
arg.TagStatus,
)
var i AudioFile
err := row.Scan(
&i.ID,
&i.FilePath,
&i.LibraryID,
&i.FileTypeID,
&i.LengthMilliseconds,
&i.SampleRate,
&i.BitDepth,
&i.Channels,
&i.Bitrate,
&i.FileSize,
&i.Title,
&i.ArtistCredit,
&i.ArtistID,
&i.AlbumID,
&i.TrackNumber,
&i.DiscNumber,
&i.TotalTracks,
&i.Year,
&i.Composer,
&i.Comment,
&i.RecordingMbid,
&i.Basename,
&i.GroupKey,
&i.ModifiedAt,
&i.PlayCount,
&i.LastPlayed,
&i.TagStatus,
)
return i, err
}
const deleteAllAudioFiles = `-- name: DeleteAllAudioFiles :exec
DELETE FROM audio_files
`
func (q *Queries) DeleteAllAudioFiles(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteAllAudioFiles)
return err
}
const deleteAudioFile = `-- name: DeleteAudioFile :exec
DELETE FROM audio_files WHERE id = ?
`
func (q *Queries) DeleteAudioFile(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, deleteAudioFile, id)
return err
}
const getAllAudioFilePaths = `-- name: GetAllAudioFilePaths :many
SELECT id, file_path FROM audio_files
`
type GetAllAudioFilePathsRow struct {
ID int64
FilePath string
}
func (q *Queries) GetAllAudioFilePaths(ctx context.Context) ([]GetAllAudioFilePathsRow, error) {
rows, err := q.db.QueryContext(ctx, getAllAudioFilePaths)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllAudioFilePathsRow
for rows.Next() {
var i GetAllAudioFilePathsRow
if err := rows.Scan(&i.ID, &i.FilePath); 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 getAudioFile = `-- name: GetAudioFile :one
SELECT id, 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, play_count, last_played, tag_status FROM audio_files WHERE id = ? LIMIT 1
`
// ---------------------------------------------------------------------
// Reads: the file row itself
// ---------------------------------------------------------------------
func (q *Queries) GetAudioFile(ctx context.Context, id int64) (AudioFile, error) {
row := q.db.QueryRowContext(ctx, getAudioFile, id)
var i AudioFile
err := row.Scan(
&i.ID,
&i.FilePath,
&i.LibraryID,
&i.FileTypeID,
&i.LengthMilliseconds,
&i.SampleRate,
&i.BitDepth,
&i.Channels,
&i.Bitrate,
&i.FileSize,
&i.Title,
&i.ArtistCredit,
&i.ArtistID,
&i.AlbumID,
&i.TrackNumber,
&i.DiscNumber,
&i.TotalTracks,
&i.Year,
&i.Composer,
&i.Comment,
&i.RecordingMbid,
&i.Basename,
&i.GroupKey,
&i.ModifiedAt,
&i.PlayCount,
&i.LastPlayed,
&i.TagStatus,
)
return i, err
}
const getAudioFileByPath = `-- name: GetAudioFileByPath :one
SELECT id, 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, play_count, last_played, tag_status FROM audio_files WHERE file_path = ? LIMIT 1
`
func (q *Queries) GetAudioFileByPath(ctx context.Context, filePath string) (AudioFile, error) {
row := q.db.QueryRowContext(ctx, getAudioFileByPath, filePath)
var i AudioFile
err := row.Scan(
&i.ID,
&i.FilePath,
&i.LibraryID,
&i.FileTypeID,
&i.LengthMilliseconds,
&i.SampleRate,
&i.BitDepth,
&i.Channels,
&i.Bitrate,
&i.FileSize,
&i.Title,
&i.ArtistCredit,
&i.ArtistID,
&i.AlbumID,
&i.TrackNumber,
&i.DiscNumber,
&i.TotalTracks,
&i.Year,
&i.Composer,
&i.Comment,
&i.RecordingMbid,
&i.Basename,
&i.GroupKey,
&i.ModifiedAt,
&i.PlayCount,
&i.LastPlayed,
&i.TagStatus,
)
return i, err
}
const getAudioFileGroupKey = `-- name: GetAudioFileGroupKey :one
SELECT group_key FROM audio_files WHERE id = ? LIMIT 1
`
func (q *Queries) GetAudioFileGroupKey(ctx context.Context, id int64) (string, error) {
row := q.db.QueryRowContext(ctx, getAudioFileGroupKey, id)
var group_key string
err := row.Scan(&group_key)
return group_key, err
}
const getAudioFilesByPaths = `-- name: GetAudioFilesByPaths :many
SELECT id, library_id, file_path, group_key FROM audio_files
WHERE file_path IN (/*SLICE:paths*/?)
`
type GetAudioFilesByPathsRow struct {
ID int64
LibraryID int64
FilePath string
GroupKey string
}
func (q *Queries) GetAudioFilesByPaths(ctx context.Context, paths []string) ([]GetAudioFilesByPathsRow, error) {
query := getAudioFilesByPaths
var queryParams []interface{}
if len(paths) > 0 {
for _, v := range paths {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:paths*/?", strings.Repeat(",?", len(paths))[1:], 1)
} else {
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 []GetAudioFilesByPathsRow
for rows.Next() {
var i GetAudioFilesByPathsRow
if err := rows.Scan(
&i.ID,
&i.LibraryID,
&i.FilePath,
&i.GroupKey,
); 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 getAudioFilesInLibrary = `-- name: GetAudioFilesInLibrary :many
SELECT id, 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, play_count, last_played, tag_status FROM audio_files WHERE library_id = ?
`
func (q *Queries) GetAudioFilesInLibrary(ctx context.Context, libraryID int64) ([]AudioFile, error) {
rows, err := q.db.QueryContext(ctx, getAudioFilesInLibrary, libraryID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AudioFile
for rows.Next() {
var i AudioFile
if err := rows.Scan(
&i.ID,
&i.FilePath,
&i.LibraryID,
&i.FileTypeID,
&i.LengthMilliseconds,
&i.SampleRate,
&i.BitDepth,
&i.Channels,
&i.Bitrate,
&i.FileSize,
&i.Title,
&i.ArtistCredit,
&i.ArtistID,
&i.AlbumID,
&i.TrackNumber,
&i.DiscNumber,
&i.TotalTracks,
&i.Year,
&i.Composer,
&i.Comment,
&i.RecordingMbid,
&i.Basename,
&i.GroupKey,
&i.ModifiedAt,
&i.PlayCount,
&i.LastPlayed,
&i.TagStatus,
); 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 getFilePathsByAlbums = `-- name: GetFilePathsByAlbums :many
SELECT album_id, library_id, file_path FROM audio_files
WHERE album_id IN (/*SLICE:album_ids*/?)
ORDER BY disc_number, track_number
`
type GetFilePathsByAlbumsRow struct {
AlbumID sql.NullInt64
LibraryID int64
FilePath string
}
// ---------------------------------------------------------------------
// 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.
// 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.
func (q *Queries) GetFilePathsByAlbums(ctx context.Context, albumIds []sql.NullInt64) ([]GetFilePathsByAlbumsRow, error) {
query := getFilePathsByAlbums
var queryParams []interface{}
if len(albumIds) > 0 {
for _, v := range albumIds {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:album_ids*/?", strings.Repeat(",?", len(albumIds))[1:], 1)
} else {
query = strings.Replace(query, "/*SLICE:album_ids*/?", "NULL", 1)
}
rows, err := q.db.QueryContext(ctx, query, queryParams...)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetFilePathsByAlbumsRow
for rows.Next() {
var i GetFilePathsByAlbumsRow
if err := rows.Scan(&i.AlbumID, &i.LibraryID, &i.FilePath); 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 getFilePathsByArtistMBID = `-- name: GetFilePathsByArtistMBID :many
SELECT DISTINCT af.file_path
FROM audio_files af
JOIN artists a ON a.id = af.artist_id
WHERE a.mbid = ?
`
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 getFilePathsByGenres = `-- 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 (/*SLICE:genres*/?)
ORDER BY af.disc_number, af.track_number
`
type GetFilePathsByGenresRow struct {
Genre string
LibraryID int64
FilePath string
}
func (q *Queries) GetFilePathsByGenres(ctx context.Context, genres []string) ([]GetFilePathsByGenresRow, error) {
query := getFilePathsByGenres
var queryParams []interface{}
if len(genres) > 0 {
for _, v := range genres {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:genres*/?", strings.Repeat(",?", len(genres))[1:], 1)
} else {
query = strings.Replace(query, "/*SLICE:genres*/?", "NULL", 1)
}
rows, err := q.db.QueryContext(ctx, query, queryParams...)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetFilePathsByGenresRow
for rows.Next() {
var i GetFilePathsByGenresRow
if err := rows.Scan(&i.Genre, &i.LibraryID, &i.FilePath); 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 getFilePathsByRecordingMBIDs = `-- name: GetFilePathsByRecordingMBIDs :many
SELECT recording_mbid, library_id, file_path FROM audio_files
WHERE recording_mbid IN (/*SLICE:mbids*/?)
ORDER BY file_path
`
type GetFilePathsByRecordingMBIDsRow struct {
RecordingMbid sql.NullString
LibraryID int64
FilePath string
}
// 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.
func (q *Queries) GetFilePathsByRecordingMBIDs(ctx context.Context, mbids []sql.NullString) ([]GetFilePathsByRecordingMBIDsRow, error) {
query := getFilePathsByRecordingMBIDs
var queryParams []interface{}
if len(mbids) > 0 {
for _, v := range mbids {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:mbids*/?", strings.Repeat(",?", len(mbids))[1:], 1)
} else {
query = strings.Replace(query, "/*SLICE:mbids*/?", "NULL", 1)
}
rows, err := q.db.QueryContext(ctx, query, queryParams...)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetFilePathsByRecordingMBIDsRow
for rows.Next() {
var i GetFilePathsByRecordingMBIDsRow
if err := rows.Scan(&i.RecordingMbid, &i.LibraryID, &i.FilePath); 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 getLibraryMaxModifiedAt = `-- name: GetLibraryMaxModifiedAt :one
SELECT CAST(COALESCE(MAX(modified_at), 0) AS INTEGER) FROM audio_files
WHERE library_id = ?
`
// Newest recorded mtime in a library, for the startup soft scan. 0 when
// the library is empty or no row has a baseline yet.
func (q *Queries) GetLibraryMaxModifiedAt(ctx context.Context, libraryID int64) (int64, error) {
row := q.db.QueryRowContext(ctx, getLibraryMaxModifiedAt, libraryID)
var column_1 int64
err := row.Scan(&column_1)
return column_1, err
}
const getRandomAudioFilePath = `-- name: GetRandomAudioFilePath :one
SELECT file_path FROM audio_files ORDER BY RANDOM() LIMIT 1
`
func (q *Queries) GetRandomAudioFilePath(ctx context.Context) (string, error) {
row := q.db.QueryRowContext(ctx, getRandomAudioFilePath)
var file_path string
err := row.Scan(&file_path)
return file_path, err
}
const getTrackByPath = `-- name: GetTrackByPath :one
SELECT id, file_path, length_milliseconds, title, artist_name, track_number, disc_number, album, genre, year, release_year, composer, file_type, sample_rate, bit_depth, channels, bitrate, file_size, library_id, play_count, last_played, cover_art_path, artist_mbid, release_group_mbid, recording_mbid, album_id, artist_id FROM track_metadata WHERE file_path = ? LIMIT 1
`
func (q *Queries) GetTrackByPath(ctx context.Context, filePath string) (TrackMetadatum, error) {
row := q.db.QueryRowContext(ctx, getTrackByPath, filePath)
var i TrackMetadatum
err := row.Scan(
&i.ID,
&i.FilePath,
&i.LengthMilliseconds,
&i.Title,
&i.ArtistName,
&i.TrackNumber,
&i.DiscNumber,
&i.Album,
&i.Genre,
&i.Year,
&i.ReleaseYear,
&i.Composer,
&i.FileType,
&i.SampleRate,
&i.BitDepth,
&i.Channels,
&i.Bitrate,
&i.FileSize,
&i.LibraryID,
&i.PlayCount,
&i.LastPlayed,
&i.CoverArtPath,
&i.ArtistMbid,
&i.ReleaseGroupMbid,
&i.RecordingMbid,
&i.AlbumID,
&i.ArtistID,
)
return i, err
}
const getTracks = `-- name: GetTracks :many
SELECT id, file_path, length_milliseconds, title, artist_name, track_number, disc_number, album, genre, year, release_year, composer, file_type, sample_rate, bit_depth, channels, bitrate, file_size, library_id, play_count, last_played, cover_art_path, artist_mbid, release_group_mbid, recording_mbid, album_id, artist_id FROM track_metadata
WHERE library_id = COALESCE(NULLIF(CAST(?1 AS INTEGER), 0), library_id)
`
// ---------------------------------------------------------------------
// Reads: tracks
// ---------------------------------------------------------------------
func (q *Queries) GetTracks(ctx context.Context, libraryID int64) ([]TrackMetadatum, error) {
rows, err := q.db.QueryContext(ctx, getTracks, libraryID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TrackMetadatum
for rows.Next() {
var i TrackMetadatum
if err := rows.Scan(
&i.ID,
&i.FilePath,
&i.LengthMilliseconds,
&i.Title,
&i.ArtistName,
&i.TrackNumber,
&i.DiscNumber,
&i.Album,
&i.Genre,
&i.Year,
&i.ReleaseYear,
&i.Composer,
&i.FileType,
&i.SampleRate,
&i.BitDepth,
&i.Channels,
&i.Bitrate,
&i.FileSize,
&i.LibraryID,
&i.PlayCount,
&i.LastPlayed,
&i.CoverArtPath,
&i.ArtistMbid,
&i.ReleaseGroupMbid,
&i.RecordingMbid,
&i.AlbumID,
&i.ArtistID,
); 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 getTracksByAlbum = `-- name: GetTracksByAlbum :many
SELECT id, file_path, length_milliseconds, title, artist_name, track_number, disc_number, album, genre, year, release_year, composer, file_type, sample_rate, bit_depth, channels, bitrate, file_size, library_id, play_count, last_played, cover_art_path, artist_mbid, release_group_mbid, recording_mbid, album_id, artist_id FROM track_metadata
WHERE album_id = ?1
AND library_id = COALESCE(NULLIF(CAST(?2 AS INTEGER), 0), library_id)
ORDER BY disc_number, track_number
`
type GetTracksByAlbumParams struct {
AlbumID sql.NullInt64
LibraryID int64
}
func (q *Queries) GetTracksByAlbum(ctx context.Context, arg GetTracksByAlbumParams) ([]TrackMetadatum, error) {
rows, err := q.db.QueryContext(ctx, getTracksByAlbum, arg.AlbumID, arg.LibraryID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TrackMetadatum
for rows.Next() {
var i TrackMetadatum
if err := rows.Scan(
&i.ID,
&i.FilePath,
&i.LengthMilliseconds,
&i.Title,
&i.ArtistName,
&i.TrackNumber,
&i.DiscNumber,
&i.Album,
&i.Genre,
&i.Year,
&i.ReleaseYear,
&i.Composer,
&i.FileType,
&i.SampleRate,
&i.BitDepth,
&i.Channels,
&i.Bitrate,
&i.FileSize,
&i.LibraryID,
&i.PlayCount,
&i.LastPlayed,
&i.CoverArtPath,
&i.ArtistMbid,
&i.ReleaseGroupMbid,
&i.RecordingMbid,
&i.AlbumID,
&i.ArtistID,
); 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 getTracksByGenre = `-- name: GetTracksByGenre :many
SELECT tm.id, tm.file_path, tm.length_milliseconds, tm.title, tm.artist_name, tm.track_number, tm.disc_number, tm.album, tm.genre, tm.year, tm.release_year, tm.composer, tm.file_type, tm.sample_rate, tm.bit_depth, tm.channels, tm.bitrate, tm.file_size, tm.library_id, tm.play_count, tm.last_played, tm.cover_art_path, tm.artist_mbid, tm.release_group_mbid, tm.recording_mbid, tm.album_id, tm.artist_id 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 = ?1
AND tm.library_id = COALESCE(NULLIF(CAST(?2 AS INTEGER), 0), tm.library_id)
`
type GetTracksByGenreParams struct {
Genre string
LibraryID int64
}
func (q *Queries) GetTracksByGenre(ctx context.Context, arg GetTracksByGenreParams) ([]TrackMetadatum, error) {
rows, err := q.db.QueryContext(ctx, getTracksByGenre, arg.Genre, arg.LibraryID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TrackMetadatum
for rows.Next() {
var i TrackMetadatum
if err := rows.Scan(
&i.ID,
&i.FilePath,
&i.LengthMilliseconds,
&i.Title,
&i.ArtistName,
&i.TrackNumber,
&i.DiscNumber,
&i.Album,
&i.Genre,
&i.Year,
&i.ReleaseYear,
&i.Composer,
&i.FileType,
&i.SampleRate,
&i.BitDepth,
&i.Channels,
&i.Bitrate,
&i.FileSize,
&i.LibraryID,
&i.PlayCount,
&i.LastPlayed,
&i.CoverArtPath,
&i.ArtistMbid,
&i.ReleaseGroupMbid,
&i.RecordingMbid,
&i.AlbumID,
&i.ArtistID,
); 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 lookupTrackMetaByPaths = `-- 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 (/*SLICE:paths*/?)
`
type LookupTrackMetaByPathsRow struct {
ID int64
FilePath string
Title string
ArtistName string
Album string
CoverArtPath string
ArtistMbid string
ReleaseGroupMbid string
RecordingMbid string
}
func (q *Queries) LookupTrackMetaByPaths(ctx context.Context, paths []string) ([]LookupTrackMetaByPathsRow, error) {
query := lookupTrackMetaByPaths
var queryParams []interface{}
if len(paths) > 0 {
for _, v := range paths {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:paths*/?", strings.Repeat(",?", len(paths))[1:], 1)
} else {
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 []LookupTrackMetaByPathsRow
for rows.Next() {
var i LookupTrackMetaByPathsRow
if err := rows.Scan(
&i.ID,
&i.FilePath,
&i.Title,
&i.ArtistName,
&i.Album,
&i.CoverArtPath,
&i.ArtistMbid,
&i.ReleaseGroupMbid,
&i.RecordingMbid,
); 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 ownedAlbumMBIDs = `-- name: OwnedAlbumMBIDs :many
SELECT DISTINCT al.mbid FROM albums al
JOIN audio_files af ON af.album_id = al.id
WHERE al.mbid IN (/*SLICE:mbids*/?)
`
func (q *Queries) OwnedAlbumMBIDs(ctx context.Context, mbids []sql.NullString) ([]sql.NullString, error) {
query := ownedAlbumMBIDs
var queryParams []interface{}
if len(mbids) > 0 {
for _, v := range mbids {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:mbids*/?", strings.Repeat(",?", len(mbids))[1:], 1)
} else {
query = strings.Replace(query, "/*SLICE:mbids*/?", "NULL", 1)
}
rows, err := q.db.QueryContext(ctx, query, queryParams...)
if err != nil {
return nil, err
}
defer rows.Close()
var items []sql.NullString
for rows.Next() {
var mbid sql.NullString
if err := rows.Scan(&mbid); err != nil {
return nil, err
}
items = append(items, mbid)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const ownedArtistMBIDs = `-- name: OwnedArtistMBIDs :many
SELECT DISTINCT a.mbid FROM artists a
JOIN audio_files af ON af.artist_id = a.id
WHERE a.mbid IN (/*SLICE:mbids*/?)
`
func (q *Queries) OwnedArtistMBIDs(ctx context.Context, mbids []sql.NullString) ([]sql.NullString, error) {
query := ownedArtistMBIDs
var queryParams []interface{}
if len(mbids) > 0 {
for _, v := range mbids {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:mbids*/?", strings.Repeat(",?", len(mbids))[1:], 1)
} else {
query = strings.Replace(query, "/*SLICE:mbids*/?", "NULL", 1)
}
rows, err := q.db.QueryContext(ctx, query, queryParams...)
if err != nil {
return nil, err
}
defer rows.Close()
var items []sql.NullString
for rows.Next() {
var mbid sql.NullString
if err := rows.Scan(&mbid); err != nil {
return nil, err
}
items = append(items, mbid)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const ownedRecordingMBIDs = `-- name: OwnedRecordingMBIDs :many
SELECT DISTINCT recording_mbid FROM audio_files
WHERE recording_mbid IN (/*SLICE:mbids*/?)
`
// ---------------------------------------------------------------------
// Ownership, asked in bulk
// ---------------------------------------------------------------------
// 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.
func (q *Queries) OwnedRecordingMBIDs(ctx context.Context, mbids []sql.NullString) ([]sql.NullString, error) {
query := ownedRecordingMBIDs
var queryParams []interface{}
if len(mbids) > 0 {
for _, v := range mbids {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:mbids*/?", strings.Repeat(",?", len(mbids))[1:], 1)
} else {
query = strings.Replace(query, "/*SLICE:mbids*/?", "NULL", 1)
}
rows, err := q.db.QueryContext(ctx, query, queryParams...)
if err != nil {
return nil, err
}
defer rows.Close()
var items []sql.NullString
for rows.Next() {
var recording_mbid sql.NullString
if err := rows.Scan(&recording_mbid); err != nil {
return nil, err
}
items = append(items, recording_mbid)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const promoteAudioFileTagStatusIfUntagged = `-- name: PromoteAudioFileTagStatusIfUntagged :exec
UPDATE audio_files
SET tag_status = 'user_confirmed'
WHERE id = ? AND tag_status = 'untagged'
`
// 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.
func (q *Queries) PromoteAudioFileTagStatusIfUntagged(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, promoteAudioFileTagStatusIfUntagged, id)
return err
}
const searchTracksByBasename = `-- 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 = ?1
)
LIMIT ?2
`
type SearchTracksByBasenameParams struct {
Basename string
Lim int64
}
type SearchTracksByBasenameRow struct {
ID int64
FilePath string
LengthMilliseconds int64
Title string
ArtistName string
Album string
}
func (q *Queries) SearchTracksByBasename(ctx context.Context, arg SearchTracksByBasenameParams) ([]SearchTracksByBasenameRow, error) {
rows, err := q.db.QueryContext(ctx, searchTracksByBasename, arg.Basename, arg.Lim)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SearchTracksByBasenameRow
for rows.Next() {
var i SearchTracksByBasenameRow
if err := rows.Scan(
&i.ID,
&i.FilePath,
&i.LengthMilliseconds,
&i.Title,
&i.ArtistName,
&i.Album,
); 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 setAudioFileGroupKey = `-- name: SetAudioFileGroupKey :exec
UPDATE audio_files SET group_key = ? WHERE id = ?
`
type SetAudioFileGroupKeyParams struct {
GroupKey string
ID int64
}
func (q *Queries) SetAudioFileGroupKey(ctx context.Context, arg SetAudioFileGroupKeyParams) error {
_, err := q.db.ExecContext(ctx, setAudioFileGroupKey, arg.GroupKey, arg.ID)
return err
}
const setAudioFileRecordingMBID = `-- name: SetAudioFileRecordingMBID :exec
UPDATE audio_files SET recording_mbid = ? WHERE id = ?
`
type SetAudioFileRecordingMBIDParams struct {
RecordingMbid sql.NullString
ID int64
}
func (q *Queries) SetAudioFileRecordingMBID(ctx context.Context, arg SetAudioFileRecordingMBIDParams) error {
_, err := q.db.ExecContext(ctx, setAudioFileRecordingMBID, arg.RecordingMbid, arg.ID)
return err
}
const updateAudioFileStat = `-- name: UpdateAudioFileStat :exec
UPDATE audio_files
SET modified_at = ?, file_size = ?
WHERE id = ?
`
type UpdateAudioFileStatParams struct {
ModifiedAt int64
FileSize int64
ID int64
}
// 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.
func (q *Queries) UpdateAudioFileStat(ctx context.Context, arg UpdateAudioFileStatParams) error {
_, err := q.db.ExecContext(ctx, updateAudioFileStat, arg.ModifiedAt, arg.FileSize, arg.ID)
return err
}
const updateAudioFileTags = `-- name: UpdateAudioFileTags :exec
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 = ?
`
type UpdateAudioFileTagsParams struct {
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
SampleRate int64
BitDepth int64
Channels int64
Bitrate int64
FileSize int64
LengthMilliseconds int64
ModifiedAt int64
ID int64
}
// 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.
func (q *Queries) UpdateAudioFileTags(ctx context.Context, arg UpdateAudioFileTagsParams) error {
_, err := q.db.ExecContext(ctx, updateAudioFileTags,
arg.Title,
arg.ArtistCredit,
arg.ArtistID,
arg.AlbumID,
arg.TrackNumber,
arg.DiscNumber,
arg.TotalTracks,
arg.Year,
arg.Composer,
arg.Comment,
arg.RecordingMbid,
arg.SampleRate,
arg.BitDepth,
arg.Channels,
arg.Bitrate,
arg.FileSize,
arg.LengthMilliseconds,
arg.ModifiedAt,
arg.ID,
)
return err
}