From 6b62380b62789bc9fc6928dd55a34aa4cbfaddcc Mon Sep 17 00:00:00 2001 From: Brock Grassy Date: Wed, 4 Feb 2026 16:14:54 -0500 Subject: [PATCH 1/8] Change missing plugin to allow for filtering albums by release type --- beetsplug/_utils/musicbrainz.py | 5 + beetsplug/missing.py | 44 ++++++- docs/changelog.rst | 7 ++ docs/plugins/missing.rst | 21 +++- test/plugins/test_missing.py | 216 +++++++++++++++++++++++++++----- 5 files changed, 256 insertions(+), 37 deletions(-) diff --git a/beetsplug/_utils/musicbrainz.py b/beetsplug/_utils/musicbrainz.py index 887a8488e..d81c6d81e 100644 --- a/beetsplug/_utils/musicbrainz.py +++ b/beetsplug/_utils/musicbrainz.py @@ -109,6 +109,7 @@ class BrowseReleaseGroupsKwargs(BrowseKwargs, total=False): artist: NotRequired[str] collection: NotRequired[str] release: NotRequired[str] + release_type: NotRequired[list[str]] class BrowseRecordingsKwargs(BrowseReleaseGroupsKwargs, total=False): @@ -283,7 +284,11 @@ class MusicBrainzAPI(RequestHandler): """Browse release groups related to the given entities. At least one of artist, collection, or release must be provided. + Optionally filter by release_type (e.g., ["album", "ep"]). """ + # MusicBrainz API uses "type" parameter for release type filtering + if release_type := kwargs.pop("release_type", None): + kwargs["type"] = "|".join(release_type) return self._get_resource("release-group", **kwargs)["release-groups"] @singledispatchmethod diff --git a/beetsplug/missing.py b/beetsplug/missing.py index 271e90b06..ee3cb8ca4 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -22,6 +22,25 @@ from typing import TYPE_CHECKING, ClassVar import requests +# Valid MusicBrainz release types for filtering release groups +VALID_RELEASE_TYPES = [ + "nat", + "album", + "single", + "ep", + "broadcast", + "other", + "compilation", + "soundtrack", + "spokenword", + "interview", + "audiobook", + "live", + "remix", + "dj-mix", + "mixtape/street", +] + from beets import config, metadata_plugins from beets.dbcore import types from beets.library import Item @@ -108,6 +127,7 @@ class MissingPlugin(MusicBrainzAPIMixin, BeetsPlugin): "count": False, "total": False, "album": False, + "release_type": ["album"], } ) @@ -133,7 +153,19 @@ class MissingPlugin(MusicBrainzAPIMixin, BeetsPlugin): "--album", dest="album", action="store_true", - help="show missing albums for artist instead of tracks", + help=( + "show missing release for artist instead of tracks. Defaults " + "to only releases of type 'album'" + ), + ) + self._command.parser.add_option( + "--release-type", + dest="release_type", + action="append", + help=( + "select release types for missing albums for artist " + f"from ({', '.join(VALID_RELEASE_TYPES)})" + ), ) self._command.parser.add_format_option() @@ -181,7 +213,7 @@ class MissingPlugin(MusicBrainzAPIMixin, BeetsPlugin): """ query.append(MB_ARTIST_QUERY) - # build dict mapping artist to set of their album ids in library + # build dict mapping artist to set of their release group ids in library album_ids_by_artist = defaultdict(set) for album in lib.albums(query): # TODO(@snejus): Some releases have different `albumartist` for the @@ -191,13 +223,17 @@ class MissingPlugin(MusicBrainzAPIMixin, BeetsPlugin): # reporting the same set of missing albums. Instead, we should # group by `mb_albumartistid` field only. artist = (album["albumartist"], album["mb_albumartistid"]) - album_ids_by_artist[artist].add(album) + album_ids_by_artist[artist].add(album["mb_releasegroupid"]) total_missing = 0 + release_type = self.config["release_type"].get() or ["album"] calculating_total = self.config["total"].get() for (artist, artist_id), album_ids in album_ids_by_artist.items(): try: - resp = self.mb_api.browse_release_groups(artist=artist_id) + resp = self.mb_api.browse_release_groups( + artist=artist_id, + release_type=release_type, + ) except requests.exceptions.RequestException: self._log.info( "Couldn't fetch info for artist '{}' ({})", diff --git a/docs/changelog.rst b/docs/changelog.rst index 7fd78aef5..14d0009f5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -20,10 +20,17 @@ New features library. This is useful when items have been imported in don't copy-move (``-C -M``) mode in the library but are later passed through the ``convert`` plugin which will regenerate new paths according to the Beets path format. +- :doc:`plugins/missing`: When running in missing album mode, allows users to + specify MusicBrainz release types to show using the ``--release-type`` flag. + The default behavior is also changed to just show releases of type ``album``. + :bug:`2661` Bug fixes ~~~~~~~~~ +- :doc:`plugins/missing`: Fix ``--album`` mode incorrectly reporting albums + already in the library as missing. The comparison now correctly uses + ``mb_releasegroupid``. - :ref:`replace`: Made ``drive_sep_replace`` regex logic more precise to prevent edge-case mismatches (e.g., a song titled "1:00 AM" would incorrectly be considered a Windows drive path). diff --git a/docs/plugins/missing.rst b/docs/plugins/missing.rst index 5c1cd5455..1a32b374d 100644 --- a/docs/plugins/missing.rst +++ b/docs/plugins/missing.rst @@ -27,12 +27,18 @@ library: -c, --count count missing tracks per album -t, --total count totals across the entire library -a, --album show missing albums for artist instead of tracks for album + --release-type show only missing albums of specified release type. + You can provide this argument multiple times to + specify multiple release types to filter to. If not + provided, defaults to just the "album" release type. …or by editing the corresponding configuration options. .. warning:: - Option ``-c`` is ignored when used with ``-a``. + Option ``-c`` is ignored when used with ``-a``, and ``--release-type`` is + ignored when not used with ``-a``. Valid release types can be shown by + running ``beet missing -h``. Configuration ------------- @@ -110,6 +116,19 @@ Print out a count of the total number of missing tracks: beet missing -t +List all missing albums of release type "compilation" in your collection: + +:: + + beet missing -a --release-type compilation + +List all missing albums of release type "compilation" and album in your +collection: + +:: + + beet missing -a --release-type compilation --release-type album + Call this plugin from other beet commands: :: diff --git a/test/plugins/test_missing.py b/test/plugins/test_missing.py index 812ed5fa3..1f492872d 100644 --- a/test/plugins/test_missing.py +++ b/test/plugins/test_missing.py @@ -1,32 +1,26 @@ +"""Tests for the `missing` plugin.""" + +import re import uuid +from unittest.mock import patch import pytest -from beets.library import Album -from beets.test.helper import IOMixin, PluginMixin, TestHelper +from beets.autotag.hooks import AlbumInfo, TrackInfo +from beets.library import Album, Item +from beets.test.helper import PluginMixin, TestHelper -@pytest.fixture -def helper(request): - helper = TestHelper() - helper.setup_beets() +class TestMissingAlbums(PluginMixin, TestHelper): + """Tests for missing albums functionality.""" - request.instance.lib = helper.lib - - yield - - helper.teardown_beets() - - -@pytest.mark.usefixtures("helper") -class TestMissingAlbums(IOMixin, PluginMixin): plugin = "missing" - album_in_lib = Album( - album="Album", - albumartist="Artist", - mb_albumartistid=str(uuid.uuid4()), - mb_albumid="album", - ) + + @pytest.fixture(autouse=True) + def _setup(self): + self.setup_beets() + yield + self.teardown_beets() @pytest.mark.parametrize( "release_from_mb,expected_output", @@ -37,14 +31,8 @@ class TestMissingAlbums(IOMixin, PluginMixin): id="missing", ), pytest.param( - {"id": album_in_lib.mb_albumid, "title": album_in_lib.album}, + {"id": "release_group_in_lib", "title": "Album"}, "", - marks=pytest.mark.xfail( - reason=( - "Album in lib must not be reported as missing." - " Needs fixing." - ) - ), id="not missing", ), ], @@ -52,11 +40,175 @@ class TestMissingAlbums(IOMixin, PluginMixin): def test_missing_artist_albums( self, requests_mock, release_from_mb, expected_output ): - self.lib.add(self.album_in_lib) + artist_mbid = str(uuid.uuid4()) + self.lib.add( + Album( + album="Album", + albumartist="Artist", + mb_albumartistid=artist_mbid, + mb_albumid="album", + mb_releasegroupid="release_group_in_lib", + ) + ) requests_mock.get( - f"/ws/2/release-group?artist={self.album_in_lib.mb_albumartistid}", + f"/ws/2/release-group?artist={artist_mbid}", json={"release-groups": [release_from_mb]}, ) - with self.configure_plugin({}): - assert self.run_with_output("missing", "--album") == expected_output + assert self.run_with_output("missing", "--album") == expected_output + + def test_release_type_filters_results(self, requests_mock): + """Test --release-type filters to only show specified type.""" + artist_mbid = str(uuid.uuid4()) + self.lib.add( + Album( + album="album", + albumartist="artist", + mb_albumartistid=artist_mbid, + mb_albumid="album", + mb_releasegroupid="album_id", + ) + ) + requests_mock.get( + re.compile(r"/ws/2/release-group.*type=compilation"), + json={ + "release-groups": [ + {"id": "compilation_id", "title": "compilation"} + ] + }, + ) + + output = self.run_with_output( + "missing", "-a", "--release-type", "compilation" + ) + + assert "artist - compilation" in output + + def test_release_type_multiple_types(self, requests_mock): + """Test multiple --release-type flags include all specified types.""" + artist_mbid = str(uuid.uuid4()) + self.lib.add( + Album( + album="album", + albumartist="artist", + mb_albumartistid=artist_mbid, + mb_albumid="album", + mb_releasegroupid="album_id", + ) + ) + requests_mock.get( + re.compile(r"/ws/2/release-group.*type=compilation%7Calbum"), + json={ + "release-groups": [ + {"id": "album2_id", "title": "title 2"}, + {"id": "compilation_id", "title": "compilation"}, + ] + }, + ) + + output = self.run_with_output( + "missing", + "-a", + "--release-type", + "compilation", + "--release-type", + "album", + ) + + assert "artist - compilation" in output + assert "artist - title 2" in output + + def test_missing_albums_total(self, requests_mock): + """Test -t flag with --album shows total count of missing albums.""" + artist_mbid = str(uuid.uuid4()) + self.lib.add( + Album( + album="album", + albumartist="artist", + mb_albumartistid=artist_mbid, + mb_albumid="album", + mb_releasegroupid="album_id", + ) + ) + requests_mock.get( + f"/ws/2/release-group?artist={artist_mbid}", + json={ + "release-groups": [ + {"id": "album_id", "title": "album"}, + {"id": "other_id", "title": "other"}, + ] + }, + ) + + output = self.run_with_output("missing", "-a", "-t") + + assert output == "1\n" + + +class TestMissingTracks(PluginMixin, TestHelper): + """Tests for missing tracks functionality.""" + + plugin = "missing" + + @pytest.fixture(autouse=True) + def _setup(self): + self.setup_beets() + yield + self.teardown_beets() + + @pytest.mark.parametrize( + "total,count,expected", + [ + (True, False, "1\n"), + (False, True, "artist - album: 1"), + ], + ) + @patch("beets.metadata_plugins.album_for_id") + def test_missing_tracks(self, album_for_id, total, count, expected): + """Test getting missing tracks works with expected output.""" + artist_mbid = str(uuid.uuid4()) + album_items = [ + Item( + album="album", + mb_albumid="81ae60d4-5b75-38df-903a-db2cfa51c2c6", + mb_releasegroupid="album_id", + mb_trackid="track_1", + mb_albumartistid=artist_mbid, + albumartist="artist", + tracktotal=3, + ), + Item( + album="album", + mb_albumid="81ae60d4-5b75-38df-903a-db2cfa51c2c6", + mb_releasegroupid="album_id", + mb_albumartistid=artist_mbid, + albumartist="artist", + tracktotal=3, + ), + Item( + album="album", + mb_albumid="81ae60d4-5b75-38df-903a-db2cfa51c2c6", + mb_releasegroupid="album_id", + mb_trackid="track_3", + mb_albumartistid=artist_mbid, + albumartist="artist", + tracktotal=3, + ), + ] + self.lib.add_album(album_items[:2]) + + album_for_id.return_value = AlbumInfo( + album_id="album_id", + album="album", + tracks=[ + TrackInfo(track_id=item.mb_trackid) for item in album_items + ], + ) + + command = ["missing"] + if total: + command.append("-t") + if count: + command.append("-c") + + assert expected in self.run_with_output(*command) From 7027060a0bfb6ea13df1ef6cddd96eba8b0459ff Mon Sep 17 00:00:00 2001 From: Brock Grassy Date: Wed, 4 Feb 2026 16:25:46 -0500 Subject: [PATCH 2/8] Fix lint and mypy --- beetsplug/_utils/musicbrainz.py | 9 ++++++--- beetsplug/missing.py | 26 +++++++++++++------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/beetsplug/_utils/musicbrainz.py b/beetsplug/_utils/musicbrainz.py index d81c6d81e..36a6b1946 100644 --- a/beetsplug/_utils/musicbrainz.py +++ b/beetsplug/_utils/musicbrainz.py @@ -286,10 +286,13 @@ class MusicBrainzAPI(RequestHandler): At least one of artist, collection, or release must be provided. Optionally filter by release_type (e.g., ["album", "ep"]). """ + api_params: dict[str, Any] = dict(kwargs) # MusicBrainz API uses "type" parameter for release type filtering - if release_type := kwargs.pop("release_type", None): - kwargs["type"] = "|".join(release_type) - return self._get_resource("release-group", **kwargs)["release-groups"] + if release_type := api_params.pop("release_type", None): + api_params["type"] = "|".join(release_type) + return self._get_resource("release-group", **api_params)[ + "release-groups" + ] @singledispatchmethod @classmethod diff --git a/beetsplug/missing.py b/beetsplug/missing.py index ee3cb8ca4..f98ed18a1 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -22,6 +22,19 @@ from typing import TYPE_CHECKING, ClassVar import requests +from beets import config, metadata_plugins +from beets.dbcore import types +from beets.library import Item +from beets.plugins import BeetsPlugin +from beets.ui import Subcommand, print_ + +from ._utils.musicbrainz import MusicBrainzAPIMixin + +if TYPE_CHECKING: + from collections.abc import Iterator + + from beets.library import Album, Library + # Valid MusicBrainz release types for filtering release groups VALID_RELEASE_TYPES = [ "nat", @@ -41,19 +54,6 @@ VALID_RELEASE_TYPES = [ "mixtape/street", ] -from beets import config, metadata_plugins -from beets.dbcore import types -from beets.library import Item -from beets.plugins import BeetsPlugin -from beets.ui import Subcommand, print_ - -from ._utils.musicbrainz import MusicBrainzAPIMixin - -if TYPE_CHECKING: - from collections.abc import Iterator - - from beets.library import Album, Library - MB_ARTIST_QUERY = r"mb_albumartistid::^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$" From 4cef8c40931ed454b5caff6114cb152453ede28d Mon Sep 17 00:00:00 2001 From: Brock Grassy Date: Fri, 13 Mar 2026 14:51:17 -0700 Subject: [PATCH 3/8] Address review comments --- beetsplug/_utils/musicbrainz.py | 12 +++--------- beetsplug/missing.py | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/beetsplug/_utils/musicbrainz.py b/beetsplug/_utils/musicbrainz.py index 36a6b1946..d31887f80 100644 --- a/beetsplug/_utils/musicbrainz.py +++ b/beetsplug/_utils/musicbrainz.py @@ -109,7 +109,7 @@ class BrowseReleaseGroupsKwargs(BrowseKwargs, total=False): artist: NotRequired[str] collection: NotRequired[str] release: NotRequired[str] - release_type: NotRequired[list[str]] + type: NotRequired[str] class BrowseRecordingsKwargs(BrowseReleaseGroupsKwargs, total=False): @@ -284,15 +284,9 @@ class MusicBrainzAPI(RequestHandler): """Browse release groups related to the given entities. At least one of artist, collection, or release must be provided. - Optionally filter by release_type (e.g., ["album", "ep"]). + Optionally filter by type (e.g., "album|ep"). """ - api_params: dict[str, Any] = dict(kwargs) - # MusicBrainz API uses "type" parameter for release type filtering - if release_type := api_params.pop("release_type", None): - api_params["type"] = "|".join(release_type) - return self._get_resource("release-group", **api_params)[ - "release-groups" - ] + return self._browse("release-group", **kwargs) @singledispatchmethod @classmethod diff --git a/beetsplug/missing.py b/beetsplug/missing.py index f98ed18a1..d606a1c21 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -232,7 +232,7 @@ class MissingPlugin(MusicBrainzAPIMixin, BeetsPlugin): try: resp = self.mb_api.browse_release_groups( artist=artist_id, - release_type=release_type, + type="|".join(release_type), ) except requests.exceptions.RequestException: self._log.info( From a3b7cfa1b33997923d06156161a865be9c6ab60f Mon Sep 17 00:00:00 2001 From: Brock Grassy Date: Fri, 13 Mar 2026 15:11:40 -0700 Subject: [PATCH 4/8] Add IOMixin to fix failures --- test/plugins/test_missing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/plugins/test_missing.py b/test/plugins/test_missing.py index 1f492872d..9818a5d43 100644 --- a/test/plugins/test_missing.py +++ b/test/plugins/test_missing.py @@ -8,10 +8,10 @@ import pytest from beets.autotag.hooks import AlbumInfo, TrackInfo from beets.library import Album, Item -from beets.test.helper import PluginMixin, TestHelper +from beets.test.helper import IOMixin, PluginMixin, TestHelper -class TestMissingAlbums(PluginMixin, TestHelper): +class TestMissingAlbums(IOMixin, PluginMixin, TestHelper): """Tests for missing albums functionality.""" plugin = "missing" @@ -145,7 +145,7 @@ class TestMissingAlbums(PluginMixin, TestHelper): assert output == "1\n" -class TestMissingTracks(PluginMixin, TestHelper): +class TestMissingTracks(IOMixin, PluginMixin, TestHelper): """Tests for missing tracks functionality.""" plugin = "missing" From c51f68be154cd07af973b596bb520c262dc8e200 Mon Sep 17 00:00:00 2001 From: Brock Grassy Date: Fri, 13 Mar 2026 15:31:09 -0700 Subject: [PATCH 5/8] Fix rebase in tests --- test/plugins/test_missing.py | 63 ++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/test/plugins/test_missing.py b/test/plugins/test_missing.py index 9818a5d43..6f30d5467 100644 --- a/test/plugins/test_missing.py +++ b/test/plugins/test_missing.py @@ -11,17 +11,24 @@ from beets.library import Album, Item from beets.test.helper import IOMixin, PluginMixin, TestHelper -class TestMissingAlbums(IOMixin, PluginMixin, TestHelper): +@pytest.fixture +def helper(request): + helper = TestHelper() + helper.setup_beets() + + request.instance.lib = helper.lib + + yield + + helper.teardown_beets() + + +@pytest.mark.usefixtures("helper") +class TestMissingAlbums(IOMixin, PluginMixin): """Tests for missing albums functionality.""" plugin = "missing" - @pytest.fixture(autouse=True) - def _setup(self): - self.setup_beets() - yield - self.teardown_beets() - @pytest.mark.parametrize( "release_from_mb,expected_output", [ @@ -55,7 +62,8 @@ class TestMissingAlbums(IOMixin, PluginMixin, TestHelper): json={"release-groups": [release_from_mb]}, ) - assert self.run_with_output("missing", "--album") == expected_output + with self.configure_plugin({}): + assert self.run_with_output("missing", "--album") == expected_output def test_release_type_filters_results(self, requests_mock): """Test --release-type filters to only show specified type.""" @@ -78,9 +86,10 @@ class TestMissingAlbums(IOMixin, PluginMixin, TestHelper): }, ) - output = self.run_with_output( - "missing", "-a", "--release-type", "compilation" - ) + with self.configure_plugin({}): + output = self.run_with_output( + "missing", "-a", "--release-type", "compilation" + ) assert "artist - compilation" in output @@ -106,14 +115,15 @@ class TestMissingAlbums(IOMixin, PluginMixin, TestHelper): }, ) - output = self.run_with_output( - "missing", - "-a", - "--release-type", - "compilation", - "--release-type", - "album", - ) + with self.configure_plugin({}): + output = self.run_with_output( + "missing", + "-a", + "--release-type", + "compilation", + "--release-type", + "album", + ) assert "artist - compilation" in output assert "artist - title 2" in output @@ -140,22 +150,18 @@ class TestMissingAlbums(IOMixin, PluginMixin, TestHelper): }, ) - output = self.run_with_output("missing", "-a", "-t") + with self.configure_plugin({}): + output = self.run_with_output("missing", "-a", "-t") assert output == "1\n" -class TestMissingTracks(IOMixin, PluginMixin, TestHelper): +@pytest.mark.usefixtures("helper") +class TestMissingTracks(IOMixin, PluginMixin): """Tests for missing tracks functionality.""" plugin = "missing" - @pytest.fixture(autouse=True) - def _setup(self): - self.setup_beets() - yield - self.teardown_beets() - @pytest.mark.parametrize( "total,count,expected", [ @@ -211,4 +217,5 @@ class TestMissingTracks(IOMixin, PluginMixin, TestHelper): if count: command.append("-c") - assert expected in self.run_with_output(*command) + with self.configure_plugin({}): + assert expected in self.run_with_output(*command) From 2c60c3eb4937e796ac1d8f28d9f0521a0e37d4fc Mon Sep 17 00:00:00 2001 From: Brock Grassy Date: Sat, 14 Mar 2026 12:02:59 -0700 Subject: [PATCH 6/8] Address comments and add new test case --- beetsplug/missing.py | 12 ++++++------ docs/plugins/missing.rst | 2 ++ test/plugins/test_missing.py | 31 +++++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/beetsplug/missing.py b/beetsplug/missing.py index d606a1c21..681e10431 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -127,7 +127,7 @@ class MissingPlugin(MusicBrainzAPIMixin, BeetsPlugin): "count": False, "total": False, "album": False, - "release_type": ["album"], + "release_types": ["album"], } ) @@ -154,13 +154,13 @@ class MissingPlugin(MusicBrainzAPIMixin, BeetsPlugin): dest="album", action="store_true", help=( - "show missing release for artist instead of tracks. Defaults " - "to only releases of type 'album'" + "show missing album releases for artist instead of tracks; " + "Defaults to only releases of type 'album'" ), ) self._command.parser.add_option( "--release-type", - dest="release_type", + dest="release_types", action="append", help=( "select release types for missing albums for artist " @@ -226,13 +226,13 @@ class MissingPlugin(MusicBrainzAPIMixin, BeetsPlugin): album_ids_by_artist[artist].add(album["mb_releasegroupid"]) total_missing = 0 - release_type = self.config["release_type"].get() or ["album"] + release_types = self.config["release_types"].as_str_seq() calculating_total = self.config["total"].get() for (artist, artist_id), album_ids in album_ids_by_artist.items(): try: resp = self.mb_api.browse_release_groups( artist=artist_id, - type="|".join(release_type), + type="|".join(release_types), ) except requests.exceptions.RequestException: self._log.info( diff --git a/docs/plugins/missing.rst b/docs/plugins/missing.rst index 1a32b374d..80fda5dbf 100644 --- a/docs/plugins/missing.rst +++ b/docs/plugins/missing.rst @@ -31,6 +31,8 @@ library: You can provide this argument multiple times to specify multiple release types to filter to. If not provided, defaults to just the "album" release type. + provided, it uses the configured + ``missing.release_type`` (default: "album"). …or by editing the corresponding configuration options. diff --git a/test/plugins/test_missing.py b/test/plugins/test_missing.py index 6f30d5467..6fab189cc 100644 --- a/test/plugins/test_missing.py +++ b/test/plugins/test_missing.py @@ -58,7 +58,7 @@ class TestMissingAlbums(IOMixin, PluginMixin): ) ) requests_mock.get( - f"/ws/2/release-group?artist={artist_mbid}", + re.compile(rf"/ws/2/release-group\?artist={artist_mbid}&.*type=album"), json={"release-groups": [release_from_mb]}, ) @@ -128,6 +128,33 @@ class TestMissingAlbums(IOMixin, PluginMixin): assert "artist - compilation" in output assert "artist - title 2" in output + def test_no_release_type_sends_empty_type_param(self, requests_mock): + """Test that empty release_types config sends empty type parameter. + + When release_types is empty, type="" is sent to the MusicBrainz API. + This behaves the same as not passing type at all, returning all + release groups without filtering by type. + """ + artist_mbid = str(uuid.uuid4()) + self.lib.add( + Album( + album="album", + albumartist="artist", + mb_albumartistid=artist_mbid, + mb_albumid="album", + mb_releasegroupid="album_id", + ) + ) + adapter = requests_mock.get( + re.compile(r"/ws/2/release-group"), + json={"release-groups": [{"id": "other_id", "title": "other"}]}, + ) + + with self.configure_plugin({"release_types": []}): + self.run_with_output("missing", "-a") + + assert adapter.last_request.qs["type"] == [""] + def test_missing_albums_total(self, requests_mock): """Test -t flag with --album shows total count of missing albums.""" artist_mbid = str(uuid.uuid4()) @@ -141,7 +168,7 @@ class TestMissingAlbums(IOMixin, PluginMixin): ) ) requests_mock.get( - f"/ws/2/release-group?artist={artist_mbid}", + re.compile(rf"/ws/2/release-group\?artist={artist_mbid}&.*type=album"), json={ "release-groups": [ {"id": "album_id", "title": "album"}, From e36c09d69d83759aa78113a815c44d351927f4dd Mon Sep 17 00:00:00 2001 From: Brock Grassy Date: Sat, 14 Mar 2026 12:09:24 -0700 Subject: [PATCH 7/8] Fix lint --- test/plugins/test_missing.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/plugins/test_missing.py b/test/plugins/test_missing.py index 6fab189cc..bfb60750e 100644 --- a/test/plugins/test_missing.py +++ b/test/plugins/test_missing.py @@ -58,7 +58,9 @@ class TestMissingAlbums(IOMixin, PluginMixin): ) ) requests_mock.get( - re.compile(rf"/ws/2/release-group\?artist={artist_mbid}&.*type=album"), + re.compile( + rf"/ws/2/release-group\?artist={artist_mbid}&.*type=album" + ), json={"release-groups": [release_from_mb]}, ) @@ -168,7 +170,9 @@ class TestMissingAlbums(IOMixin, PluginMixin): ) ) requests_mock.get( - re.compile(rf"/ws/2/release-group\?artist={artist_mbid}&.*type=album"), + re.compile( + rf"/ws/2/release-group\?artist={artist_mbid}&.*type=album" + ), json={ "release-groups": [ {"id": "album_id", "title": "album"}, From d21470042a06cb11ab4d493de635f19114ff0694 Mon Sep 17 00:00:00 2001 From: Brock Grassy Date: Sun, 15 Mar 2026 05:22:25 -0700 Subject: [PATCH 8/8] Address comments --- beetsplug/missing.py | 12 +++++++----- test/plugins/test_missing.py | 27 ++++++++++----------------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/beetsplug/missing.py b/beetsplug/missing.py index 681e10431..f8312d5eb 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -159,12 +159,12 @@ class MissingPlugin(MusicBrainzAPIMixin, BeetsPlugin): ), ) self._command.parser.add_option( - "--release-type", - dest="release_types", + "--release-types", action="append", + dest="release_types", help=( - "select release types for missing albums for artist " - f"from ({', '.join(VALID_RELEASE_TYPES)})" + "comma-separated list of release types for missing albums " + f"(valid: {', '.join(VALID_RELEASE_TYPES)})" ), ) self._command.parser.add_format_option() @@ -226,7 +226,9 @@ class MissingPlugin(MusicBrainzAPIMixin, BeetsPlugin): album_ids_by_artist[artist].add(album["mb_releasegroupid"]) total_missing = 0 - release_types = self.config["release_types"].as_str_seq() + release_types = [] + for rt in self.config["release_types"].as_str_seq(): + release_types.extend(rt.split(",")) calculating_total = self.config["total"].get() for (artist, artist_id), album_ids in album_ids_by_artist.items(): try: diff --git a/test/plugins/test_missing.py b/test/plugins/test_missing.py index bfb60750e..30b8356a6 100644 --- a/test/plugins/test_missing.py +++ b/test/plugins/test_missing.py @@ -67,8 +67,8 @@ class TestMissingAlbums(IOMixin, PluginMixin): with self.configure_plugin({}): assert self.run_with_output("missing", "--album") == expected_output - def test_release_type_filters_results(self, requests_mock): - """Test --release-type filters to only show specified type.""" + def test_release_types_filters_results(self, requests_mock): + """Test --release-types filters to only show specified type.""" artist_mbid = str(uuid.uuid4()) self.lib.add( Album( @@ -90,13 +90,13 @@ class TestMissingAlbums(IOMixin, PluginMixin): with self.configure_plugin({}): output = self.run_with_output( - "missing", "-a", "--release-type", "compilation" + "missing", "-a", "--release-types", "compilation" ) assert "artist - compilation" in output - def test_release_type_multiple_types(self, requests_mock): - """Test multiple --release-type flags include all specified types.""" + def test_release_types_comma_separated(self, requests_mock): + """Test --release-types with comma-separated values.""" artist_mbid = str(uuid.uuid4()) self.lib.add( Album( @@ -121,22 +121,15 @@ class TestMissingAlbums(IOMixin, PluginMixin): output = self.run_with_output( "missing", "-a", - "--release-type", - "compilation", - "--release-type", - "album", + "--release-types", + "compilation,album", ) assert "artist - compilation" in output assert "artist - title 2" in output - def test_no_release_type_sends_empty_type_param(self, requests_mock): - """Test that empty release_types config sends empty type parameter. - - When release_types is empty, type="" is sent to the MusicBrainz API. - This behaves the same as not passing type at all, returning all - release groups without filtering by type. - """ + def test_empty_release_types_config_sends_empty_type(self, requests_mock): + """Test that release_types: [] in config sends type="" to the API.""" artist_mbid = str(uuid.uuid4()) self.lib.add( Album( @@ -149,7 +142,7 @@ class TestMissingAlbums(IOMixin, PluginMixin): ) adapter = requests_mock.get( re.compile(r"/ws/2/release-group"), - json={"release-groups": [{"id": "other_id", "title": "other"}]}, + json={"release-groups": []}, ) with self.configure_plugin({"release_types": []}):