Change missing plugin to allow for filtering albums by release type

This commit is contained in:
Brock Grassy 2025-01-10 22:56:09 -05:00
parent b4f0dbf53b
commit 26593a7e2e
4 changed files with 263 additions and 7 deletions

View file

@ -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 '{}' ({}) - '{}'",

View file

@ -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

View file

@ -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:
::

View file

@ -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