-
Notifications
You must be signed in to change notification settings - Fork 2k
Album tagging for Musicbrainz single import #6160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there - I've reviewed your changes and they look great!
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `beetsplug/musicbrainz.py:522` </location>
<code_context>
+ 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)
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against empty release lists to prevent IndexError.
Accessing rel_list[0] without checking if rel_list is non-empty can cause an IndexError. Add a check to ensure rel_list contains elements before accessing its first item.
</issue_to_address>
### Comment 2
<location> `beetsplug/musicbrainz.py:861-871` </location>
<code_context>
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
+ yield self.track_info(r)
- yield from filter(
</code_context>
<issue_to_address>
**suggestion:** Consider logging or surfacing exceptions for better debugging.
Silent exception handling in item_candidates may hide errors. Logging exceptions or adding context will make troubleshooting easier.
```suggestion
import logging
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 as exc:
logging.exception(
f"Exception in item_candidates for recording ID '{rec_id}': {exc}"
)
# Fall back for test environments where get_recording_by_id is not mocked
yield self.track_info(r)
```
</issue_to_address>
### Comment 3
<location> `test/plugins/test_musicbrainz.py:1045-1054` </location>
<code_context>
+ def test_item_candidates_includes_album_from_release_list(self, monkeypatch, mb):
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding tests for multiple releases in the release-list.
Please add a test with multiple releases in the release-list to verify that the first release is always selected for album info.
</issue_to_address>
### Comment 4
<location> `test/plugins/test_musicbrainz.py:1080-1082` </location>
<code_context>
+ # 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"
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Test does not cover missing or malformed release-list edge cases.
Please add tests for missing, empty, or malformed release-list scenarios to verify robust handling and prevent incorrect album assignment or errors.
Suggested implementation:
```python
# 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_missing_release_list(monkeypatch):
# Simulate MusicBrainz response with no release-list
def mock_item_candidates(item, artist, title):
class Candidate:
track_id = "some-track-id"
album = None
album_id = None
return [Candidate()]
monkeypatch.setattr(mb, "item_candidates", mock_item_candidates)
candidates = list(mb.item_candidates(Item(), "Artist", "Title"))
assert len(candidates) == 1
candidate = candidates[0]
assert candidate.album is None
assert candidate.album_id is None
def test_empty_release_list(monkeypatch):
# Simulate MusicBrainz response with empty release-list
def mock_item_candidates(item, artist, title):
class Candidate:
track_id = "some-track-id"
album = None
album_id = None
return [Candidate()]
monkeypatch.setattr(mb, "item_candidates", mock_item_candidates)
candidates = list(mb.item_candidates(Item(), "Artist", "Title"))
assert len(candidates) == 1
candidate = candidates[0]
assert candidate.album is None
assert candidate.album_id is None
def test_malformed_release_list(monkeypatch):
# Simulate MusicBrainz response with malformed release-list
def mock_item_candidates(item, artist, title):
class Candidate:
track_id = "some-track-id"
album = None
album_id = None
return [Candidate()]
monkeypatch.setattr(mb, "item_candidates", mock_item_candidates)
candidates = list(mb.item_candidates(Item(), "Artist", "Title"))
assert len(candidates) == 1
candidate = candidates[0]
assert candidate.album is None
assert candidate.album_id is None
```
If your actual `item_candidates` implementation handles these cases differently (e.g., raises exceptions or assigns default values), you may need to adjust the assertions or the mock implementations accordingly. Also, ensure that the `mb` and `Item` objects are properly imported or available in the test context.
</issue_to_address>
### Comment 5
<location> `beetsplug/musicbrainz.py:866-867` </location>
<code_context>
def item_candidates(
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
yield self.track_info(r)
</code_context>
<issue_to_address>
**suggestion (code-quality):** Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
```suggestion
if ti := self.track_for_id(rec_id):
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #6160 +/- ##
==========================================
+ Coverage 67.93% 67.94% +0.01%
==========================================
Files 137 137
Lines 18677 18697 +20
Branches 3155 3161 +6
==========================================
+ Hits 12688 12704 +16
+ Misses 5324 5323 -1
- Partials 665 670 +5
🚀 New features to boost your workflow:
|
|
Thanks for the submission. And yeah we have this eg for discogs and spotify, but there it's easier since eg one discogs track is on one album, there is no such thing as multiple albums for one recording (aka track). discogs searches for single tracks look up albums anyway. up to now for musicbrainz singletons, keeping them non-album tracks was a design choice. so what's interesting here is that we assume simply the first release in the list is the right one. it's better than nothing but is there a pattern? how can we know the first is the best idea? maybe some examples? there is no such thing as asking mb for a master release? or first ever released? what is the order of the releases in that list? |
Fixes #5886
This PR resolves an issue where standalone tracks imported via item_candidates() were missing album information even when the MusicBrainz API response included a release-list.
Previously, Beets correctly matched individual recordings but failed to propagate the associated album title and ID from the release-list field, causing imported items to appear as Non-Album Tracks despite having a valid album relationship.
This fix ensures that when a recording returned by MusicBrainz includes one or more release-list entries, the first associated release’s title and id are used to populate album and album_id for that candidate.
A new test, test_item_candidates_includes_album_from_release_list, verifies this behavior by mocking a MusicBrainz recording response that includes a release-list and confirming that the album data is correctly attached to the resulting TrackInfo object.
[ ]Documentation (N/A — behavior fix, no user-facing config changes).
[X] Changelog (add an entry noting improved MusicBrainz album tagging for single-track imports).
[X] Tests (added test_item_candidates_includes_album_from_release_list to test_musicbrainz.py).