diff --git a/beets/autotag/hooks.py b/beets/autotag/hooks.py index 7cd215fc49..b809609ea4 100644 --- a/beets/autotag/hooks.py +++ b/beets/autotag/hooks.py @@ -16,236 +16,201 @@ from __future__ import annotations +from copy import deepcopy from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar -from beets import logging +from typing_extensions import Self if TYPE_CHECKING: from beets.library import Item from .distance import Distance -log = logging.getLogger("beets") - V = TypeVar("V") # Classes used to represent candidate options. class AttrDict(dict[str, V]): - """A dictionary that supports attribute ("dot") access, so `d.field` - is equivalent to `d['field']`. - """ + """Mapping enabling attribute-style access to stored metadata values.""" + + def copy(self) -> Self: + return deepcopy(self) def __getattr__(self, attr: str) -> V: if attr in self: return self[attr] - else: - raise AttributeError - def __setattr__(self, key: str, value: V): + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{attr}'" + ) + + def __setattr__(self, key: str, value: V) -> None: self.__setitem__(key, value) - def __hash__(self): + def __hash__(self) -> int: # type: ignore[override] return id(self) -class AlbumInfo(AttrDict[Any]): - """Describes a canonical release that may be used to match a release - in the library. Consists of these data members: +class Info(AttrDict[Any]): + """Container for metadata about a musical entity.""" + + def __init__( + self, + album: str | None = None, + artist_credit: str | None = None, + artist_id: str | None = None, + artist: str | None = None, + artists_credit: list[str] | None = None, + artists_ids: list[str] | None = None, + artists: list[str] | None = None, + artist_sort: str | None = None, + artists_sort: list[str] | None = None, + data_source: str | None = None, + data_url: str | None = None, + genre: str | None = None, + media: str | None = None, + **kwargs, + ) -> None: + self.album = album + self.artist = artist + self.artist_credit = artist_credit + self.artist_id = artist_id + self.artists = artists or [] + self.artists_credit = artists_credit or [] + self.artists_ids = artists_ids or [] + self.artist_sort = artist_sort + self.artists_sort = artists_sort or [] + self.data_source = data_source + self.data_url = data_url + self.genre = genre + self.media = media + self.update(kwargs) + - - ``album``: the release title - - ``album_id``: MusicBrainz ID; UUID fragment only - - ``artist``: name of the release's primary artist - - ``artist_id`` - - ``tracks``: list of TrackInfo objects making up the release +class AlbumInfo(Info): + """Metadata snapshot representing a single album candidate. - ``mediums`` along with the fields up through ``tracks`` are required. - The others are optional and may be None. + Aggregates track entries and album-wide context gathered from an external + provider. Used during matching to evaluate similarity against a group of + user items, and later to drive tagging decisions once selected. """ - # TYPING: are all of these correct? I've assumed optional strings def __init__( self, tracks: list[TrackInfo], - album: str | None = None, + *, album_id: str | None = None, - artist: str | None = None, - artist_id: str | None = None, - artists: list[str] | None = None, - artists_ids: list[str] | None = None, - asin: str | None = None, + albumdisambig: str | None = None, + albumstatus: str | None = None, albumtype: str | None = None, albumtypes: list[str] | None = None, - va: bool = False, - year: int | None = None, - month: int | None = None, + asin: str | None = None, + barcode: str | None = None, + catalognum: str | None = None, + country: str | None = None, day: int | None = None, + discogs_albumid: str | None = None, + discogs_artistid: str | None = None, + discogs_labelid: str | None = None, label: str | None = None, - barcode: str | None = None, + language: str | None = None, mediums: int | None = None, - artist_sort: str | None = None, - artists_sort: list[str] | None = None, - releasegroup_id: str | None = None, + month: int | None = None, + original_day: int | None = None, + original_month: int | None = None, + original_year: int | None = None, release_group_title: str | None = None, - catalognum: str | None = None, + releasegroup_id: str | None = None, + releasegroupdisambig: str | None = None, script: str | None = None, - language: str | None = None, - country: str | None = None, style: str | None = None, - genre: str | None = None, - albumstatus: str | None = None, - media: str | None = None, - albumdisambig: str | None = None, - releasegroupdisambig: str | None = None, - artist_credit: str | None = None, - artists_credit: list[str] | None = None, - original_year: int | None = None, - original_month: int | None = None, - original_day: int | None = None, - data_source: str | None = None, - data_url: str | None = None, - discogs_albumid: str | None = None, - discogs_labelid: str | None = None, - discogs_artistid: str | None = None, + va: bool = False, + year: int | None = None, **kwargs, - ): - self.album = album - self.album_id = album_id - self.artist = artist - self.artist_id = artist_id - self.artists = artists or [] - self.artists_ids = artists_ids or [] + ) -> None: self.tracks = tracks - self.asin = asin + self.album_id = album_id + self.albumdisambig = albumdisambig + self.albumstatus = albumstatus self.albumtype = albumtype self.albumtypes = albumtypes or [] - self.va = va - self.year = year - self.month = month + self.asin = asin + self.barcode = barcode + self.catalognum = catalognum + self.country = country self.day = day + self.discogs_albumid = discogs_albumid + self.discogs_artistid = discogs_artistid + self.discogs_labelid = discogs_labelid self.label = label - self.barcode = barcode + self.language = language self.mediums = mediums - self.artist_sort = artist_sort - self.artists_sort = artists_sort or [] - self.releasegroup_id = releasegroup_id + self.month = month + self.original_day = original_day + self.original_month = original_month + self.original_year = original_year self.release_group_title = release_group_title - self.catalognum = catalognum + self.releasegroup_id = releasegroup_id + self.releasegroupdisambig = releasegroupdisambig self.script = script - self.language = language - self.country = country self.style = style - self.genre = genre - self.albumstatus = albumstatus - self.media = media - self.albumdisambig = albumdisambig - self.releasegroupdisambig = releasegroupdisambig - self.artist_credit = artist_credit - self.artists_credit = artists_credit or [] - self.original_year = original_year - self.original_month = original_month - self.original_day = original_day - self.data_source = data_source - self.data_url = data_url - self.discogs_albumid = discogs_albumid - self.discogs_labelid = discogs_labelid - self.discogs_artistid = discogs_artistid - self.update(kwargs) - - def copy(self) -> AlbumInfo: - dupe = AlbumInfo([]) - dupe.update(self) - dupe.tracks = [track.copy() for track in self.tracks] - return dupe - + self.va = va + self.year = year + super().__init__(**kwargs) -class TrackInfo(AttrDict[Any]): - """Describes a canonical track present on a release. Appears as part - of an AlbumInfo's ``tracks`` list. Consists of these data members: - - ``title``: name of the track - - ``track_id``: MusicBrainz ID; UUID fragment only +class TrackInfo(Info): + """Metadata snapshot for a single track candidate. - Only ``title`` and ``track_id`` are required. The rest of the fields - may be None. The indices ``index``, ``medium``, and ``medium_index`` - are all 1-based. + Captures identifying details and creative credits used to compare against + a user's item. Instances often originate within an AlbumInfo but may also + stand alone for singleton matching. """ - # TYPING: are all of these correct? I've assumed optional strings def __init__( self, - title: str | None = None, - track_id: str | None = None, - release_track_id: str | None = None, - artist: str | None = None, - artist_id: str | None = None, - artists: list[str] | None = None, - artists_ids: list[str] | None = None, - length: float | None = None, + *, + arranger: str | None = None, + bpm: str | None = None, + composer: str | None = None, + composer_sort: str | None = None, + disctitle: str | None = None, index: int | None = None, + initial_key: str | None = None, + length: float | None = None, + lyricist: str | None = None, + mb_workid: str | None = None, medium: int | None = None, medium_index: int | None = None, medium_total: int | None = None, - artist_sort: str | None = None, - artists_sort: list[str] | None = None, - disctitle: str | None = None, - artist_credit: str | None = None, - artists_credit: list[str] | None = None, - data_source: str | None = None, - data_url: str | None = None, - media: str | None = None, - lyricist: str | None = None, - composer: str | None = None, - composer_sort: str | None = None, - arranger: str | None = None, + release_track_id: str | None = None, + title: str | None = None, track_alt: str | None = None, + track_id: str | None = None, work: str | None = None, - mb_workid: str | None = None, work_disambig: str | None = None, - bpm: str | None = None, - initial_key: str | None = None, - genre: str | None = None, - album: str | None = None, **kwargs, - ): - self.title = title - self.track_id = track_id - self.release_track_id = release_track_id - self.artist = artist - self.artist_id = artist_id - self.artists = artists or [] - self.artists_ids = artists_ids or [] - self.length = length + ) -> None: + self.arranger = arranger + self.bpm = bpm + self.composer = composer + self.composer_sort = composer_sort + self.disctitle = disctitle self.index = index - self.media = media + self.initial_key = initial_key + self.length = length + self.lyricist = lyricist + self.mb_workid = mb_workid self.medium = medium self.medium_index = medium_index self.medium_total = medium_total - self.artist_sort = artist_sort - self.artists_sort = artists_sort or [] - self.disctitle = disctitle - self.artist_credit = artist_credit - self.artists_credit = artists_credit or [] - self.data_source = data_source - self.data_url = data_url - self.lyricist = lyricist - self.composer = composer - self.composer_sort = composer_sort - self.arranger = arranger + self.release_track_id = release_track_id + self.title = title self.track_alt = track_alt + self.track_id = track_id self.work = work - self.mb_workid = mb_workid self.work_disambig = work_disambig - self.bpm = bpm - self.initial_key = initial_key - self.genre = genre - self.album = album - self.update(kwargs) - - def copy(self) -> TrackInfo: - dupe = TrackInfo() - dupe.update(self) - return dupe + super().__init__(**kwargs) # Structures that compose all the information for a candidate match.