navidrome/core/agents/lastfm/agent.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

323 lines
9.2 KiB
Go
Raw Normal View History

package lastfm
2021-02-07 22:46:15 +01:00
import (
"context"
2022-10-01 01:33:39 +02:00
"errors"
2021-02-07 22:46:15 +01:00
"net/http"
"regexp"
"strconv"
"strings"
2021-02-07 22:46:15 +01:00
"github.com/navidrome/navidrome/conf"
2021-02-08 05:26:05 +01:00
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/agents"
"github.com/navidrome/navidrome/core/scrobbler"
2021-02-07 22:46:15 +01:00
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
2021-02-07 22:46:15 +01:00
)
const (
lastFMAgentName = "lastfm"
sessionKeyProperty = "LastFMSessionKey"
)
2021-02-08 22:33:09 +01:00
var ignoredBiographies = []string{
// Unknown Artist
`<a href="https://www.last.fm/music/`,
}
2021-02-07 22:46:15 +01:00
type lastfmAgent struct {
ds model.DataStore
sessionKeys *agents.SessionKeys
apiKey string
secret string
lang string
2023-01-19 15:34:39 +01:00
client *client
2021-02-07 22:46:15 +01:00
}
func lastFMConstructor(ds model.DataStore) *lastfmAgent {
2021-02-07 22:46:15 +01:00
l := &lastfmAgent{
ds: ds,
lang: conf.Server.LastFM.Language,
apiKey: conf.Server.LastFM.ApiKey,
secret: conf.Server.LastFM.Secret,
sessionKeys: &agents.SessionKeys{DataStore: ds, KeyName: sessionKeyProperty},
2021-02-07 22:46:15 +01:00
}
hc := &http.Client{
Timeout: consts.DefaultHttpClientTimeOut,
}
chc := utils.NewCachedHTTPClient(hc, consts.DefaultHttpClientTimeOut)
2023-01-19 15:34:39 +01:00
l.client = newClient(l.apiKey, l.secret, l.lang, chc)
2021-02-07 22:46:15 +01:00
return l
}
2021-02-08 16:14:29 +01:00
func (l *lastfmAgent) AgentName() string {
2021-02-08 22:33:09 +01:00
return lastFMAgentName
2021-02-08 16:14:29 +01:00
}
var imageRegex = regexp.MustCompile(`u\/(\d+)`)
func (l *lastfmAgent) GetAlbumInfo(ctx context.Context, name, artist, mbid string) (*agents.AlbumInfo, error) {
a, err := l.callAlbumGetInfo(ctx, name, artist, mbid)
if err != nil {
return nil, err
}
response := agents.AlbumInfo{
Name: a.Name,
MBID: a.MBID,
Description: a.Description.Summary,
URL: a.URL,
Images: make([]agents.ExternalImage, 0),
}
// Last.fm can return duplicate sizes.
seenSizes := map[int]bool{}
// This assumes that Last.fm returns images with size small, medium, and large.
// This is true as of December 29, 2022
for _, img := range a.Image {
size := imageRegex.FindStringSubmatch(img.URL)
// Last.fm can return images without URL
if len(size) == 0 || len(size[0]) < 4 {
log.Trace(ctx, "LastFM/albuminfo image URL does not match expected regex or is empty", "url", img.URL, "size", img.Size)
continue
}
numericSize, err := strconv.Atoi(size[0][2:])
if err != nil {
log.Error(ctx, "LastFM/albuminfo image URL does not match expected regex", "url", img.URL, "size", img.Size, err)
return nil, err
} else {
if _, exists := seenSizes[numericSize]; !exists {
response.Images = append(response.Images, agents.ExternalImage{
Size: numericSize,
URL: img.URL,
})
seenSizes[numericSize] = true
}
}
}
return &response, nil
}
func (l *lastfmAgent) GetArtistMBID(ctx context.Context, id string, name string) (string, error) {
a, err := l.callArtistGetInfo(ctx, name, "")
2021-02-07 22:46:15 +01:00
if err != nil {
return "", err
}
if a.MBID == "" {
return "", agents.ErrNotFound
2021-02-07 22:46:15 +01:00
}
return a.MBID, nil
}
func (l *lastfmAgent) GetArtistURL(ctx context.Context, id, name, mbid string) (string, error) {
a, err := l.callArtistGetInfo(ctx, name, mbid)
2021-02-07 22:46:15 +01:00
if err != nil {
return "", err
}
if a.URL == "" {
return "", agents.ErrNotFound
2021-02-07 22:46:15 +01:00
}
return a.URL, nil
}
func (l *lastfmAgent) GetArtistBiography(ctx context.Context, id, name, mbid string) (string, error) {
a, err := l.callArtistGetInfo(ctx, name, mbid)
2021-02-07 22:46:15 +01:00
if err != nil {
return "", err
}
a.Bio.Summary = strings.TrimSpace(a.Bio.Summary)
2021-02-07 22:46:15 +01:00
if a.Bio.Summary == "" {
return "", agents.ErrNotFound
2021-02-07 22:46:15 +01:00
}
for _, ign := range ignoredBiographies {
if strings.HasPrefix(a.Bio.Summary, ign) {
return "", nil
}
}
2021-02-07 22:46:15 +01:00
return a.Bio.Summary, nil
}
func (l *lastfmAgent) GetSimilarArtists(ctx context.Context, id, name, mbid string, limit int) ([]agents.Artist, error) {
resp, err := l.callArtistGetSimilar(ctx, name, mbid, limit)
2021-02-07 22:46:15 +01:00
if err != nil {
return nil, err
}
if len(resp) == 0 {
return nil, agents.ErrNotFound
2021-02-07 22:46:15 +01:00
}
var res []agents.Artist
2021-02-07 22:46:15 +01:00
for _, a := range resp {
res = append(res, agents.Artist{
2021-02-07 22:46:15 +01:00
Name: a.Name,
MBID: a.MBID,
})
}
return res, nil
}
func (l *lastfmAgent) GetArtistTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]agents.Song, error) {
resp, err := l.callArtistGetTopTracks(ctx, artistName, mbid, count)
2021-02-07 22:46:15 +01:00
if err != nil {
return nil, err
}
if len(resp) == 0 {
return nil, agents.ErrNotFound
2021-02-07 22:46:15 +01:00
}
var res []agents.Song
2021-02-07 22:46:15 +01:00
for _, t := range resp {
res = append(res, agents.Song{
2021-02-07 22:46:15 +01:00
Name: t.Name,
MBID: t.MBID,
})
}
return res, nil
}
func (l *lastfmAgent) callAlbumGetInfo(ctx context.Context, name, artist, mbid string) (*Album, error) {
2023-01-19 15:34:39 +01:00
a, err := l.client.albumGetInfo(ctx, name, artist, mbid)
var lfErr *lastFMError
isLastFMError := errors.As(err, &lfErr)
if mbid != "" && (isLastFMError && lfErr.Code == 6) {
log.Warn(ctx, "LastFM/album.getInfo could not find album by mbid, trying again", "album", name, "mbid", mbid)
return l.callAlbumGetInfo(ctx, name, artist, "")
}
if err != nil {
if isLastFMError && lfErr.Code == 6 {
log.Debug(ctx, "Album not found", "album", name, "mbid", mbid, err)
} else {
log.Error(ctx, "Error calling LastFM/album.getInfo", "album", name, "mbid", mbid, err)
}
return nil, err
}
return a, nil
}
func (l *lastfmAgent) callArtistGetInfo(ctx context.Context, name string, mbid string) (*Artist, error) {
2023-01-19 15:34:39 +01:00
a, err := l.client.artistGetInfo(ctx, name, mbid)
2022-10-01 01:33:39 +02:00
var lfErr *lastFMError
isLastFMError := errors.As(err, &lfErr)
if mbid != "" && ((err == nil && a.Name == "[unknown]") || (isLastFMError && lfErr.Code == 6)) {
log.Warn(ctx, "LastFM/artist.getInfo could not find artist by mbid, trying again", "artist", name, "mbid", mbid)
return l.callArtistGetInfo(ctx, name, "")
}
2021-02-07 22:46:15 +01:00
if err != nil {
log.Error(ctx, "Error calling LastFM/artist.getInfo", "artist", name, "mbid", mbid, err)
2021-02-07 22:46:15 +01:00
return nil, err
}
return a, nil
}
func (l *lastfmAgent) callArtistGetSimilar(ctx context.Context, name string, mbid string, limit int) ([]Artist, error) {
2023-01-19 15:34:39 +01:00
s, err := l.client.artistGetSimilar(ctx, name, mbid, limit)
2022-10-01 01:33:39 +02:00
var lfErr *lastFMError
isLastFMError := errors.As(err, &lfErr)
if mbid != "" && ((err == nil && s.Attr.Artist == "[unknown]") || (isLastFMError && lfErr.Code == 6)) {
log.Warn(ctx, "LastFM/artist.getSimilar could not find artist by mbid, trying again", "artist", name, "mbid", mbid)
return l.callArtistGetSimilar(ctx, name, "", limit)
}
2021-02-07 22:46:15 +01:00
if err != nil {
log.Error(ctx, "Error calling LastFM/artist.getSimilar", "artist", name, "mbid", mbid, err)
2021-02-07 22:46:15 +01:00
return nil, err
}
return s.Artists, nil
2021-02-07 22:46:15 +01:00
}
func (l *lastfmAgent) callArtistGetTopTracks(ctx context.Context, artistName, mbid string, count int) ([]Track, error) {
2023-01-19 15:34:39 +01:00
t, err := l.client.artistGetTopTracks(ctx, artistName, mbid, count)
2022-10-01 01:33:39 +02:00
var lfErr *lastFMError
isLastFMError := errors.As(err, &lfErr)
if mbid != "" && ((err == nil && t.Attr.Artist == "[unknown]") || (isLastFMError && lfErr.Code == 6)) {
log.Warn(ctx, "LastFM/artist.getTopTracks could not find artist by mbid, trying again", "artist", artistName, "mbid", mbid)
return l.callArtistGetTopTracks(ctx, artistName, "", count)
}
2021-02-07 22:46:15 +01:00
if err != nil {
log.Error(ctx, "Error calling LastFM/artist.getTopTracks", "artist", artistName, "mbid", mbid, err)
2021-02-07 22:46:15 +01:00
return nil, err
}
return t.Track, nil
2021-02-07 22:46:15 +01:00
}
func (l *lastfmAgent) NowPlaying(ctx context.Context, userId string, track *model.MediaFile) error {
sk, err := l.sessionKeys.Get(ctx, userId)
2021-06-24 06:01:05 +02:00
if err != nil || sk == "" {
return scrobbler.ErrNotAuthorized
}
2021-06-24 06:01:05 +02:00
2023-01-19 15:34:39 +01:00
err = l.client.updateNowPlaying(ctx, sk, ScrobbleInfo{
artist: track.Artist,
track: track.Title,
album: track.Album,
trackNumber: track.TrackNumber,
mbid: track.MbzRecordingID,
duration: int(track.Duration),
albumArtist: track.AlbumArtist,
})
if err != nil {
2021-06-24 06:01:05 +02:00
log.Warn(ctx, "Last.fm client.updateNowPlaying returned error", "track", track.Title, err)
return scrobbler.ErrUnrecoverable
}
return nil
}
2021-06-24 06:01:05 +02:00
func (l *lastfmAgent) Scrobble(ctx context.Context, userId string, s scrobbler.Scrobble) error {
sk, err := l.sessionKeys.Get(ctx, userId)
2021-06-24 06:01:05 +02:00
if err != nil || sk == "" {
return scrobbler.ErrNotAuthorized
}
if s.Duration <= 30 {
log.Debug(ctx, "Skipping Last.fm scrobble for short song", "track", s.Title, "duration", s.Duration)
return nil
}
2023-01-19 15:34:39 +01:00
err = l.client.scrobble(ctx, sk, ScrobbleInfo{
2021-06-24 06:01:05 +02:00
artist: s.Artist,
track: s.Title,
album: s.Album,
trackNumber: s.TrackNumber,
mbid: s.MbzRecordingID,
2021-06-24 06:01:05 +02:00
duration: int(s.Duration),
albumArtist: s.AlbumArtist,
timestamp: s.TimeStamp,
})
if err == nil {
return nil
}
2022-10-01 01:33:39 +02:00
var lfErr *lastFMError
isLastFMError := errors.As(err, &lfErr)
2021-06-24 06:01:05 +02:00
if !isLastFMError {
log.Warn(ctx, "Last.fm client.scrobble returned error", "track", s.Title, err)
return scrobbler.ErrRetryLater
}
2021-06-24 06:01:05 +02:00
if lfErr.Code == 11 || lfErr.Code == 16 {
return scrobbler.ErrRetryLater
}
return scrobbler.ErrUnrecoverable
}
func (l *lastfmAgent) IsAuthorized(ctx context.Context, userId string) bool {
sk, err := l.sessionKeys.Get(ctx, userId)
return err == nil && sk != ""
}
2021-02-07 22:46:15 +01:00
func init() {
conf.AddHook(func() {
if conf.Server.LastFM.Enabled {
agents.Register(lastFMAgentName, func(ds model.DataStore) agents.Interface {
return lastFMConstructor(ds)
})
scrobbler.Register(lastFMAgentName, func(ds model.DataStore) scrobbler.Scrobbler {
return lastFMConstructor(ds)
})
2021-02-07 22:46:15 +01:00
}
})
}