fix: AcoustID decides any one-album mismatch the lengths cannot, not only copies each cut to its slot (#6169)
release / govulncheck (push) Successful in 13s
release / web (push) Successful in 1m21s
release / go (push) Successful in 1m37s
release / integration (push) Successful in 5m3s
release / android (push) Successful in 5m6s
release / Build signed APK (releases and dev) (push) Successful in 5m14s
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

The first pass on the operator's library held 16 groups. In several, every
copy's own lookup names one recording (Wombats Record Shop/Tormentor,
Alchemist (2)/Prelude, Monday Chill 8/9, 404sessions), but one copy sits 3-10s
from its slot, so the every-copy-fits precondition kept AcoustID out. Now:
copies whose lookups all agree are that recording, whatever their
fingerprints read; copies of the same audio are asked about as before; and
the track list places the recording in one copy's slot, or none. Copies
filed as one song are not settled here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 10:52:42 -04:00
co-authored by Claude Opus 5.5
parent 18d31e9cdd
commit 3a482bd715
3 changed files with 103 additions and 64 deletions
+41 -45
View File
@@ -34,13 +34,16 @@ import (
// in the other slots are removed and their tracks marked missing, so the
// reacquisition sweeper wants the album back and a good copy fills them.
// When every slot's song is clearly not the file, every copy goes.
// - trimmed: one recording cut to each of several slots' lengths (Savant's
// Mortals held one as 16 of its 50 tracks), so every slot fits and the
// lengths cannot say. AcoustID names the recording and the track list
// names each slot's: the copy in that recording's slot is kept and the
// rest go, or every copy when the recording is in none of their slots.
// The release the files came from is set aside, so the album fetched
// again is not the same files (#6169).
// - named: the lengths cannot say (Savant's Mortals held one recording as
// 16 of its 50 tracks, each cut to its own slot's length), but AcoustID
// can. When every copy's lookup names one recording, or the copies are
// the same audio and a lookup names any, the track list says whose slot
// that recording is: that copy is kept and the rest go, or every copy
// when it is in none of their slots. Same audio that AcoustID does not
// know at all is no copy's song, and every copy goes.
//
// Whenever copies go, the release the files came from is set aside, so the
// album fetched again is not the same files (#6169).
//
// Anything less certain stays with the operator.
@@ -219,12 +222,15 @@ func settleWrongSlots(
copies[i] = slotCopy{Title: m.Title, Artist: m.ArtistName, DurationMs: m.DurationMs}
}
keep, losers, ok := wrongSlots(copies, tracks)
if !ok && sameAudio(g) && trimmedToSlots(copies, tracks) {
if !ok {
recordings, known := groupRecordings(ctx, logger, g, src.Identify, &identifies)
if !known {
continue
}
keep, losers, ok = trimmedCopies(copies, tracks, recordings)
if keep, losers, ok = namedCopies(copies, tracks, recordings); !ok {
logger.Info("duplicate resolve: AcoustID's answer does not place the recording in one slot; left for review",
"group_id", syncpkg.FormatUUID(g.id), "recordings", recordings)
}
}
if !ok {
continue
@@ -252,39 +258,17 @@ func sameAudio(g *resolveGroup) bool {
return g.tier == "exact" || (g.worstBER != nil && *g.worstBER <= sameAudioBER)
}
// trimmedToSlots reports whether every copy is as long as its own slot's song
// and they are filed as more than one song: one recording cut to several
// slots' lengths, which the lengths alone cannot place.
func trimmedToSlots(copies []slotCopy, tracks []dbq.MbReleaseTrack) bool {
keys := map[string]bool{}
for _, c := range copies {
key := MatchTitleKey(c.Title, c.Artist)
fits := false
for _, t := range tracks {
if t.LengthMs > 0 && MatchTitleKey(t.Title, c.Artist) == key && absMs(t.LengthMs-c.DurationMs) <= slotFit {
fits = true
break
}
}
if !fits {
return false
}
keys[key] = true
}
return len(keys) > 1
}
// trimmedCopies decides a group trimmedToSlots holds, from the recordings
// AcoustID says it may be. The track list names each slot's recording: the
// copy in a slot of one of them is kept and the others are losers. When none
// of the copies' slots is one of them, or AcoustID knows none (recordings
// empty), every copy is another song's and all go. ok is false when they fall
// in more than one copy's slot, or the track list names no recording for a
// copy's slot.
// namedCopies decides a one-album mismatch the lengths could not, from the
// recordings AcoustID says it may be. The track list names each slot's
// recording: the copy in a slot of one of them is kept and the others are
// losers. When none of the copies' slots is one of them, or AcoustID knows
// none (recordings empty), every copy is another song's and all go. ok is
// false when they fall in more than one copy's slot, the track list names no
// recording for a copy's slot, or the copies are all filed as one song.
//
// Copies sharing the kept copy's title are one song held twice, as in
// wrongSlots: neither kept nor lost here.
func trimmedCopies(copies []slotCopy, tracks []dbq.MbReleaseTrack, recordings []string) (keep int, losers []int, ok bool) {
func namedCopies(copies []slotCopy, tracks []dbq.MbReleaseTrack, recordings []string) (keep int, losers []int, ok bool) {
keys := make([]string, len(copies))
theirs := map[string]bool{}
for i, c := range copies {
@@ -316,22 +300,28 @@ func trimmedCopies(copies []slotCopy, tracks []dbq.MbReleaseTrack, recordings []
losers = append(losers, i)
}
}
if len(losers) == 0 {
return -1, nil, false
}
return keep, losers, true
}
// groupRecordings is what AcoustID says a group's recording may be: the
// recordings the copies' own lookups matched, none when every copy's lookup
// found no match, or else a lookup of one copy now, while the pass has
// lookups left. known is false when there is no answer yet, and the group
// waits for a later pass.
// groupRecordings is what AcoustID says a group's recording may be. Copies
// whose own lookups all name one recording are that recording, however their
// fingerprints compare. Otherwise only copies that are the same audio
// (sameAudio) are asked about: the recordings any copy's lookup matched, none
// when every copy's lookup found no match, or else a lookup of one copy now,
// while the pass has lookups left. known is false when there is no answer
// yet, and the group waits.
func groupRecordings(
ctx context.Context, logger *slog.Logger, g *resolveGroup, identify RecordingIdentifier, left *int,
) (recordings []string, known bool) {
noMatch := 0
matched, noMatch := 0, 0
for _, m := range g.members {
switch {
case m.AcoustidState == nil:
case *m.AcoustidState == lookupMatched && m.AcoustidRecording != nil:
matched++
if !slices.Contains(recordings, *m.AcoustidRecording) {
recordings = append(recordings, *m.AcoustidRecording)
}
@@ -339,6 +329,12 @@ func groupRecordings(
noMatch++
}
}
if matched == len(g.members) && len(recordings) == 1 {
return recordings, true
}
if !sameAudio(g) {
return nil, false
}
if len(recordings) > 0 || noMatch == len(g.members) {
return recordings, true
}