mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 16:42:42 +01:00
Create common info class (#5963)
### Context See https://github.com/beetbox/beets/pull/5916 where we've come across a need to define common logic between `TrackInfo` and `AlbumInfo`. ### Changes - Introduce generic `Info` base (extends `AttrDict`) used by `AlbumInfo` / `TrackInfo` to centralize shared attributes and initialisation logic. - Sort keyword parameters in each constructor alphabetically and make them explicit. - Deduplicate and simplify shared `copy()` method using `copy.deepcopy` - Improve type hints and documentation. - Drop unused logging artifacts. ## Summary by Sourcery Refactor metadata-handling classes by extracting common functionality into a new Info base, updating AlbumInfo and TrackInfo to extend it with explicit sorted parameters, unify their copy logic, improve type annotations and docs, and drop obsolete logging code New Features: - Introduce a generic Info base class to centralize shared logic for AlbumInfo and TrackInfo Enhancements: - Alphabetically sort and explicitly declare constructor keyword parameters for consistency - Unify and simplify the copy() implementation in AttrDict using deepcopy - Enhance type hints and documentation for metadata classes Chores: - Remove unused logging imports and artifacts
This commit is contained in:
commit
f24beca085
1 changed files with 132 additions and 167 deletions
|
|
@ -16,236 +16,201 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar
|
from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar
|
||||||
|
|
||||||
from beets import logging
|
from typing_extensions import Self
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from beets.library import Item
|
from beets.library import Item
|
||||||
|
|
||||||
from .distance import Distance
|
from .distance import Distance
|
||||||
|
|
||||||
log = logging.getLogger("beets")
|
|
||||||
|
|
||||||
V = TypeVar("V")
|
V = TypeVar("V")
|
||||||
|
|
||||||
|
|
||||||
# Classes used to represent candidate options.
|
# Classes used to represent candidate options.
|
||||||
class AttrDict(dict[str, V]):
|
class AttrDict(dict[str, V]):
|
||||||
"""A dictionary that supports attribute ("dot") access, so `d.field`
|
"""Mapping enabling attribute-style access to stored metadata values."""
|
||||||
is equivalent to `d['field']`.
|
|
||||||
"""
|
def copy(self) -> Self:
|
||||||
|
return deepcopy(self)
|
||||||
|
|
||||||
def __getattr__(self, attr: str) -> V:
|
def __getattr__(self, attr: str) -> V:
|
||||||
if attr in self:
|
if attr in self:
|
||||||
return self[attr]
|
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)
|
self.__setitem__(key, value)
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self) -> int: # type: ignore[override]
|
||||||
return id(self)
|
return id(self)
|
||||||
|
|
||||||
|
|
||||||
class AlbumInfo(AttrDict[Any]):
|
class Info(AttrDict[Any]):
|
||||||
"""Describes a canonical release that may be used to match a release
|
"""Container for metadata about a musical entity."""
|
||||||
in the library. Consists of these data members:
|
|
||||||
|
|
||||||
- ``album``: the release title
|
def __init__(
|
||||||
- ``album_id``: MusicBrainz ID; UUID fragment only
|
self,
|
||||||
- ``artist``: name of the release's primary artist
|
album: str | None = None,
|
||||||
- ``artist_id``
|
artist_credit: str | None = None,
|
||||||
- ``tracks``: list of TrackInfo objects making up the release
|
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)
|
||||||
|
|
||||||
``mediums`` along with the fields up through ``tracks`` are required.
|
|
||||||
The others are optional and may be None.
|
class AlbumInfo(Info):
|
||||||
|
"""Metadata snapshot representing a single album candidate.
|
||||||
|
|
||||||
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
tracks: list[TrackInfo],
|
tracks: list[TrackInfo],
|
||||||
album: str | None = None,
|
*,
|
||||||
album_id: str | None = None,
|
album_id: str | None = None,
|
||||||
artist: str | None = None,
|
albumdisambig: str | None = None,
|
||||||
artist_id: str | None = None,
|
albumstatus: str | None = None,
|
||||||
artists: list[str] | None = None,
|
|
||||||
artists_ids: list[str] | None = None,
|
|
||||||
asin: str | None = None,
|
|
||||||
albumtype: str | None = None,
|
albumtype: str | None = None,
|
||||||
albumtypes: list[str] | None = None,
|
albumtypes: list[str] | 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,
|
||||||
|
language: str | None = None,
|
||||||
|
mediums: int | 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,
|
||||||
|
releasegroup_id: str | None = None,
|
||||||
|
releasegroupdisambig: str | None = None,
|
||||||
|
script: str | None = None,
|
||||||
|
style: str | None = None,
|
||||||
va: bool = False,
|
va: bool = False,
|
||||||
year: int | None = None,
|
year: int | None = None,
|
||||||
month: int | None = None,
|
|
||||||
day: int | None = None,
|
|
||||||
label: str | None = None,
|
|
||||||
barcode: str | None = None,
|
|
||||||
mediums: int | None = None,
|
|
||||||
artist_sort: str | None = None,
|
|
||||||
artists_sort: list[str] | None = None,
|
|
||||||
releasegroup_id: str | None = None,
|
|
||||||
release_group_title: str | None = None,
|
|
||||||
catalognum: 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,
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
) -> None:
|
||||||
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 []
|
|
||||||
self.tracks = tracks
|
self.tracks = tracks
|
||||||
self.asin = asin
|
self.album_id = album_id
|
||||||
|
self.albumdisambig = albumdisambig
|
||||||
|
self.albumstatus = albumstatus
|
||||||
self.albumtype = albumtype
|
self.albumtype = albumtype
|
||||||
self.albumtypes = albumtypes or []
|
self.albumtypes = albumtypes or []
|
||||||
|
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.language = language
|
||||||
|
self.mediums = mediums
|
||||||
|
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.releasegroup_id = releasegroup_id
|
||||||
|
self.releasegroupdisambig = releasegroupdisambig
|
||||||
|
self.script = script
|
||||||
|
self.style = style
|
||||||
self.va = va
|
self.va = va
|
||||||
self.year = year
|
self.year = year
|
||||||
self.month = month
|
super().__init__(**kwargs)
|
||||||
self.day = day
|
|
||||||
self.label = label
|
|
||||||
self.barcode = barcode
|
|
||||||
self.mediums = mediums
|
|
||||||
self.artist_sort = artist_sort
|
|
||||||
self.artists_sort = artists_sort or []
|
|
||||||
self.releasegroup_id = releasegroup_id
|
|
||||||
self.release_group_title = release_group_title
|
|
||||||
self.catalognum = catalognum
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class TrackInfo(AttrDict[Any]):
|
class TrackInfo(Info):
|
||||||
"""Describes a canonical track present on a release. Appears as part
|
"""Metadata snapshot for a single track candidate.
|
||||||
of an AlbumInfo's ``tracks`` list. Consists of these data members:
|
|
||||||
|
|
||||||
- ``title``: name of the track
|
Captures identifying details and creative credits used to compare against
|
||||||
- ``track_id``: MusicBrainz ID; UUID fragment only
|
a user's item. Instances often originate within an AlbumInfo but may also
|
||||||
|
stand alone for singleton matching.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TYPING: are all of these correct? I've assumed optional strings
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
title: str | None = None,
|
*,
|
||||||
track_id: str | None = None,
|
arranger: str | None = None,
|
||||||
release_track_id: str | None = None,
|
bpm: str | None = None,
|
||||||
artist: str | None = None,
|
composer: str | None = None,
|
||||||
artist_id: str | None = None,
|
composer_sort: str | None = None,
|
||||||
artists: list[str] | None = None,
|
disctitle: str | None = None,
|
||||||
artists_ids: list[str] | None = None,
|
|
||||||
length: float | None = None,
|
|
||||||
index: int | 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: int | None = None,
|
||||||
medium_index: int | None = None,
|
medium_index: int | None = None,
|
||||||
medium_total: int | None = None,
|
medium_total: int | None = None,
|
||||||
artist_sort: str | None = None,
|
release_track_id: str | None = None,
|
||||||
artists_sort: list[str] | None = None,
|
title: 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,
|
|
||||||
track_alt: str | None = None,
|
track_alt: str | None = None,
|
||||||
|
track_id: str | None = None,
|
||||||
work: str | None = None,
|
work: str | None = None,
|
||||||
mb_workid: str | None = None,
|
|
||||||
work_disambig: 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,
|
**kwargs,
|
||||||
):
|
) -> None:
|
||||||
self.title = title
|
self.arranger = arranger
|
||||||
self.track_id = track_id
|
self.bpm = bpm
|
||||||
self.release_track_id = release_track_id
|
self.composer = composer
|
||||||
self.artist = artist
|
self.composer_sort = composer_sort
|
||||||
self.artist_id = artist_id
|
self.disctitle = disctitle
|
||||||
self.artists = artists or []
|
|
||||||
self.artists_ids = artists_ids or []
|
|
||||||
self.length = length
|
|
||||||
self.index = index
|
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 = medium
|
||||||
self.medium_index = medium_index
|
self.medium_index = medium_index
|
||||||
self.medium_total = medium_total
|
self.medium_total = medium_total
|
||||||
self.artist_sort = artist_sort
|
self.release_track_id = release_track_id
|
||||||
self.artists_sort = artists_sort or []
|
self.title = title
|
||||||
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.track_alt = track_alt
|
self.track_alt = track_alt
|
||||||
|
self.track_id = track_id
|
||||||
self.work = work
|
self.work = work
|
||||||
self.mb_workid = mb_workid
|
|
||||||
self.work_disambig = work_disambig
|
self.work_disambig = work_disambig
|
||||||
self.bpm = bpm
|
super().__init__(**kwargs)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
# Structures that compose all the information for a candidate match.
|
# Structures that compose all the information for a candidate match.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue