Skip to content

Commit 91ece4c

Browse files
Fix #6068 - Multivalue fields are now supported & tested.
1 parent de29348 commit 91ece4c

File tree

3 files changed

+234
-74
lines changed

3 files changed

+234
-74
lines changed

beetsplug/discogs.py

Lines changed: 152 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from __future__ import annotations
2020

21+
import copy
2122
import http.client
2223
import json
2324
import os
@@ -105,6 +106,24 @@ class Track(TypedDict):
105106
sub_tracks: NotRequired[list[Track]]
106107

107108

109+
class ArtistInfo(TypedDict):
110+
artist: str
111+
artists: list[str]
112+
artist_credit: str
113+
artists_credit: list[str]
114+
artist_id: str
115+
artists_ids: list[str]
116+
117+
118+
class AlbumArtistInfo(ArtistInfo):
119+
albumartist: str
120+
albumartists: list[str]
121+
albumartist_credit: str
122+
albumartists_credit: list[str]
123+
albumartist_id: str
124+
albumartists_ids: list[str]
125+
126+
108127
class DiscogsPlugin(MetadataSourcePlugin):
109128
def __init__(self):
110129
super().__init__()
@@ -261,7 +280,6 @@ def track_for_id(self, track_id: str) -> TrackInfo | None:
261280
for track in album.tracks:
262281
if track.track_id == track_id:
263282
return track
264-
265283
return None
266284

267285
def get_albums(self, query: str) -> Iterable[AlbumInfo]:
@@ -346,6 +364,121 @@ def get_artist_with_anv(
346364
artist, artist_id = self.get_artist(artist_list, join_key="join")
347365
return self.strip_disambiguation(artist), artist_id
348366

367+
def build_albumartistinfo(self, artists: list[Artist]) -> AlbumArtistInfo:
368+
info = self.build_artistinfo(artists, album_artist=True)
369+
albumartist: AlbumArtistInfo = {
370+
**info,
371+
"albumartist": info["artist"],
372+
"albumartist_id": info["artist_id"],
373+
"albumartists": info["artists"],
374+
"albumartists_ids": info["artists_ids"],
375+
"albumartist_credit": info["artist_credit"],
376+
"albumartists_credit": info["artists_credit"],
377+
}
378+
return albumartist
379+
380+
def build_artistinfo(
381+
self,
382+
given_artists: list[Artist],
383+
given_info: ArtistInfo | None = None,
384+
album_artist: bool = False,
385+
) -> ArtistInfo:
386+
"""Iterates through a discogs result and builds
387+
up the artist fields. Does not contribute to
388+
artist_sort as Discogs does not define that.
389+
390+
:param artists: A list of Discogs Artist objects
391+
392+
:param album_artist: If building an album artist,
393+
we need to account for the album_artist anv parameter.
394+
:return an ArtistInfo dictionary.
395+
"""
396+
info: ArtistInfo = {
397+
"artist": "",
398+
"artist_id": "",
399+
"artists": [],
400+
"artists_ids": [],
401+
"artist_credit": "",
402+
"artists_credit": [],
403+
}
404+
if given_info:
405+
info = copy.deepcopy(given_info)
406+
407+
a_anv: bool = self.config["anv"]["artist"].get(bool)
408+
ac_anv: bool = self.config["anv"]["artist_credit"].get(bool)
409+
aa_anv: bool = self.config["anv"]["album_artist"].get(bool)
410+
feat_str: str = f" {self.config['featured_string'].get(str)} "
411+
412+
artist = ""
413+
artist_anv = ""
414+
artists: list[str] = []
415+
artists_anv: list[str] = []
416+
417+
join = ""
418+
featured_flag = False
419+
# Iterate through building the artist strings
420+
for a in given_artists:
421+
# Get the artist name
422+
name = self.strip_disambiguation(a["name"])
423+
discogs_id = a["id"]
424+
anv = a.get("anv", name)
425+
role = a.get("role", "").lower()
426+
# Check if the artist is Various
427+
if name.lower() == "various":
428+
name = config["va_name"].as_str()
429+
anv = name
430+
431+
if "featuring" in role:
432+
if not featured_flag:
433+
artist += feat_str
434+
artist_anv += feat_str
435+
artist += name
436+
artist_anv += anv
437+
featured_flag = True
438+
else:
439+
artist = self._join_artist(artist, name, join)
440+
artist_anv = self._join_artist(artist_anv, anv, join)
441+
elif role and "featuring" not in role:
442+
continue
443+
else:
444+
artist = self._join_artist(artist, name, join)
445+
artist_anv = self._join_artist(artist_anv, anv, join)
446+
artists.append(name)
447+
artists_anv.append(anv)
448+
# Only the first ID is set for the singular field
449+
if not info["artist_id"]:
450+
info["artist_id"] = discogs_id
451+
info["artists_ids"].append(discogs_id)
452+
# Update join for the next artist
453+
join = a.get("join", "")
454+
# Assign fields as necessary
455+
if (a_anv and not album_artist) or (aa_anv and album_artist):
456+
info["artist"] += artist_anv
457+
info["artists"] += artists_anv
458+
else:
459+
info["artist"] += artist
460+
info["artists"] += artists
461+
462+
if ac_anv:
463+
info["artist_credit"] += artist_anv
464+
info["artists_credit"] += artists_anv
465+
else:
466+
info["artist_credit"] += artist
467+
info["artists_credit"] += artists
468+
return info
469+
470+
def _join_artist(self, base: str, artist: str, join: str) -> str:
471+
# Expand the artist field
472+
if not base:
473+
base = artist
474+
else:
475+
if join:
476+
base += f" {join} "
477+
else:
478+
base += ", "
479+
base += artist
480+
return base
481+
349482
def get_album_info(self, result: Release) -> AlbumInfo | None:
350483
"""Returns an AlbumInfo object for a discogs Release object."""
351484
# Explicitly reload the `Release` fields, as they might not be yet
@@ -375,11 +508,8 @@ def get_album_info(self, result: Release) -> AlbumInfo | None:
375508
return None
376509

377510
artist_data = [a.data for a in result.artists]
378-
album_artist, album_artist_id = self.get_artist_with_anv(artist_data)
379-
album_artist_anv, _ = self.get_artist_with_anv(
380-
artist_data, use_anv=True
381-
)
382-
artist_credit = album_artist_anv
511+
# Information for the album artist
512+
albumartist: AlbumArtistInfo = self.build_albumartistinfo(artist_data)
383513

384514
album = re.sub(r" +", " ", result.title)
385515
album_id = result.data["id"]
@@ -388,18 +518,11 @@ def get_album_info(self, result: Release) -> AlbumInfo | None:
388518
# information and leave us with skeleton `Artist` objects that will
389519
# each make an API call just to get the same data back.
390520
tracks = self.get_tracks(
391-
result.data["tracklist"],
392-
(album_artist, album_artist_anv, album_artist_id),
521+
result.data["tracklist"], self.build_artistinfo(artist_data)
393522
)
394523

395-
# Assign ANV to the proper fields for tagging
396-
if not self.config["anv"]["artist_credit"]:
397-
artist_credit = album_artist
398-
if self.config["anv"]["album_artist"]:
399-
album_artist = album_artist_anv
400-
401524
# Extract information for the optional AlbumInfo fields, if possible.
402-
va = result.data["artists"][0].get("name", "").lower() == "various"
525+
va = albumartist["albumartist"] == config["va_name"].as_str()
403526
year = result.data.get("year")
404527
mediums = [t.medium for t in tracks]
405528
country = result.data.get("country")
@@ -431,11 +554,7 @@ def get_album_info(self, result: Release) -> AlbumInfo | None:
431554
cover_art_url = self.select_cover_art(result)
432555

433556
# Additional cleanups
434-
# (various artists name, catalog number, media, disambiguation).
435-
if va:
436-
va_name = config["va_name"].as_str()
437-
album_artist = va_name
438-
artist_credit = va_name
557+
# (catalog number, media, disambiguation).
439558
if catalogno == "none":
440559
catalogno = None
441560
# Explicitly set the `media` for the tracks, since it is expected by
@@ -458,9 +577,7 @@ def get_album_info(self, result: Release) -> AlbumInfo | None:
458577
return AlbumInfo(
459578
album=album,
460579
album_id=album_id,
461-
artist=album_artist,
462-
artist_credit=artist_credit,
463-
artist_id=album_artist_id,
580+
**albumartist, # Unpacks values to satisfy the keyword arguments
464581
tracks=tracks,
465582
albumtype=albumtype,
466583
va=va,
@@ -478,7 +595,7 @@ def get_album_info(self, result: Release) -> AlbumInfo | None:
478595
data_url=data_url,
479596
discogs_albumid=discogs_albumid,
480597
discogs_labelid=labelid,
481-
discogs_artistid=album_artist_id,
598+
discogs_artistid=albumartist["albumartist_id"],
482599
cover_art_url=cover_art_url,
483600
)
484601

@@ -503,7 +620,7 @@ def format(self, classification: Iterable[str]) -> str | None:
503620
def _process_clean_tracklist(
504621
self,
505622
clean_tracklist: list[Track],
506-
album_artist_data: tuple[str, str, str | None],
623+
albumartistinfo: ArtistInfo,
507624
) -> tuple[
508625
list[TrackInfo],
509626
dict[int, str],
@@ -531,7 +648,7 @@ def _process_clean_tracklist(
531648
divisions += next_divisions
532649
del next_divisions[:]
533650
track_info, medium, medium_index = self.get_track_info(
534-
track, index, divisions, album_artist_data
651+
track, index, divisions, albumartistinfo
535652
)
536653
track_info.track_alt = track["position"]
537654
tracks.append(track_info)
@@ -565,7 +682,7 @@ def _process_clean_tracklist(
565682
def get_tracks(
566683
self,
567684
tracklist: list[Track],
568-
album_artist_data: tuple[str, str, str | None],
685+
albumartistinfo: ArtistInfo,
569686
) -> list[TrackInfo]:
570687
"""Returns a list of TrackInfo objects for a discogs tracklist."""
571688
try:
@@ -578,7 +695,7 @@ def get_tracks(
578695
self._log.error("uncaught exception in coalesce_tracks: {}", exc)
579696
clean_tracklist = tracklist
580697
processed = self._process_clean_tracklist(
581-
clean_tracklist, album_artist_data
698+
clean_tracklist, albumartistinfo
582699
)
583700
(
584701
tracks,
@@ -754,16 +871,11 @@ def get_track_info(
754871
track: Track,
755872
index: int,
756873
divisions: list[str],
757-
album_artist_data: tuple[str, str, str | None],
874+
albumartistinfo: ArtistInfo,
758875
) -> tuple[TrackInfo, str | None, str | None]:
759876
"""Returns a TrackInfo object for a discogs track."""
760877

761-
artist, artist_anv, artist_id = album_artist_data
762-
artist_credit = artist_anv
763-
if not self.config["anv"]["artist_credit"]:
764-
artist_credit = artist
765-
if self.config["anv"]["artist"]:
766-
artist = artist_anv
878+
artistinfo = albumartistinfo.copy()
767879

768880
title = track["title"]
769881
if self.config["index_tracks"]:
@@ -775,39 +887,19 @@ def get_track_info(
775887

776888
# If artists are found on the track, we will use those instead
777889
if artists := track.get("artists", []):
778-
artist, artist_id = self.get_artist_with_anv(
779-
artists, self.config["anv"]["artist"]
780-
)
781-
artist_credit, _ = self.get_artist_with_anv(
782-
artists, self.config["anv"]["artist_credit"]
783-
)
890+
artistinfo = self.build_artistinfo(artists)
891+
784892
length = self.get_track_length(track["duration"])
785893

786894
# Add featured artists
787895
if extraartists := track.get("extraartists", []):
788-
featured_list = [
789-
artist
790-
for artist in extraartists
791-
if "Featuring" in artist["role"]
792-
]
793-
featured, _ = self.get_artist_with_anv(
794-
featured_list, self.config["anv"]["artist"]
795-
)
796-
featured_credit, _ = self.get_artist_with_anv(
797-
featured_list, self.config["anv"]["artist_credit"]
798-
)
799-
if featured:
800-
artist += f" {self.config['featured_string']} {featured}"
801-
artist_credit += (
802-
f" {self.config['featured_string']} {featured_credit}"
803-
)
896+
artistinfo = self.build_artistinfo(extraartists, artistinfo)
897+
804898
return (
805899
TrackInfo(
806900
title=title,
807901
track_id=track_id,
808-
artist_credit=artist_credit,
809-
artist=artist,
810-
artist_id=artist_id,
902+
**artistinfo,
811903
length=length,
812904
index=index,
813905
),

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ New features:
2626
- :doc:`plugins/mbpseudo`: Add a new `mbpseudo` plugin to proactively receive
2727
MusicBrainz pseudo-releases as recommendations during import.
2828
- Added support for Python 3.13.
29+
- :doc:`plugins/discogs`: Added support for multi value fields. :bug:`6068`
2930

3031
Bug fixes:
3132

0 commit comments

Comments
 (0)