diff --git a/beetsplug/missing.py b/beetsplug/missing.py index cbdda4599..0c93bc1bc 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -19,7 +19,7 @@ from collections import defaultdict from collections.abc import Iterator import musicbrainzngs -from musicbrainzngs.musicbrainz import MusicBrainzError +from musicbrainzngs.musicbrainz import VALID_RELEASE_TYPES, MusicBrainzError from beets import config, metadata_plugins from beets.dbcore import types @@ -100,6 +100,7 @@ class MissingPlugin(BeetsPlugin): "count": False, "total": False, "album": False, + "release_type": ["album"], } ) @@ -127,6 +128,15 @@ class MissingPlugin(BeetsPlugin): action="store_true", help="show missing albums for artist instead of tracks", ) + 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() def commands(self): @@ -151,7 +161,7 @@ class MissingPlugin(BeetsPlugin): fmt = config["format_album" if count else "format_item"].get() if total: - print(sum([_missing_count(a) for a in albums])) + self._log.info(str(sum([_missing_count(a) for a in albums]))) return # Default format string for count mode. @@ -161,11 +171,11 @@ class MissingPlugin(BeetsPlugin): for album in albums: if count: if _missing_count(album): - print_(format(album, fmt)) + self._log.info(format(album, fmt)) else: for item in self._missing(album): - print_(format(item, fmt)) + self._log.info(format(item, fmt)) def _missing_albums(self, lib: Library, query: list[str]) -> None: """Print a listing of albums missing from each artist in the library @@ -186,10 +196,15 @@ class MissingPlugin(BeetsPlugin): album_ids_by_artist[artist].add(album) 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 = musicbrainzngs.browse_release_groups(artist=artist_id) + resp = musicbrainzngs.browse_release_groups( + artist=artist_id, + release_type=release_type, + ) + release_groups = resp["release-group-list"] except MusicBrainzError as err: self._log.info( "Couldn't fetch info for artist '{}' ({}) - '{}'", diff --git a/docs/changelog.rst b/docs/changelog.rst index 19026eafe..6a513bef0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -54,6 +54,10 @@ Bug fixes: endpoints. Previously, due to single-quotes (ie. string literal) in the SQL query, the query eg. `GET /item/values/albumartist` would return the literal "albumartist" instead of a list of unique album artists. +* :doc:`plugins/missing`: When running in missing album mode, allows users to specify + MusicBrainz release types that they want to show using the ``--release-type`` + flag. The default behavior is also changed to just show releases of type + ``album``. :bug:`2661` For plugin developers: @@ -435,7 +439,25 @@ Other changes: wrong (outdated) commit. Now the tag is created in the same workflow step right after committing the version update. :bug:`5539` - :doc:`/plugins/smartplaylist`: URL-encode additional item ``fields`` within - generated EXTM3U playlists instead of JSON-encoding them. + generated EXTM3U playlists inst + # build dict mapping artist to list of all albums + for artist, albums in albums_by_artist.items(): + if artist[1] is None or artist[1] == "": + albs_no_mbid = ["'" + a["album"] + "'" for a in albums] + self._log.info( + "No musicbrainz ID for artist '{}' found in album(s) {}; " + "skipping", + artist[0], + ", ".join(albs_no_mbid), + ) + continue + + try: + resp = musicbrainzngs.browse_release_groups( + artist=artist[1], + release_type=release_type, + ) +ead of JSON-encoding them. - typehints: ``./beets/importer.py`` file now has improved typehints. - typehints: ``./beets/plugins.py`` file now includes typehints. - :doc:`plugins/ftintitle`: Optimize the plugin by avoiding unnecessary writes diff --git a/docs/plugins/missing.rst b/docs/plugins/missing.rst index 10842933c..ea5a5b3b3 100644 --- a/docs/plugins/missing.rst +++ b/docs/plugins/missing.rst @@ -26,12 +26,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 ------------- @@ -109,6 +115,18 @@ 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 new file mode 100644 index 000000000..b07c356fa --- /dev/null +++ b/test/plugins/test_missing.py @@ -0,0 +1,201 @@ +# This file is part of beets. +# Copyright 2016, Stig Inge Lea Bjornsen. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + + +"""Tests for the `missing` plugin.""" + +import itertools +from unittest.mock import patch + +import pytest + +from beets.autotag.hooks import AlbumInfo, TrackInfo +from beets.library import Item +from beets.test.helper import PluginMixin, TestHelper, capture_log + + +def mock_browse_release_groups( + artist: str, + release_type: list[str], +): + """Helper to mock getting an artist's release groups of multiple release types.""" + release_groups = [ + {"id": "album_id", "title": "title", "release_type": "album"}, + {"id": "album2_id", "title": "title 2", "release_type": "album"}, + { + "id": "compilation_id", + "title": "compilation", + "release_type": "compilation", + }, + ] + + return { + "release-group-list": [ + x for x in release_groups if x["release_type"] in release_type + ] + } + + +class TestMissingPlugin(PluginMixin, TestHelper): + # The minimum mtime of the files to be imported + plugin = "missing" + + def setup_method(self, method): + """Setup pristine beets config and items for testing.""" + self.setup_beets() + self.album_items = [ + Item( + album="album", + mb_albumid="81ae60d4-5b75-38df-903a-db2cfa51c2c6", + mb_releasegroupid="album_id", + mb_trackid="track_1", + mb_albumartistid="artist_id", + albumartist="artist", + tracktotal=3, + ), + Item( + album="album", + mb_albumid="81ae60d4-5b75-38df-903a-db2cfa51c2c6", + mb_releasegroupid="album_id", + mb_albumartistid="artist_id", + 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_id", + albumartist="artist", + tracktotal=3, + ), + ] + + def teardown_method(self, method): + """Clean all beets data.""" + self.teardown_beets() + + @pytest.mark.parametrize( + "total,count", + list(itertools.product((True, False), repeat=2)), + ) + @patch("beets.autotag.hooks.album_for_mbid") + def test_missing_tracks(self, album_for_mbid, total, count): + """Test getting missing tracks works with expected logs.""" + self.lib.add_album(self.album_items[:2]) + + album_for_mbid.return_value = AlbumInfo( + album_id="album_id", + album="album", + tracks=[ + TrackInfo(track_id=album_item.mb_trackid) + for album_item in self.album_items + ], + ) + + with capture_log() as logs: + command = ["missing"] + if total: + command.append("-t") + if count: + command.append("-c") + self.run_command(*command) + + if not total and not count: + assert ( + f"missing: track {self.album_items[-1].mb_trackid} in album album_id" + ) in logs + + if not total and count: + assert "missing: artist - album: 1" in logs + + assert ("missing: 1" in logs) == total + + def test_missing_albums(self): + """Test getting missing albums works with expected logs.""" + with patch( + "musicbrainzngs.browse_release_groups", + wraps=mock_browse_release_groups, + ): + self.lib.add_album(self.album_items) + + with capture_log() as logs: + command = ["missing", "-a"] + self.run_command(*command) + + assert "missing: artist - compilation" not in logs + assert "missing: artist - title" not in logs + assert "missing: artist - title 2" in logs + + def test_missing_albums_compilation(self): + """Test getting missing albums works for a specific release type.""" + with patch( + "musicbrainzngs.browse_release_groups", + wraps=mock_browse_release_groups, + ): + self.lib.add_album(self.album_items) + + with capture_log() as logs: + command = ["missing", "-a", "--release-type", "compilation"] + self.run_command(*command) + + assert "missing: artist - compilation" in logs + assert "missing: artist - title" not in logs + assert "missing: artist - title 2" not in logs + + def test_missing_albums_all(self): + """Test getting missing albums works for all release types.""" + with patch( + "musicbrainzngs.browse_release_groups", + wraps=mock_browse_release_groups, + ): + self.lib.add_album(self.album_items) + + with capture_log() as logs: + command = [ + "missing", + "-a", + "--release-type", + "compilation", + "--release-type", + "album", + ] + self.run_command(*command) + + assert "missing: artist - compilation" in logs + assert "missing: artist - title" not in logs + assert "missing: artist - title 2" in logs + + def test_missing_albums_total(self): + """Test getting missing albums works with the total flag.""" + with patch( + "musicbrainzngs.browse_release_groups", + wraps=mock_browse_release_groups, + ): + self.lib.add_album(self.album_items) + + with capture_log() as logs: + command = [ + "missing", + "-a", + "-t", + ] + self.run_command(*command) + + assert "missing: 1" in logs + # Specific missing logs omitted if total provided + assert "missing: artist - compilation" not in logs + assert "missing: artist - title" not in logs + assert "missing: artist - title 2" not in logs