diff --git a/beetsplug/discogs.py b/beetsplug/discogs.py index ee432f11f..727d8fbb7 100644 --- a/beetsplug/discogs.py +++ b/beetsplug/discogs.py @@ -97,8 +97,12 @@ class DiscogsPlugin(MetadataSourcePlugin): "user_token": "", "separator": ", ", "index_tracks": False, + "featured_label": "Feat.", "append_style_genre": False, "strip_disambiguation": True, + "album_artist_anv": False, + "track_artist_anv": False, + "artist_credit_anv": True } ) self.config["apikey"].redact = True @@ -302,6 +306,19 @@ class DiscogsPlugin(MetadataSourcePlugin): return media, albumtype + def get_artist(self, artists, use_anv=False) -> tuple[str, str | None]: + """ Iterates through a discogs result, fetching data + if the artist anv is to be used, maps that to the name. + Calls the parent class get_artist method.""" + artist_data = [] + for artist in artists: + if use_anv and (anv := artist.get("anv", "")): + artist["name"] = anv + artist_data.append(artist) + artist, artist_id = super().get_artist( + artist_data, join_key="join") + return self.strip_disambiguation(artist), artist_id + def get_album_info(self, result): """Returns an AlbumInfo object for a discogs Release object.""" # Explicitly reload the `Release` fields, as they might not be yet @@ -330,8 +347,12 @@ class DiscogsPlugin(MetadataSourcePlugin): self._log.warning("Release does not contain the required fields") return None - artist, artist_id = self.get_artist( - [a.data for a in result.artists], join_key="join" + artist_data = [a.data for a in result.artists] + album_artist, album_artist_id = self.get_artist(artist_data, + self.config["album_artist_anv"] + ) + artist_credit, _ = self.get_artist(artist_data, + self.config["artist_credit_anv"] ) album = re.sub(r" +", " ", result.title) album_id = result.data["id"] @@ -339,7 +360,8 @@ class DiscogsPlugin(MetadataSourcePlugin): # convenient `.tracklist` property, which will strip out useful artist # information and leave us with skeleton `Artist` objects that will # each make an API call just to get the same data back. - tracks = self.get_tracks(result.data["tracklist"], artist, artist_id) + tracks = self.get_tracks(result.data["tracklist"], (album_artist, album_artist_id, + artist_credit)) # Extract information for the optional AlbumInfo fields, if possible. va = result.data["artists"][0].get("name", "").lower() == "various" @@ -376,9 +398,7 @@ class DiscogsPlugin(MetadataSourcePlugin): # Additional cleanups # (various artists name, catalog number, media, disambiguation). if va: - artist = config["va_name"].as_str() - else: - artist = self.strip_disambiguation(artist) + album_artist, artist_credit = config["va_name"].as_str() if catalogno == "none": catalogno = None # Explicitly set the `media` for the tracks, since it is expected by @@ -401,9 +421,9 @@ class DiscogsPlugin(MetadataSourcePlugin): return AlbumInfo( album=album, album_id=album_id, - artist=artist, - artist_credit=artist, - artist_id=artist_id, + artist=album_artist, + artist_credit=artist_credit, + artist_id=album_artist_id, tracks=tracks, albumtype=albumtype, va=va, @@ -421,7 +441,7 @@ class DiscogsPlugin(MetadataSourcePlugin): data_url=data_url, discogs_albumid=discogs_albumid, discogs_labelid=labelid, - discogs_artistid=artist_id, + discogs_artistid=album_artist_id, cover_art_url=cover_art_url, ) @@ -443,7 +463,7 @@ class DiscogsPlugin(MetadataSourcePlugin): else: return None - def get_tracks(self, tracklist, album_artist, album_artist_id): + def get_tracks(self, tracklist, album_artist_data): """Returns a list of TrackInfo objects for a discogs tracklist.""" try: clean_tracklist = self.coalesce_tracks(tracklist) @@ -469,7 +489,7 @@ class DiscogsPlugin(MetadataSourcePlugin): divisions += next_divisions del next_divisions[:] track_info = self.get_track_info( - track, index, divisions, album_artist, album_artist_id + track, index, divisions, album_artist_data ) track_info.track_alt = track["position"] tracks.append(track_info) @@ -638,9 +658,11 @@ class DiscogsPlugin(MetadataSourcePlugin): return DISAMBIGUATION_RE.sub("", text) def get_track_info( - self, track, index, divisions, album_artist, album_artist_id + self, track, index, divisions, album_artist_data ): """Returns a TrackInfo object for a discogs track.""" + album_artist, album_artist_id, artist_credit = album_artist_data + title = track["title"] if self.config["index_tracks"]: prefix = ", ".join(divisions) @@ -648,28 +670,39 @@ class DiscogsPlugin(MetadataSourcePlugin): title = f"{prefix}: {title}" track_id = None medium, medium_index, _ = self.get_track_index(track["position"]) - artist, artist_id = self.get_artist( - track.get("artists", []), join_key="join" - ) - # If no artist and artist is returned, set to match album artist - if not artist: - artist = album_artist - artist_id = album_artist_id + + artist = album_artist + artist_credit = album_artist + artist_id = album_artist_id + + # If artists are found on the track, we will use those instead + if (artists := track.get("artists", [])): + artist, artist_id = self.get_artist(artists, + self.config["track_artist_anv"] + ) + artist_credit, _ = self.get_artist(artists, + self.config["artist_credit_anv"] + ) length = self.get_track_length(track["duration"]) + # Add featured artists - extraartists = track.get("extraartists", []) - featured = [ - artist["name"] - for artist in extraartists - if "Featuring" in artist["role"] - ] - if featured: - artist = f"{artist} feat. {', '.join(featured)}" - artist = self.strip_disambiguation(artist) + if (extraartists := track.get("extraartists", [])): + featured_list = [ + artist for artist + in extraartists + if "Featuring" + in artist["role"]] + featured, _ = self.get_artist(featured_list, + self.config["track_artist_anv"]) + featured_credit, _ = self.get_artist(featured_list, + self.config["artist_credit_anv"]) + if featured: + artist = f"{artist} {self.config['featured_label']} {featured}" + artist_credit = f"{artist_credit} {self.config['featured_label']} {featured_credit}" return TrackInfo( title=title, track_id=track_id, - artist_credit=artist, + artist_credit=artist_credit, artist=artist, artist_id=artist_id, length=length, diff --git a/test/plugins/test_discogs.py b/test/plugins/test_discogs.py index a7d1c8407..97c177736 100644 --- a/test/plugins/test_discogs.py +++ b/test/plugins/test_discogs.py @@ -452,6 +452,72 @@ class DGAlbumInfoTest(BeetsTestCase): assert d.label == "LABEL NAME (5)" config["discogs"]["strip_disambiguation"] = True + def test_use_anv(self): + test_cases = [ + ({ + "track_artist": False, + "album_artist": False, + "artist_credit": False + }, + { + "album_artist": "ARTIST NAME & SOLOIST", + "album_artist_credit": "ARTIST NAME & SOLOIST", + "track_artist": "ARTIST Feat. PERFORMER", + "track_artist_credit": "ARTIST Feat. PERFORMER" + }), + ({ + "track_artist": True, + "album_artist": False, + "artist_credit": False + }, + { + "album_artist": "ARTIST NAME & SOLOIST", + "album_artist_credit": "ARTIST NAME & SOLOIST", + "track_artist": "ARTY Feat. FORMER", + "track_artist_credit": "ARTIST Feat. PERFORMER" + })] + data = { + "id": 123, + "uri": "https://www.discogs.com/release/123456-something", + "tracklist": [{ + "title": "track", + "position": "A", + "type_": "track", + "duration": "5:44", + "artists": [{ + "name": "ARTIST", + "tracks": "", + "anv": "ARTY", + "id": 11146 + }], + "extraartists": [{ + "name": "PERFORMER", + "role": "Featuring", + "anv": "FORMER", + "id": 787 + }], + }], + "artists": [ + {"name": "ARTIST NAME", "anv": "ARTISTIC", "id": 321, "join": "&"}, + {"name": "SOLOIST", "anv": "SOLO", "id": 445, "join": ""}, + ], + "title": "title", + } + release = Bag( + data=data, + title=data["title"], + artists=[Bag(data=d) for d in data["artists"]], + ) + for test_case in test_cases: + config_input, expected_output = test_case + r = DiscogsPlugin().get_album_info(release) + config["album_artist_anv"] = config_input["album_artist"] + config["track_artist_anv"] = config_input["track_artist"] + config["artist_credit_anv"] = config_input["artist_credit"] + assert r.artist == expected_output["album_artist"] + assert r.artist_credit == expected_output["album_artist_credit"] + assert r.tracks[0].artist == expected_output["track_artist"] + assert r.tracks[0].artist_credit == expected_output["track_artist_credit"] @pytest.mark.parametrize( "track, expected_artist", @@ -469,23 +535,27 @@ class DGAlbumInfoTest(BeetsTestCase): "extraartists": [ { "name": "SOLOIST", + "id": 3, "role": "Featuring", }, { "name": "PERFORMER (1)", + "id": 5, "role": "Other Role, Featuring", }, { "name": "RANDOM", + "id": 8, "role": "Written-By", }, { "name": "MUSICIAN", + "id": 10, "role": "Featuring [Uncredited]", }, ], }, - "NEW ARTIST, VOCALIST feat. SOLOIST, PERFORMER, MUSICIAN", + "NEW ARTIST, VOCALIST Feat. SOLOIST, PERFORMER, MUSICIAN", ), ], ) @@ -494,7 +564,7 @@ def test_parse_featured_artists(track, expected_artist): """Tests the plugins ability to parse a featured artist. Initial check with one featured artist, two featured artists, and three. Ignores artists that are not listed as featured.""" - t = DiscogsPlugin().get_track_info(track, 1, 1, "ARTIST", 2) + t = DiscogsPlugin().get_track_info(track, 1, 1, ("ARTIST", 2, "ARTIST CREDIT")) assert t.artist == expected_artist @@ -520,109 +590,6 @@ def test_get_media_and_albumtype(formats, expected_media, expected_albumtype): assert result == (expected_media, expected_albumtype) -@patch("beetsplug.discogs.DiscogsPlugin.setup", Mock()) -@pytest.mark.parametrize( - "config_input, expected_output", - [ - ({ - "track_artist": False, - "album_artist": False, - "artist_credit": False - }, - { - "album_artist": "ARTIST NAME & SOLOIST", - "album_artist_credit": "ARTIST NAME & SOLOIST", - "track_artist": "ARTIST feat. PERFORMER", - "track_artist_credit": "ARTIST feat. PERFORMER" - }), - ({ - "album_artist": False, - "track_artist": True, - "artist_credit": False - }, - { - "album_artist": "ARTIST NAME & SOLOIST", - "album_artist_credit": "ARTIST NAME & SOLOIST", - "track_artist": "ARTY feat. FORMER", - "track_artist_credit": "ARTIST feat. PERFORMER" - }), - ({ - "album_artist": True, - "track_artist": False, - "artist_credit": False - }, - { - "album_artist": "ARTY & SOLO", - "album_artist_credit": "ARTIST NAME & SOLOIST", - "track_artist": "ARTIST feat. PERFORMER", - "track_artist_credit": "ARTIST feat. PERFORMER" - }), - ({ - "album_artist": True, - "track_artist": False, - "artist_credit": False - }, - { - "album_artist": "ARTY & SOLO", - "album_artist_credit": "ARTIST NAME & SOLOIST", - "track_artist": "ARTIST feat. PERFORMER", - "track_artist_credit": "ARTIST Feat. PERFORMER" - }), - ({ - "album_artist": False, - "track_artist": False, - "artist_credit": True - }, - { - "album_artist": "ARTIST NAME & SOLOIST", - "album_artist_credit": "ARTY & SOLO", - "track_artist": "ARTIST feat. PERFORMER", - "track_artist_credit": "ARTY Feat. FORMER" - }) - -]) -def test_use_anv(config_input, expected_output): - d = DiscogsPlugin() - d.config["album_artist_anv"] = config_input["album_artist"] - d.config["track_artist_anv"] = config_input["track_artist"] - d.config["artist_credit_anv"] = config_input["artist_credit"] - data = { - "id": 123, - "uri": "https://www.discogs.com/release/123456-something", - "tracklist": [{ - "title": "track", - "position": "A", - "type_": "track", - "duration": "5:44", - "artists": [{ - "name": "ARTIST", - "tracks": "", - "anv": "ARTY", - "id": 11146 - }], - "extraartists": [{ - "name": "PERFORMER", - "role": "Featuring", - "anv": "FORMER", - "id": 787 - }], - }], - "artists": [ - {"name": "ARTIST NAME", "anv": "ARTY", "id": 321, "join": "&"}, - {"name": "SOLOIST", "anv": "SOLO", "id": 445, "join": ""}, - ], - "title": "title", - } - release = Bag( - data=data, - title=data["title"], - artists=[Bag(data=d) for d in data["artists"]], - ) - r = d.get_album_info(release) - assert r.artist == expected_output["album_artist"] - assert r.artist_credit == expected_output["album_artist_credit"] - assert r.tracks[0].artist == expected_output["track_artist"] - assert r.tracks[0].artist_credit == expected_output["track_artist_credit"] @pytest.mark.parametrize( "position, medium, index, subindex",