From f168c90edeb1f5c613c16221fda86e4ab8086260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sun, 19 Apr 2026 03:19:03 +0100 Subject: [PATCH 1/3] Centralise field definitions --- beets/library/fields.py | 102 ++++++++++++++++ beets/library/models.py | 260 +++++++++++++++++----------------------- 2 files changed, 214 insertions(+), 148 deletions(-) create mode 100644 beets/library/fields.py diff --git a/beets/library/fields.py b/beets/library/fields.py new file mode 100644 index 0000000000..582b1d0b8d --- /dev/null +++ b/beets/library/fields.py @@ -0,0 +1,102 @@ +from beets.dbcore import types + +TYPE_BY_FIELD: dict[str, types.Type] = { + "acoustid_fingerprint": types.STRING, + "acoustid_id": types.STRING, + "added": types.DATE, + "albumartist_credit": types.STRING, + "albumartists_credit": types.MULTI_VALUE_DSV, + "albumartist_sort": types.STRING, + "albumartists_sort": types.MULTI_VALUE_DSV, + "albumartists": types.MULTI_VALUE_DSV, + "albumartist": types.STRING, + "albumdisambig": types.STRING, + "album_id": types.FOREIGN_ID, + "albumstatus": types.STRING, + "album": types.STRING, + "albumtypes": types.SEMICOLON_SPACE_DSV, + "albumtype": types.STRING, + "arrangers": types.MULTI_VALUE_DSV, + "arrangers_ids": types.MULTI_VALUE_DSV, + "artist_credit": types.STRING, + "artists_credit": types.MULTI_VALUE_DSV, + "artists_ids": types.MULTI_VALUE_DSV, + "artist_sort": types.STRING, + "artists_sort": types.MULTI_VALUE_DSV, + "artists": types.MULTI_VALUE_DSV, + "artist": types.STRING, + "artpath": types.NullPathType(), + "asin": types.STRING, + "barcode": types.STRING, + "bitdepth": types.INTEGER, + "bitrate_mode": types.STRING, + "bitrate": types.ScaledInt(1000, "kbps"), + "bpm": types.INTEGER, + "catalognum": types.STRING, + "channels": types.INTEGER, + "comments": types.STRING, + "composer_sort": types.STRING, + "composers": types.MULTI_VALUE_DSV, + "composers_ids": types.MULTI_VALUE_DSV, + "comp": types.BOOLEAN, + "country": types.STRING, + "data_source": types.STRING, + "day": types.PaddedInt(2), + "discogs_albumid": types.INTEGER, + "discogs_artistid": types.INTEGER, + "discogs_labelid": types.INTEGER, + "disctitle": types.STRING, + "disctotal": types.PaddedInt(2), + "disc": types.PaddedInt(2), + "encoder_info": types.STRING, + "encoder_settings": types.STRING, + "encoder": types.STRING, + "format": types.STRING, + "genres": types.MULTI_VALUE_DSV, + "grouping": types.STRING, + "id": types.PRIMARY_ID, + "initial_key": types.MusicalKey(), + "isrc": types.STRING, + "label": types.STRING, + "language": types.STRING, + "length": types.DurationType(), + "lyricists": types.MULTI_VALUE_DSV, + "lyricists_ids": types.MULTI_VALUE_DSV, + "lyrics": types.STRING, + "mb_albumartistids": types.MULTI_VALUE_DSV, + "mb_albumartistid": types.STRING, + "mb_albumid": types.STRING, + "mb_artistids": types.MULTI_VALUE_DSV, + "mb_artistid": types.STRING, + "mb_releasegroupid": types.STRING, + "mb_releasetrackid": types.STRING, + "mb_trackid": types.STRING, + "mb_workid": types.STRING, + "media": types.STRING, + "month": types.PaddedInt(2), + "mtime": types.DATE, + "original_day": types.PaddedInt(2), + "original_month": types.PaddedInt(2), + "original_year": types.PaddedInt(4), + "path": types.PathType(), + "r128_album_gain": types.NULL_FLOAT, + "r128_track_gain": types.NULL_FLOAT, + "releasegroupdisambig": types.STRING, + "release_group_title": types.STRING, + "remixers": types.MULTI_VALUE_DSV, + "remixers_ids": types.MULTI_VALUE_DSV, + "rg_album_gain": types.NULL_FLOAT, + "rg_album_peak": types.NULL_FLOAT, + "rg_track_gain": types.NULL_FLOAT, + "rg_track_peak": types.NULL_FLOAT, + "samplerate": types.ScaledInt(1000, "kHz"), + "script": types.STRING, + "style": types.STRING, + "title": types.STRING, + "trackdisambig": types.STRING, + "tracktotal": types.PaddedInt(2), + "track": types.PaddedInt(2), + "work_disambig": types.STRING, + "work": types.STRING, + "year": types.PaddedInt(4), +} diff --git a/beets/library/models.py b/beets/library/models.py index e3afa64a4b..84c3c4cf86 100644 --- a/beets/library/models.py +++ b/beets/library/models.py @@ -28,10 +28,12 @@ from beets.util.functemplate import Template, template from .exceptions import FileOperationError, ReadError, WriteError +from .fields import TYPE_BY_FIELD from .queries import PF_KEY_DEFAULT, parse_query_string if TYPE_CHECKING: - from ..dbcore.query import FieldQuery, FieldQueryType + from beets.dbcore.query import FieldQuery, FieldQueryType + from .library import Library # noqa: F401 log = logging.getLogger("beets") @@ -40,17 +42,23 @@ class LibModel(dbcore.Model["Library"]): """Shared concrete functionality for Items and Albums.""" + _field_names: ClassVar[set[str]] + # Config key that specifies how an instance should be formatted. _format_config_key: str path: bytes length: float + @cached_classproperty + def _fields(cls) -> dict[str, types.Type]: + return {f: TYPE_BY_FIELD[f] for f in sorted(cls._field_names)} + @cached_classproperty def _types(cls) -> dict[str, types.Type]: """Return the types of the fields in this model.""" return { **plugins.types(cls), # type: ignore[arg-type] - "data_source": types.STRING, + "data_source": TYPE_BY_FIELD["data_source"], } @cached_classproperty @@ -246,57 +254,57 @@ class Album(LibModel): _table = "albums" _flex_table = "album_attributes" _always_dirty = True - _fields: ClassVar[dict[str, types.Type]] = { - "id": types.PRIMARY_ID, - "artpath": types.NullPathType(), - "added": types.DATE, - "albumartist": types.STRING, - "albumartist_sort": types.STRING, - "albumartist_credit": types.STRING, - "albumartists": types.MULTI_VALUE_DSV, - "albumartists_sort": types.MULTI_VALUE_DSV, - "albumartists_credit": types.MULTI_VALUE_DSV, - "album": types.STRING, - "genres": types.MULTI_VALUE_DSV, - "style": types.STRING, - "discogs_albumid": types.INTEGER, - "discogs_artistid": types.INTEGER, - "discogs_labelid": types.INTEGER, - "year": types.PaddedInt(4), - "month": types.PaddedInt(2), - "day": types.PaddedInt(2), - "disctotal": types.PaddedInt(2), - "comp": types.BOOLEAN, - "mb_albumid": types.STRING, - "mb_albumartistid": types.STRING, - "mb_albumartistids": types.MULTI_VALUE_DSV, - "albumtype": types.STRING, - "albumtypes": types.SEMICOLON_SPACE_DSV, - "label": types.STRING, - "barcode": types.STRING, - "mb_releasegroupid": types.STRING, - "release_group_title": types.STRING, - "asin": types.STRING, - "catalognum": types.STRING, - "script": types.STRING, - "language": types.STRING, - "country": types.STRING, - "albumstatus": types.STRING, - "albumdisambig": types.STRING, - "releasegroupdisambig": types.STRING, - "rg_album_gain": types.NULL_FLOAT, - "rg_album_peak": types.NULL_FLOAT, - "r128_album_gain": types.NULL_FLOAT, - "original_year": types.PaddedInt(4), - "original_month": types.PaddedInt(2), - "original_day": types.PaddedInt(2), + _field_names: ClassVar[set[str]] = { + "added", + "album", + "albumartist", + "albumartist_credit", + "albumartists", + "albumartists_credit", + "albumartist_sort", + "albumartists_sort", + "albumdisambig", + "albumstatus", + "albumtype", + "albumtypes", + "artpath", + "asin", + "barcode", + "catalognum", + "comp", + "country", + "day", + "discogs_albumid", + "discogs_artistid", + "discogs_labelid", + "disctotal", + "genres", + "id", + "label", + "language", + "mb_albumartistid", + "mb_albumartistids", + "mb_albumid", + "mb_releasegroupid", + "month", + "original_day", + "original_month", + "original_year", + "r128_album_gain", + "releasegroupdisambig", + "release_group_title", + "rg_album_gain", + "rg_album_peak", + "script", + "style", + "year", } _search_fields = ("album", "albumartist", "genres") @cached_classproperty def _types(cls) -> dict[str, types.Type]: - return {**super()._types, "path": types.PathType()} + return {**super()._types, "path": TYPE_BY_FIELD["path"]} _sorts: ClassVar[dict[str, type[dbcore.query.FieldSort]]] = { "albumartist": dbcore.query.SmartArtistSort, @@ -650,103 +658,61 @@ class Item(LibModel): _table = "items" _flex_table = "item_attributes" - _fields: ClassVar[dict[str, types.Type]] = { - "id": types.PRIMARY_ID, - "path": types.PathType(), - "album_id": types.FOREIGN_ID, - "title": types.STRING, - "artist": types.STRING, - "artists": types.MULTI_VALUE_DSV, - "artists_ids": types.MULTI_VALUE_DSV, - "artist_sort": types.STRING, - "artists_sort": types.MULTI_VALUE_DSV, - "artist_credit": types.STRING, - "artists_credit": types.MULTI_VALUE_DSV, - "remixers": types.MULTI_VALUE_DSV, - "remixers_ids": types.MULTI_VALUE_DSV, - "album": types.STRING, - "albumartist": types.STRING, - "albumartists": types.MULTI_VALUE_DSV, - "albumartist_sort": types.STRING, - "albumartists_sort": types.MULTI_VALUE_DSV, - "albumartist_credit": types.STRING, - "albumartists_credit": types.MULTI_VALUE_DSV, - "genres": types.MULTI_VALUE_DSV, - "style": types.STRING, - "discogs_albumid": types.INTEGER, - "discogs_artistid": types.INTEGER, - "discogs_labelid": types.INTEGER, - "lyricists": types.MULTI_VALUE_DSV, - "lyricists_ids": types.MULTI_VALUE_DSV, - "composers": types.MULTI_VALUE_DSV, - "composer_sort": types.STRING, - "composers_ids": types.MULTI_VALUE_DSV, - "work": types.STRING, - "mb_workid": types.STRING, - "work_disambig": types.STRING, - "arrangers": types.MULTI_VALUE_DSV, - "arrangers_ids": types.MULTI_VALUE_DSV, - "grouping": types.STRING, - "year": types.PaddedInt(4), - "month": types.PaddedInt(2), - "day": types.PaddedInt(2), - "track": types.PaddedInt(2), - "tracktotal": types.PaddedInt(2), - "disc": types.PaddedInt(2), - "disctotal": types.PaddedInt(2), - "lyrics": types.STRING, - "comments": types.STRING, - "bpm": types.INTEGER, - "comp": types.BOOLEAN, - "mb_trackid": types.STRING, - "mb_albumid": types.STRING, - "mb_artistid": types.STRING, - "mb_artistids": types.MULTI_VALUE_DSV, - "mb_albumartistid": types.STRING, - "mb_albumartistids": types.MULTI_VALUE_DSV, - "mb_releasetrackid": types.STRING, - "trackdisambig": types.STRING, - "albumtype": types.STRING, - "albumtypes": types.SEMICOLON_SPACE_DSV, - "label": types.STRING, - "barcode": types.STRING, - "acoustid_fingerprint": types.STRING, - "acoustid_id": types.STRING, - "mb_releasegroupid": types.STRING, - "release_group_title": types.STRING, - "asin": types.STRING, - "isrc": types.STRING, - "catalognum": types.STRING, - "script": types.STRING, - "language": types.STRING, - "country": types.STRING, - "albumstatus": types.STRING, - "media": types.STRING, - "albumdisambig": types.STRING, - "releasegroupdisambig": types.STRING, - "disctitle": types.STRING, - "encoder": types.STRING, - "rg_track_gain": types.NULL_FLOAT, - "rg_track_peak": types.NULL_FLOAT, - "rg_album_gain": types.NULL_FLOAT, - "rg_album_peak": types.NULL_FLOAT, - "r128_track_gain": types.NULL_FLOAT, - "r128_album_gain": types.NULL_FLOAT, - "original_year": types.PaddedInt(4), - "original_month": types.PaddedInt(2), - "original_day": types.PaddedInt(2), - "initial_key": types.MusicalKey(), - "length": types.DurationType(), - "bitrate": types.ScaledInt(1000, "kbps"), - "bitrate_mode": types.STRING, - "encoder_info": types.STRING, - "encoder_settings": types.STRING, - "format": types.STRING, - "samplerate": types.ScaledInt(1000, "kHz"), - "bitdepth": types.INTEGER, - "channels": types.INTEGER, - "mtime": types.DATE, - "added": types.DATE, + _field_names: ClassVar[set[str]] = (Album._field_names - {"artpath"}) | { + "acoustid_fingerprint", + "acoustid_id", + "album_id", + "arrangers", + "arrangers_ids", + "artist", + "artist_credit", + "artist_sort", + "artists", + "artists_credit", + "artists_ids", + "artists_sort", + "bitdepth", + "bitrate", + "bitrate_mode", + "bpm", + "channels", + "comments", + "composer_sort", + "composers", + "composers_ids", + "disc", + "disctitle", + "encoder", + "encoder_info", + "encoder_settings", + "format", + "grouping", + "initial_key", + "isrc", + "length", + "lyricists", + "lyricists_ids", + "lyrics", + "mb_artistid", + "mb_artistids", + "mb_releasetrackid", + "mb_trackid", + "mb_workid", + "media", + "mtime", + "path", + "r128_track_gain", + "remixers", + "remixers_ids", + "rg_track_gain", + "rg_track_peak", + "samplerate", + "title", + "track", + "trackdisambig", + "tracktotal", + "work", + "work_disambig", } _indices = (dbcore.Index("idx_item_album_id", ("album_id",)),) @@ -763,15 +729,13 @@ class Item(LibModel): # Any kind of field (fixed, flexible, and computed) may be a media # field. Only these fields are read from disk in `read` and written in # `write`. - _media_fields = set(MediaFile.readable_fields()).intersection( - _fields.keys() - ) + _media_fields = set(MediaFile.readable_fields()) & _field_names # Set of item fields that are backed by *writable* `MediaFile` tag # fields. # This excludes fields that represent audio data, such as `bitrate` or # `length`. - _media_tag_fields = set(MediaFile.fields()).intersection(_fields.keys()) + _media_tag_fields = set(MediaFile.fields()) & _field_names _formatter = FormattedItemMapping From 47beb8f7b2173fbb460b1da1b0268a9bb6d4f0d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sun, 19 Apr 2026 03:35:41 +0100 Subject: [PATCH 2/3] Simplify Album.item_keys --- beets/library/models.py | 49 ++--------------------------------------- 1 file changed, 2 insertions(+), 47 deletions(-) diff --git a/beets/library/models.py b/beets/library/models.py index 84c3c4cf86..e3e84cd342 100644 --- a/beets/library/models.py +++ b/beets/library/models.py @@ -187,10 +187,7 @@ def album_keys(self): if self.included_keys == self.ALL_KEYS: # Performance note: this triggers a database query. for key in self.album.keys(computed=True): - if ( - key in Album.item_keys - or key not in self.item._fields.keys() - ): + if key in Album.item_keys or key not in self.item._fields: album_keys.append(key) else: album_keys = self.included_keys @@ -312,49 +309,7 @@ def _types(cls) -> dict[str, types.Type]: } # List of keys that are set on an album's items. - item_keys: ClassVar[list[str]] = [ - "added", - "albumartist", - "albumartists", - "albumartist_sort", - "albumartists_sort", - "albumartist_credit", - "albumartists_credit", - "album", - "genres", - "style", - "discogs_albumid", - "discogs_artistid", - "discogs_labelid", - "year", - "month", - "day", - "disctotal", - "comp", - "mb_albumid", - "mb_albumartistid", - "mb_albumartistids", - "albumtype", - "albumtypes", - "label", - "barcode", - "mb_releasegroupid", - "asin", - "catalognum", - "script", - "language", - "country", - "albumstatus", - "albumdisambig", - "releasegroupdisambig", - "release_group_title", - "rg_album_gain", - "rg_album_peak", - "r128_album_gain", - "original_year", - "original_month", - "original_day", - ] + item_keys: ClassVar[set[str]] = _field_names - {"artpath", "id"} _format_config_key = "format_album" From e2e8e80ee7424cd165cab4644c93c438f0f3d84e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sun, 19 Apr 2026 04:20:40 +0100 Subject: [PATCH 3/3] Fix coverage measurement I found that beetsplug/musicbrainz.py does not appear under https://app.codecov.io/gh/beetbox/beets/tree/master/beetsplug. Setting coverage source as repo root should fix this. --- pyproject.toml | 3 +-- setup.cfg | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 930614afce..572211081a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -270,8 +270,7 @@ ref = "test" # measure coverage across logical branches # show which tests cover specific lines in the code (see the HTML report) env.OPTS = """ ---cov=beets ---cov=beetsplug +--cov=. --cov-report=xml:.reports/coverage.xml --cov-report=html:.reports/html --cov-branch diff --git a/setup.cfg b/setup.cfg index 3615c6aee3..a6209a6421 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,7 @@ data_file = .reports/coverage/data branch = true relative_files = true omit = + test/* beets/test/* beetsplug/_typing.py