Files
minstrel/internal/db/dbq/tracks.sql.go
T
bvandeusen e2432caa65 fix(library): extract recording MBID → tracks.mbid; unblock LB similarity
tracks.mbid was 100% NULL: the scanner only extracted album + artist
MBIDs, never the recording MBID. The ListenBrainz similarity worker is
gated on tracks.mbid IS NOT NULL, so track_similarity could never
populate — starving For-You/radio's strongest candidate source
(lb_similar) on every library.

- mbids.go: add extractRecordingMBID (mbz.Recording / Picard
  musicbrainz_recordingid). Separate fn so extractMBIDs' signature +
  unit tests stay untouched.
- scanner.go: persist recording MBID via UpsertTrack (heals on the
  file_path conflict, so re-scans backfill for free).
- BackfillTrackMBIDs: one-shot pass mirroring BackfillMBIDs, wired as
  scan Stage 2b (idempotent via SetTrackMbidIfNull, gated by
  BackfillCap, log-only progress).
- migration 0029: tracks_mbid_unique (0002, written when the column
  was always NULL) wrongly assumes one MBID == one track. A recording
  appears on multiple releases, so rows legitimately share a recording
  MBID. Replace with a non-unique partial index. Zero-risk: column is
  100% NULL at migration time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:49:38 -04:00

565 lines
14 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: tracks.sql
package dbq
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const countTracksByAlbum = `-- name: CountTracksByAlbum :one
SELECT count(*) FROM tracks WHERE album_id = $1
`
func (q *Queries) CountTracksByAlbum(ctx context.Context, albumID pgtype.UUID) (int64, error) {
row := q.db.QueryRow(ctx, countTracksByAlbum, albumID)
var count int64
err := row.Scan(&count)
return count, err
}
const countTracksByArtist = `-- name: CountTracksByArtist :one
SELECT COUNT(*) FROM tracks WHERE artist_id = $1
`
// Used by request-progress reporting to count tracks ingested under a
// matched artist (sum across all the artist's albums) while a request
// is still in flight.
func (q *Queries) CountTracksByArtist(ctx context.Context, artistID pgtype.UUID) (int64, error) {
row := q.db.QueryRow(ctx, countTracksByArtist, artistID)
var count int64
err := row.Scan(&count)
return count, err
}
const countTracksMatching = `-- name: CountTracksMatching :one
SELECT COUNT(*) FROM tracks
WHERE title ILIKE '%' || $1::text || '%'
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
WHERE q.user_id = $2 AND q.track_id = tracks.id
)
`
type CountTracksMatchingParams struct {
Column1 string
UserID pgtype.UUID
}
// $1 = title query, $2 = user_id (NULL to skip quarantine filter).
func (q *Queries) CountTracksMatching(ctx context.Context, arg CountTracksMatchingParams) (int64, error) {
row := q.db.QueryRow(ctx, countTracksMatching, arg.Column1, arg.UserID)
var count int64
err := row.Scan(&count)
return count, err
}
const deleteTrack = `-- name: DeleteTrack :one
DELETE FROM tracks WHERE id = $1
RETURNING id, album_id, artist_id, file_path, mbid
`
type DeleteTrackRow struct {
ID pgtype.UUID
AlbumID pgtype.UUID
ArtistID pgtype.UUID
FilePath string
Mbid *string
}
// M7 #372: hard delete with FK cascade. The CASCADE on track_id from
// play_events / general_likes_tracks / lidarr_quarantine /
// lidarr_quarantine_actions handles their cleanup. RETURNING gives us
// album_id + artist_id for the album-empty / artist-empty cascade
// checks the service does next.
func (q *Queries) DeleteTrack(ctx context.Context, id pgtype.UUID) (DeleteTrackRow, error) {
row := q.db.QueryRow(ctx, deleteTrack, id)
var i DeleteTrackRow
err := row.Scan(
&i.ID,
&i.AlbumID,
&i.ArtistID,
&i.FilePath,
&i.Mbid,
)
return i, err
}
const getTrackByID = `-- name: GetTrackByID :one
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at FROM tracks WHERE id = $1
`
func (q *Queries) GetTrackByID(ctx context.Context, id pgtype.UUID) (Track, error) {
row := q.db.QueryRow(ctx, getTrackByID, id)
var i Track
err := row.Scan(
&i.ID,
&i.Title,
&i.AlbumID,
&i.ArtistID,
&i.TrackNumber,
&i.DiscNumber,
&i.DurationMs,
&i.FilePath,
&i.FileSize,
&i.FileFormat,
&i.Bitrate,
&i.Mbid,
&i.Genre,
&i.AddedAt,
&i.UpdatedAt,
)
return i, err
}
const getTrackByPath = `-- name: GetTrackByPath :one
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at FROM tracks WHERE file_path = $1
`
func (q *Queries) GetTrackByPath(ctx context.Context, filePath string) (Track, error) {
row := q.db.QueryRow(ctx, getTrackByPath, filePath)
var i Track
err := row.Scan(
&i.ID,
&i.Title,
&i.AlbumID,
&i.ArtistID,
&i.TrackNumber,
&i.DiscNumber,
&i.DurationMs,
&i.FilePath,
&i.FileSize,
&i.FileFormat,
&i.Bitrate,
&i.Mbid,
&i.Genre,
&i.AddedAt,
&i.UpdatedAt,
)
return i, err
}
const getTracksByIDs = `-- name: GetTracksByIDs :many
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at FROM tracks WHERE id = ANY($1::uuid[])
`
// Batched lookup used by /api/library/sync to hydrate upsert payloads
// (#357). Mirror of GetArtistsByIDs.
func (q *Queries) GetTracksByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Track, error) {
rows, err := q.db.Query(ctx, getTracksByIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Track
for rows.Next() {
var i Track
if err := rows.Scan(
&i.ID,
&i.Title,
&i.AlbumID,
&i.ArtistID,
&i.TrackNumber,
&i.DiscNumber,
&i.DurationMs,
&i.FilePath,
&i.FileSize,
&i.FileFormat,
&i.Bitrate,
&i.Mbid,
&i.Genre,
&i.AddedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listArtistTracksForUser = `-- name: ListArtistTracksForUser :many
SELECT t.id, t.title, t.album_id, t.artist_id, t.track_number, t.disc_number, t.duration_ms, t.file_path, t.file_size, t.file_format, t.bitrate, t.mbid, t.genre, t.added_at, t.updated_at,
albums.title AS album_title,
artists.name AS artist_name
FROM tracks t
JOIN albums ON albums.id = t.album_id
JOIN artists ON artists.id = t.artist_id
WHERE t.artist_id = $1
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
WHERE q.user_id = $2 AND q.track_id = t.id
)
ORDER BY albums.release_date NULLS LAST, albums.sort_title,
t.disc_number NULLS FIRST, t.track_number NULLS FIRST, t.id
`
type ListArtistTracksForUserParams struct {
ArtistID pgtype.UUID
UserID pgtype.UUID
}
type ListArtistTracksForUserRow struct {
Track Track
AlbumTitle string
ArtistName string
}
// M6a: every track for the artist across their albums, with album_title
// and artist_name joined. Honors per-user lidarr_quarantine. Used by
// /api/artists/{id}/tracks for the artist-card play affordance, which
// shuffles client-side. Ordering matches album/track natural order so
// the shuffle has a deterministic input.
func (q *Queries) ListArtistTracksForUser(ctx context.Context, arg ListArtistTracksForUserParams) ([]ListArtistTracksForUserRow, error) {
rows, err := q.db.Query(ctx, listArtistTracksForUser, arg.ArtistID, arg.UserID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListArtistTracksForUserRow
for rows.Next() {
var i ListArtistTracksForUserRow
if err := rows.Scan(
&i.Track.ID,
&i.Track.Title,
&i.Track.AlbumID,
&i.Track.ArtistID,
&i.Track.TrackNumber,
&i.Track.DiscNumber,
&i.Track.DurationMs,
&i.Track.FilePath,
&i.Track.FileSize,
&i.Track.FileFormat,
&i.Track.Bitrate,
&i.Track.Mbid,
&i.Track.Genre,
&i.Track.AddedAt,
&i.Track.UpdatedAt,
&i.AlbumTitle,
&i.ArtistName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listRandomTracksForUser = `-- name: ListRandomTracksForUser :many
SELECT t.id, t.title, t.album_id, t.artist_id, t.track_number, t.disc_number, t.duration_ms, t.file_path, t.file_size, t.file_format, t.bitrate, t.mbid, t.genre, t.added_at, t.updated_at,
albums.title AS album_title,
artists.name AS artist_name
FROM tracks t
JOIN albums ON albums.id = t.album_id
JOIN artists ON artists.id = t.artist_id
WHERE NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
)
ORDER BY random()
LIMIT $2
`
type ListRandomTracksForUserParams struct {
UserID pgtype.UUID
Limit int32
}
type ListRandomTracksForUserRow struct {
Track Track
AlbumTitle string
ArtistName string
}
// #427 S4: backing query for GET /api/library/shuffle — the online
// source for "Shuffle all". N random tracks across the whole
// library, per-user-quarantine filtered. $1 user_id, $2 limit.
func (q *Queries) ListRandomTracksForUser(ctx context.Context, arg ListRandomTracksForUserParams) ([]ListRandomTracksForUserRow, error) {
rows, err := q.db.Query(ctx, listRandomTracksForUser, arg.UserID, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListRandomTracksForUserRow
for rows.Next() {
var i ListRandomTracksForUserRow
if err := rows.Scan(
&i.Track.ID,
&i.Track.Title,
&i.Track.AlbumID,
&i.Track.ArtistID,
&i.Track.TrackNumber,
&i.Track.DiscNumber,
&i.Track.DurationMs,
&i.Track.FilePath,
&i.Track.FileSize,
&i.Track.FileFormat,
&i.Track.Bitrate,
&i.Track.Mbid,
&i.Track.Genre,
&i.Track.AddedAt,
&i.Track.UpdatedAt,
&i.AlbumTitle,
&i.ArtistName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listTracksByAlbum = `-- name: ListTracksByAlbum :many
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at FROM tracks
WHERE album_id = $1
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
WHERE q.user_id = $2 AND q.track_id = tracks.id
)
ORDER BY disc_number NULLS LAST, track_number NULLS LAST
`
type ListTracksByAlbumParams struct {
AlbumID pgtype.UUID
UserID pgtype.UUID
}
// $1 = album_id, $2 = user_id. Pass pgtype.UUID{Valid: false} (NULL)
// to skip the per-user quarantine filter; the NOT EXISTS clause on
// a NULL user_id never matches a row, so every track passes through.
func (q *Queries) ListTracksByAlbum(ctx context.Context, arg ListTracksByAlbumParams) ([]Track, error) {
rows, err := q.db.Query(ctx, listTracksByAlbum, arg.AlbumID, arg.UserID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Track
for rows.Next() {
var i Track
if err := rows.Scan(
&i.ID,
&i.Title,
&i.AlbumID,
&i.ArtistID,
&i.TrackNumber,
&i.DiscNumber,
&i.DurationMs,
&i.FilePath,
&i.FileSize,
&i.FileFormat,
&i.Bitrate,
&i.Mbid,
&i.Genre,
&i.AddedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listTracksMissingMbidWithPath = `-- name: ListTracksMissingMbidWithPath :many
SELECT id, file_path
FROM tracks
WHERE mbid IS NULL
ORDER BY id
LIMIT $1
`
type ListTracksMissingMbidWithPathRow struct {
ID pgtype.UUID
FilePath string
}
// Track recording-MBID backfill: tracks with NULL mbid that still have
// a file to re-read. $1 caps the batch (mirrors the album backfill).
func (q *Queries) ListTracksMissingMbidWithPath(ctx context.Context, limit int32) ([]ListTracksMissingMbidWithPathRow, error) {
rows, err := q.db.Query(ctx, listTracksMissingMbidWithPath, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListTracksMissingMbidWithPathRow
for rows.Next() {
var i ListTracksMissingMbidWithPathRow
if err := rows.Scan(&i.ID, &i.FilePath); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const searchTracks = `-- name: SearchTracks :many
SELECT id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at FROM tracks
WHERE title ILIKE '%' || $1::text || '%'
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
WHERE q.user_id = $2 AND q.track_id = tracks.id
)
ORDER BY title
LIMIT $3 OFFSET $4
`
type SearchTracksParams struct {
Column1 string
UserID pgtype.UUID
Limit int32
Offset int32
}
// $1 = title query, $2 = user_id (NULL to skip quarantine filter),
// $3 = limit, $4 = offset.
func (q *Queries) SearchTracks(ctx context.Context, arg SearchTracksParams) ([]Track, error) {
rows, err := q.db.Query(ctx, searchTracks,
arg.Column1,
arg.UserID,
arg.Limit,
arg.Offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Track
for rows.Next() {
var i Track
if err := rows.Scan(
&i.ID,
&i.Title,
&i.AlbumID,
&i.ArtistID,
&i.TrackNumber,
&i.DiscNumber,
&i.DurationMs,
&i.FilePath,
&i.FileSize,
&i.FileFormat,
&i.Bitrate,
&i.Mbid,
&i.Genre,
&i.AddedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const setTrackMbidIfNull = `-- name: SetTrackMbidIfNull :exec
UPDATE tracks
SET mbid = $2, updated_at = now()
WHERE id = $1 AND mbid IS NULL
`
type SetTrackMbidIfNullParams struct {
ID pgtype.UUID
Mbid *string
}
// Heal a track's recording MBID only while still NULL — idempotent, so
// re-running the backfill is a no-op for already-healed rows.
func (q *Queries) SetTrackMbidIfNull(ctx context.Context, arg SetTrackMbidIfNullParams) error {
_, err := q.db.Exec(ctx, setTrackMbidIfNull, arg.ID, arg.Mbid)
return err
}
const upsertTrack = `-- name: UpsertTrack :one
INSERT INTO tracks (
title, album_id, artist_id, track_number, disc_number,
duration_ms, file_path, file_size, file_format, bitrate, mbid, genre
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (file_path) DO UPDATE SET
title = EXCLUDED.title,
album_id = EXCLUDED.album_id,
artist_id = EXCLUDED.artist_id,
track_number = EXCLUDED.track_number,
disc_number = EXCLUDED.disc_number,
duration_ms = EXCLUDED.duration_ms,
file_size = EXCLUDED.file_size,
file_format = EXCLUDED.file_format,
bitrate = EXCLUDED.bitrate,
mbid = EXCLUDED.mbid,
genre = EXCLUDED.genre,
updated_at = now()
RETURNING id, title, album_id, artist_id, track_number, disc_number, duration_ms, file_path, file_size, file_format, bitrate, mbid, genre, added_at, updated_at
`
type UpsertTrackParams struct {
Title string
AlbumID pgtype.UUID
ArtistID pgtype.UUID
TrackNumber *int32
DiscNumber *int32
DurationMs int32
FilePath string
FileSize int64
FileFormat string
Bitrate *int32
Mbid *string
Genre *string
}
// file_path is the canonical identity for library scan; mbid is secondary.
func (q *Queries) UpsertTrack(ctx context.Context, arg UpsertTrackParams) (Track, error) {
row := q.db.QueryRow(ctx, upsertTrack,
arg.Title,
arg.AlbumID,
arg.ArtistID,
arg.TrackNumber,
arg.DiscNumber,
arg.DurationMs,
arg.FilePath,
arg.FileSize,
arg.FileFormat,
arg.Bitrate,
arg.Mbid,
arg.Genre,
)
var i Track
err := row.Scan(
&i.ID,
&i.Title,
&i.AlbumID,
&i.ArtistID,
&i.TrackNumber,
&i.DiscNumber,
&i.DurationMs,
&i.FilePath,
&i.FileSize,
&i.FileFormat,
&i.Bitrate,
&i.Mbid,
&i.Genre,
&i.AddedAt,
&i.UpdatedAt,
)
return i, err
}