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
+30
View File
@@ -13,6 +13,7 @@ import (
"git.fabledsword.com/bvandeusen/minstrel/internal/audit"
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
"git.fabledsword.com/bvandeusen/minstrel/internal/musicbrainz"
"git.fabledsword.com/bvandeusen/minstrel/internal/notifications"
syncpkg "git.fabledsword.com/bvandeusen/minstrel/internal/sync"
)
@@ -33,6 +34,10 @@ import (
// Anything less certain stays with the operator.
const (
// trackListFetchesPerPass is how many albums' track lists a pass may ask
// MusicBrainz for, when the catalogue has never read them. The resolver
// runs hourly, so a backlog drains over a few hours without a burst.
trackListFetchesPerPass = 5
// slotFit is how close a file must be to a slot's MusicBrainz length to be
// that slot's song, and slotOther how far away every length of a slot's
// title must be for the file to be another song's.
@@ -143,9 +148,14 @@ func dismissDistinct(
// to settled.
func settleWrongSlots(
ctx context.Context, pool *pgxpool.Pool, logger *slog.Logger, groups []*resolveGroup, settled map[pgtype.UUID]bool,
fetch TrackListFetcher,
) (int, []string) {
q := dbq.New(pool)
tracksByGroup := map[string][]dbq.MbReleaseTrack{}
fetches := trackListFetchesPerPass
if fetch == nil {
fetches = 0
}
removed := 0
var names []string
for _, g := range groups {
@@ -163,6 +173,18 @@ func settleWrongSlots(
logger.Warn("duplicate resolve: reading the album's track list failed", "release_group", rg, "err", err)
continue
}
if len(tracks) == 0 && fetches > 0 && neverRead(ctx, q, rg) {
fetches--
if err := fetch(ctx, rg); err != nil {
logger.Info("duplicate resolve: fetching the album's track list failed", "release_group", rg, "err", err)
if errors.Is(err, musicbrainz.ErrUnreachable) {
fetches = 0
}
} else if tracks, err = q.ListMBReleaseTracksForGroup(ctx, rg); err != nil {
logger.Warn("duplicate resolve: reading the album's track list failed", "release_group", rg, "err", err)
continue
}
}
tracksByGroup[rg] = tracks
}
copies := make([]slotCopy, len(g.members))
@@ -187,6 +209,14 @@ func settleWrongSlots(
return removed, names
}
// neverRead reports whether the catalogue has never read a release group's
// releases. A group read and found empty is not asked about again: the
// catalogue's own refresh revisits it.
func neverRead(ctx context.Context, q *dbq.Queries, rg string) bool {
g, err := q.GetMBReleaseGroup(ctx, rg)
return errors.Is(err, pgx.ErrNoRows) || (err == nil && !g.ReleasesRefreshedAt.Valid)
}
// removeWrongSlots deletes each loser's file, then marks its track missing in
// the same transaction that closes the group. The row is kept, with its likes
// and plays: it is the slot, and a good copy fetched again takes it over. A