Get album info (when available) from Last.fm, add getAlbumInfo endpoint (#2061)

* lastfm album.getInfo, getAlbuminfo(2) endpoints

* ... for description and reduce not found log level

* address first comments

* return all images

* Update migration timestamp

* Handle a few edge cases

* Add CoverArtPriority option to retrieve albumart from external sources

* Make agents methods more descriptive

* Use Last.fm name consistently

Co-authored-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Kendall Garner 2023-01-18 01:22:54 +00:00 committed by GitHub
parent 5564f00838
commit 93adda66d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 797 additions and 188 deletions

View file

@ -28,10 +28,12 @@ const (
)
type ExternalMetadata interface {
UpdateAlbumInfo(ctx context.Context, id string) (*model.Album, error)
UpdateArtistInfo(ctx context.Context, id string, count int, includeNotPresent bool) (*model.Artist, error)
SimilarSongs(ctx context.Context, id string, count int) (model.MediaFiles, error)
TopSongs(ctx context.Context, artist string, count int) (model.MediaFiles, error)
ArtistImage(ctx context.Context, id string) (*url.URL, error)
AlbumImage(ctx context.Context, id string) (*url.URL, error)
}
type externalMetadata struct {
@ -39,6 +41,11 @@ type externalMetadata struct {
ag *agents.Agents
}
type auxAlbum struct {
model.Album
Name string
}
type auxArtist struct {
model.Artist
Name string
@ -48,6 +55,97 @@ func NewExternalMetadata(ds model.DataStore, agents *agents.Agents) ExternalMeta
return &externalMetadata{ds: ds, ag: agents}
}
func (e *externalMetadata) getAlbum(ctx context.Context, id string) (*auxAlbum, error) {
var entity interface{}
entity, err := model.GetEntityByID(ctx, e.ds, id)
if err != nil {
return nil, err
}
var album auxAlbum
switch v := entity.(type) {
case *model.Album:
album.Album = *v
album.Name = clearName(v.Name)
case *model.MediaFile:
return e.getAlbum(ctx, v.AlbumID)
default:
return nil, model.ErrNotFound
}
return &album, nil
}
func (e *externalMetadata) UpdateAlbumInfo(ctx context.Context, id string) (*model.Album, error) {
album, err := e.getAlbum(ctx, id)
if err != nil {
log.Info(ctx, "Not found", "id", id)
return nil, err
}
if album.ExternalInfoUpdatedAt.IsZero() {
log.Debug(ctx, "AlbumInfo not cached. Retrieving it now", "updatedAt", album.ExternalInfoUpdatedAt, "id", id, "name", album.Name)
err = e.refreshAlbumInfo(ctx, album)
if err != nil {
return nil, err
}
}
if time.Since(album.ExternalInfoUpdatedAt) > consts.AlbumInfoTimeToLive {
log.Debug("Found expired cached AlbumInfo, refreshing in the background", "updatedAt", album.ExternalInfoUpdatedAt, "name", album.Name)
go func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
err := e.refreshAlbumInfo(ctx, album)
if err != nil {
log.Error("Error refreshing AlbumInfo", "id", id, "name", album.Name, err)
}
}()
}
return &album.Album, nil
}
func (e *externalMetadata) refreshAlbumInfo(ctx context.Context, album *auxAlbum) error {
info, err := e.ag.GetAlbumInfo(ctx, album.Name, album.AlbumArtist, album.MbzAlbumID)
if errors.Is(err, agents.ErrNotFound) {
return nil
}
if err != nil {
return err
}
album.ExternalInfoUpdatedAt = time.Now()
album.ExternalUrl = info.URL
if info.Description != "" {
album.Description = info.Description
}
if len(info.Images) > 0 {
sort.Slice(info.Images, func(i, j int) bool {
return info.Images[i].Size > info.Images[j].Size
})
album.LargeImageUrl = info.Images[0].URL
if len(info.Images) >= 2 {
album.MediumImageUrl = info.Images[1].URL
}
if len(info.Images) >= 3 {
album.SmallImageUrl = info.Images[2].URL
}
}
err = e.ds.Album(ctx).Put(&album.Album)
if err != nil {
log.Error(ctx, "Error trying to update album external information", "id", album.ID, "name", album.Name, err)
}
log.Trace(ctx, "AlbumInfo collected", "album", album)
return nil
}
func (e *externalMetadata) getArtist(ctx context.Context, id string) (*auxArtist, error) {
var entity interface{}
entity, err := model.GetEntityByID(ctx, e.ds, id)
@ -116,7 +214,7 @@ func (e *externalMetadata) UpdateArtistInfo(ctx context.Context, id string, simi
func (e *externalMetadata) refreshArtistInfo(ctx context.Context, artist *auxArtist) error {
// Get MBID first, if it is not yet available
if artist.MbzArtistID == "" {
mbid, err := e.ag.GetMBID(ctx, artist.ID, artist.Name)
mbid, err := e.ag.GetArtistMBID(ctx, artist.ID, artist.Name)
if mbid != "" && err == nil {
artist.MbzArtistID = mbid
}
@ -234,6 +332,34 @@ func (e *externalMetadata) ArtistImage(ctx context.Context, id string) (*url.URL
return url.Parse(imageUrl)
}
func (e *externalMetadata) AlbumImage(ctx context.Context, id string) (*url.URL, error) {
album, err := e.getAlbum(ctx, id)
if err != nil {
return nil, err
}
info, err := e.ag.GetAlbumInfo(ctx, album.Name, album.AlbumArtist, album.MbzAlbumID)
if errors.Is(err, agents.ErrNotFound) {
return nil, err
}
if utils.IsCtxDone(ctx) {
log.Warn(ctx, "AlbumImage call canceled", ctx.Err())
return nil, ctx.Err()
}
// Return the biggest image
var img agents.ExternalImage
for _, i := range info.Images {
if img.Size <= i.Size {
img = i
}
}
if img.URL == "" {
return nil, agents.ErrNotFound
}
return url.Parse(img.URL)
}
func (e *externalMetadata) TopSongs(ctx context.Context, artistName string, count int) (model.MediaFiles, error) {
artist, err := e.findArtistByName(ctx, artistName)
if err != nil {
@ -245,7 +371,7 @@ func (e *externalMetadata) TopSongs(ctx context.Context, artistName string, coun
}
func (e *externalMetadata) getMatchingTopSongs(ctx context.Context, agent agents.ArtistTopSongsRetriever, artist *auxArtist, count int) (model.MediaFiles, error) {
songs, err := agent.GetTopSongs(ctx, artist.ID, artist.Name, artist.MbzArtistID, count)
songs, err := agent.GetArtistTopSongs(ctx, artist.ID, artist.Name, artist.MbzArtistID, count)
if errors.Is(err, agents.ErrNotFound) {
return nil, nil
}
@ -300,7 +426,7 @@ func (e *externalMetadata) findMatchingTrack(ctx context.Context, mbid string, a
}
func (e *externalMetadata) callGetURL(ctx context.Context, agent agents.ArtistURLRetriever, artist *auxArtist) {
url, err := agent.GetURL(ctx, artist.ID, artist.Name, artist.MbzArtistID)
url, err := agent.GetArtistURL(ctx, artist.ID, artist.Name, artist.MbzArtistID)
if url == "" || err != nil {
return
}
@ -308,7 +434,7 @@ func (e *externalMetadata) callGetURL(ctx context.Context, agent agents.ArtistUR
}
func (e *externalMetadata) callGetBiography(ctx context.Context, agent agents.ArtistBiographyRetriever, artist *auxArtist) {
bio, err := agent.GetBiography(ctx, artist.ID, clearName(artist.Name), artist.MbzArtistID)
bio, err := agent.GetArtistBiography(ctx, artist.ID, clearName(artist.Name), artist.MbzArtistID)
if bio == "" || err != nil {
return
}
@ -318,7 +444,7 @@ func (e *externalMetadata) callGetBiography(ctx context.Context, agent agents.Ar
}
func (e *externalMetadata) callGetImage(ctx context.Context, agent agents.ArtistImageRetriever, artist *auxArtist) {
images, err := agent.GetImages(ctx, artist.ID, artist.Name, artist.MbzArtistID)
images, err := agent.GetArtistImages(ctx, artist.ID, artist.Name, artist.MbzArtistID)
if len(images) == 0 || err != nil {
return
}
@ -337,7 +463,7 @@ func (e *externalMetadata) callGetImage(ctx context.Context, agent agents.Artist
func (e *externalMetadata) callGetSimilar(ctx context.Context, agent agents.ArtistSimilarRetriever, artist *auxArtist,
limit int, includeNotPresent bool) {
similar, err := agent.GetSimilar(ctx, artist.ID, artist.Name, artist.MbzArtistID, limit)
similar, err := agent.GetSimilarArtists(ctx, artist.ID, artist.Name, artist.MbzArtistID, limit)
if len(similar) == 0 || err != nil {
return
}