From f81684e188c856283d06602b777a4ccc4818752d Mon Sep 17 00:00:00 2001 From: Sebastian Mohr Date: Thu, 17 Jul 2025 17:08:34 +0200 Subject: [PATCH] Moved construct search into SearchApiMetadataSource to dedupe some deezer and spotify functionalities. --- beets/metadata_plugins.py | 37 +++++++++++++++++++++++++++++++++++++ beetsplug/deezer.py | 22 ---------------------- beetsplug/spotify.py | 27 --------------------------- docs/changelog.rst | 5 +++++ docs/plugins/deezer.rst | 15 +++++++++++---- 5 files changed, 53 insertions(+), 53 deletions(-) diff --git a/beets/metadata_plugins.py b/beets/metadata_plugins.py index 9d69633d6..7e333a783 100644 --- a/beets/metadata_plugins.py +++ b/beets/metadata_plugins.py @@ -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 diff --git a/beetsplug/deezer.py b/beetsplug/deezer.py index 8815e3d59..abb7d80c4 100644 --- a/beetsplug/deezer.py +++ b/beetsplug/deezer.py @@ -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[ diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index fa5dc5c52..cdbed655e 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -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"], diff --git a/docs/changelog.rst b/docs/changelog.rst index 52d9cd89e..a9e85f9e0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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: diff --git a/docs/plugins/deezer.rst b/docs/plugins/deezer.rst index e58252e84..ec44a530f 100644 --- a/docs/plugins/deezer.rst +++ b/docs/plugins/deezer.rst @@ -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.