Moved construct search into SearchApiMetadataSource to dedupe some

deezer and spotify functionalities.
This commit is contained in:
Sebastian Mohr 2025-07-17 17:08:34 +02:00
parent e208d4bee3
commit f81684e188
5 changed files with 53 additions and 53 deletions

View file

@ -14,6 +14,7 @@ import warnings
from typing import TYPE_CHECKING, Generic, Literal, Sequence, TypedDict, TypeVar
from typing_extensions import NotRequired
import unidecode
from beets.util import cached_classproperty
from beets.util.id_extractors import extract_release_id
@ -334,6 +335,14 @@ class SearchApiMetadataSourcePlugin(
of identifiers for the requested type (album or track).
"""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.config.add(
{
"search_query_ascii": False,
}
)
@abc.abstractmethod
def _search_api(
self,
@ -382,6 +391,34 @@ class SearchApiMetadataSourcePlugin(
self.tracks_for_ids([result["id"] for result in results if result]),
)
def _construct_search_query(
self, filters: SearchFilter, keywords: str = ""
) -> str:
"""Construct a query string with the specified filters and keywords to
be provided to the Spotify (or similar) Search API.
At the moment, this is used to construct a query string for:
- Spotify (https://developer.spotify.com/documentation/web-api/reference/search).
- Deezer (https://developers.deezer.com/api/search).
:param filters: Field filters to apply.
:param keywords: Query keywords to use.
:return: Query string to be provided to the Search API.
"""
query_components = [
keywords,
" ".join(f'{k}:"{v}"' for k, v in filters.items()),
]
query = " ".join([q for q in query_components if q])
if not isinstance(query, str):
query = query.decode("utf8")
if self.config["search_query_ascii"].get():
query = unidecode.unidecode(query)
return query
# Dynamically copy methods to BeetsPlugin for legacy support
# TODO: Remove this in the future major release, v3.0.0

View file

@ -21,7 +21,6 @@ import time
from typing import TYPE_CHECKING, Literal, Sequence
import requests
import unidecode
from beets import ui
from beets.autotag import AlbumInfo, TrackInfo
@ -216,27 +215,6 @@ class DeezerPlugin(SearchApiMetadataSourcePlugin[IDResponse]):
deezer_updated=time.time(),
)
@staticmethod
def _construct_search_query(
filters: SearchFilter, keywords: str = ""
) -> str:
"""Construct a query string with the specified filters and keywords to
be provided to the Deezer Search API
(https://developers.deezer.com/api/search).
:param filters: Field filters to apply.
:param keywords: (Optional) Query keywords to use.
:return: Query string to be provided to the Search API.
"""
query_components = [
keywords,
" ".join(f'{k}:"{v}"' for k, v in filters.items()),
]
query = " ".join([q for q in query_components if q])
if not isinstance(query, str):
query = query.decode("utf8")
return unidecode.unidecode(query)
def _search_api(
self,
query_type: Literal[

View file

@ -29,7 +29,6 @@ from typing import TYPE_CHECKING, Any, Literal, Sequence, Union
import confuse
import requests
import unidecode
from beets import ui
from beets.autotag.hooks import AlbumInfo, TrackInfo
@ -139,7 +138,6 @@ class SpotifyPlugin(
"client_id": "4e414367a1d14c75a5c5129a627fcab8",
"client_secret": "f82bdc09b2254f1a8286815d02fd46dc",
"tokenfile": "spotify_token.json",
"search_query_ascii": False,
}
)
self.config["client_id"].redact = True
@ -422,31 +420,6 @@ class SpotifyPlugin(
track.medium_total = medium_total
return track
def _construct_search_query(
self, filters: SearchFilter, keywords: str = ""
) -> str:
"""Construct a query string with the specified filters and keywords to
be provided to the Spotify Search API
(https://developer.spotify.com/documentation/web-api/reference/search).
:param filters: (Optional) Field filters to apply.
:param keywords: (Optional) Query keywords to use.
:return: Query string to be provided to the Search API.
"""
query_components = [
keywords,
" ".join(f"{k}:{v}" for k, v in filters.items()),
]
query = " ".join([q for q in query_components if q])
if not isinstance(query, str):
query = query.decode("utf8")
if self.config["search_query_ascii"].get():
query = unidecode.unidecode(query)
return query
def _search_api(
self,
query_type: Literal["album", "track"],

View file

@ -57,6 +57,11 @@ Bug fixes:
:bug:`5930`
- :doc:`plugins/chroma`: AcoustID lookup HTTP requests will now time out after
10 seconds, rather than hanging the entire import process.
- :doc:`/plugins/deezer`: Fix the issue with that every query to deezer was
ascii encoded. This resulted in bad matches for queries that contained special
e.g. non latin characters as 盗作. If you want to keep the legacy behavior
set the config option ``deezer.search_query_ascii: yes``.
:bug:`5860`
For packagers:

View file

@ -29,7 +29,14 @@ Configuration
This plugin can be configured like other metadata source plugins as described in
:ref:`metadata-source-plugin-configuration`.
The ``deezer`` plugin provides an additional command ``deezerupdate`` to update
the ``rank`` information from Deezer. The ``rank`` (ranges from 0 to 1M) is a
global indicator of a song's popularity on Deezer that is updated daily based on
streams. The higher the ``rank``, the more popular the track is.
The default options should work as-is, but there are some options you can put
in config.yaml under the ``deezer:`` section:
- **search_query_ascii**: If set to ``yes``, the search query will be converted to
ASCII before being sent to Deezer. Converting searches to ASCII can
enhance search results in some cases, but in general, it is not recommended.
For instance `artist:deadmau5 album:4×4` will be converted to
`artist:deadmau5 album:4x4` (notice `×!=x`).
Default: ``no``.
The ``deezer`` plugin provides an additional command ``deezerupdate`` to update the ``rank`` information from Deezer. The ``rank`` (ranges from 0 to 1M) is a global indicator of a song's popularity on Deezer that is updated daily based on streams. The higher the ``rank``, the more popular the track is.