Skip to content
32 changes: 29 additions & 3 deletions beetsplug/musicbrainz.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"isrcs",
"work-level-rels",
"artist-rels",
"releases",
}
& set(musicbrainzngs.VALID_INCLUDES["recording"])
)
Expand Down Expand Up @@ -513,6 +514,21 @@
for extra_trackdata in extra_trackdatas:
info.update(extra_trackdata)

# If this recording includes a release list, attach album metadata so
# recording-based matches still write album/album_id.
rel_list = recording.get("release-list") or recording.get("release_list")
if rel_list:
# Pick the first release as a reasonable default.
rel0 = rel_list[0] or {}
# Handle both shapes: {'id','title',...} or {'release': {'id','title',...}}
rel0_dict = rel0.get("release", rel0)
album_title = rel0_dict.get("title")
album_id = rel0_dict.get("id")
if album_title:
info.album = album_title
if album_id:
info.album_id = album_id

return info

def album_info(self, release: JSONDict) -> beets.autotag.hooks.AlbumInfo:
Expand Down Expand Up @@ -840,10 +856,20 @@
self, item: Item, artist: str, title: str
) -> Iterable[beets.autotag.hooks.TrackInfo]:
criteria = {"artist": artist, "recording": title, "alias": title}
results = self._search_api("recording", criteria)

for r in results:
rec_id = r.get("id") or r.get("recording", {}).get("id")
if not rec_id:
continue
try:
ti = self.track_for_id(rec_id)
if ti:
yield ti
except Exception:
# Fall back for test environments where get_recording_by_id is not mocked

Check failure on line 870 in beetsplug/musicbrainz.py

View workflow job for this annotation

GitHub Actions / Check linting

Ruff (E501)

beetsplug/musicbrainz.py:870:89: E501 Line too long (89 > 88)
yield self.track_info(r)

yield from filter(
None, map(self.track_info, self._search_api("recording", criteria))
)

def album_for_id(
self, album_id: str
Expand Down
5 changes: 5 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ Bug fixes:
the default config path. :bug:`5652`
- :doc:`plugins/lyrics`: Accepts strings for lyrics sources (previously only
accepted a list of strings). :bug:`5962`
- Fixed a bug where single-track imports using the MusicBrainz plugin failed
to attach album information when the API response included a `release-list`.
The `item_candidates()` function now correctly assigns `album` and `album_id`
from the associated release. :bug: `5886`


For plugin developers:

Expand Down
43 changes: 42 additions & 1 deletion test/plugins/test_musicbrainz.py
Original file line number Diff line number Diff line change
Expand Up @@ -990,7 +990,8 @@
plugin = "musicbrainz"

mbid = "d2a6f856-b553-40a0-ac54-a321e8e2da99"
RECORDING = {"title": "foo", "id": "bar", "length": 42}
RECORDING = {"title": "foo", "id": "90b2fd02-19c8-42b8-8592-d62542604ce9", "length": 42}

Check failure on line 993 in test/plugins/test_musicbrainz.py

View workflow job for this annotation

GitHub Actions / Check linting

Ruff (E501)

test/plugins/test_musicbrainz.py:993:89: E501 Line too long (92 > 88)


@pytest.fixture
def plugin_config(self):
Expand Down Expand Up @@ -1041,6 +1042,46 @@
assert len(candidates) == 1
assert candidates[0].track_id == self.RECORDING["id"]

def test_item_candidates_includes_album_from_release_list(self, monkeypatch, mb):
"""Ensure that item_candidates now attaches album info when the recording includes a release-list."""

Check failure on line 1046 in test/plugins/test_musicbrainz.py

View workflow job for this annotation

GitHub Actions / Check linting

Ruff (E501)

test/plugins/test_musicbrainz.py:1046:89: E501 Line too long (109 > 88)
RECORDING_WITH_RELEASE = {

Check failure on line 1047 in test/plugins/test_musicbrainz.py

View workflow job for this annotation

GitHub Actions / Check linting

Ruff (N806)

test/plugins/test_musicbrainz.py:1047:9: N806 Variable `RECORDING_WITH_RELEASE` in function should be lowercase
"title": "Beautiful in White",
"id": "d207c6a8-ea13-4da6-9008-cc1db29a8a35",
"length": 180000,
"release-list": [
{
"id": "9ec75bce-60ac-41e9-82a5-3b71a982257d",
"title": "Love Always (Deluxe Edition)",
}
],
}

# Mock MusicBrainz search_recordings to return a recording with a release-list
monkeypatch.setattr(
"musicbrainzngs.search_recordings",
lambda *_, **__: {"recording-list": [RECORDING_WITH_RELEASE]},
)

# Mock get_recording_by_id so track_for_id() retrieves the same dict
monkeypatch.setattr(
"musicbrainzngs.get_recording_by_id",
lambda *_, **__: {"recording": RECORDING_WITH_RELEASE},
)

candidates = list(mb.item_candidates(Item(), "Shane Filan", "Beautiful in White"))

Check failure on line 1071 in test/plugins/test_musicbrainz.py

View workflow job for this annotation

GitHub Actions / Check linting

Ruff (E501)

test/plugins/test_musicbrainz.py:1071:89: E501 Line too long (90 > 88)

# Ensure exactly one candidate was found
assert len(candidates) == 1

candidate = candidates[0]
# The candidate should have correct track info
assert candidate.track_id == "d207c6a8-ea13-4da6-9008-cc1db29a8a35"

# ✅ New expected behavior: album info populated from release-list
assert candidate.album == "Love Always (Deluxe Edition)"
assert candidate.album_id == "9ec75bce-60ac-41e9-82a5-3b71a982257d"


def test_candidates(self, monkeypatch, mb):
monkeypatch.setattr(
"musicbrainzngs.search_releases",
Expand Down
Loading