feat(library): fold a missing track into its on-disk replacement (M485 #5286 #5287)
release / web (push) Successful in 2m21s
release / go (push) Successful in 2m34s
release / govulncheck (push) Successful in 40s
release / integration (push) Successful in 6m17s
release / android (push) Successful in 6m36s
release / Build signed APK (releases and dev) (push) Successful in 6m6s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m19s
release / Verify release artifacts (tag releases only) (push) Skipped

A track marked missing whose replacement is already on disk under another
row — on the operator's library 355 of 451 missing tracks, nearly all Lidarr
mp3 -> flac upgrades — is folded into the replacement: likes, plays, playlist
entries and tags move across and the missing row goes. Move adoption could
not catch these: the replacements were re-encodes (no shared audio hash) with
no recording MBID at import.

Pairs (ListMissingTrackPairs): the same recording MBID within the album
group, or the same album row and title ignoring case. Each side must have
exactly one candidate; conflicting MBIDs refuse a pair. No duration or track
position gate: on the 142 pairs known to be one recording, 18% differed by
over 2s and the poorly tagged set is where numbering is broken (spike #5274).

Each pair folds in its own transaction after locking both rows and checking
the pair still holds. Runs automatically (operator, 2026-10-07) after a full
scan, after a watcher batch that added or updated tracks, and after an
AcoustID pass that matched any track. The first scan after deploy repairs the
existing rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 17:23:32 -04:00
co-authored by Claude Opus 5.5
parent 2df28345b4
commit 865a3176c9
6 changed files with 714 additions and 0 deletions
+13
View File
@@ -175,6 +175,14 @@ func (s *Scanner) Scan(ctx context.Context, progressCb func(Stats)) (Stats, erro
if err := ctx.Err(); err != nil {
return stats, err
}
// PHASE 4 — fold missing rows into replacements this scan brought in or
// that were already here (M485). After processing, so both this scan's
// marks and its arrivals are in. Not fatal: an unfolded pair is only a row
// left in the missing list, and the next scan tries again.
if _, err := PairMissingTracks(ctx, s.pool, s.logger); err != nil && ctx.Err() == nil {
s.logger.Warn("library scan: missing-pair pass failed", "err", err)
}
return stats, nil
}
@@ -466,6 +474,11 @@ func (s *Scanner) ScanFiles(ctx context.Context, paths []string) ([]pgtype.UUID,
"skipped", stats.Skipped,
"errored", stats.Errored,
)
// A file the watcher just brought in may be the replacement for a row
// an earlier scan marked missing (M485).
if _, err := PairMissingTracks(ctx, s.pool, s.logger); err != nil && ctx.Err() == nil {
s.logger.Warn("library watch: missing-pair pass failed", "err", err)
}
}
return changed, nil
}