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>
This commit is contained in:
@@ -376,6 +376,41 @@ func (q *Queries) ListTracksByAlbum(ctx context.Context, arg ListTracksByAlbumPa
|
||||
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 || '%'
|
||||
@@ -437,6 +472,24 @@ func (q *Queries) SearchTracks(ctx context.Context, arg SearchTracksParams) ([]T
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Reverts to the unique partial index. This FAILS if any recording
|
||||
-- MBID is shared across releases in the library (the exact case 0029
|
||||
-- exists to allow) — dedupe tracks.mbid before rolling back.
|
||||
|
||||
DROP INDEX IF EXISTS tracks_mbid_idx;
|
||||
CREATE UNIQUE INDEX tracks_mbid_unique ON tracks (mbid) WHERE mbid IS NOT NULL;
|
||||
@@ -0,0 +1,15 @@
|
||||
-- tracks.mbid holds the MusicBrainz *recording* MBID — the id the
|
||||
-- ListenBrainz similarity pipeline (ListPlayedTracksNeedingSimilarity,
|
||||
-- GetTracksByMBIDs) matches on. A single recording legitimately appears
|
||||
-- on multiple releases (album + compilation + single), so multiple
|
||||
-- tracks rows share one recording MBID.
|
||||
--
|
||||
-- tracks_mbid_unique (added in 0002, when this column was always NULL
|
||||
-- and never populated) wrongly assumes one MBID == one track. Now that
|
||||
-- the scanner extracts the recording MBID, that uniqueness throws
|
||||
-- 23505 for any recording present on >1 release. Replace it with a
|
||||
-- non-unique partial index serving the same lookups. Zero-risk: the
|
||||
-- column is 100% NULL at migration time.
|
||||
|
||||
DROP INDEX IF EXISTS tracks_mbid_unique;
|
||||
CREATE INDEX IF NOT EXISTS tracks_mbid_idx ON tracks (mbid) WHERE mbid IS NOT NULL;
|
||||
@@ -19,6 +19,22 @@ ON CONFLICT (file_path) DO UPDATE SET
|
||||
updated_at = now()
|
||||
RETURNING *;
|
||||
|
||||
-- name: ListTracksMissingMbidWithPath :many
|
||||
-- Track recording-MBID backfill: tracks with NULL mbid that still have
|
||||
-- a file to re-read. $1 caps the batch (mirrors the album backfill).
|
||||
SELECT id, file_path
|
||||
FROM tracks
|
||||
WHERE mbid IS NULL
|
||||
ORDER BY id
|
||||
LIMIT $1;
|
||||
|
||||
-- name: SetTrackMbidIfNull :exec
|
||||
-- Heal a track's recording MBID only while still NULL — idempotent, so
|
||||
-- re-running the backfill is a no-op for already-healed rows.
|
||||
UPDATE tracks
|
||||
SET mbid = $2, updated_at = now()
|
||||
WHERE id = $1 AND mbid IS NULL;
|
||||
|
||||
-- name: GetTrackByID :one
|
||||
SELECT * FROM tracks WHERE id = $1;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user