This commit is contained in:
Brock Grassy 2026-02-04 21:32:11 +00:00 committed by GitHub
commit c34a58649b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 261 additions and 36 deletions

View file

@ -76,6 +76,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):
@ -235,8 +236,15 @@ 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"]).
"""
return self._get_resource("release-group", **kwargs)["release-groups"]
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"
]
@singledispatchmethod
@classmethod

View file

@ -35,6 +35,25 @@ if TYPE_CHECKING:
from beets.library import Album, Library
# 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",
]
MB_ARTIST_QUERY = r"mb_albumartistid::^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$"
@ -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 '{}' ({})",

View file

@ -9,8 +9,17 @@ Unreleased
New features:
- :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``.
For packagers:
Other changes:

View file

@ -25,12 +25,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
-------------
@ -108,6 +114,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:
::

View file

@ -1,29 +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.autotag.hooks import AlbumInfo, TrackInfo
from beets.library import Album, Item
from beets.test.helper import PluginMixin, TestHelper
@pytest.fixture
def helper():
helper = TestHelper()
helper.setup_beets()
class TestMissingAlbums(PluginMixin, TestHelper):
"""Tests for missing albums functionality."""
yield helper
helper.teardown_beets()
class TestMissingAlbums(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",
@ -34,28 +31,184 @@ class TestMissingAlbums(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",
),
],
)
def test_missing_artist_albums(
self, requests_mock, helper, release_from_mb, expected_output
self, requests_mock, release_from_mb, expected_output
):
helper.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 (
helper.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)