Files
navidrome/adapters/deezer/deezer_test.go
Deluan Quintão 7b523d6b61 feat(agents): support multiple languages for Last.fm and Deezer metadata (#4952)
* feat(lastfm): support multiple languages for album and artist info retrieval

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(lastfm): improve content validation for album and artist descriptions

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor(lastfm): remove single language test and clarify languages field in configuration

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(deezer): support multiple languages for artist bio retrieval

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor(lastfm): rename ignoredBiographies to ignoredContent for clarity

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-01-29 13:05:51 -05:00

172 lines
5.4 KiB
Go

package deezer
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/core/agents"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("deezerAgent", func() {
var ctx context.Context
BeforeEach(func() {
ctx = context.Background()
DeferCleanup(configtest.SetupConfig())
conf.Server.Deezer.Enabled = true
})
Describe("deezerConstructor", func() {
It("uses configured languages", func() {
conf.Server.Deezer.Languages = []string{"pt", "en"}
agent := deezerConstructor(&tests.MockDataStore{}).(*deezerAgent)
Expect(agent.languages).To(Equal([]string{"pt", "en"}))
})
})
Describe("GetArtistBiography - Language Fallback", func() {
var agent *deezerAgent
var httpClient *langAwareHttpClient
BeforeEach(func() {
httpClient = newLangAwareHttpClient()
// Mock search artist (returns Michael Jackson)
fSearch, _ := os.Open("tests/fixtures/deezer.search.artist.json")
httpClient.searchResponse = &http.Response{Body: fSearch, StatusCode: 200}
// Mock JWT token
testJWT := createTestJWT(5 * time.Minute)
httpClient.jwtResponse = &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBufferString(fmt.Sprintf(`{"jwt":"%s","refresh_token":""}`, testJWT))),
}
})
setupAgent := func(languages []string) {
conf.Server.Deezer.Languages = languages
agent = &deezerAgent{
dataStore: &tests.MockDataStore{},
client: newClient(httpClient),
languages: languages,
}
}
It("returns content in first language when available (1 bio API call)", func() {
setupAgent([]string{"fr", "en"})
// French biography available
fFr, _ := os.Open("tests/fixtures/deezer.artist.bio.fr.json")
httpClient.bioResponses["fr"] = &http.Response{Body: fFr, StatusCode: 200}
bio, err := agent.GetArtistBiography(ctx, "", "Michael Jackson", "")
Expect(err).ToNot(HaveOccurred())
Expect(bio).To(ContainSubstring("Guy-Manuel de Homem Christo et Thomas Bangalter"))
Expect(httpClient.bioRequestCount).To(Equal(1))
Expect(httpClient.bioRequests[0].Header.Get("Accept-Language")).To(Equal("fr"))
})
It("falls back to second language when first returns empty (2 bio API calls)", func() {
setupAgent([]string{"ja", "en"})
// Japanese returns empty biography
fJa, _ := os.Open("tests/fixtures/deezer.artist.bio.empty.json")
httpClient.bioResponses["ja"] = &http.Response{Body: fJa, StatusCode: 200}
// English returns full biography
fEn, _ := os.Open("tests/fixtures/deezer.artist.bio.en.json")
httpClient.bioResponses["en"] = &http.Response{Body: fEn, StatusCode: 200}
bio, err := agent.GetArtistBiography(ctx, "", "Michael Jackson", "")
Expect(err).ToNot(HaveOccurred())
Expect(bio).To(ContainSubstring("Schoolmates Thomas and Guy-Manuel"))
Expect(httpClient.bioRequestCount).To(Equal(2))
Expect(httpClient.bioRequests[0].Header.Get("Accept-Language")).To(Equal("ja"))
Expect(httpClient.bioRequests[1].Header.Get("Accept-Language")).To(Equal("en"))
})
It("returns ErrNotFound when all languages return empty", func() {
setupAgent([]string{"ja", "xx"})
// Both languages return empty biography
fJa, _ := os.Open("tests/fixtures/deezer.artist.bio.empty.json")
httpClient.bioResponses["ja"] = &http.Response{Body: fJa, StatusCode: 200}
fXx, _ := os.Open("tests/fixtures/deezer.artist.bio.empty.json")
httpClient.bioResponses["xx"] = &http.Response{Body: fXx, StatusCode: 200}
_, err := agent.GetArtistBiography(ctx, "", "Michael Jackson", "")
Expect(err).To(MatchError(agents.ErrNotFound))
Expect(httpClient.bioRequestCount).To(Equal(2))
})
})
})
// langAwareHttpClient is a mock HTTP client that returns different responses based on the Accept-Language header
type langAwareHttpClient struct {
searchResponse *http.Response
jwtResponse *http.Response
bioResponses map[string]*http.Response
bioRequests []*http.Request
bioRequestCount int
}
func newLangAwareHttpClient() *langAwareHttpClient {
return &langAwareHttpClient{
bioResponses: make(map[string]*http.Response),
bioRequests: make([]*http.Request, 0),
}
}
func (c *langAwareHttpClient) Do(req *http.Request) (*http.Response, error) {
// Handle search artist request
if req.URL.Host == "api.deezer.com" && req.URL.Path == "/search/artist" {
if c.searchResponse != nil {
return c.searchResponse, nil
}
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBufferString(`{"data":[],"total":0}`)),
}, nil
}
// Handle JWT token request
if req.URL.Host == "auth.deezer.com" && req.URL.Path == "/login/anonymous" {
if c.jwtResponse != nil {
return c.jwtResponse, nil
}
return &http.Response{
StatusCode: 500,
Body: io.NopCloser(bytes.NewBufferString(`{"error":"no mock"}`)),
}, nil
}
// Handle bio request (GraphQL API)
if req.URL.Host == "pipe.deezer.com" && req.URL.Path == "/api" {
c.bioRequestCount++
c.bioRequests = append(c.bioRequests, req)
lang := req.Header.Get("Accept-Language")
if resp, ok := c.bioResponses[lang]; ok {
return resp, nil
}
// Return empty bio by default
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewBufferString(`{"data":{"artist":{"bio":{"full":""}}}}`)),
}, nil
}
panic("URL not mocked: " + req.URL.String())
}