From 06e7f91897bd1e09f1360ef947e35ea4ff89dd7f Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 15 Jul 2026 10:49:26 +0200 Subject: [PATCH] fix(mb): resolve same-named albums to the Album, not the Single MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- worker/lyra_worker/_musicbrainz.py | 24 ++++++++++++++++---- worker/tests/test_musicbrainz.py | 36 ++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 worker/tests/test_musicbrainz.py diff --git a/worker/lyra_worker/_musicbrainz.py b/worker/lyra_worker/_musicbrainz.py index e73b34f..49fade4 100644 --- a/worker/lyra_worker/_musicbrainz.py +++ b/worker/lyra_worker/_musicbrainz.py @@ -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) diff --git a/worker/tests/test_musicbrainz.py b/worker/tests/test_musicbrainz.py new file mode 100644 index 0000000..90ee381 --- /dev/null +++ b/worker/tests/test_musicbrainz.py @@ -0,0 +1,36 @@ +from lyra_worker._musicbrainz import _best_release_group + +# Offline unit tests for the pure release-group selection logic. The live resolve() +# path (network) is covered by test_musicbrainz_live.py. + + +def _rg(title, primary_type): + return {"id": title.lower(), "title": title, "primary-type": primary_type} + + +def test_prefers_album_over_same_titled_single(): + # Real-world "Off the Wall" bug: MusicBrainz returns the Single release-group + # first, tied on title with the Album. We must pick the Album so a 10-track + # download isn't tagged from the 2-track single's tracklist. + groups = [_rg("Off the Wall", "Single"), _rg("Off the Wall", "Album")] + assert _best_release_group("Off the Wall", groups)["primary-type"] == "Album" + + +def test_album_preference_is_order_independent(): + groups = [_rg("Off the Wall", "Album"), _rg("Off the Wall", "Single")] + assert _best_release_group("Off the Wall", groups)["primary-type"] == "Album" + + +def test_closer_title_still_wins_over_album_type(): + # Title similarity dominates; the album-type preference only breaks ties. + groups = [_rg("Continuum", "Single"), _rg("Continuum Deluxe Reissue", "Album")] + assert _best_release_group("Continuum", groups)["title"] == "Continuum" + + +def test_below_threshold_returns_none(): + groups = [_rg("Something Totally Different", "Album")] + assert _best_release_group("Off the Wall", groups) is None + + +def test_empty_groups_returns_none(): + assert _best_release_group("Off the Wall", []) is None