feat: the duplicate resolver fetches a missing album track list from MusicBrainz to decide a wrong slot, five albums a pass (M498)
release / govulncheck (push) Successful in 14s
release / web (push) Successful in 1m16s
release / go (push) Successful in 1m33s
release / integration (push) Successful in 5m0s
release / android (push) Successful in 5m7s
release / Build signed APK (releases and dev) (push) Successful in 5m20s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 15s
release / Verify release artifacts (tag releases only) (push) Skipped

20 of the 34 groups left for review had no cached track list for their album:
the catalogue only reads one when the wanted list or the importer needs it.
The resolver now asks catalogue.RefreshReleaseGroup for an album it must
decide and the catalogue has never read, through the shared MusicBrainz pacer,
at most five a pass (hourly). An unreachable MusicBrainz stops the pass's
fetching; a group read and found empty is not asked about again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 09:02:53 -04:00
co-authored by Claude Opus 5.5
parent c5868c00cc
commit c8e855ae4a
4 changed files with 85 additions and 9 deletions
+24 -6
View File
@@ -112,10 +112,24 @@ func mergePlan(g *resolveGroup) (ok bool, survivor int) {
return false, -1
}
// TrackListFetcher reads a release group's releases and track lists from
// MusicBrainz into the catalogue (catalogue.Service.RefreshReleaseGroup).
type TrackListFetcher func(ctx context.Context, releaseGroupMBID string) error
// ResolveDuplicates runs one pass. act is the operator's auto-resolve setting;
// with it off, the pass only classifies.
// with it off, the pass only classifies. It reads only track lists already in
// the catalogue; ResolveDuplicatesFetching may also fetch missing ones.
func ResolveDuplicates(
ctx context.Context, pool *pgxpool.Pool, logger *slog.Logger, dataDir string, act bool,
) (DuplicateResolveResult, error) {
return ResolveDuplicatesFetching(ctx, pool, logger, dataDir, act, nil)
}
// ResolveDuplicatesFetching is ResolveDuplicates that asks MusicBrainz, through
// fetch, for the track list of an album whose wrong slot it must decide and
// the catalogue has never read: a few per pass (trackListFetchesPerPass).
func ResolveDuplicatesFetching(
ctx context.Context, pool *pgxpool.Pool, logger *slog.Logger, dataDir string, act bool, fetch TrackListFetcher,
) (DuplicateResolveResult, error) {
if logger == nil {
logger = slog.Default()
@@ -182,7 +196,7 @@ func ResolveDuplicates(
}
res.Dismissed = dismissDistinct(ctx, pool, logger, groups, merged)
wrong, names := settleWrongSlots(ctx, pool, logger, groups, merged)
wrong, names := settleWrongSlots(ctx, pool, logger, groups, merged, fetch)
res.WrongFiles = wrong
mergedNames = append(mergedNames, names...)
@@ -375,14 +389,18 @@ type DuplicateResolveWorker struct {
logger *slog.Logger
dataDir string
settings *FingerprintSettingsService
fetch TrackListFetcher
tick time.Duration
}
// NewDuplicateResolveWorker builds a worker with the production cadence.
// NewDuplicateResolveWorker builds a worker with the production cadence. fetch
// reads a missing track list from MusicBrainz; nil reads only what is held.
func NewDuplicateResolveWorker(
pool *pgxpool.Pool, logger *slog.Logger, dataDir string, settings *FingerprintSettingsService,
pool *pgxpool.Pool, logger *slog.Logger, dataDir string, settings *FingerprintSettingsService, fetch TrackListFetcher,
) *DuplicateResolveWorker {
return &DuplicateResolveWorker{pool: pool, logger: logger, dataDir: dataDir, settings: settings, tick: duplicateResolveTick}
return &DuplicateResolveWorker{
pool: pool, logger: logger, dataDir: dataDir, settings: settings, fetch: fetch, tick: duplicateResolveTick,
}
}
// Run blocks until ctx is cancelled, running a pass at start and then each tick.
@@ -418,7 +436,7 @@ func (w *DuplicateResolveWorker) tickOnce(ctx context.Context) {
}
return
}
res, err := ResolveDuplicates(ctx, w.pool, w.logger, w.dataDir, w.settings.Get().AutoResolve)
res, err := ResolveDuplicatesFetching(ctx, w.pool, w.logger, w.dataDir, w.settings.Get().AutoResolve, w.fetch)
if err != nil && ctx.Err() == nil {
w.logger.Warn("duplicate resolve: pass failed", "err", err)
}