// 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 adoptTrackPath = `-- name: AdoptTrackPath :execrows UPDATE tracks SET file_path = $1, missing_since = NULL WHERE id = $2 AND missing_since IS NOT NULL ` type AdoptTrackPathParams struct { FilePath string ID pgtype.UUID } // Re-points a missing row at the path its file turned up on, and clears the // mark. The caller's normal UpsertTrack then conflicts on file_path and updates // THIS row in place, so the track id survives and its likes, play history and // playlist memberships come with it. // // `missing_since IS NOT NULL` again, this time as a race guard: two files can't // both adopt the same row, and :execrows reports 0 to whichever loses. func (q *Queries) AdoptTrackPath(ctx context.Context, arg AdoptTrackPathParams) (int64, error) { result, err := q.db.Exec(ctx, adoptTrackPath, arg.FilePath, arg.ID) if err != nil { return 0, err } return result.RowsAffected(), nil } const clearTracksMissing = `-- name: ClearTracksMissing :execrows UPDATE tracks SET missing_since = NULL WHERE id = ANY($1::uuid[]) AND missing_since IS NOT NULL ` // Clears the mark on rows whose file is back. Runs independently of the mtime // skip check, so a file that reappears unchanged is un-marked even though the // scanner skips re-reading its tags. func (q *Queries) ClearTracksMissing(ctx context.Context, ids []pgtype.UUID) (int64, error) { result, err := q.db.Exec(ctx, clearTracksMissing, ids) if err != nil { return 0, err } return result.RowsAffected(), nil } 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 findMissingTrackByFingerprint = `-- name: FindMissingTrackByFingerprint :many SELECT id, file_path FROM tracks WHERE missing_since IS NOT NULL AND file_size = $1 AND duration_ms = $2 LIMIT 2 ` type FindMissingTrackByFingerprintParams struct { FileSize int64 DurationMs int32 } type FindMissingTrackByFingerprintRow struct { ID pgtype.UUID FilePath string } // Move detection fallback for files with no MBID (#2528). Exact byte size AND // exact decoded duration is a strong pair: a plain move or rename preserves // both, while a re-encode changes at least one — and a re-encode genuinely is a // different file, so failing to match there is correct rather than a gap. // // Same missing-only constraint and same LIMIT 2 rationale as the MBID variant. func (q *Queries) FindMissingTrackByFingerprint(ctx context.Context, arg FindMissingTrackByFingerprintParams) ([]FindMissingTrackByFingerprintRow, error) { rows, err := q.db.Query(ctx, findMissingTrackByFingerprint, arg.FileSize, arg.DurationMs) if err != nil { return nil, err } defer rows.Close() var items []FindMissingTrackByFingerprintRow for rows.Next() { var i FindMissingTrackByFingerprintRow 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 findMissingTrackByMbid = `-- name: FindMissingTrackByMbid :many SELECT id, file_path FROM tracks WHERE missing_since IS NOT NULL AND mbid IS NOT NULL AND mbid = $1::text LIMIT 2 ` type FindMissingTrackByMbidRow struct { ID pgtype.UUID FilePath string } // Move detection, strongest signal (#2528). A file that turned up at a new path // carrying a recording MBID we already have on a MISSING row is that recording, // moved — not a new track. // // `missing_since IS NOT NULL` is the safety constraint, not an optimisation: a // row whose file is present elsewhere on disk is a DUPLICATE, and re-pointing // its file_path would corrupt the copy that still exists. // // LIMIT 2 because the caller only needs to know "exactly one" vs "more than // one" — an ambiguous match must not be adopted arbitrarily. func (q *Queries) FindMissingTrackByMbid(ctx context.Context, mbid string) ([]FindMissingTrackByMbidRow, error) { rows, err := q.db.Query(ctx, findMissingTrackByMbid, mbid) if err != nil { return nil, err } defer rows.Close() var items []FindMissingTrackByMbidRow for rows.Next() { var i FindMissingTrackByMbidRow 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 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, tag_source, tag_sources_version, tag_read_version, missing_since 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, &i.TagSource, &i.TagSourcesVersion, &i.TagReadVersion, &i.MissingSince, ) 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, tag_source, tag_sources_version, tag_read_version, missing_since 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, &i.TagSource, &i.TagSourcesVersion, &i.TagReadVersion, &i.MissingSince, ) 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, tag_source, tag_sources_version, tag_read_version, missing_since 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, &i.TagSource, &i.TagSourcesVersion, &i.TagReadVersion, &i.MissingSince, ); 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, t.tag_source, t.tag_sources_version, t.tag_read_version, t.missing_since, 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.Track.TagSource, &i.Track.TagSourcesVersion, &i.Track.TagReadVersion, &i.Track.MissingSince, &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, t.tag_source, t.tag_sources_version, t.tag_read_version, t.missing_since, 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.Track.TagSource, &i.Track.TagSourcesVersion, &i.Track.TagReadVersion, &i.Track.MissingSince, &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 listTrackPathsForReconcile = `-- name: ListTrackPathsForReconcile :many SELECT id, file_path, missing_since FROM tracks ` type ListTrackPathsForReconcileRow struct { ID pgtype.UUID FilePath string MissingSince pgtype.Timestamptz } // Every row's path + current missing mark, for the scanner's reconcile pass // (#2523). Deliberately unfiltered and unpaged: reconcile has to compare the // WHOLE table against what the walk saw, and a filtered subset would let rows // outside it drift forever. Three narrow columns keep it cheap even on a // library of a few hundred thousand tracks. func (q *Queries) ListTrackPathsForReconcile(ctx context.Context) ([]ListTrackPathsForReconcileRow, error) { rows, err := q.db.Query(ctx, listTrackPathsForReconcile) if err != nil { return nil, err } defer rows.Close() var items []ListTrackPathsForReconcileRow for rows.Next() { var i ListTrackPathsForReconcileRow if err := rows.Scan(&i.ID, &i.FilePath, &i.MissingSince); 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, tag_source, tag_sources_version, tag_read_version, missing_since 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, &i.TagSource, &i.TagSourcesVersion, &i.TagReadVersion, &i.MissingSince, ); 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 markTracksMissing = `-- name: MarkTracksMissing :execrows UPDATE tracks SET missing_since = now() WHERE id = ANY($1::uuid[]) AND missing_since IS NULL ` // Marks rows whose file the walk did not see. `missing_since IS NULL` in the // predicate makes this idempotent: a row already marked keeps its ORIGINAL // timestamp, so "how long has it been gone" survives repeated scans. Losing // that would make any age-based cleanup policy meaningless. // // updated_at is deliberately NOT touched. It tracks content changes and gates // the scanner's mtime skip; moving it here would make a returning file look // newer than its own mtime and stop its tags being re-read. func (q *Queries) MarkTracksMissing(ctx context.Context, ids []pgtype.UUID) (int64, error) { result, err := q.db.Exec(ctx, markTracksMissing, ids) if err != nil { return 0, err } return result.RowsAffected(), 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, tag_source, tag_sources_version, tag_read_version, missing_since 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, &i.TagSource, &i.TagSourcesVersion, &i.TagReadVersion, &i.MissingSince, ); 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, tag_read_version ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) 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, -- Stamped on update too, so a tag-repair pass marks rows as done and the -- next scan can short-circuit them again (#2499). tag_read_version = EXCLUDED.tag_read_version, 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, tag_source, tag_sources_version, tag_read_version, missing_since ` 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 TagReadVersion int16 } // 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, arg.TagReadVersion, ) 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, &i.TagSource, &i.TagSourcesVersion, &i.TagReadVersion, &i.MissingSince, ) return i, err }