From 1271b711f75ab7f576987f9516cc4908d71c6491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sat, 7 Feb 2026 22:26:17 +0000 Subject: [PATCH 1/5] Format MusicBrainz search terms and escape Lucene special chars Add a helper to lower/strip and escape Lucene query syntax. Use it when building search queries and add unit tests. --- beetsplug/_utils/musicbrainz.py | 27 +++++++++++++++++++++----- docs/changelog.rst | 2 ++ test/plugins/utils/test_musicbrainz.py | 14 +++++++++++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/beetsplug/_utils/musicbrainz.py b/beetsplug/_utils/musicbrainz.py index 2fc821df9..bb278b954 100644 --- a/beetsplug/_utils/musicbrainz.py +++ b/beetsplug/_utils/musicbrainz.py @@ -11,9 +11,10 @@ logic throughout the codebase. from __future__ import annotations import operator +import re from dataclasses import dataclass, field from functools import cached_property, singledispatchmethod, wraps -from itertools import groupby +from itertools import groupby, starmap from typing import TYPE_CHECKING, Any, Literal, ParamSpec, TypedDict, TypeVar from requests_ratelimiter import LimiterMixin @@ -33,6 +34,9 @@ if TYPE_CHECKING: log = logging.getLogger(__name__) +LUCENE_SPECIAL_CHAR_PAT = re.compile(r'([-+&|!(){}[\]^"~*?:\\/])') + + class LimiterTimeoutSession(LimiterMixin, TimeoutAndRetrySession): """HTTP session that enforces rate limits.""" @@ -181,6 +185,21 @@ class MusicBrainzAPI(RequestHandler): def _browse(self, entity: Entity, **kwargs) -> list[JSONDict]: return self._get_resource(entity, **kwargs).get(f"{entity}s", []) + @staticmethod + def format_search_term(field: str, term: str) -> str: + """Format a search term for the MusicBrainz API. + + See https://lucene.apache.org/core/4_3_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html + """ + if not (term := term.lower().strip()): + return "" + + term = LUCENE_SPECIAL_CHAR_PAT.sub(r"\\\1", term) + if field: + term = f"{field}:({term})" + + return term + def search( self, entity: Entity, @@ -195,10 +214,8 @@ class MusicBrainzAPI(RequestHandler): - 'value' is empty, in which case the filter is ignored * Values are lowercased and stripped of whitespace. """ - query = " AND ".join( - ":".join(filter(None, (k, f'"{_v}"'))) - for k, v in filters.items() - if (_v := v.lower().strip()) + query = " ".join( + filter(None, starmap(self.format_search_term, filters.items())) ) log.debug("Searching for MusicBrainz {}s with: {!r}", entity, query) kwargs["query"] = query diff --git a/docs/changelog.rst b/docs/changelog.rst index 25a0c1365..bd2243d67 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,8 @@ New features: Bug fixes: +- :doc:`plugins/musicbrainz`: Fix search terms escaping. :bug:`6347` + For packagers: Other changes: diff --git a/test/plugins/utils/test_musicbrainz.py b/test/plugins/utils/test_musicbrainz.py index 291f50eb5..c7363f516 100644 --- a/test/plugins/utils/test_musicbrainz.py +++ b/test/plugins/utils/test_musicbrainz.py @@ -1,3 +1,5 @@ +import pytest + from beetsplug._utils.musicbrainz import MusicBrainzAPI @@ -80,3 +82,15 @@ def test_group_relations(): }, ], } + + +@pytest.mark.parametrize( + "field, term, expected", + [ + ("artist", ' AC/DC + "[Live]" ', r"artist:(ac\/dc \+ \"\[live\]\")"), + ("", "Foo:Bar", r"foo\:bar"), + ("artist", " ", ""), + ], +) +def test_format_search_term(field, term, expected): + assert MusicBrainzAPI.format_search_term(field, term) == expected From aeee7b6da4d863647d7d7e50c8fb53afa19aa420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sat, 7 Feb 2026 22:26:52 +0000 Subject: [PATCH 2/5] Musicbrainz: Fix support for alias, tracks extra tags --- beetsplug/musicbrainz.py | 2 ++ docs/changelog.rst | 2 ++ docs/plugins/musicbrainz.rst | 14 +++++++++++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/beetsplug/musicbrainz.py b/beetsplug/musicbrainz.py index aac20e9ac..cceb1f05f 100644 --- a/beetsplug/musicbrainz.py +++ b/beetsplug/musicbrainz.py @@ -56,6 +56,8 @@ FIELDS_TO_MB_KEYS = { "label": "label", "media": "format", "year": "date", + "tracks": "tracks", + "alias": "alias", } diff --git a/docs/changelog.rst b/docs/changelog.rst index bd2243d67..36166b07a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,8 @@ New features: Bug fixes: - :doc:`plugins/musicbrainz`: Fix search terms escaping. :bug:`6347` +- :doc:`plugins/musicbrainz`: Fix support for ``alias`` and ``tracks`` + :conf:`plugins.musicbrainz:extra_tags`. For packagers: diff --git a/docs/plugins/musicbrainz.rst b/docs/plugins/musicbrainz.rst index 60c3bc4a2..2baa36776 100644 --- a/docs/plugins/musicbrainz.rst +++ b/docs/plugins/musicbrainz.rst @@ -93,15 +93,23 @@ Default This setting should improve the autotagger results if the metadata with the given tags match the metadata returned by MusicBrainz. - Note that the only tags supported by this setting are: ``barcode``, - ``catalognum``, ``country``, ``label``, ``media``, and ``year``. + Tags supported by this setting: + + * ``alias`` (also search for release aliases matching the query) + * ``barcode`` + * ``catalognum`` + * ``country`` + * ``label`` + * ``media`` + * ``tracks`` (number of tracks on the release) + * ``year`` Example: .. code-block:: yaml musicbrainz: - extra_tags: [barcode, catalognum, country, label, media, year] + extra_tags: [alias, barcode, catalognum, country, label, media, tracks, year] .. conf:: genres :default: no From 84b22fddd5b48e776dfb9ce4dcc0202955e692cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sat, 7 Feb 2026 22:28:00 +0000 Subject: [PATCH 3/5] Order mbcollection, mbpseudo, mbsubmit after musicbrainz plugin in docs --- docs/plugins/index.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 1583ac5ab..0a461b857 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -100,15 +100,15 @@ databases. They share the following configuration options: listenbrainz loadext lyrics - mbcollection - mbpseudo - mbsubmit mbsync metasync missing mpdstats mpdupdate musicbrainz + mbcollection + mbpseudo + mbsubmit parentwork permissions play From df1573ce9db4a95596f17f5be95dcfab1e69a819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sat, 7 Feb 2026 22:36:43 +0000 Subject: [PATCH 4/5] Make MusicBrainzAPI logs visible --- beetsplug/_utils/musicbrainz.py | 2 +- beetsplug/bpd/__init__.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/beetsplug/_utils/musicbrainz.py b/beetsplug/_utils/musicbrainz.py index bb278b954..57201e909 100644 --- a/beetsplug/_utils/musicbrainz.py +++ b/beetsplug/_utils/musicbrainz.py @@ -31,7 +31,7 @@ if TYPE_CHECKING: from .._typing import JSONDict -log = logging.getLogger(__name__) +log = logging.getLogger("beets") LUCENE_SPECIAL_CHAR_PAT = re.compile(r'([-+&|!(){}[\]^"~*?:\\/])') diff --git a/beetsplug/bpd/__init__.py b/beetsplug/bpd/__init__.py index 30126f370..9496e9a78 100644 --- a/beetsplug/bpd/__init__.py +++ b/beetsplug/bpd/__init__.py @@ -30,7 +30,7 @@ from typing import TYPE_CHECKING, ClassVar import beets import beets.ui -from beets import dbcore, logging +from beets import dbcore from beets.library import Item from beets.plugins import BeetsPlugin from beets.util import as_string, bluelet @@ -39,8 +39,6 @@ from beetsplug._utils import vfs if TYPE_CHECKING: from beets.dbcore.query import Query -log = logging.getLogger(__name__) - try: from . import gstplayer From 8f81e1d9138fa8a1300c82fa0c6b8299acdad9bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sat, 7 Feb 2026 22:41:17 +0000 Subject: [PATCH 5/5] Update ownership --- .github/CODEOWNERS | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fe4ce3378..ca727391a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,5 +3,11 @@ # Specific ownerships: /beets/metadata_plugins.py @semohr + /beetsplug/titlecase.py @henry-oberholtzer + /beetsplug/mbpseudo.py @asardaes + +/beetsplug/_utils/requests.py @snejus +/beetsplug/_utils/musicbrainz.py @snejus +/beetsplug/musicbrainz.py @snejus