feat: duplicates settle themselves: retitled copies across releases link, alike versions on one album are dismissed, and a file in another song's slot is removed so the album is fetched again (M498 #6159-#6162)
release / govulncheck (push) Successful in 14s
release / web (push) Failing after 1m26s
release / go (push) Successful in 1m35s
release / integration (push) Successful in 5m8s
release / android (push) Successful in 5m12s
release / Build signed APK (releases and dev) (push) Successful in 5m26s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 14s
release / web (push) Failing after 1m26s
release / go (push) Successful in 1m35s
release / integration (push) Successful in 5m8s
release / android (push) Successful in 5m12s
release / Build signed APK (releases and dev) (push) Successful in 5m26s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
The operator's library had 536 groups waiting for review, 2026-10-19. A sample
showed most were one recording under two titles on two releases.
- The headline and the Duplicates count leave out cross-release groups, which
the resolver keeps and links: the headline read 3,479 when 536 needed review.
- Any group spanning albums is cross_release and is linked as one song,
whatever its titles say. Nothing is deleted.
- One album, two versions named as such ("Lovesick (Instrumental)") and not
byte-identical: a new class, distinct, dismissed by the resolver.
- One album, one recording under different song titles (mismatch): the
album's cached MusicBrainz track list decides. When exactly one slot's song
is as long as the file and every other slot's song is clearly not, the other
copies are removed and their tracks marked missing, so the reacquisition
sweeper wants the album back. Otherwise the group stays for review.
- The duplicates_found notice moves from the sweep to the resolver: it names
only groups a pass first left for review.
- Resolved automatically lists dismissals and wrong-slot removals beside
merges.
Migration 0094 adds 'distinct' to the class CHECK (rule 36).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,11 @@ import (
|
||||
// - Except that a video rip is no release's copy. When every copy but one is
|
||||
// a rip, across releases or under another title, the rips are merged into
|
||||
// the one that is not.
|
||||
// - Two versions of a song on one album that sound alike are not duplicates:
|
||||
// the group is dismissed (duplicate_settle.go).
|
||||
// - One file filling several of an album's track slots: MusicBrainz's track
|
||||
// lengths say which slot it belongs to, and the copies in the other slots
|
||||
// are removed so the album is fetched again (duplicate_settle.go).
|
||||
// - Anything else waits for the operator.
|
||||
|
||||
const (
|
||||
@@ -50,6 +55,9 @@ type DuplicateResolveResult struct {
|
||||
MergeFailed int
|
||||
// SongsLinked counts cross-release groups newly linked as one song.
|
||||
SongsLinked int
|
||||
// Dismissed counts groups of distinct versions dismissed; WrongFiles the
|
||||
// copies removed from track slots that were not theirs.
|
||||
Dismissed, WrongFiles int
|
||||
// HeldBack counts tracks newly held back from radio and the mixes as video
|
||||
// rips; Released counts held-back tracks whose name no longer says so.
|
||||
HeldBack, Released int64
|
||||
@@ -96,7 +104,7 @@ func mergePlan(g *resolveGroup) (ok bool, survivor int) {
|
||||
switch g.class {
|
||||
case ClassSameRelease:
|
||||
return true, -1
|
||||
case ClassCrossRelease, ClassMismatch:
|
||||
case ClassCrossRelease:
|
||||
if i := g.lone(); i >= 0 {
|
||||
return true, i
|
||||
}
|
||||
@@ -136,6 +144,7 @@ func ResolveDuplicates(
|
||||
}
|
||||
|
||||
if !act {
|
||||
notifyNewForReview(ctx, groups, nil)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -172,17 +181,23 @@ func ResolveDuplicates(
|
||||
}
|
||||
}
|
||||
|
||||
res.Dismissed = dismissDistinct(ctx, pool, logger, groups, merged)
|
||||
wrong, names := settleWrongSlots(ctx, pool, logger, groups, merged)
|
||||
res.WrongFiles = wrong
|
||||
mergedNames = append(mergedNames, names...)
|
||||
|
||||
linked, err := linkCrossRelease(ctx, pool, logger, groups, merged)
|
||||
res.SongsLinked = linked
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if res.Merged > 0 {
|
||||
if settledCopies := res.Merged + res.WrongFiles; settledCopies > 0 {
|
||||
notifyAdmins(ctx, notifications.KindDuplicatesResolved, notifications.Payload{
|
||||
Count: int64(res.Merged), Items: mergedNames,
|
||||
Count: int64(settledCopies), Items: mergedNames[:min(len(mergedNames), notifyItems)],
|
||||
})
|
||||
}
|
||||
notifyNewForReview(ctx, groups, merged)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -276,7 +291,7 @@ func resolveNote(g *resolveGroup) string {
|
||||
return "Each copy belongs to its own release, so none is removed; they count as one song."
|
||||
}
|
||||
case ClassMismatch:
|
||||
return "Identical audio filed under different titles: one file carries another song's tags."
|
||||
return "One recording fills several of this album's track slots, and MusicBrainz's track lengths could not say which is its own."
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -409,5 +424,6 @@ func (w *DuplicateResolveWorker) tickOnce(ctx context.Context) {
|
||||
}
|
||||
w.logger.Info("duplicate resolve complete",
|
||||
"groups", res.Groups, "merged", res.Merged, "merge_failed", res.MergeFailed,
|
||||
"songs_linked", res.SongsLinked, "held_back", res.HeldBack, "released", res.Released)
|
||||
"songs_linked", res.SongsLinked, "dismissed", res.Dismissed, "wrong_files", res.WrongFiles,
|
||||
"held_back", res.HeldBack, "released", res.Released)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user