diff --git a/beets/config_default.yaml b/beets/config_default.yaml index c0bab8056e..b0b495a225 100644 --- a/beets/config_default.yaml +++ b/beets/config_default.yaml @@ -7,7 +7,6 @@ statefile: state.pickle # --------------- Plugins --------------- plugins: [musicbrainz] - pluginpath: [] # --------------- Import --------------- diff --git a/beets/library/models.py b/beets/library/models.py index cbee2a411e..69daeade30 100644 --- a/beets/library/models.py +++ b/beets/library/models.py @@ -265,6 +265,7 @@ class Album(LibModel): "language": types.STRING, "country": types.STRING, "albumstatus": types.STRING, + "media": types.STRING, "albumdisambig": types.STRING, "releasegroupdisambig": types.STRING, "rg_album_gain": types.NULL_FLOAT, @@ -320,6 +321,7 @@ def _types(cls) -> dict[str, types.Type]: "language", "country", "albumstatus", + "media", "albumdisambig", "releasegroupdisambig", "release_group_title", @@ -354,6 +356,18 @@ def art_filepath(self) -> Path | None: """The path to album's cover picture as pathlib.Path.""" return Path(os.fsdecode(self.artpath)) if self.artpath else None + @property + def media(self): + """Return a list of distinct media types for the items in this album.""" + if not self.items(): + return [] + media_set = { + str(item.media) + for item in self.items() + if getattr(item, "media", None) + } + return list(media_set) + @classmethod def _getters(cls): # In addition to plugin-provided computed fields, also expose @@ -361,6 +375,7 @@ def _getters(cls): getters = plugins.album_field_getters() getters["path"] = Album.item_dir getters["albumtotal"] = Album._albumtotal + getters["media_types"] = lambda a: a.media_types return getters def items(self): diff --git a/docs/changelog.rst b/docs/changelog.rst index d95de38c52..8f2d108d24 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -27,6 +27,7 @@ New features: - :doc:`plugins/mbpseudo`: Add a new `mbpseudo` plugin to proactively receive MusicBrainz pseudo-releases as recommendations during import. - Added support for Python 3.13. +- Added album-level `$media` field derived from items’ media metadata. Bug fixes: diff --git a/test/test_media_field.py b/test/test_media_field.py new file mode 100644 index 0000000000..e2410c6adb --- /dev/null +++ b/test/test_media_field.py @@ -0,0 +1,48 @@ +import unittest + +from beets.library import Item, Library + + +class MediaFieldTest(unittest.TestCase): + def setUp(self): + self.lib = Library(":memory:") + self.lib.add_album = self.lib.add_album + + def add_album_with_items(self, items_data): + items = [] + for data in items_data: + item = Item(**data) + items.append(item) + album = self.lib.add_album(items) + return album + + def test_album_media_field_multiple_types(self): + items_data = [ + {"title": "Track 1", "artist": "Artist A", "media": "CD"}, + {"title": "Track 2", "artist": "Artist A", "media": "Vinyl"}, + ] + album = self.add_album_with_items(items_data) + media = album.media + assert sorted(media) == ["CD", "Vinyl"] + + def test_album_media_field_single_type(self): + items_data = [ + {"title": "Track 1", "artist": "Artist A", "media": "CD"}, + {"title": "Track 2", "artist": "Artist A", "media": "CD"}, + ] + album = self.add_album_with_items(items_data) + media = album.media + assert media == ["CD"] + + def test_album_with_no_media(self): + items_data = [ + {"title": "Track 1", "artist": "Artist A"}, + {"title": "Track 2", "artist": "Artist A"}, + ] + album = self.add_album_with_items(items_data) + media = album.media + assert media == [] + + +if __name__ == "__main__": + unittest.main()