Incomplete implementation of agents
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Constructor func(ctx context.Context) Interface
|
||||
|
||||
type Interface interface{}
|
||||
|
||||
type Artist struct {
|
||||
Name string
|
||||
MBID string
|
||||
}
|
||||
|
||||
type ArtistImage struct {
|
||||
URL string
|
||||
Size int
|
||||
}
|
||||
|
||||
type Track struct {
|
||||
Name string
|
||||
MBID string
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
||||
|
||||
type ArtistMBIDRetriever interface {
|
||||
GetMBID(name string) (string, error)
|
||||
}
|
||||
|
||||
type ArtistURLRetriever interface {
|
||||
GetURL(name, mbid string) (string, error)
|
||||
}
|
||||
|
||||
type ArtistBiographyRetriever interface {
|
||||
GetBiography(name, mbid string) (string, error)
|
||||
}
|
||||
|
||||
type ArtistSimilarRetriever interface {
|
||||
GetSimilar(name, mbid string, limit int) ([]Artist, error)
|
||||
}
|
||||
|
||||
type ArtistImageRetriever interface {
|
||||
GetImages(name, mbid string) ([]ArtistImage, error)
|
||||
}
|
||||
|
||||
type ArtistTopSongsRetriever interface {
|
||||
GetTopSongs(artistName, mbid string, count int) ([]Track, error)
|
||||
}
|
||||
|
||||
var Map map[string]Constructor
|
||||
|
||||
func Register(name string, init Constructor) {
|
||||
if Map == nil {
|
||||
Map = make(map[string]Constructor)
|
||||
}
|
||||
Map[name] = init
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/core/lastfm"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
)
|
||||
|
||||
type lastfmAgent struct {
|
||||
ctx context.Context
|
||||
apiKey string
|
||||
lang string
|
||||
client *lastfm.Client
|
||||
}
|
||||
|
||||
func lastFMConstructor(ctx context.Context) Interface {
|
||||
if conf.Server.LastFM.ApiKey == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
l := &lastfmAgent{
|
||||
ctx: ctx,
|
||||
apiKey: conf.Server.LastFM.ApiKey,
|
||||
lang: conf.Server.LastFM.Language,
|
||||
}
|
||||
l.client = lastfm.NewClient(l.apiKey, l.lang, http.DefaultClient)
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *lastfmAgent) GetMBID(name string) (string, error) {
|
||||
a, err := l.callArtistGetInfo(name, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if a.MBID == "" {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
return a.MBID, nil
|
||||
}
|
||||
|
||||
func (l *lastfmAgent) GetURL(name, mbid string) (string, error) {
|
||||
a, err := l.callArtistGetInfo(name, mbid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if a.URL == "" {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
return a.URL, nil
|
||||
|
||||
}
|
||||
|
||||
func (l *lastfmAgent) GetBiography(name, mbid string) (string, error) {
|
||||
a, err := l.callArtistGetInfo(name, mbid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if a.Bio.Summary == "" {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
return a.Bio.Summary, nil
|
||||
}
|
||||
|
||||
func (l *lastfmAgent) GetSimilar(name, mbid string, limit int) ([]Artist, error) {
|
||||
resp, err := l.callArtistGetSimilar(name, mbid, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(resp) == 0 {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
var res []Artist
|
||||
for _, a := range resp {
|
||||
res = append(res, Artist{
|
||||
Name: a.Name,
|
||||
MBID: a.MBID,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (l *lastfmAgent) GetTopSongs(artistName, mbid string, count int) ([]Track, error) {
|
||||
resp, err := l.callArtistGetTopTracks(artistName, mbid, count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(resp) == 0 {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
var res []Track
|
||||
for _, t := range resp {
|
||||
res = append(res, Track{
|
||||
Name: t.Name,
|
||||
MBID: t.MBID,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (l *lastfmAgent) callArtistGetInfo(name string, mbid string) (*lastfm.Artist, error) {
|
||||
a, err := l.client.ArtistGetInfo(l.ctx, name)
|
||||
if err != nil {
|
||||
log.Error(l.ctx, "Error calling LastFM/artist.getInfo", "artist", name, "mbid", mbid)
|
||||
return nil, err
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (l *lastfmAgent) callArtistGetSimilar(name string, mbid string, limit int) ([]lastfm.Artist, error) {
|
||||
s, err := l.client.ArtistGetSimilar(l.ctx, name, limit)
|
||||
if err != nil {
|
||||
log.Error(l.ctx, "Error calling LastFM/artist.getSimilar", "artist", name, "mbid", mbid)
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (l *lastfmAgent) callArtistGetTopTracks(artistName, mbid string, count int) ([]lastfm.Track, error) {
|
||||
t, err := l.client.ArtistGetTopTracks(l.ctx, artistName, count)
|
||||
if err != nil {
|
||||
log.Error(l.ctx, "Error calling LastFM/artist.getTopTracks", "artist", artistName, "mbid", mbid)
|
||||
return nil, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
conf.AddHook(func() {
|
||||
if conf.Server.LastFM.ApiKey != "" {
|
||||
log.Info("Last.FM integration is ENABLED")
|
||||
Register("lastfm", lastFMConstructor)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/core/spotify"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/xrash/smetrics"
|
||||
)
|
||||
|
||||
type spotifyAgent struct {
|
||||
ctx context.Context
|
||||
id string
|
||||
secret string
|
||||
client *spotify.Client
|
||||
}
|
||||
|
||||
func spotifyConstructor(ctx context.Context) Interface {
|
||||
if conf.Server.Spotify.ID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
l := &spotifyAgent{
|
||||
ctx: ctx,
|
||||
id: conf.Server.Spotify.ID,
|
||||
secret: conf.Server.Spotify.Secret,
|
||||
}
|
||||
l.client = spotify.NewClient(l.id, l.secret, http.DefaultClient)
|
||||
return l
|
||||
}
|
||||
|
||||
func (s *spotifyAgent) GetImages(name, mbid string) ([]ArtistImage, error) {
|
||||
a, err := s.searchArtist(name)
|
||||
if err != nil {
|
||||
if err == model.ErrNotFound {
|
||||
log.Warn(s.ctx, "Artist not found in Spotify", "artist", name)
|
||||
} else {
|
||||
log.Error(s.ctx, "Error calling Spotify", "artist", name, err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res []ArtistImage
|
||||
for _, img := range a.Images {
|
||||
res = append(res, ArtistImage{
|
||||
URL: img.URL,
|
||||
Size: img.Width,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *spotifyAgent) searchArtist(name string) (*spotify.Artist, error) {
|
||||
artists, err := s.client.SearchArtists(s.ctx, name, 40)
|
||||
if err != nil || len(artists) == 0 {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
name = strings.ToLower(name)
|
||||
|
||||
// Sort results, prioritizing artists with images, with similar names and with high popularity, in this order
|
||||
sort.Slice(artists, func(i, j int) bool {
|
||||
ai := fmt.Sprintf("%-5t-%03d-%04d", len(artists[i].Images) == 0, smetrics.WagnerFischer(name, strings.ToLower(artists[i].Name), 1, 1, 2), 1000-artists[i].Popularity)
|
||||
aj := fmt.Sprintf("%-5t-%03d-%04d", len(artists[j].Images) == 0, smetrics.WagnerFischer(name, strings.ToLower(artists[j].Name), 1, 1, 2), 1000-artists[j].Popularity)
|
||||
return ai < aj
|
||||
})
|
||||
|
||||
// If the first one has the same name, that's the one
|
||||
if strings.ToLower(artists[0].Name) != name {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
return &artists[0], err
|
||||
}
|
||||
|
||||
func init() {
|
||||
conf.AddHook(func() {
|
||||
if conf.Server.Spotify.ID != "" && conf.Server.Spotify.Secret != "" {
|
||||
log.Info("Spotify integration is ENABLED")
|
||||
Register("spotify", spotifyConstructor)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user