From 0d8d3bfadf86198d2c550ef90a826e99ed510d2e Mon Sep 17 00:00:00 2001 From: jdoe29103 Date: Mon, 9 Mar 2026 16:59:20 -0400 Subject: [PATCH] Add `discogs.extra_tags` and updated documentation --- beetsplug/discogs/__init__.py | 54 +++++++++++++++++++++++++++++++++-- docs/plugins/discogs.rst | 26 +++++++++++++++++ test/plugins/test_discogs.py | 36 +++++++++++++++++++++++ 3 files changed, 113 insertions(+), 3 deletions(-) diff --git a/beetsplug/discogs/__init__.py b/beetsplug/discogs/__init__.py index f09ed74be..b3d013edc 100644 --- a/beetsplug/discogs/__init__.py +++ b/beetsplug/discogs/__init__.py @@ -25,7 +25,7 @@ import re import socket import time import traceback -from functools import cache +from functools import cache, cached_property from string import ascii_lowercase from typing import TYPE_CHECKING @@ -36,7 +36,7 @@ from requests.exceptions import ConnectionError import beets import beets.ui -from beets import config +from beets import config, util from beets.autotag.distance import string_dist from beets.autotag.hooks import AlbumInfo, TrackInfo from beets.metadata_plugins import IDResponse, SearchApiMetadataSourcePlugin @@ -80,6 +80,15 @@ TRACK_INDEX_RE = re.compile( re.VERBOSE, ) +FIELDS_TO_DISCOGS_KEYS = { + "barcode": "barcode", + "catalognum": "catno", + "country": "country", + "label": "label", + "media": "format", + "year": "year", +} + class DiscogsPlugin(SearchApiMetadataSourcePlugin[IDResponse]): def __init__(self): @@ -95,6 +104,7 @@ class DiscogsPlugin(SearchApiMetadataSourcePlugin[IDResponse]): "append_style_genre": False, "strip_disambiguation": True, "featured_string": "Feat.", + "extra_tags": [], "anv": { "artist_credit": True, "artist": False, @@ -107,6 +117,25 @@ class DiscogsPlugin(SearchApiMetadataSourcePlugin[IDResponse]): self.config["user_token"].redact = True self.setup() + @cached_property + def extra_discogs_field_by_tag(self) -> dict[str, str]: + """Map configured extra tags to Discogs API search parameters. + + Process user configuration to determine which additional Discogs + fields should be included in search queries. + """ + field_by_tag = { + tag: FIELDS_TO_DISCOGS_KEYS[tag] + for tag in self.config["extra_tags"].as_str_seq() + if tag in FIELDS_TO_DISCOGS_KEYS + } + if field_by_tag: + self._log.debug( + "Discogs additional search filters from tags: {}", field_by_tag + ) + + return field_by_tag + def setup(self, session=None) -> None: """Create the `discogs_client` field. Authenticate if necessary.""" c_key = self.config["apikey"].as_str() @@ -256,7 +285,26 @@ class DiscogsPlugin(SearchApiMetadataSourcePlugin[IDResponse]): # can also negate an otherwise positive result. query = re.sub(r"(?i)\b(CD|disc|vinyl)\s*\d+", "", query) - return query, {"type": "release"} + filters: dict[str, str] = {"type": "release"} + + for tag, api_field in self.extra_discogs_field_by_tag.items(): + # The Discogs search API does not provide direct equivalents for + # MusicBrainz "alias" or "tracks" search fields, so we ignore + # those tags if configured. + if tag in {"alias", "tracks"}: + continue + + most_common, _count = util.plurality(item.get(tag) for item in items) + if most_common is None: + continue + + value = str(most_common) + if tag == "catalognum": + value = value.replace(" ", "") + + filters[api_field] = value + + return query, filters def get_search_response(self, params: SearchParams) -> Sequence[IDResponse]: """Search Discogs releases and return raw result mappings with IDs.""" diff --git a/docs/plugins/discogs.rst b/docs/plugins/discogs.rst index f88685720..1c5a8ae6a 100644 --- a/docs/plugins/discogs.rst +++ b/docs/plugins/discogs.rst @@ -80,6 +80,7 @@ Default separator: ', ' strip_disambiguation: yes featured_string: Feat. + extra_tags: [] anv: artist_credit: yes artist: no @@ -144,6 +145,31 @@ Default Configure the string used for noting featured artists. Useful if you prefer ``Featuring`` or ``ft.``. +.. conf:: extra_tags + :default: [] + + By default, beets will only use the artist and album information to query Discogs. Additional tags to be queried can be supplied with the + ``extra_tags`` setting. + + This setting should improve the autotagger results if the metadata with the + given tags match the metadata returned by Discogs. + + Tags supported by this setting: + + * ``barcode`` + * ``catalognum`` + * ``country`` + * ``label`` + * ``media`` + * ``year`` + + Example: + + .. code-block:: yaml + + discogs: + extra_tags: [barcode, catalognum, country, label, media, year] + .. conf:: anv This configuration option is dedicated to handling Artist Name diff --git a/test/plugins/test_discogs.py b/test/plugins/test_discogs.py index cef84e3a9..426690c24 100644 --- a/test/plugins/test_discogs.py +++ b/test/plugins/test_discogs.py @@ -19,6 +19,7 @@ from unittest.mock import Mock, patch import pytest from beets import config +from beets.library import Item from beets.test._common import Bag from beets.test.helper import BeetsTestCase, capture_log from beetsplug.discogs import ArtistState, DiscogsPlugin @@ -357,6 +358,41 @@ class DGAlbumInfoTest(BeetsTestCase): assert d is None assert "Release does not contain the required fields" in logs[0] + +@patch("beetsplug.discogs.DiscogsPlugin.setup", Mock()) +class DGSearchQueryTest(BeetsTestCase): + def test_default_search_filters_without_extra_tags(self): + """Discogs search uses only the type filter when no extra_tags are set.""" + plugin = DiscogsPlugin() + items = [Item()] + + query, filters = plugin.get_search_query_with_filters( + "album", items, "Artist", "Album", False + ) + + assert "Album" in query + assert filters == {"type": "release"} + + def test_extra_tags_populate_discogs_filters(self): + """Configured extra_tags should populate Discogs search filters.""" + config["discogs"]["extra_tags"] = ["label", "catalognum"] + plugin = DiscogsPlugin() + + items = [ + Item(catalognum="ABC 123", label="abc"), + Item(catalognum="ABC 123", label="abc"), + Item(catalognum="ABC 123", label="def"), + ] + + _query, filters = plugin.get_search_query_with_filters( + "album", items, "Artist", "Album", False + ) + + assert filters["type"] == "release" + assert filters["label"] == "abc" + # Catalog number should have whitespace removed. + assert filters["catno"] == "ABC123" + def test_default_genre_style_settings(self): """Test genre default settings, genres to genre, styles to style""" release = self._make_release_from_positions(["1", "2"])