From f61b4eee210d692f50d37d3bad5b3714e28a094d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sat, 18 Jul 2026 16:40:10 -0400 Subject: [PATCH] fix(deezer): pick most-popular artist among same-name matches (#5808) * fix(deezer): pick most-popular artist among same-name matches The Deezer agent searched with order=RANKING and always took the top-ranked result (artists[0]) as long as its name matched. Deezer's RANKING order isn't reliable for homonyms, so for names shared by several artists (e.g. "Queen") it locked onto a low-popularity artist whose Top Tracks are empty, leaving getTopSongs empty. Among the exact-name matches, select the one with the highest fan count instead. This resolves "Queen" to the real band (Deezer ID 412) and preserves the existing ErrNotFound behavior when nothing matches the name exactly. Fixes #5802 * fix(deezer): improve artist disambiguation by ranking exact-case names Signed-off-by: Deluan --------- Signed-off-by: Deluan --- adapters/deezer/deezer.go | 31 +++++++++++++++--- adapters/deezer/deezer_test.go | 60 ++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/adapters/deezer/deezer.go b/adapters/deezer/deezer.go index ed3071766..d8e832cf1 100644 --- a/adapters/deezer/deezer.go +++ b/adapters/deezer/deezer.go @@ -1,10 +1,12 @@ package deezer import ( + "cmp" "context" "errors" "fmt" "net/http" + "slices" "strings" "github.com/navidrome/navidrome/conf" @@ -95,13 +97,32 @@ func (s *deezerAgent) searchArtist(ctx context.Context, name string) (*Artist, e } } - // If the first one has the same name, that's the one - if !strings.EqualFold(artists[0].Name, name) { - log.Trace(ctx, "Top artist do not match", "searched_name", name, "found_name", artists[0].Name) + // Deezer's RANKING order isn't reliable for homonyms: rank name matches + // ahead of non-matches, prefer an exact-case match, then the most fans. + rank := func(a Artist) int { + switch { + case a.Name == name: + return 2 + case strings.EqualFold(a.Name, name): + return 1 + default: + return 0 + } + } + slices.SortFunc(artists, func(a, b Artist) int { + return cmp.Or( + cmp.Compare(rank(b), rank(a)), + cmp.Compare(b.NbFan, a.NbFan), + cmp.Compare(a.ID, b.ID), + ) + }) + best := artists[0] + if !strings.EqualFold(best.Name, name) { + log.Trace(ctx, "No artist matched the searched name", "searched_name", name, "found_name", artists[0].Name) return nil, agents.ErrNotFound } - log.Trace(ctx, "Found artist", "name", artists[0].Name, "id", artists[0].ID, "link", artists[0].Link) - return &artists[0], err + log.Trace(ctx, "Found artist", "name", best.Name, "id", best.ID, "link", best.Link, "nb_fan", best.NbFan) + return new(best), nil } func (s *deezerAgent) GetSimilarArtists(ctx context.Context, _, name, _ string, limit int) ([]agents.Artist, error) { diff --git a/adapters/deezer/deezer_test.go b/adapters/deezer/deezer_test.go index 4dd251585..f478af115 100644 --- a/adapters/deezer/deezer_test.go +++ b/adapters/deezer/deezer_test.go @@ -34,6 +34,66 @@ var _ = Describe("deezerAgent", func() { }) }) + Describe("searchArtist", func() { + var agent *deezerAgent + var httpClient *fakeHttpClient + + BeforeEach(func() { + httpClient = &fakeHttpClient{} + agent = &deezerAgent{ + dataStore: &tests.MockDataStore{}, + client: newClient(httpClient), + } + }) + + It("picks the exact-name match with the most fans when several share the name", func() { + // Deezer RANKING order returns a low-popularity homonym first (see issue #5802) + httpClient.mock("https://api.deezer.com/search/artist", http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`{"data":[ + {"id":61045802,"name":"Queen","nb_fan":75}, + {"id":141954732,"name":"Queen","nb_fan":397}, + {"id":135041032,"name":"Queen(Ares)","nb_fan":133}, + {"id":183179807,"name":"Queen","nb_fan":53}, + {"id":412,"name":"Queen","nb_fan":12744378} + ],"total":5}`)), + }) + + artist, err := agent.searchArtist(ctx, "Queen") + + Expect(err).ToNot(HaveOccurred()) + Expect(artist.ID).To(Equal(412)) + }) + + It("matches the name case-insensitively", func() { + httpClient.mock("https://api.deezer.com/search/artist", http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`{"data":[ + {"id":1,"name":"QUEEN","nb_fan":10}, + {"id":2,"name":"queen","nb_fan":20} + ],"total":2}`)), + }) + + artist, err := agent.searchArtist(ctx, "Queen") + + Expect(err).ToNot(HaveOccurred()) + Expect(artist.ID).To(Equal(2)) + }) + + It("returns ErrNotFound when no result matches the name exactly", func() { + httpClient.mock("https://api.deezer.com/search/artist", http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`{"data":[ + {"id":1,"name":"Queens of the Stone Age","nb_fan":100} + ],"total":1}`)), + }) + + _, err := agent.searchArtist(ctx, "Queen") + + Expect(err).To(MatchError(agents.ErrNotFound)) + }) + }) + Describe("GetArtistBiography - Language Fallback", func() { var agent *deezerAgent var httpClient *langAwareHttpClient