fix: a second copy of a track counts once, joining words don't part two titles, and a newer matcher retries an older one's refusals (M502 #5569)
release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m18s
release / go (push) Successful in 1m35s
release / integration (push) Failing after 5m33s
release / android (push) Successful in 5m47s
release / Build signed APK (releases and dev) (push) Successful in 6m4s
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 second run's refusals were mostly albums holding a track twice (a
two-track single with three files), which no edition can pair, and
release names that differ only in "+" against "/". Tracks are now read
once per disc, number and title, and "and", "&" and "+" count for nothing
when titles are compared; an edition's own title also counts as the
album's name.

library_album_matches records which matcher version decided each album.
Refusals by an older version are matched again, even after the listing is
done, so an improvement reaches them without a migration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 17:44:06 -04:00
co-authored by Claude Opus 5.5
parent 80d2fdc3ab
commit bf409d5f3f
8 changed files with 146 additions and 34 deletions
+25 -5
View File
@@ -85,8 +85,13 @@ type albumMatch struct {
Detail string
}
// tracksToMatch reads an album's tracks, each once: a second copy of a
// track (the same disc, number and title, as a duplicate file in another
// format is) would otherwise count against the match as a track no edition
// has.
func tracksToMatch(rows []dbq.ListAlbumTracksToMatchRow) []libraryTrack {
out := make([]libraryTrack, 0, len(rows))
seen := make(map[string]bool, len(rows))
for _, r := range rows {
t := libraryTrack{Title: r.Title, LengthMs: int(r.DurationMs), Recording: deref(r.Mbid)}
if r.DiscNumber != nil {
@@ -95,6 +100,11 @@ func tracksToMatch(rows []dbq.ListAlbumTracksToMatchRow) []libraryTrack {
if r.TrackNumber != nil {
t.Number = int(*r.TrackNumber)
}
key := fmt.Sprintf("%d/%d/%s", max(t.Disc, 1), t.Number, strings.Join(release.Tokens(t.Title), " "))
if seen[key] {
continue
}
seen[key] = true
out = append(out, t)
}
return out
@@ -165,9 +175,15 @@ func agrees(n, tracks int) bool {
return tracks > 0 && n*10 >= tracks*matchAgreement
}
// sameTitle: the same words, not merely one holding the other.
// sameTitle: the same words, not merely one holding the other. "And",
// "&" and "+" join two titles however a release spells it ("Brain Stew +
// Jaded", "Brain Stew / Jaded"), so they count for nothing.
func sameTitle(a, b string) bool {
return slices.Equal(release.Tokens(a), release.Tokens(b))
words := func(s string) []string {
return slices.DeleteFunc(release.Tokens(s), func(w string) bool { return w == "and" })
}
wa, wb := words(a), words(b)
return len(wa) > 0 && slices.Equal(wa, wb)
}
// matchAlbum finds the release group a library album is. A MusicBrainz
@@ -204,7 +220,9 @@ func (s *Service) matchAlbum(ctx context.Context, a dbq.ListAlbumsToMatchRow, tr
continue
}
tried[c.Group] = true
n, size, miss, err := s.agreementWith(ctx, c.Group, tracks, sameTitle(c.Title, a.Title))
n, size, miss, err := s.agreementWith(ctx, c.Group, tracks, func(edition string) bool {
return sameTitle(c.Title, a.Title) || sameTitle(edition, a.Title)
})
if err != nil {
return err
}
@@ -328,7 +346,9 @@ func (s *Service) searchCandidates(ctx context.Context, a dbq.ListAlbumsToMatchR
// agreementWith is how many of the album's tracks the group's best edition
// agrees with, that edition's length, and how it differs. A group
// MusicBrainz no longer has, or lists no edition of, agrees with none (-1).
func (s *Service) agreementWith(ctx context.Context, group string, tracks []libraryTrack, titlesEnough bool) (n, size int, miss string, err error) {
// ownName says whether an edition, by its title, is called what the album
// is, so that its track titles are enough.
func (s *Service) agreementWith(ctx context.Context, group string, tracks []libraryTrack, ownName func(edition string) bool) (n, size int, miss string, err error) {
eds, err := s.cat.Releases(ctx, group)
switch {
case errors.Is(err, catalogue.ErrNotFound):
@@ -343,7 +363,7 @@ func (s *Service) agreementWith(ctx context.Context, group string, tracks []libr
edition = append(edition, editionTrack{Medium: int(t.MediumPosition), Position: int(t.Position),
Title: t.Title, LengthMs: int(t.LengthMs), Recording: t.RecordingMbid})
}
if got, m := trackAgreement(tracks, edition, titlesEnough); got > n || (got == n && abs(len(edition)-len(tracks)) < abs(size-len(tracks))) {
if got, m := trackAgreement(tracks, edition, ownName(e.Release.Title)); got > n || (got == n && abs(len(edition)-len(tracks)) < abs(size-len(tracks))) {
n, size, miss = got, len(edition), m
}
}