feat(library): AcoustID lookup worker fills the MBIDs tags leave empty (M401 #3920 #3921)
release / web (push) Successful in 1m44s
release / go (push) Successful in 2m1s
release / govulncheck (push) Successful in 17s
release / integration (push) Successful in 5m22s
release / android (push) Successful in 5m48s
release / Build signed APK (releases and dev) (push) Successful in 5m53s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 2m6s
release / Verify release artifacts (tag releases only) (push) Skipped
release / web (push) Successful in 1m44s
release / go (push) Successful in 2m1s
release / govulncheck (push) Successful in 17s
release / integration (push) Successful in 5m22s
release / android (push) Successful in 5m48s
release / Build signed APK (releases and dev) (push) Successful in 5m53s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 2m6s
release / Verify release artifacts (tag releases only) (push) Skipped
Migration 0069 adds tracks.mbid_source (tag | acoustid), a lookup state per track (matched | ambiguous | no_match | failed) and the acoustid_settings row (off, no key, min score 0.85). The file's tag outranks a lookup (D4). UpsertTrack keeps a looked-up id through a re-read that finds no tag id and replaces it as soon as one appears. SetTrackMbidFromAcoustID refuses to write over a tag id. The worker fingerprints each untagged track with fpcalc's compressed print, looks it up and writes an id only when D5 settles it: one recording at or above the threshold, or one left after matching title and length. Ambiguous and no-match results write nothing. A key AcoustID refuses, or the service being unreachable, stops the pass and is reported in the worker's status. It never counts as a verdict on a track. A changed file drops its lookup in the scan. The re-lookup takes back an id that no longer matches. Admin API: GET /api/admin/library/acoustid (settings, status, coverage by source), PUT …/acoustid-settings (write-only key), POST …/acoustid/run, GET …/acoustid/unsettled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: acoustid.sql
|
||||
|
||||
package dbq
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const clearTrackAcoustIDMbid = `-- name: ClearTrackAcoustIDMbid :exec
|
||||
UPDATE tracks
|
||||
SET mbid = NULL, mbid_source = NULL
|
||||
WHERE id = $1
|
||||
AND mbid_source = 'acoustid'
|
||||
`
|
||||
|
||||
// A re-lookup of changed bytes that no longer matches takes back the id an
|
||||
// earlier lookup wrote. A tag id is never touched.
|
||||
func (q *Queries) ClearTrackAcoustIDMbid(ctx context.Context, id pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, clearTrackAcoustIDMbid, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteAcoustIDLookup = `-- name: DeleteAcoustIDLookup :exec
|
||||
DELETE FROM track_acoustid_lookups WHERE track_id = $1
|
||||
`
|
||||
|
||||
// The scan drops the lookup of bytes that are gone, so the worker checks the
|
||||
// new ones.
|
||||
func (q *Queries) DeleteAcoustIDLookup(ctx context.Context, trackID pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, deleteAcoustIDLookup, trackID)
|
||||
return err
|
||||
}
|
||||
|
||||
const getAcoustIDSettings = `-- name: GetAcoustIDSettings :one
|
||||
SELECT id, enabled, api_key, min_score, updated_at FROM acoustid_settings WHERE id = true
|
||||
`
|
||||
|
||||
func (q *Queries) GetAcoustIDSettings(ctx context.Context) (AcoustidSetting, error) {
|
||||
row := q.db.QueryRow(ctx, getAcoustIDSettings)
|
||||
var i AcoustidSetting
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Enabled,
|
||||
&i.ApiKey,
|
||||
&i.MinScore,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getMbidCoverage = `-- name: GetMbidCoverage :one
|
||||
SELECT count(*)::bigint AS total,
|
||||
count(*) FILTER (WHERE t.mbid_source = 'tag')::bigint AS from_tag,
|
||||
count(*) FILTER (WHERE t.mbid_source = 'acoustid')::bigint AS from_acoustid,
|
||||
count(*) FILTER (WHERE l.state = 'ambiguous')::bigint AS ambiguous,
|
||||
count(*) FILTER (WHERE l.state = 'no_match')::bigint AS no_match,
|
||||
count(*) FILTER (WHERE l.state = 'failed')::bigint AS failed,
|
||||
count(*) FILTER (
|
||||
WHERE l.track_id IS NULL AND (t.mbid IS NULL OR t.mbid_source = 'acoustid')
|
||||
)::bigint AS pending
|
||||
FROM tracks t
|
||||
LEFT JOIN track_acoustid_lookups l ON l.track_id = t.id
|
||||
WHERE t.missing_since IS NULL
|
||||
`
|
||||
|
||||
type GetMbidCoverageRow struct {
|
||||
Total int64
|
||||
FromTag int64
|
||||
FromAcoustid int64
|
||||
Ambiguous int64
|
||||
NoMatch int64
|
||||
Failed int64
|
||||
Pending int64
|
||||
}
|
||||
|
||||
// The admin gauge: present tracks by where their MBID came from, and the
|
||||
// lookup states of those a lookup has seen. pending is what the worker's
|
||||
// queue holds.
|
||||
func (q *Queries) GetMbidCoverage(ctx context.Context) (GetMbidCoverageRow, error) {
|
||||
row := q.db.QueryRow(ctx, getMbidCoverage)
|
||||
var i GetMbidCoverageRow
|
||||
err := row.Scan(
|
||||
&i.Total,
|
||||
&i.FromTag,
|
||||
&i.FromAcoustid,
|
||||
&i.Ambiguous,
|
||||
&i.NoMatch,
|
||||
&i.Failed,
|
||||
&i.Pending,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listTracksNeedingAcoustIDLookup = `-- name: ListTracksNeedingAcoustIDLookup :many
|
||||
|
||||
SELECT t.id, t.file_path, t.title, t.duration_ms
|
||||
FROM tracks t
|
||||
WHERE (t.mbid IS NULL OR t.mbid_source = 'acoustid')
|
||||
AND t.missing_since IS NULL
|
||||
AND t.id > $1::uuid
|
||||
AND NOT EXISTS (SELECT 1 FROM track_acoustid_lookups l WHERE l.track_id = t.id)
|
||||
ORDER BY t.id
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
type ListTracksNeedingAcoustIDLookupParams struct {
|
||||
AfterID pgtype.UUID
|
||||
BatchLimit int32
|
||||
}
|
||||
|
||||
type ListTracksNeedingAcoustIDLookupRow struct {
|
||||
ID pgtype.UUID
|
||||
FilePath string
|
||||
Title string
|
||||
DurationMs int32
|
||||
}
|
||||
|
||||
// AcoustID recording-id lookup (M401). Migration 0069 says what each
|
||||
// lookup state means.
|
||||
// The lookup worker's queue: present tracks with no tag MBID and no lookup
|
||||
// since their file last changed. A looked-up id is included once its row is
|
||||
// gone (the file changed), so the new bytes are checked rather than trusted.
|
||||
// Keyset-paged on id, as the loudness queue is, so a pass ends even when
|
||||
// every attempt is inconclusive and writes nothing.
|
||||
func (q *Queries) ListTracksNeedingAcoustIDLookup(ctx context.Context, arg ListTracksNeedingAcoustIDLookupParams) ([]ListTracksNeedingAcoustIDLookupRow, error) {
|
||||
rows, err := q.db.Query(ctx, listTracksNeedingAcoustIDLookup, arg.AfterID, arg.BatchLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListTracksNeedingAcoustIDLookupRow
|
||||
for rows.Next() {
|
||||
var i ListTracksNeedingAcoustIDLookupRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.FilePath,
|
||||
&i.Title,
|
||||
&i.DurationMs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listUnsettledAcoustIDLookups = `-- name: ListUnsettledAcoustIDLookups :many
|
||||
SELECT t.id, t.title, t.file_path, ar.name AS artist_name, al.title AS album_title,
|
||||
l.state, l.best_score, l.candidates, l.detail, l.looked_up_at
|
||||
FROM track_acoustid_lookups l
|
||||
JOIN tracks t ON t.id = l.track_id
|
||||
JOIN artists ar ON ar.id = t.artist_id
|
||||
JOIN albums al ON al.id = t.album_id
|
||||
WHERE l.state IN ('ambiguous', 'failed')
|
||||
AND t.missing_since IS NULL
|
||||
ORDER BY ar.name, al.title, t.disc_number NULLS FIRST, t.track_number NULLS FIRST, t.title
|
||||
LIMIT $1
|
||||
`
|
||||
|
||||
type ListUnsettledAcoustIDLookupsRow struct {
|
||||
ID pgtype.UUID
|
||||
Title string
|
||||
FilePath string
|
||||
ArtistName string
|
||||
AlbumTitle string
|
||||
State string
|
||||
BestScore *float32
|
||||
Candidates int32
|
||||
Detail *string
|
||||
LookedUpAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
// The admin list of tracks a lookup could not settle (ambiguous or failed),
|
||||
// so they are findable rather than silently empty.
|
||||
func (q *Queries) ListUnsettledAcoustIDLookups(ctx context.Context, rowLimit int32) ([]ListUnsettledAcoustIDLookupsRow, error) {
|
||||
rows, err := q.db.Query(ctx, listUnsettledAcoustIDLookups, rowLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListUnsettledAcoustIDLookupsRow
|
||||
for rows.Next() {
|
||||
var i ListUnsettledAcoustIDLookupsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Title,
|
||||
&i.FilePath,
|
||||
&i.ArtistName,
|
||||
&i.AlbumTitle,
|
||||
&i.State,
|
||||
&i.BestScore,
|
||||
&i.Candidates,
|
||||
&i.Detail,
|
||||
&i.LookedUpAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const recordAcoustIDLookup = `-- name: RecordAcoustIDLookup :exec
|
||||
INSERT INTO track_acoustid_lookups (track_id, state, best_score, candidates, recording_mbid, detail)
|
||||
VALUES ($1, $2, $3, $4,
|
||||
$5, $6)
|
||||
ON CONFLICT (track_id) DO UPDATE SET
|
||||
state = EXCLUDED.state,
|
||||
best_score = EXCLUDED.best_score,
|
||||
candidates = EXCLUDED.candidates,
|
||||
recording_mbid = EXCLUDED.recording_mbid,
|
||||
detail = EXCLUDED.detail,
|
||||
looked_up_at = now()
|
||||
`
|
||||
|
||||
type RecordAcoustIDLookupParams struct {
|
||||
TrackID pgtype.UUID
|
||||
State string
|
||||
BestScore *float32
|
||||
Candidates int32
|
||||
RecordingMbid *string
|
||||
Detail *string
|
||||
}
|
||||
|
||||
func (q *Queries) RecordAcoustIDLookup(ctx context.Context, arg RecordAcoustIDLookupParams) error {
|
||||
_, err := q.db.Exec(ctx, recordAcoustIDLookup,
|
||||
arg.TrackID,
|
||||
arg.State,
|
||||
arg.BestScore,
|
||||
arg.Candidates,
|
||||
arg.RecordingMbid,
|
||||
arg.Detail,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const setTrackMbidFromAcoustID = `-- name: SetTrackMbidFromAcoustID :execrows
|
||||
UPDATE tracks
|
||||
SET mbid = $1, mbid_source = 'acoustid'
|
||||
WHERE id = $2
|
||||
AND (mbid IS NULL OR mbid_source = 'acoustid')
|
||||
`
|
||||
|
||||
type SetTrackMbidFromAcoustIDParams struct {
|
||||
Mbid *string
|
||||
ID pgtype.UUID
|
||||
}
|
||||
|
||||
// Never over a tag id (D4): the guard is here, not only in the worker, so a
|
||||
// tag the scan read between the worker's list and this write still wins.
|
||||
// updated_at is left alone: it gates the scan's mtime skip, and moving it
|
||||
// past a file's mtime would hide a change made just before the lookup.
|
||||
func (q *Queries) SetTrackMbidFromAcoustID(ctx context.Context, arg SetTrackMbidFromAcoustIDParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, setTrackMbidFromAcoustID, arg.Mbid, arg.ID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const updateAcoustIDSettings = `-- name: UpdateAcoustIDSettings :one
|
||||
UPDATE acoustid_settings
|
||||
SET enabled = $1,
|
||||
min_score = $2,
|
||||
api_key = CASE WHEN $3::boolean
|
||||
THEN NULLIF($4::text, '')
|
||||
ELSE api_key END,
|
||||
updated_at = now()
|
||||
WHERE id = true
|
||||
RETURNING id, enabled, api_key, min_score, updated_at
|
||||
`
|
||||
|
||||
type UpdateAcoustIDSettingsParams struct {
|
||||
Enabled bool
|
||||
MinScore float32
|
||||
SetApiKey bool
|
||||
ApiKey string
|
||||
}
|
||||
|
||||
// The key is write-only from the admin card: set_api_key false keeps the
|
||||
// stored one, and an empty api_key with set_api_key true clears it.
|
||||
func (q *Queries) UpdateAcoustIDSettings(ctx context.Context, arg UpdateAcoustIDSettingsParams) (AcoustidSetting, error) {
|
||||
row := q.db.QueryRow(ctx, updateAcoustIDSettings,
|
||||
arg.Enabled,
|
||||
arg.MinScore,
|
||||
arg.SetApiKey,
|
||||
arg.ApiKey,
|
||||
)
|
||||
var i AcoustidSetting
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Enabled,
|
||||
&i.ApiKey,
|
||||
&i.MinScore,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -261,7 +261,7 @@ func (q *Queries) InsertSkipEvent(ctx context.Context, arg InsertSkipEventParams
|
||||
}
|
||||
|
||||
const listRecentSessionTracks = `-- name: ListRecentSessionTracks :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 FROM tracks t
|
||||
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, t.mbid_source FROM tracks t
|
||||
JOIN play_events pe ON pe.track_id = t.id
|
||||
WHERE pe.session_id = $1
|
||||
AND pe.started_at < $2
|
||||
@@ -307,6 +307,7 @@ func (q *Queries) ListRecentSessionTracks(ctx context.Context, arg ListRecentSes
|
||||
&i.TagSourcesVersion,
|
||||
&i.TagReadVersion,
|
||||
&i.MissingSince,
|
||||
&i.MbidSource,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
const listUserHistory = `-- name: ListUserHistory :many
|
||||
SELECT pe.id AS event_id,
|
||||
pe.started_at,
|
||||
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,
|
||||
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, t.mbid_source,
|
||||
albums.title AS album_title,
|
||||
artists.name AS artist_name
|
||||
FROM play_events pe
|
||||
@@ -81,6 +81,7 @@ func (q *Queries) ListUserHistory(ctx context.Context, arg ListUserHistoryParams
|
||||
&i.Track.TagSourcesVersion,
|
||||
&i.Track.TagReadVersion,
|
||||
&i.Track.MissingSince,
|
||||
&i.Track.MbidSource,
|
||||
&i.AlbumTitle,
|
||||
&i.ArtistName,
|
||||
); err != nil {
|
||||
|
||||
@@ -259,7 +259,7 @@ func (q *Queries) ListLikedTrackIDs(ctx context.Context, userID pgtype.UUID) ([]
|
||||
}
|
||||
|
||||
const listLikedTrackRows = `-- name: ListLikedTrackRows :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 FROM tracks t
|
||||
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, t.mbid_source FROM tracks t
|
||||
JOIN general_likes l ON l.track_id = t.id
|
||||
WHERE l.user_id = $1
|
||||
ORDER BY l.liked_at DESC
|
||||
@@ -301,6 +301,7 @@ func (q *Queries) ListLikedTrackRows(ctx context.Context, arg ListLikedTrackRows
|
||||
&i.TagSourcesVersion,
|
||||
&i.TagReadVersion,
|
||||
&i.MissingSince,
|
||||
&i.MbidSource,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ func (q *Queries) MergeCopyTrackTags(ctx context.Context, arg MergeCopyTrackTags
|
||||
|
||||
const mergeInheritTrackMbid = `-- name: MergeInheritTrackMbid :exec
|
||||
UPDATE tracks AS survivor
|
||||
SET mbid = loser.mbid
|
||||
SET mbid = loser.mbid, mbid_source = loser.mbid_source
|
||||
FROM tracks AS loser
|
||||
WHERE survivor.id = $1::uuid
|
||||
AND loser.id = $2::uuid
|
||||
|
||||
@@ -187,6 +187,14 @@ func (ns NullLidarrRequestStatus) Value() (driver.Value, error) {
|
||||
return string(ns.LidarrRequestStatus), nil
|
||||
}
|
||||
|
||||
type AcoustidSetting struct {
|
||||
ID bool
|
||||
Enabled bool
|
||||
ApiKey *string
|
||||
MinScore float32
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type Album struct {
|
||||
ID pgtype.UUID
|
||||
Title string
|
||||
@@ -720,6 +728,17 @@ type Track struct {
|
||||
TagSourcesVersion int32
|
||||
TagReadVersion int16
|
||||
MissingSince pgtype.Timestamptz
|
||||
MbidSource *string
|
||||
}
|
||||
|
||||
type TrackAcoustidLookup struct {
|
||||
TrackID pgtype.UUID
|
||||
State string
|
||||
BestScore *float32
|
||||
Candidates int32
|
||||
RecordingMbid *string
|
||||
Detail *string
|
||||
LookedUpAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type TrackFingerprint struct {
|
||||
|
||||
@@ -208,7 +208,7 @@ WITH plays AS (
|
||||
WHERE user_id = $2 AND was_skipped = false
|
||||
GROUP BY track_id
|
||||
)
|
||||
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,
|
||||
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, t.mbid_source,
|
||||
albums.title AS album_title,
|
||||
artists.name AS artist_name
|
||||
FROM plays p
|
||||
@@ -270,6 +270,7 @@ func (q *Queries) ListMostPlayedTracksForArtist(ctx context.Context, arg ListMos
|
||||
&i.Track.TagSourcesVersion,
|
||||
&i.Track.TagReadVersion,
|
||||
&i.Track.MissingSince,
|
||||
&i.Track.MbidSource,
|
||||
&i.AlbumTitle,
|
||||
&i.ArtistName,
|
||||
); err != nil {
|
||||
@@ -290,7 +291,7 @@ WITH plays AS (
|
||||
WHERE user_id = $1 AND was_skipped = false
|
||||
GROUP BY track_id
|
||||
)
|
||||
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,
|
||||
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, t.mbid_source,
|
||||
albums.title AS album_title,
|
||||
artists.name AS artist_name
|
||||
FROM plays p
|
||||
@@ -354,6 +355,7 @@ func (q *Queries) ListMostPlayedTracksForUser(ctx context.Context, arg ListMostP
|
||||
&i.Track.TagSourcesVersion,
|
||||
&i.Track.TagReadVersion,
|
||||
&i.Track.MissingSince,
|
||||
&i.Track.MbidSource,
|
||||
&i.AlbumTitle,
|
||||
&i.ArtistName,
|
||||
); err != nil {
|
||||
@@ -691,7 +693,7 @@ func (q *Queries) ListRediscoverArtistsForUser(ctx context.Context, arg ListRedi
|
||||
|
||||
const loadRadioCandidates = `-- name: LoadRadioCandidates :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,
|
||||
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, t.mbid_source,
|
||||
(l.user_id IS NOT NULL)::bool AS is_liked,
|
||||
pe.last_played_at::timestamptz AS last_played_at,
|
||||
pe.play_count,
|
||||
@@ -772,6 +774,7 @@ func (q *Queries) LoadRadioCandidates(ctx context.Context, arg LoadRadioCandidat
|
||||
&i.Track.TagSourcesVersion,
|
||||
&i.Track.TagReadVersion,
|
||||
&i.Track.MissingSince,
|
||||
&i.Track.MbidSource,
|
||||
&i.IsLiked,
|
||||
&i.LastPlayedAt,
|
||||
&i.PlayCount,
|
||||
@@ -904,7 +907,7 @@ random_fill AS (
|
||||
LIMIT $9
|
||||
)
|
||||
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,
|
||||
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, t.mbid_source,
|
||||
(l.user_id IS NOT NULL)::bool AS is_liked,
|
||||
pe.last_played_at::timestamptz AS last_played_at,
|
||||
pe.play_count,
|
||||
@@ -1031,6 +1034,7 @@ func (q *Queries) LoadRadioCandidatesV2(ctx context.Context, arg LoadRadioCandid
|
||||
&i.Track.TagSourcesVersion,
|
||||
&i.Track.TagReadVersion,
|
||||
&i.Track.MissingSince,
|
||||
&i.Track.MbidSource,
|
||||
&i.IsLiked,
|
||||
&i.LastPlayedAt,
|
||||
&i.PlayCount,
|
||||
|
||||
@@ -233,7 +233,7 @@ func (q *Queries) FindMissingTrackByMbid(ctx context.Context, mbid string) ([]Fi
|
||||
}
|
||||
|
||||
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
|
||||
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, mbid_source FROM tracks WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetTrackByID(ctx context.Context, id pgtype.UUID) (Track, error) {
|
||||
@@ -259,12 +259,13 @@ func (q *Queries) GetTrackByID(ctx context.Context, id pgtype.UUID) (Track, erro
|
||||
&i.TagSourcesVersion,
|
||||
&i.TagReadVersion,
|
||||
&i.MissingSince,
|
||||
&i.MbidSource,
|
||||
)
|
||||
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
|
||||
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, mbid_source FROM tracks WHERE file_path = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetTrackByPath(ctx context.Context, filePath string) (Track, error) {
|
||||
@@ -290,12 +291,13 @@ func (q *Queries) GetTrackByPath(ctx context.Context, filePath string) (Track, e
|
||||
&i.TagSourcesVersion,
|
||||
&i.TagReadVersion,
|
||||
&i.MissingSince,
|
||||
&i.MbidSource,
|
||||
)
|
||||
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[])
|
||||
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, mbid_source FROM tracks WHERE id = ANY($1::uuid[])
|
||||
`
|
||||
|
||||
// Batched lookup used by /api/library/sync to hydrate upsert payloads
|
||||
@@ -329,6 +331,7 @@ func (q *Queries) GetTracksByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([
|
||||
&i.TagSourcesVersion,
|
||||
&i.TagReadVersion,
|
||||
&i.MissingSince,
|
||||
&i.MbidSource,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -341,7 +344,7 @@ func (q *Queries) GetTracksByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([
|
||||
}
|
||||
|
||||
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,
|
||||
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, t.mbid_source,
|
||||
albums.title AS album_title,
|
||||
artists.name AS artist_name
|
||||
FROM tracks t
|
||||
@@ -401,6 +404,7 @@ func (q *Queries) ListArtistTracksForUser(ctx context.Context, arg ListArtistTra
|
||||
&i.Track.TagSourcesVersion,
|
||||
&i.Track.TagReadVersion,
|
||||
&i.Track.MissingSince,
|
||||
&i.Track.MbidSource,
|
||||
&i.AlbumTitle,
|
||||
&i.ArtistName,
|
||||
); err != nil {
|
||||
@@ -501,7 +505,7 @@ func (q *Queries) ListMissingTracks(ctx context.Context, arg ListMissingTracksPa
|
||||
}
|
||||
|
||||
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,
|
||||
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, t.mbid_source,
|
||||
albums.title AS album_title,
|
||||
artists.name AS artist_name
|
||||
FROM tracks t
|
||||
@@ -558,6 +562,7 @@ func (q *Queries) ListRandomTracksForUser(ctx context.Context, arg ListRandomTra
|
||||
&i.Track.TagSourcesVersion,
|
||||
&i.Track.TagReadVersion,
|
||||
&i.Track.MissingSince,
|
||||
&i.Track.MbidSource,
|
||||
&i.AlbumTitle,
|
||||
&i.ArtistName,
|
||||
); err != nil {
|
||||
@@ -607,7 +612,7 @@ func (q *Queries) ListTrackPathsForReconcile(ctx context.Context) ([]ListTrackPa
|
||||
}
|
||||
|
||||
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
|
||||
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, mbid_source FROM tracks
|
||||
WHERE album_id = $1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM lidarr_quarantine q
|
||||
@@ -653,6 +658,7 @@ func (q *Queries) ListTracksByAlbum(ctx context.Context, arg ListTracksByAlbumPa
|
||||
&i.TagSourcesVersion,
|
||||
&i.TagReadVersion,
|
||||
&i.MissingSince,
|
||||
&i.MbidSource,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -726,7 +732,7 @@ func (q *Queries) MarkTracksMissing(ctx context.Context, ids []pgtype.UUID) (int
|
||||
}
|
||||
|
||||
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
|
||||
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, mbid_source FROM tracks
|
||||
WHERE title ILIKE '%' || $1::text || '%'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM lidarr_quarantine q
|
||||
@@ -779,6 +785,7 @@ func (q *Queries) SearchTracks(ctx context.Context, arg SearchTracksParams) ([]T
|
||||
&i.TagSourcesVersion,
|
||||
&i.TagReadVersion,
|
||||
&i.MissingSince,
|
||||
&i.MbidSource,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -792,7 +799,7 @@ func (q *Queries) SearchTracks(ctx context.Context, arg SearchTracksParams) ([]T
|
||||
|
||||
const setTrackMbidIfNull = `-- name: SetTrackMbidIfNull :exec
|
||||
UPDATE tracks
|
||||
SET mbid = $2, updated_at = now()
|
||||
SET mbid = $2, mbid_source = 'tag', updated_at = now()
|
||||
WHERE id = $1 AND mbid IS NULL
|
||||
`
|
||||
|
||||
@@ -812,8 +819,9 @@ 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)
|
||||
tag_read_version, mbid_source
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
|
||||
CASE WHEN $11::text IS NOT NULL THEN 'tag' END)
|
||||
ON CONFLICT (file_path) DO UPDATE SET
|
||||
title = EXCLUDED.title,
|
||||
album_id = EXCLUDED.album_id,
|
||||
@@ -824,13 +832,17 @@ ON CONFLICT (file_path) DO UPDATE SET
|
||||
file_size = EXCLUDED.file_size,
|
||||
file_format = EXCLUDED.file_format,
|
||||
bitrate = EXCLUDED.bitrate,
|
||||
mbid = EXCLUDED.mbid,
|
||||
-- Both read the row as it was before this statement.
|
||||
mbid = CASE WHEN EXCLUDED.mbid IS NULL AND tracks.mbid_source = 'acoustid'
|
||||
THEN tracks.mbid ELSE EXCLUDED.mbid END,
|
||||
mbid_source = CASE WHEN EXCLUDED.mbid IS NOT NULL THEN 'tag'
|
||||
WHEN tracks.mbid_source = 'acoustid' THEN 'acoustid' END,
|
||||
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
|
||||
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, mbid_source
|
||||
`
|
||||
|
||||
type UpsertTrackParams struct {
|
||||
@@ -850,6 +862,8 @@ type UpsertTrackParams struct {
|
||||
}
|
||||
|
||||
// file_path is the canonical identity for library scan; mbid is secondary.
|
||||
// mbid is the file's tag. A looked-up id (mbid_source 'acoustid', M401)
|
||||
// survives a re-read that finds no tag id, and a tag id replaces it (D4).
|
||||
func (q *Queries) UpsertTrack(ctx context.Context, arg UpsertTrackParams) (Track, error) {
|
||||
row := q.db.QueryRow(ctx, upsertTrack,
|
||||
arg.Title,
|
||||
@@ -887,6 +901,7 @@ func (q *Queries) UpsertTrack(ctx context.Context, arg UpsertTrackParams) (Track
|
||||
&i.TagSourcesVersion,
|
||||
&i.TagReadVersion,
|
||||
&i.MissingSince,
|
||||
&i.MbidSource,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user