fix(mb): resolve same-named albums to the Album, not the Single

MusicBrainz search returns release-groups tied on title similarity in an
arbitrary order; `max(groups, key=_ratio)` kept the first, so Michael
Jackson's "Off the Wall" resolved to the 2-track *single* release-group
(Off the Wall / Get on the Floor) instead of the 10-track album. Because
plan_track_names labels files purely by position, that short tracklist got
mapped onto the full album — track 2 became "Get on the Floor" instead of
"Rock with You", and the real "Off the Wall"/"Get on the Floor" files kept
their streamrip names, yielding duplicate titles.

Extract selection into a pure `_best_release_group()` that breaks title-
similarity ties toward primary-type == "Album". Similarity still dominates;
album preference only settles ties. Live-verified: Off the Wall now resolves
to the 10-track album with track 2 = "Rock With You".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-15 10:49:26 +02:00
parent 0443f10a3b
commit 06e7f91897
2 changed files with 56 additions and 4 deletions
+20 -4
View File
@@ -9,6 +9,24 @@ def _ratio(a: str, b: str) -> float:
return SequenceMatcher(None, a.strip().casefold(), b.strip().casefold()).ratio()
def _is_album(g: dict) -> bool:
return (g.get("primary-type") or "").casefold() == "album"
def _best_release_group(album: str, groups: list) -> dict | None:
"""Choose the best release-group match for `album`. Highest title similarity wins;
an Album primary-type breaks ties so a famous album (e.g. Michael Jackson's "Off the
Wall") isn't resolved to its same-named single, whose short tracklist would then be
mapped positionally onto the full album's files. Returns None below the title threshold.
Pure — no network I/O."""
if not groups:
return None
best = max(groups, key=lambda g: (_ratio(album, g.get("title", "")), _is_album(g)))
if _ratio(album, best.get("title", "")) < _MIN_TITLE_RATIO:
return None
return best
class MusicBrainzResolver:
"""Real MbResolver using the MusicBrainz webservice via musicbrainzngs.
@@ -28,10 +46,8 @@ class MusicBrainzResolver:
res = musicbrainzngs.search_release_groups(query=album, artist=artist, limit=5)
groups = res.get("release-group-list", [])
if not groups:
return None
rg = max(groups, key=lambda g: _ratio(album, g.get("title", "")))
if _ratio(album, rg.get("title", "")) < _MIN_TITLE_RATIO:
rg = _best_release_group(album, groups)
if rg is None:
return None
canonical_album = rg.get("title", album)