mirror of
https://github.com/beetbox/beets.git
synced 2026-01-04 23:12:51 +01:00
Change missing plugin to allow for filtering albums by release type
This commit is contained in:
parent
b4f0dbf53b
commit
26593a7e2e
4 changed files with 263 additions and 7 deletions
|
|
@ -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 '{}' ({}) - '{}'",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
::
|
||||
|
|
|
|||
201
test/plugins/test_missing.py
Normal file
201
test/plugins/test_missing.py
Normal 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
|
||||
Loading…
Reference in a new issue