From 37ef43466f1eae55afe748a8d6f90a608082ac4c Mon Sep 17 00:00:00 2001 From: Brock Grassy Date: Fri, 10 Jan 2025 22:56:09 -0500 Subject: [PATCH 1/3] Change missing plugin to allow for filtering albums by release type --- beetsplug/missing.py | 31 ++++-- docs/changelog.rst | 5 + docs/plugins/missing.rst | 18 +++- test/plugins/test_missing.py | 201 +++++++++++++++++++++++++++++++++++ 4 files changed, 245 insertions(+), 10 deletions(-) create mode 100644 test/plugins/test_missing.py diff --git a/beetsplug/missing.py b/beetsplug/missing.py index d5e4deda1b..bec5d0f0c4 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -18,14 +18,14 @@ from collections import defaultdict import musicbrainzngs -from musicbrainzngs.musicbrainz import MusicBrainzError +from musicbrainzngs.musicbrainz import VALID_RELEASE_TYPES, MusicBrainzError from beets import config from beets.autotag import hooks from beets.dbcore import types from beets.library import Item from beets.plugins import BeetsPlugin -from beets.ui import Subcommand, decargs, print_ +from beets.ui import Subcommand, decargs def _missing_count(album): @@ -98,6 +98,7 @@ def __init__(self): "count": False, "total": False, "album": False, + "release_type": ["album"], } ) @@ -125,6 +126,15 @@ def __init__(self): 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): @@ -149,7 +159,7 @@ def _missing_tracks(self, lib, query): 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. @@ -159,17 +169,18 @@ def _missing_tracks(self, lib, query): 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, query): """Print a listing of albums missing from each artist in the library matching query. """ total = self.config["total"].get() + release_type = self.config["release_type"].get() or ["album"] albums = lib.albums(query) # build dict mapping artist to list of their albums in library @@ -193,7 +204,10 @@ def _missing_albums(self, lib, query): continue try: - resp = musicbrainzngs.browse_release_groups(artist=artist[1]) + resp = musicbrainzngs.browse_release_groups( + artist=artist[1], + release_type=release_type, + ) release_groups = resp["release-group-list"] except MusicBrainzError as err: self._log.info( @@ -206,6 +220,7 @@ def _missing_albums(self, lib, query): missing = [] present = [] + print(artist, albums, release_groups) for rg in release_groups: missing.append(rg) for alb in albums: @@ -221,10 +236,10 @@ def _missing_albums(self, lib, query): missing_titles = {rg["title"] for rg in missing} for release_title in missing_titles: - print_("{} - {}".format(artist[0], release_title)) + self._log.info("{} - {}".format(artist[0], release_title)) if total: - print(total_missing) + self._log.info(str(total_missing)) def _missing(self, album): """Query MusicBrainz to determine items missing from `album`.""" diff --git a/docs/changelog.rst b/docs/changelog.rst index 186749d461..0e73d1221f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -49,6 +49,11 @@ 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/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` 2.2.0 (December 02, 2024) ------------------------- diff --git a/docs/plugins/missing.rst b/docs/plugins/missing.rst index 38a0d5a088..e787f5350b 100644 --- a/docs/plugins/missing.rst +++ b/docs/plugins/missing.rst @@ -22,10 +22,16 @@ tracks over your whole library, using command-line switches:: -c, --count count missing tracks per album -t, --total count total of missing tracks or albums -a, --album show missing albums for artist instead of tracks + --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 corresponding options. -Note that ``-c`` is ignored when used with ``-a``. +Note that ``-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 ------------- @@ -64,10 +70,18 @@ List all missing tracks in your collection:: beet missing -List all missing albums in your collection:: +List all missing albums of release type "album" in your collection:: beet missing -a +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 + List all missing tracks from 2008:: beet missing year:2008 diff --git a/test/plugins/test_missing.py b/test/plugins/test_missing.py new file mode 100644 index 0000000000..b07c356faa --- /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 From 5f66e727f5d8b66cd586e176c2280ba36e293e02 Mon Sep 17 00:00:00 2001 From: Brock Grassy Date: Fri, 10 Jan 2025 23:04:22 -0500 Subject: [PATCH 2/3] Add more descriptive argparse help string --- beetsplug/missing.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/beetsplug/missing.py b/beetsplug/missing.py index bec5d0f0c4..a4d944a4a3 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -124,7 +124,10 @@ def __init__(self): "--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", From d9b17ca1eaf69db34ec78539a295fab0dc28a901 Mon Sep 17 00:00:00 2001 From: Brock Grassy Date: Sat, 11 Jan 2025 20:55:37 -0500 Subject: [PATCH 3/3] Fix formatting --- beetsplug/missing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/missing.py b/beetsplug/missing.py index a4d944a4a3..54a2d1e9b2 100644 --- a/beetsplug/missing.py +++ b/beetsplug/missing.py @@ -127,7 +127,7 @@ def __init__(self): help=( "show missing release for artist instead of tracks. Defaults " "to only releases of type 'album'" - ) + ), ) self._command.parser.add_option( "--release-type",