Fix mb search term formatting (#6354)

Fixes #6347

- Fixed MusicBrainz Lucene query formatting in
`MusicBrainzAPI.format_search_term()` (lowercase + trim + escape Lucene
special chars).
- Fixed `plugins.musicbrainz:extra_tags` support by mapping `alias` and
`tracks` into MusicBrainz search fields.
- Adjusted logging to make MusicBrainz API logs visible under the shared
`beets` logger (and removed an unused per-module logger in
`beetsplug.bpd`).
This commit is contained in:
Šarūnas Nejus 2026-02-08 07:20:11 +00:00 committed by GitHub
commit 3b89d722ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 64 additions and 15 deletions

6
.github/CODEOWNERS vendored
View file

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

View file

@ -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
@ -30,7 +31,10 @@ if TYPE_CHECKING:
from .._typing import JSONDict
log = logging.getLogger(__name__)
log = logging.getLogger("beets")
LUCENE_SPECIAL_CHAR_PAT = re.compile(r'([-+&|!(){}[\]^"~*?:\\/])')
class LimiterTimeoutSession(LimiterMixin, TimeoutAndRetrySession):
@ -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

View file

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

View file

@ -56,6 +56,8 @@ FIELDS_TO_MB_KEYS = {
"label": "label",
"media": "format",
"year": "date",
"tracks": "tracks",
"alias": "alias",
}

View file

@ -11,6 +11,10 @@ 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:
Other changes:

View file

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

View file

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

View file

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