Fix: Spotify plugin unable to recognize Chinese and Japanese albums. (#5705)

Fixes an issue where each spotify query was converted to ascii before sending. Adds a 
new config option to enable legacy behaviour.

A file called japanese_track_request.json was made to mimic the Spotify
API response since I don't have the credentials. Entries in that will
need to be modified with the actual entries.

Co-authored-by: Sebastian Mohr <sebastian@mohrenclan.de>
Co-authored-by: Sebastian Mohr <39738318+semohr@users.noreply.github.com>
Co-authored-by: J0J0 Todos <2733783+JOJ0@users.noreply.github.com>
This commit is contained in:
dhruvravii 2025-07-01 14:38:54 +05:30 committed by GitHub
parent a005941a56
commit dd6cb538ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 188 additions and 7 deletions

View file

@ -106,6 +106,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
"client_id": "4e414367a1d14c75a5c5129a627fcab8",
"client_secret": "f82bdc09b2254f1a8286815d02fd46dc",
"tokenfile": "spotify_token.json",
"search_query_ascii": False,
}
)
self.config["client_id"].redact = True
@ -388,9 +389,8 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
track.medium_total = medium_total
return track
@staticmethod
def _construct_search_query(
filters: dict[str, str], keywords: str = ""
self, filters: dict[str, str], keywords: str = ""
) -> str:
"""Construct a query string with the specified filters and keywords to
be provided to the Spotify Search API
@ -407,7 +407,11 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
query = " ".join([q for q in query_components if q])
if not isinstance(query, str):
query = query.decode("utf8")
return unidecode.unidecode(query)
if self.config["search_query_ascii"].get():
query = unidecode.unidecode(query)
return query
def _search_api(
self,
@ -424,6 +428,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
:param keywords: (Optional) Query keywords to use.
"""
query = self._construct_search_query(keywords=keywords, filters=filters)
self._log.debug(f"Searching {self.data_source} for '{query}'")
try:
response = self._handle_response(
@ -560,6 +565,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
query = self._construct_search_query(
keywords=keywords, filters=query_filters
)
failures.append(query)
continue

View file

@ -39,7 +39,12 @@ Bug fixes:
:bug:`5797`
* :doc:`plugins/musicbrainz`: Fix the MusicBrainz search not taking into
account the album/recording aliases
* :doc:`/plugins/spotify`: Fix the issue with that every query to spotify 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 ``spotify.search_query_ascii: yes``.
:bug:`5699`
For packagers:
* Optional ``extra_tags`` parameter has been removed from

View file

@ -83,6 +83,13 @@ in config.yaml under the ``spotify:`` section:
track/album/artist fields before sending them to Spotify. Can be useful for
changing certain abbreviations, like ft. -> feat. See the examples below.
Default: None.
- **search_query_ascii**: If set to ``yes``, the search query will be converted to
ASCII before being sent to Spotify. 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``.
Here's an example::
@ -92,6 +99,7 @@ Here's an example::
region_filter: US
show_failures: on
tiebreak: first
search_query_ascii: no
regex: [
{

View file

@ -7,7 +7,7 @@ import responses
from beets.library import Item
from beets.test import _common
from beets.test.helper import BeetsTestCase
from beets.test.helper import PluginTestCase
from beetsplug import spotify
@ -23,10 +23,11 @@ def _params(url):
return parse_qs(urlparse(url).query)
class SpotifyPluginTest(BeetsTestCase):
class SpotifyPluginTest(PluginTestCase):
plugin = "spotify"
@responses.activate
def setUp(self):
super().setUp()
responses.add(
responses.POST,
spotify.SpotifyPlugin.oauth_token_url,
@ -39,6 +40,7 @@ class SpotifyPluginTest(BeetsTestCase):
"scope": "",
},
)
super().setUp()
self.spotify = spotify.SpotifyPlugin()
opts = ArgumentsMock("list", False)
self.spotify._parse_opts(opts)
@ -176,3 +178,74 @@ class SpotifyPluginTest(BeetsTestCase):
results = self.spotify._match_library_tracks(self.lib, "Happy")
assert 1 == len(results)
assert "6NPVjNh8Jhru9xOmyQigds" == results[0]["id"]
@responses.activate
def test_japanese_track(self):
"""Ensure non-ASCII characters remain unchanged in search queries"""
# Path to the mock JSON file for the Japanese track
json_file = os.path.join(
_common.RSRC, b"spotify", b"japanese_track_request.json"
)
# Load the mock JSON response
with open(json_file, "rb") as f:
response_body = f.read()
# Mock Spotify Search API response
responses.add(
responses.GET,
spotify.SpotifyPlugin.search_url,
body=response_body,
status=200,
content_type="application/json",
)
# Create a mock item with Japanese metadata
item = Item(
mb_trackid="56789",
album="盗作",
albumartist="ヨルシカ",
title="思想犯",
length=10,
)
item.add(self.lib)
# Search without ascii encoding
with self.configure_plugin(
{
"search_query_ascii": False,
}
):
assert self.spotify.config["search_query_ascii"].get() is False
# Call the method to match library tracks
results = self.spotify._match_library_tracks(self.lib, item.title)
# Assertions to verify results
assert results is not None
assert 1 == len(results)
assert results[0]["name"] == item.title
assert results[0]["artists"][0]["name"] == item.albumartist
assert results[0]["album"]["name"] == item.album
# Verify search query parameters
params = _params(responses.calls[0].request.url)
query = params["q"][0]
assert item.title in query
assert f"artist:{item.albumartist}" in query
assert f"album:{item.album}" in query
assert not query.isascii()
# Is not found in the library if ascii encoding is enabled
with self.configure_plugin(
{
"search_query_ascii": True,
}
):
assert self.spotify.config["search_query_ascii"].get() is True
results = self.spotify._match_library_tracks(self.lib, item.title)
params = _params(responses.calls[1].request.url)
query = params["q"][0]
assert query.isascii()

View file

@ -0,0 +1,89 @@
{
"tracks":{
"href":"https://api.spotify.com/v1/search?query=Happy+album%3ADespicable+Me+2+artist%3APharrell+Williams&offset=0&limit=20&type=track",
"items":[
{
"album":{
"album_type":"compilation",
"available_markets":[
"AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA",
"CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO",
"EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK",
"HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV",
"MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA",
"PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI",
"SK", "SV", "TR", "TW", "US", "UY"
],
"external_urls":{
"spotify":"https://open.spotify.com/album/5l3zEmMrOhOzG8d8s83GOL"
},
"href":"https://api.spotify.com/v1/albums/5l3zEmMrOhOzG8d8s83GOL",
"id":"5l3zEmMrOhOzG8d8s83GOL",
"images":[
{
"height":640,
"width":640,
"url":"https://i.scdn.co/image/cb7905340c132365bbaee3f17498f062858382e8"
},
{
"height":300,
"width":300,
"url":"https://i.scdn.co/image/af369120f0b20099d6784ab31c88256113f10ffb"
},
{
"height":64,
"width":64,
"url":"https://i.scdn.co/image/9dad385ddf2e7db0bef20cec1fcbdb08689d9ae8"
}
],
"name":"盗作",
"type":"album",
"uri":"spotify:album:5l3zEmMrOhOzG8d8s83GOL"
},
"artists":[
{
"external_urls":{
"spotify":"https://open.spotify.com/artist/2RdwBSPQiwcmiDo9kixcl8"
},
"href":"https://api.spotify.com/v1/artists/2RdwBSPQiwcmiDo9kixcl8",
"id":"2RdwBSPQiwcmiDo9kixcl8",
"name":"ヨルシカ",
"type":"artist",
"uri":"spotify:artist:2RdwBSPQiwcmiDo9kixcl8"
}
],
"available_markets":[
"AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA",
"CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO",
"EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK",
"HN", "HU", "IE", "IS", "IT", "LI", "LT", "LU", "LV",
"MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA",
"PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SI",
"SK", "SV", "TR", "TW", "US", "UY"
],
"disc_number":1,
"duration_ms":233305,
"explicit":false,
"external_ids":{
"isrc":"USQ4E1300686"
},
"external_urls":{
"spotify":"https://open.spotify.com/track/6NPVjNh8Jhru9xOmyQigds"
},
"href":"https://api.spotify.com/v1/tracks/6NPVjNh8Jhru9xOmyQigds",
"id":"6NPVjNh8Jhru9xOmyQigds",
"name":"思想犯",
"popularity":89,
"preview_url":"https://p.scdn.co/mp3-preview/6b00000be293e6b25f61c33e206a0c522b5cbc87",
"track_number":4,
"type":"track",
"uri":"spotify:track:6NPVjNh8Jhru9xOmyQigds"
}
],
"limit":20,
"next":null,
"offset":0,
"previous":null,
"total":1
}
}