Skip to content
Merged
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
283 changes: 124 additions & 159 deletions beets/autotag/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading