Skip to content

Commit 2ec65ed

Browse files
committed
Deduplicate candidate methods using _search_api method
1 parent 0102f3c commit 2ec65ed

File tree

2 files changed

+100
-131
lines changed

2 files changed

+100
-131
lines changed

beetsplug/musicbrainz.py

Lines changed: 38 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
if TYPE_CHECKING:
3535
from collections.abc import Iterator, Sequence
36+
from typing import Literal
3637

3738
from beets.library import Item
3839

@@ -769,24 +770,46 @@ def get_album_criteria(
769770
self, items: list[Item], artist: str, album: str, va_likely: bool
770771
) -> dict[str, str]:
771772
criteria = {
772-
"release": album.lower().strip(),
773+
"release": album,
773774
"tracks": str(len(items)),
774-
} | (
775-
{"arid": VARIOUS_ARTISTS_ID}
776-
if va_likely
777-
else {"artist": artist.lower().strip()}
778-
)
775+
} | ({"arid": VARIOUS_ARTISTS_ID} if va_likely else {"artist": artist})
779776

780777
for tag, mb_field in self.extra_mb_field_by_tag.items():
781778
most_common, _ = util.plurality(i.get(tag) for i in items)
782-
value = str(most_common).lower().strip()
779+
value = str(most_common)
783780
if tag == "catalognum":
784781
value = value.replace(" ", "")
785-
if value:
786-
criteria[mb_field] = value
782+
783+
criteria[mb_field] = value
787784

788785
return criteria
789786

787+
def _search_api(
788+
self,
789+
query_type: Literal["recording", "release"],
790+
filters: dict[str, str],
791+
) -> list[JSONDict]:
792+
"""Perform MusicBrainz API search and return results.
793+
794+
Execute a search against the MusicBrainz API for recordings or releases
795+
using the provided criteria. Handles API errors by converting them into
796+
MusicBrainzAPIError exceptions with contextual information.
797+
"""
798+
filters = {
799+
k: _v for k, v in filters.items() if (_v := v.lower().strip())
800+
}
801+
self._log.debug(
802+
"Searching for MusicBrainz {}s with: {!r}", query_type, filters
803+
)
804+
try:
805+
method = getattr(musicbrainzngs, f"search_{query_type}s")
806+
res = method(limit=self.config["searchlimit"].get(int), **filters)
807+
except musicbrainzngs.MusicBrainzError as exc:
808+
raise MusicBrainzAPIError(
809+
exc, f"{query_type} search", filters, traceback.format_exc()
810+
)
811+
return res[f"{query_type}-list"]
812+
790813
def candidates(
791814
self,
792815
items: list[Item],
@@ -795,54 +818,19 @@ def candidates(
795818
va_likely: bool,
796819
extra_tags: dict[str, Any] | None = None,
797820
) -> Iterator[beets.autotag.hooks.AlbumInfo]:
798-
"""Searches for a single album ("release" in MusicBrainz parlance)
799-
and returns an iterator over AlbumInfo objects. May raise a
800-
MusicBrainzAPIError.
801-
802-
The query consists of an artist name, an album name, and,
803-
optionally, a number of tracks on the album and any other extra tags.
804-
"""
805821
criteria = self.get_album_criteria(items, artist, album, va_likely)
822+
release_ids = (r["id"] for r in self._search_api("release", criteria))
806823

807-
try:
808-
self._log.debug(
809-
"Searching for MusicBrainz releases with: {!r}", criteria
810-
)
811-
res = musicbrainzngs.search_releases(
812-
limit=self.config["searchlimit"].get(int), **criteria
813-
)
814-
except musicbrainzngs.MusicBrainzError as exc:
815-
raise MusicBrainzAPIError(
816-
exc, "release search", criteria, traceback.format_exc()
817-
)
818-
for release in res["release-list"]:
819-
# The search result is missing some data (namely, the tracks),
820-
# so we just use the ID and fetch the rest of the information.
821-
albuminfo = self.album_for_id(release["id"])
822-
if albuminfo is not None:
823-
yield albuminfo
824+
yield from filter(None, map(self.album_for_id, release_ids))
824825

825826
def item_candidates(
826827
self, item: Item, artist: str, title: str
827828
) -> Iterator[beets.autotag.hooks.TrackInfo]:
828-
"""Searches for a single track and returns an iterable of TrackInfo
829-
objects. May raise a MusicBrainzAPIError.
830-
"""
831-
criteria = {
832-
"artist": artist.lower().strip(),
833-
"recording": title.lower().strip(),
834-
}
829+
criteria = {"artist": artist, "recording": title}
835830

836-
try:
837-
res = musicbrainzngs.search_recordings(
838-
limit=self.config["searchlimit"].get(int), **criteria
839-
)
840-
except musicbrainzngs.MusicBrainzError as exc:
841-
raise MusicBrainzAPIError(
842-
exc, "recording search", criteria, traceback.format_exc()
843-
)
844-
for recording in res["recording-list"]:
845-
yield self.track_info(recording)
831+
yield from filter(
832+
None, map(self.track_info, self._search_api("recording", criteria))
833+
)
846834

847835
def album_for_id(
848836
self, album_id: str

test/plugins/test_musicbrainz.py

Lines changed: 62 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -757,79 +757,6 @@ def test_alias(self):
757757

758758

759759
class MBLibraryTest(MusicBrainzTestCase):
760-
def test_match_track(self):
761-
with mock.patch("musicbrainzngs.search_recordings") as p:
762-
p.return_value = {
763-
"recording-list": [
764-
{
765-
"title": "foo",
766-
"id": "bar",
767-
"length": 42,
768-
}
769-
],
770-
}
771-
ti = list(self.mb.item_candidates(None, "hello", "there"))[0]
772-
773-
p.assert_called_with(artist="hello", recording="there", limit=5)
774-
assert ti.title == "foo"
775-
assert ti.track_id == "bar"
776-
777-
def test_candidates(self):
778-
mbid = "d2a6f856-b553-40a0-ac54-a321e8e2da99"
779-
with mock.patch("musicbrainzngs.search_releases") as sp:
780-
sp.return_value = {
781-
"release-list": [
782-
{
783-
"id": mbid,
784-
}
785-
],
786-
}
787-
with mock.patch("musicbrainzngs.get_release_by_id") as gp:
788-
gp.return_value = {
789-
"release": {
790-
"title": "hi",
791-
"id": mbid,
792-
"status": "status",
793-
"medium-list": [
794-
{
795-
"track-list": [
796-
{
797-
"id": "baz",
798-
"recording": {
799-
"title": "foo",
800-
"id": "bar",
801-
"length": 42,
802-
},
803-
"position": 9,
804-
"number": "A1",
805-
}
806-
],
807-
"position": 5,
808-
}
809-
],
810-
"artist-credit": [
811-
{
812-
"artist": {
813-
"name": "some-artist",
814-
"id": "some-id",
815-
},
816-
}
817-
],
818-
"release-group": {
819-
"id": "another-id",
820-
},
821-
}
822-
}
823-
824-
ai = list(self.mb.candidates([], "hello", "there", False))[0]
825-
826-
sp.assert_called_with(
827-
artist="hello", release="there", tracks="0", limit=5
828-
)
829-
gp.assert_called_with(mbid, mock.ANY)
830-
assert ai.tracks[0].title == "foo"
831-
assert ai.album == "hi"
832-
833760
def test_follow_pseudo_releases(self):
834761
side_effect = [
835762
{
@@ -1061,37 +988,91 @@ def test_pseudo_releases_with_unsupported_links(self):
1061988
class TestMusicBrainzPlugin(PluginMixin):
1062989
plugin = "musicbrainz"
1063990

991+
mbid = "d2a6f856-b553-40a0-ac54-a321e8e2da99"
992+
RECORDING = {"title": "foo", "id": "bar", "length": 42}
993+
1064994
@pytest.fixture
1065-
def mb_plugin(self, plugin_config):
995+
def plugin_config(self):
996+
return {}
997+
998+
@pytest.fixture
999+
def mb(self, plugin_config):
10661000
self.config[self.plugin].set(plugin_config)
10671001

10681002
return musicbrainz.MusicBrainzPlugin()
10691003

10701004
@pytest.mark.parametrize(
10711005
"plugin_config,va_likely,expected_additional_criteria",
10721006
[
1073-
({}, False, {"artist": "artist"}),
1007+
({}, False, {"artist": "Artist "}),
10741008
({}, True, {"arid": "89ad4ac3-39f7-470e-963a-56509c546377"}),
10751009
(
10761010
{"extra_tags": ["label", "catalognum"]},
10771011
False,
1078-
{"artist": "artist", "label": "abc", "catno": "abc123"},
1012+
{"artist": "Artist ", "label": "abc", "catno": "ABC123"},
10791013
),
10801014
],
10811015
)
10821016
def test_get_album_criteria(
1083-
self, mb_plugin, va_likely, expected_additional_criteria
1017+
self, mb, va_likely, expected_additional_criteria
10841018
):
10851019
items = [
10861020
Item(catalognum="ABC 123", label="abc"),
10871021
Item(catalognum="ABC 123", label="abc"),
10881022
Item(catalognum="ABC 123", label="def"),
10891023
]
10901024

1091-
assert mb_plugin.get_album_criteria(
1092-
items, "Artist ", " Album", va_likely
1093-
) == {
1094-
"release": "album",
1025+
assert mb.get_album_criteria(items, "Artist ", " Album", va_likely) == {
1026+
"release": " Album",
10951027
"tracks": str(len(items)),
10961028
**expected_additional_criteria,
10971029
}
1030+
1031+
def test_item_candidates(self, monkeypatch, mb):
1032+
monkeypatch.setattr(
1033+
"musicbrainzngs.search_recordings",
1034+
lambda *_, **__: {"recording-list": [self.RECORDING]},
1035+
)
1036+
1037+
candidates = list(mb.item_candidates(Item(), "hello", "there"))
1038+
1039+
assert len(candidates) == 1
1040+
assert candidates[0].track_id == self.RECORDING["id"]
1041+
1042+
def test_candidates(self, monkeypatch, mb):
1043+
monkeypatch.setattr(
1044+
"musicbrainzngs.search_releases",
1045+
lambda *_, **__: {"release-list": [{"id": self.mbid}]},
1046+
)
1047+
monkeypatch.setattr(
1048+
"musicbrainzngs.get_release_by_id",
1049+
lambda *_, **__: {
1050+
"release": {
1051+
"title": "hi",
1052+
"id": self.mbid,
1053+
"status": "status",
1054+
"medium-list": [
1055+
{
1056+
"track-list": [
1057+
{
1058+
"id": "baz",
1059+
"recording": self.RECORDING,
1060+
"position": 9,
1061+
"number": "A1",
1062+
}
1063+
],
1064+
"position": 5,
1065+
}
1066+
],
1067+
"artist-credit": [
1068+
{"artist": {"name": "some-artist", "id": "some-id"}}
1069+
],
1070+
"release-group": {"id": "another-id"},
1071+
}
1072+
},
1073+
)
1074+
candidates = list(mb.candidates([], "hello", "there", False))
1075+
1076+
assert len(candidates) == 1
1077+
assert candidates[0].tracks[0].track_id == self.RECORDING["id"]
1078+
assert candidates[0].album == "hi"

0 commit comments

Comments
 (0)