mirror of
https://github.com/navidrome/navidrome.git
synced 2025-12-23 23:18:05 -05:00
* feat(deezer): add functions to fetch related artists, biographies, and top tracks for an artist Signed-off-by: Deluan <deluan@navidrome.org> * feat(deezer): add language support for Deezer API client Signed-off-by: Deluan <deluan@navidrome.org> * fix(deezer): Use GraphQL API for translated biographies The previous implementation scraped the __DZR_APP_STATE__ from HTML, which only contained English content. The actual biography displayed on Deezer's website comes from their GraphQL API at pipe.deezer.com, which properly respects the Accept-Language header and returns translated content. This change: - Switches from HTML scraping to the GraphQL API - Uses Accept-Language header instead of URL path for language - Updates tests to match the new implementation - Removes unused HTML fixture file Signed-off-by: Deluan <deluan@navidrome.org> * refactor(deezer): move JWT token handling to a separate file for better organization Signed-off-by: Deluan <deluan@navidrome.org> * feat(deezer): enhance JWT token handling with expiration validation Signed-off-by: Deluan <deluan@navidrome.org> * refactor(deezer): change log level for unknown agent warnings from Warn to Debug Signed-off-by: Deluan <deluan@navidrome.org> * fix(deezer): reduce JWT token expiration buffer from 10 minutes to 1 minute Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
149 lines
3.6 KiB
Go
149 lines
3.6 KiB
Go
package deezer
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/consts"
|
|
"github.com/navidrome/navidrome/core/agents"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/utils/cache"
|
|
"github.com/navidrome/navidrome/utils/slice"
|
|
)
|
|
|
|
const deezerAgentName = "deezer"
|
|
const deezerApiPictureXlSize = 1000
|
|
const deezerApiPictureBigSize = 500
|
|
const deezerApiPictureMediumSize = 250
|
|
const deezerApiPictureSmallSize = 56
|
|
const deezerArtistSearchLimit = 50
|
|
|
|
type deezerAgent struct {
|
|
dataStore model.DataStore
|
|
client *client
|
|
}
|
|
|
|
func deezerConstructor(dataStore model.DataStore) agents.Interface {
|
|
agent := &deezerAgent{dataStore: dataStore}
|
|
httpClient := &http.Client{
|
|
Timeout: consts.DefaultHttpClientTimeOut,
|
|
}
|
|
cachedHttpClient := cache.NewHTTPClient(httpClient, consts.DefaultHttpClientTimeOut)
|
|
agent.client = newClient(cachedHttpClient, conf.Server.Deezer.Language)
|
|
return agent
|
|
}
|
|
|
|
func (s *deezerAgent) AgentName() string {
|
|
return deezerAgentName
|
|
}
|
|
|
|
func (s *deezerAgent) GetArtistImages(ctx context.Context, _, name, _ string) ([]agents.ExternalImage, error) {
|
|
artist, err := s.searchArtist(ctx, name)
|
|
if err != nil {
|
|
if errors.Is(err, agents.ErrNotFound) {
|
|
log.Warn(ctx, "Artist not found in deezer", "artist", name)
|
|
} else {
|
|
log.Error(ctx, "Error calling deezer", "artist", name, err)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
var res []agents.ExternalImage
|
|
possibleImages := []struct {
|
|
URL string
|
|
Size int
|
|
}{
|
|
{artist.PictureXl, deezerApiPictureXlSize},
|
|
{artist.PictureBig, deezerApiPictureBigSize},
|
|
{artist.PictureMedium, deezerApiPictureMediumSize},
|
|
{artist.PictureSmall, deezerApiPictureSmallSize},
|
|
}
|
|
for _, imgData := range possibleImages {
|
|
if imgData.URL != "" {
|
|
res = append(res, agents.ExternalImage{
|
|
URL: imgData.URL,
|
|
Size: imgData.Size,
|
|
})
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (s *deezerAgent) searchArtist(ctx context.Context, name string) (*Artist, error) {
|
|
artists, err := s.client.searchArtists(ctx, name, deezerArtistSearchLimit)
|
|
if errors.Is(err, ErrNotFound) || len(artists) == 0 {
|
|
return nil, agents.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If the first one has the same name, that's the one
|
|
if !strings.EqualFold(artists[0].Name, name) {
|
|
return nil, agents.ErrNotFound
|
|
}
|
|
return &artists[0], err
|
|
}
|
|
|
|
func (s *deezerAgent) GetSimilarArtists(ctx context.Context, _, name, _ string, limit int) ([]agents.Artist, error) {
|
|
artist, err := s.searchArtist(ctx, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
related, err := s.client.getRelatedArtists(ctx, artist.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := slice.Map(related, func(r Artist) agents.Artist {
|
|
return agents.Artist{
|
|
Name: r.Name,
|
|
}
|
|
})
|
|
if len(res) > limit {
|
|
res = res[:limit]
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (s *deezerAgent) GetArtistTopSongs(ctx context.Context, _, artistName, _ string, count int) ([]agents.Song, error) {
|
|
artist, err := s.searchArtist(ctx, artistName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tracks, err := s.client.getTopTracks(ctx, artist.ID, count)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := slice.Map(tracks, func(r Track) agents.Song {
|
|
return agents.Song{
|
|
Name: r.Title,
|
|
}
|
|
})
|
|
return res, nil
|
|
}
|
|
|
|
func (s *deezerAgent) GetArtistBiography(ctx context.Context, _, name, _ string) (string, error) {
|
|
artist, err := s.searchArtist(ctx, name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return s.client.getArtistBio(ctx, artist.ID)
|
|
}
|
|
|
|
func init() {
|
|
conf.AddHook(func() {
|
|
if conf.Server.Deezer.Enabled {
|
|
agents.Register(deezerAgentName, deezerConstructor)
|
|
}
|
|
})
|
|
}
|