Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 155 additions & 60 deletions beetsplug/discogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import traceback
from functools import cache
from string import ascii_lowercase
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING, NamedTuple, cast

import confuse
from discogs_client import Client, Master, Release
Expand Down Expand Up @@ -121,6 +121,18 @@
super().__init__(**kwargs)


class ArtistCreditParts(NamedTuple):
name: str
artist_id: str | None
names: tuple[str, ...]
ids: tuple[str, ...]


class ArtistCreditData(NamedTuple):
default: ArtistCreditParts
anv: ArtistCreditParts


class DiscogsPlugin(MetadataSourcePlugin):
def __init__(self):
super().__init__()
Expand Down Expand Up @@ -343,24 +355,48 @@

return media, albumtype

def get_artist_with_anv(
self, artists: list[Artist], use_anv: bool = 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_list: list[dict[str | int, str]] = []
def _artist_credit_parts(
self,
artists: list[Artist],
*,
use_anv: bool,
) -> ArtistCreditParts:
if not artists:
return ArtistCreditParts("", None, tuple(), tuple())

formatted: list[dict[str | int, str | None]] = []
names: list[str] = []
ids: list[str] = []

for artist_data in artists:
a: dict[str | int, str] = {
"name": artist_data["name"],
"id": artist_data["id"],
"join": artist_data.get("join", ""),
}
if use_anv and (anv := artist_data.get("anv", "")):
a["name"] = anv
artist_list.append(a)
artist, artist_id = self.get_artist(artist_list, join_key="join")
return self.strip_disambiguation(artist), artist_id
name = artist_data.get("anv") if use_anv else None
if not name:
name = artist_data["name"]

stripped_name = self.strip_disambiguation(name)
artist_id = artist_data.get("id")
str_id = str(artist_id) if artist_id is not None else None
formatted.append(
{
"name": stripped_name,
"id": str_id,
"join": artist_data.get("join", ""),
}
)
names.append(stripped_name)
if str_id is not None:
ids.append(str_id)

artist, artist_id = self.get_artist(formatted, join_key="join")

Check failure on line 390 in beetsplug/discogs.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Argument 1 to "get_artist" of "MetadataSourcePlugin" has incompatible type "list[dict[str | int, str | None]]"; expected "Iterable[dict[str | int, str]]"
return ArtistCreditParts(artist, artist_id, tuple(names), tuple(ids))

def _build_artist_credit_data(
self, artists: list[Artist]
) -> ArtistCreditData:
return ArtistCreditData(
default=self._artist_credit_parts(artists, use_anv=False),
anv=self._artist_credit_parts(artists, use_anv=True),
)

def get_album_info(self, result: Release) -> AlbumInfo | None:
"""Returns an AlbumInfo object for a discogs Release object."""
Expand Down Expand Up @@ -391,11 +427,18 @@
return None

artist_data = [a.data for a in result.artists]
album_artist, album_artist_id = self.get_artist_with_anv(artist_data)
album_artist_anv, _ = self.get_artist_with_anv(
artist_data, use_anv=True
)
artist_credit = album_artist_anv
album_artist_data = self._build_artist_credit_data(artist_data)
album_artist_default = album_artist_data.default
album_artist_anv = album_artist_data.anv

album_artist = album_artist_default.name
album_artist_id = album_artist_default.artist_id
album_artists = list(album_artist_default.names)
album_artists_sort = list(album_artists)
album_artists_ids = list(album_artist_default.ids)

artist_credit = album_artist_anv.name
album_artists_credit = list(album_artist_anv.names)

album = re.sub(r" +", " ", result.title)
album_id = result.data["id"]
Expand All @@ -405,14 +448,17 @@
# each make an API call just to get the same data back.
tracks = self.get_tracks(
result.data["tracklist"],
(album_artist, album_artist_anv, album_artist_id),
album_artist_data,
)

# Assign ANV to the proper fields for tagging
if not self.config["anv"]["artist_credit"]:
artist_credit = album_artist
artist_credit = album_artist_default.name
album_artists_credit = list(album_artist_default.names)
if self.config["anv"]["album_artist"]:
album_artist = album_artist_anv
album_artist = album_artist_anv.name
album_artists = list(album_artist_anv.names)
album_artists_sort = list(album_artist_anv.names)

# Extract information for the optional AlbumInfo fields, if possible.
va = result.data["artists"][0].get("name", "").lower() == "various"
Expand All @@ -432,9 +478,12 @@

# Extract information for the optional AlbumInfo fields that are
# contained on nested discogs fields.
media, albumtype = self.get_media_and_albumtype(
result.data.get("formats")
)
formats = result.data.get("formats")
media, albumtype = self.get_media_and_albumtype(formats)
albumtypes_list: list[str] = []
if formats and (first_format := formats[0]):
if descriptions := first_format.get("descriptions"):
albumtypes_list = list(descriptions)
Comment on lines +482 to +486
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The albumtypes extraction logic is duplicated. The method get_media_and_albumtype already extracts descriptions from formats, but then lines 483-486 extract the same information again separately. Consider either:

  1. Modifying get_media_and_albumtype to return a third value (the list of descriptions), or
  2. Extracting albumtypes_list directly from the albumtype string returned by get_media_and_albumtype by splitting on ", ".
Suggested change
media, albumtype = self.get_media_and_albumtype(formats)
albumtypes_list: list[str] = []
if formats and (first_format := formats[0]):
if descriptions := first_format.get("descriptions"):
albumtypes_list = list(descriptions)
media, albumtype, albumtypes_list = self.get_media_and_albumtype(formats)

Copilot uses AI. Check for mistakes.

label = catalogno = labelid = None
if result.data.get("labels"):
Expand All @@ -452,6 +501,9 @@
va_name = config["va_name"].as_str()
album_artist = va_name
artist_credit = va_name
album_artists = [va_name]
album_artists_credit = [va_name]
album_artists_sort = [va_name]
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When handling Various Artists releases, the code resets album_artist, artist_credit, album_artists, album_artists_credit, and album_artists_sort to the VA name, but it does not reset album_artists_ids. This leaves the IDs from the original artists, which is inconsistent. For VA releases, album_artists_ids should be reset to an empty list, since the VA name typically doesn't have an associated artist ID.

Suggested change
album_artists_sort = [va_name]
album_artists_sort = [va_name]
album_artists_ids = []

Copilot uses AI. Check for mistakes.
if catalogno == "none":
catalogno = None
# Explicitly set the `media` for the tracks, since it is expected by
Expand All @@ -477,8 +529,14 @@
artist=album_artist,
artist_credit=artist_credit,
artist_id=album_artist_id,
artist_sort=album_artist,
artists=album_artists,
artists_credit=album_artists_credit,
artists_ids=album_artists_ids,
artists_sort=album_artists_sort,
tracks=tracks,
albumtype=albumtype,
albumtypes=albumtypes_list,
va=va,
year=year,
label=label,
Expand Down Expand Up @@ -519,7 +577,7 @@
def _process_clean_tracklist(
self,
clean_tracklist: list[Track],
album_artist_data: tuple[str, str, str | None],
album_artist_data: ArtistCreditData,
) -> tuple[list[TrackInfo], dict[int, str], int, list[str], list[str]]:
# Distinct works and intra-work divisions, as defined by index tracks.
tracks: list[TrackInfo] = []
Expand Down Expand Up @@ -555,7 +613,7 @@
def get_tracks(
self,
tracklist: list[Track],
album_artist_data: tuple[str, str, str | None],
album_artist_data: ArtistCreditData,
) -> list[TrackInfo]:
"""Returns a list of TrackInfo objects for a discogs tracklist."""
try:
Expand Down Expand Up @@ -737,17 +795,10 @@
track: Track,
index: int,
divisions: list[str],
album_artist_data: tuple[str, str, str | None],
album_artist_data: ArtistCreditData,
) -> IntermediateTrackInfo:
"""Returns a TrackInfo object for a discogs track."""

artist, artist_anv, artist_id = album_artist_data
artist_credit = artist_anv
if not self.config["anv"]["artist_credit"]:
artist_credit = artist
if self.config["anv"]["artist"]:
artist = artist_anv

title = track["title"]
if self.config["index_tracks"]:
prefix = ", ".join(divisions)
Expand All @@ -756,40 +807,84 @@
track_id = None
medium, medium_index, _ = self.get_track_index(track["position"])

# If artists are found on the track, we will use those instead
if artists := track.get("artists", []):
artist, artist_id = self.get_artist_with_anv(
artists, self.config["anv"]["artist"]
)
artist_credit, _ = self.get_artist_with_anv(
artists, self.config["anv"]["artist_credit"]
)
artist_variants = album_artist_data
if track.get("artists"):
artist_variants = self._build_artist_credit_data(track["artists"])

default_variant = artist_variants.default
anv_variant = artist_variants.anv

artist = default_variant.name
artist_names = list(default_variant.names)
artists_ids = list(default_variant.ids)
artist_id = default_variant.artist_id

artist_credit = anv_variant.name
artist_credit_names = list(anv_variant.names)

if self.config["anv"]["artist"]:
artist = anv_variant.name
artist_names = list(anv_variant.names)

if not self.config["anv"]["artist_credit"]:
artist_credit = default_variant.name
artist_credit_names = list(default_variant.names)

length = self.get_track_length(track["duration"])

# Add featured artists
if extraartists := track.get("extraartists", []):
featured_list = [
artist
for artist in extraartists
if "Featuring" in artist["role"]
extraartist
for extraartist in extraartists
if "Featuring" in extraartist.get("role", "")
]
featured, _ = self.get_artist_with_anv(
featured_list, self.config["anv"]["artist"]
)
featured_credit, _ = self.get_artist_with_anv(
featured_list, self.config["anv"]["artist_credit"]
)
if featured:
artist += f" {self.config['featured_string']} {featured}"
artist_credit += (
f" {self.config['featured_string']} {featured_credit}"
if featured_list:
featured_variants = self._build_artist_credit_data(
cast(list[Artist], featured_list)
)

featured_for_artist_variant = (
featured_variants.anv
if self.config["anv"]["artist"]
else featured_variants.default
)
featured_for_credit_variant = (
featured_variants.anv
if self.config["anv"]["artist_credit"]
else featured_variants.default
)

if featured_for_artist_variant.name:
artist += (
f" {self.config['featured_string']} "
f"{featured_for_artist_variant.name}"
)
artist_names.extend(
list(featured_for_artist_variant.names)
)

if featured_for_credit_variant.name:
artist_credit += (
f" {self.config['featured_string']} "
f"{featured_for_credit_variant.name}"
)
artist_credit_names.extend(
list(featured_for_credit_variant.names)
)

artists_ids.extend(list(featured_variants.default.ids))

return IntermediateTrackInfo(
title=title,
track_id=track_id,
artist_credit=artist_credit,
artist=artist,
artist_id=artist_id,
artists=artist_names,
artists_credit=artist_credit_names,
artists_ids=artists_ids,
artists_sort=list(artist_names),
length=length,
index=index,
medium_str=medium,
Expand Down
Loading
Loading