mirror of
https://github.com/beetbox/beets.git
synced 2026-02-09 17:01:55 +01:00
Merge dda1e8329e into cdfb813910
This commit is contained in:
commit
c34a58649b
5 changed files with 261 additions and 36 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 '{}' ({})",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
::
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue