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:
@@ -167,3 +167,96 @@ func readMBIDsForFile(path string, logger *slog.Logger) (albumMBID, artistMBID s
|
||||
}
|
||||
return extractMBIDs(meta)
|
||||
}
|
||||
|
||||
// BackfillTrackMBIDsResult tallies the track recording-MBID backfill.
|
||||
type BackfillTrackMBIDsResult struct {
|
||||
Processed int // Tracks considered (mbid IS NULL).
|
||||
Healed int // Tracks whose recording MBID we set.
|
||||
Skipped int // Tracks whose file was missing / untagged / write failed.
|
||||
}
|
||||
|
||||
// BackfillTrackMBIDs walks tracks with NULL mbid, re-reads each file's
|
||||
// tags, and persists the MusicBrainz recording MBID. This is the
|
||||
// counterpart to BackfillMBIDs (which heals album/artist MBIDs); it
|
||||
// unblocks the ListenBrainz similarity pipeline, which is gated on
|
||||
// tracks.mbid IS NOT NULL.
|
||||
//
|
||||
// Idempotent via SetTrackMbidIfNull: re-running costs the same N file
|
||||
// reads but is a no-op for already-healed rows. limit caps one call;
|
||||
// pass -1 for unbounded (caller normally batches per scan).
|
||||
func BackfillTrackMBIDs(ctx context.Context, pool *pgxpool.Pool, logger *slog.Logger,
|
||||
limit int, progressCb func(BackfillTrackMBIDsResult)) (BackfillTrackMBIDsResult, error) {
|
||||
q := dbq.New(pool)
|
||||
queryLimit := int32(limit)
|
||||
if limit < 0 {
|
||||
queryLimit = 1<<31 - 1 // effectively unbounded
|
||||
}
|
||||
|
||||
rows, err := q.ListTracksMissingMbidWithPath(ctx, queryLimit)
|
||||
if err != nil {
|
||||
return BackfillTrackMBIDsResult{}, fmt.Errorf("list tracks missing mbid: %w", err)
|
||||
}
|
||||
|
||||
var res BackfillTrackMBIDsResult
|
||||
for i, r := range rows {
|
||||
if ctx.Err() != nil {
|
||||
return res, ctx.Err()
|
||||
}
|
||||
res.Processed++
|
||||
|
||||
rec := readRecordingMBIDForFile(r.FilePath, logger)
|
||||
if rec == "" {
|
||||
res.Skipped++
|
||||
if progressCb != nil {
|
||||
progressCb(res)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
m := rec
|
||||
if uerr := q.SetTrackMbidIfNull(ctx, dbq.SetTrackMbidIfNullParams{
|
||||
ID: r.ID, Mbid: &m,
|
||||
}); uerr != nil {
|
||||
logger.Warn("track mbid backfill: set failed",
|
||||
"track_id", r.ID, "err", uerr)
|
||||
res.Skipped++
|
||||
if progressCb != nil {
|
||||
progressCb(res)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
res.Healed++
|
||||
if (i+1)%500 == 0 {
|
||||
logger.Info("track mbid backfill progress",
|
||||
"processed", res.Processed, "healed", res.Healed, "skipped", res.Skipped)
|
||||
}
|
||||
if progressCb != nil {
|
||||
progressCb(res)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("track mbid backfill complete",
|
||||
"processed", res.Processed, "healed", res.Healed, "skipped", res.Skipped)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// readRecordingMBIDForFile opens one audio file and returns its
|
||||
// MusicBrainz recording MBID, or "" when the file can't be opened, the
|
||||
// tag read fails, or the tag is absent. Errors are logged at Warn, not
|
||||
// propagated — one broken file must not halt the backfill.
|
||||
func readRecordingMBIDForFile(path string, logger *slog.Logger) string {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
logger.Warn("track mbid backfill: open failed", "path", path, "err", err)
|
||||
return ""
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
meta, err := tag.ReadFrom(f)
|
||||
if err != nil {
|
||||
logger.Warn("track mbid backfill: tag read failed", "path", path, "err", err)
|
||||
return ""
|
||||
}
|
||||
return extractRecordingMBID(meta)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user