From 681f27feb69b9d662901f41712274a4a195259b3 Mon Sep 17 00:00:00 2001 From: Piotr Szpetkowski Date: Thu, 30 Jul 2026 21:18:59 +0200 Subject: [PATCH] fix(deezer): use free text for singleton searches Singleton searches built the query as ` artist:"<artist>"`. Deezer discards unquoted free text as soon as a query contains any field:"value" filter, so that was evaluated as `artist:"<artist>"` alone - every track by the artist, in Deezer's own relevance order, truncated to `search_limit` (default 5). Substituting nonsense for the title returns a byte-identical result set. For any artist with more releases than that window, the track being imported was simply never among the candidates offered. Filtering on the title as well (`track:"<title>" artist:"<artist>"`) is not a fix: `artist:` matches loosely enough to return unrelated artists, so the two filters can intersect to nothing even for a correctly tagged file. `track:"Get Lucky" artist:"Daft Punk"` returns zero results, while the plain free text `Get Lucky Daft Punk` returns the right track first. Measured over 12 tracks at the default `search_limit`, counting the wanted track appearing anywhere in the results: `<title> artist:"..."` 6/12, mean rank 1.50 `track:"..." artist:"..."` 7/12, mean rank 1.00 free text 10/12, mean rank 1.00 Album searches are unchanged; `album:"<name>"` has no equivalent problem. Adds test/plugins/test_deezer.py, which did not exist. --- beetsplug/deezer.py | 17 ++++++++++++--- docs/changelog.rst | 7 +++++++ test/plugins/test_deezer.py | 41 +++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 test/plugins/test_deezer.py diff --git a/beetsplug/deezer.py b/beetsplug/deezer.py index 65d98f5d0e..6ea1726313 100644 --- a/beetsplug/deezer.py +++ b/beetsplug/deezer.py @@ -215,9 +215,20 @@ def get_search_query_with_filters( name: str, va_likely: bool, ) -> tuple[str, dict[str, str]]: - query = f'album:"{name}"' if query_type == "album" else name - if query_type == "track" or not va_likely: - query += f' artist:"{artist}"' + if query_type == "album": + query = f'album:"{name}"' + if not va_likely: + query += f' artist:"{artist}"' + else: + # Deezer drops unquoted free text as soon as the query carries any + # field:"value" filter, so `<title> artist:"<artist>"` degenerated + # into "every track by this artist", truncated to `search_limit`. + # The wanted track routinely fell outside that window. Filtering on + # the title instead is no better, because `artist:` is fuzzy enough + # to match unrelated artists ("Pan Da Punk" for "Daft Punk"), so the + # two filters can intersect to nothing even for a well-tagged file. + # Plain free text lets Deezer's own relevance ranking do the work. + query = f"{name} {artist}".strip() return query, {} diff --git a/docs/changelog.rst b/docs/changelog.rst index 8316ff9a3c..2702d108fe 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -30,6 +30,13 @@ Bug fixes - :doc:`plugins/lyrics`: ``beet lyrics`` no longer crashes with an ``AttributeError`` on tracks that have no stored lyrics when ``force`` is enabled; a missing lyrics body is now treated as empty text. :bug:`6860` +- :doc:`plugins/deezer`: Singleton searches now use plain free text rather than + ``<title> artist:"<artist>"``. Deezer discards unquoted free text as soon as a + query contains any ``field:"value"`` filter, so the old query was evaluated as + ``artist:"<artist>"`` alone -- every track by the artist, in Deezer's own + relevance order and truncated to ``search_limit``. For artists with more + releases than that window, the track being imported was never among the + candidates offered. .. For plugin developers diff --git a/test/plugins/test_deezer.py b/test/plugins/test_deezer.py new file mode 100644 index 0000000000..e62514824d --- /dev/null +++ b/test/plugins/test_deezer.py @@ -0,0 +1,41 @@ +import pytest + +from beets.library import Item +from beetsplug.deezer import DeezerPlugin + + +@pytest.fixture +def plugin(): + return DeezerPlugin() + + +class TestSearchQuery: + def test_track_query_is_free_text(self, plugin): + query, filters = plugin.get_search_query_with_filters( + "track", [Item()], "Artist", "Title", False + ) + + assert query == "Title Artist" + assert filters == {} + + def test_track_query_tolerates_missing_artist(self, plugin): + query, _ = plugin.get_search_query_with_filters( + "track", [Item()], "", "Title", False + ) + + assert query == "Title" + + def test_album_query_filters_on_album_and_artist(self, plugin): + query, filters = plugin.get_search_query_with_filters( + "album", [Item()], "Artist", "Album", False + ) + + assert query == 'album:"Album" artist:"Artist"' + assert filters == {} + + def test_album_query_omits_artist_for_various_artists(self, plugin): + query, _ = plugin.get_search_query_with_filters( + "album", [Item()], "Various Artists", "Album", True + ) + + assert query == 'album:"Album"'