missing: support non-musicbrainz data sources

This commit is contained in:
Šarūnas Nejus 2025-02-15 21:29:54 +00:00
parent 6a192d0bdb
commit 4c1f217ce0
No known key found for this signature in database
GPG key ID: DD28F6704DBE3435
6 changed files with 27 additions and 50 deletions

View file

@ -71,7 +71,7 @@ class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin):
self._log.error("Error fetching data from {}\n Error: {}", url, e) self._log.error("Error fetching data from {}\n Error: {}", url, e)
return None return None
if "error" in data: if "error" in data:
self._log.error("Deezer API error: {}", data["error"]["message"]) self._log.debug("Deezer API error: {}", data["error"]["message"])
return None return None
return data return data

View file

@ -29,7 +29,6 @@ from string import ascii_lowercase
import confuse import confuse
from discogs_client import Client, Master, Release from discogs_client import Client, Master, Release
from discogs_client import __version__ as dc_string
from discogs_client.exceptions import DiscogsAPIError from discogs_client.exceptions import DiscogsAPIError
from requests.exceptions import ConnectionError from requests.exceptions import ConnectionError
from typing_extensions import TypedDict from typing_extensions import TypedDict
@ -64,7 +63,6 @@ class ReleaseFormat(TypedDict):
class DiscogsPlugin(BeetsPlugin): class DiscogsPlugin(BeetsPlugin):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.check_discogs_client()
self.config.add( self.config.add(
{ {
"apikey": API_KEY, "apikey": API_KEY,
@ -80,24 +78,7 @@ class DiscogsPlugin(BeetsPlugin):
self.config["apikey"].redact = True self.config["apikey"].redact = True
self.config["apisecret"].redact = True self.config["apisecret"].redact = True
self.config["user_token"].redact = True self.config["user_token"].redact = True
self.discogs_client = None self.setup()
self.register_listener("import_begin", self.setup)
def check_discogs_client(self):
"""Ensure python3-discogs-client version >= 2.3.15"""
dc_min_version = [2, 3, 15]
dc_version = [int(elem) for elem in dc_string.split(".")]
min_len = min(len(dc_version), len(dc_min_version))
gt_min = [
(elem > elem_min)
for elem, elem_min in zip(
dc_version[:min_len], dc_min_version[:min_len]
)
]
if True not in gt_min:
self._log.warning(
"python3-discogs-client version should be >= 2.3.15"
)
def setup(self, session=None): def setup(self, session=None):
"""Create the `discogs_client` field. Authenticate if necessary.""" """Create the `discogs_client` field. Authenticate if necessary."""
@ -179,13 +160,9 @@ class DiscogsPlugin(BeetsPlugin):
"""Returns a list of AlbumInfo objects for discogs search results """Returns a list of AlbumInfo objects for discogs search results
matching an album and artist (if not various). matching an album and artist (if not various).
""" """
if not self.discogs_client:
return
if not album and not artist: if not album and not artist:
self._log.debug( self._log.debug(
"Skipping Discogs query. Files missing album and " "Skipping Discogs query. Files missing album and artist tags."
"artist tags."
) )
return [] return []
@ -254,12 +231,9 @@ class DiscogsPlugin(BeetsPlugin):
:return: Candidate TrackInfo objects. :return: Candidate TrackInfo objects.
:rtype: list[beets.autotag.hooks.TrackInfo] :rtype: list[beets.autotag.hooks.TrackInfo]
""" """
if not self.discogs_client:
return []
if not artist and not title: if not artist and not title:
self._log.debug( self._log.debug(
"Skipping Discogs query. File missing artist and " "title tags." "Skipping Discogs query. File missing artist and title tags."
) )
return [] return []
@ -290,9 +264,6 @@ class DiscogsPlugin(BeetsPlugin):
"""Fetches an album by its Discogs ID and returns an AlbumInfo object """Fetches an album by its Discogs ID and returns an AlbumInfo object
or None if the album is not found. or None if the album is not found.
""" """
if not self.discogs_client:
return
self._log.debug("Searching for release {0}", album_id) self._log.debug("Searching for release {0}", album_id)
discogs_id = extract_discogs_id_regex(album_id) discogs_id = extract_discogs_id_regex(album_id)

View file

@ -16,6 +16,7 @@
"""List missing tracks.""" """List missing tracks."""
from collections import defaultdict from collections import defaultdict
from collections.abc import Iterator
import musicbrainzngs import musicbrainzngs
from musicbrainzngs.musicbrainz import MusicBrainzError from musicbrainzngs.musicbrainz import MusicBrainzError
@ -23,7 +24,7 @@ from musicbrainzngs.musicbrainz import MusicBrainzError
from beets import config from beets import config
from beets.autotag import hooks from beets.autotag import hooks
from beets.dbcore import types from beets.dbcore import types
from beets.library import Item from beets.library import Album, Item
from beets.plugins import BeetsPlugin from beets.plugins import BeetsPlugin
from beets.ui import Subcommand, decargs, print_ from beets.ui import Subcommand, decargs, print_
@ -226,19 +227,20 @@ class MissingPlugin(BeetsPlugin):
if total: if total:
print(total_missing) print(total_missing)
def _missing(self, album): def _missing(self, album: Album) -> Iterator[Item]:
"""Query MusicBrainz to determine items missing from `album`.""" """Query MusicBrainz to determine items missing from `album`."""
item_mbids = [x.mb_trackid for x in album.items()] if len(album.items()) == album.albumtotal:
if len(list(album.items())) < album.albumtotal: return
# fetch missing items
# TODO: Implement caching that without breaking other stuff item_mbids = {x.mb_trackid for x in album.items()}
album_info = hooks.album_for_mbid(album.mb_albumid) # fetch missing items
for track_info in getattr(album_info, "tracks", []): # TODO: Implement caching that without breaking other stuff
if album_info := hooks.album_for_id(album.mb_albumid):
for track_info in album_info.tracks:
if track_info.track_id not in item_mbids: if track_info.track_id not in item_mbids:
item = _item(track_info, album_info, album.id)
self._log.debug( self._log.debug(
"track {0} in album {1}", "track {0} in album {1}",
track_info.track_id, track_info.track_id,
album_info.album_id, album_info.album_id,
) )
yield item yield _item(track_info, album_info, album.id)

View file

@ -21,6 +21,7 @@ New features:
when fetching lyrics. when fetching lyrics.
* :doc:`plugins/lyrics`: Rewrite lyrics translation functionality to use Azure * :doc:`plugins/lyrics`: Rewrite lyrics translation functionality to use Azure
AI Translator API and add relevant instructions to the documentation. AI Translator API and add relevant instructions to the documentation.
* :doc:`plugins/missing`: Add support for all metadata sources.
Bug fixes: Bug fixes:

View file

@ -1,17 +1,17 @@
Missing Plugin Missing Plugin
============== ==============
This plugin adds a new command, ``missing`` or ``miss``, which finds This plugin adds a new command, ``missing`` or ``miss``, which finds and lists
and lists, for every album in your collection, which or how many missing tracks for albums in your collection. Each album requires one network
tracks are missing. Listing missing files requires one network call to call to album data source.
MusicBrainz. Merely counting missing files avoids any network calls.
Usage Usage
----- -----
Add the ``missing`` plugin to your configuration (see :ref:`using-plugins`). Add the ``missing`` plugin to your configuration (see :ref:`using-plugins`). By
By default, the ``beet missing`` command lists the names of tracks that your default, the ``beet missing`` command fetches album information from the origin
library is missing from each album. It can also list the names of albums that data source and lists names of the **tracks** that are missing from your
library. It can also list the names of albums that
your library is missing from each artist. your library is missing from each artist.
You can customize the output format, count You can customize the output format, count
the number of missing tracks per album, or total up the number of missing the number of missing tracks per album, or total up the number of missing

View file

@ -14,6 +14,8 @@
"""Tests for discogs plugin.""" """Tests for discogs plugin."""
from unittest.mock import Mock, patch
import pytest import pytest
from beets import config from beets import config
@ -23,6 +25,7 @@ from beets.util.id_extractors import extract_discogs_id_regex
from beetsplug.discogs import DiscogsPlugin from beetsplug.discogs import DiscogsPlugin
@patch("beetsplug.discogs.DiscogsPlugin.setup", Mock())
class DGAlbumInfoTest(BeetsTestCase): class DGAlbumInfoTest(BeetsTestCase):
def _make_release(self, tracks=None): def _make_release(self, tracks=None):
"""Returns a Bag that mimics a discogs_client.Release. The list """Returns a Bag that mimics a discogs_client.Release. The list