From 07535e151840184af33928f216f5a104b3cd3af9 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sun, 18 Oct 2020 19:10:11 -0400 Subject: [PATCH] Add ExternalInformation core service (not a great name, I know) --- cmd/wire_gen.go | 5 +- core/external_info.go | 169 ++++++++++++++++++++++++++++++ core/lastfm/client.go | 3 +- core/lastfm/client_test.go | 9 +- core/spotify/client.go | 12 ++- core/spotify/client_test.go | 13 +-- core/wire_providers.go | 24 +++++ model/artist.go | 1 + model/artist_info.go | 11 ++ persistence/artist_repository.go | 12 +++ server/subsonic/api.go | 5 +- server/subsonic/browsing.go | 80 +++++++++++--- server/subsonic/wire_gen.go | 5 +- server/subsonic/wire_injectors.go | 2 +- 14 files changed, 313 insertions(+), 38 deletions(-) create mode 100644 core/external_info.go create mode 100644 model/artist_info.go diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 40d864e9..d7a7e89f 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -50,7 +50,10 @@ func CreateSubsonicAPIRouter() (*subsonic.Router, error) { mediaStreamer := core.NewMediaStreamer(dataStore, transcoderTranscoder, transcodingCache) archiver := core.NewArchiver(dataStore) players := engine.NewPlayers(dataStore) - router := subsonic.New(artwork, listGenerator, playlists, mediaStreamer, archiver, players, dataStore) + lastFMClient := core.LastFMNewClient() + spotifyClient := core.SpotifyNewClient() + externalInfo := core.NewExternalInfo(dataStore, lastFMClient, spotifyClient) + router := subsonic.New(artwork, listGenerator, playlists, mediaStreamer, archiver, players, externalInfo, dataStore) return router, nil } diff --git a/core/external_info.go b/core/external_info.go new file mode 100644 index 00000000..ce434424 --- /dev/null +++ b/core/external_info.go @@ -0,0 +1,169 @@ +package core + +import ( + "context" + "sort" + "strings" + "sync" + "time" + + "github.com/deluan/navidrome/core/lastfm" + "github.com/deluan/navidrome/core/spotify" + "github.com/deluan/navidrome/log" + "github.com/deluan/navidrome/model" + "github.com/microcosm-cc/bluemonday" +) + +const placeholderArtistImageSmallUrl = "https://lastfm.freetls.fastly.net/i/u/64s/2a96cbd8b46e442fc41c2b86b821562f.png" +const placeholderArtistImageMediumUrl = "https://lastfm.freetls.fastly.net/i/u/174s/2a96cbd8b46e442fc41c2b86b821562f.png" +const placeholderArtistImageLargeUrl = "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png" + +type ExternalInfo interface { + ArtistInfo(ctx context.Context, artistId string, includeNotPresent bool, count int) (*model.ArtistInfo, error) +} + +type LastFMClient interface { + ArtistGetInfo(ctx context.Context, name string) (*lastfm.Artist, error) +} + +type SpotifyClient interface { + ArtistImages(ctx context.Context, name string) ([]spotify.Image, error) +} + +func NewExternalInfo(ds model.DataStore, lfm LastFMClient, spf SpotifyClient) ExternalInfo { + return &externalInfo{ds: ds, lfm: lfm, spf: spf} +} + +type externalInfo struct { + ds model.DataStore + lfm LastFMClient + spf SpotifyClient +} + +func (e *externalInfo) ArtistInfo(ctx context.Context, artistId string, + includeNotPresent bool, count int) (*model.ArtistInfo, error) { + info := model.ArtistInfo{ID: artistId} + + artist, err := e.ds.Artist(ctx).Get(artistId) + if err != nil { + return nil, err + } + info.Name = artist.Name + + // TODO Load from local: artist.jpg/png/webp, artist.json (with the remaining info) + + var wg sync.WaitGroup + e.callArtistInfo(ctx, artist, includeNotPresent, &wg, &info) + e.callArtistImages(ctx, artist, &wg, &info) + wg.Wait() + + // Use placeholders if could not get from external sources + e.setBio(&info, "Biography not available") + e.setSmallImageUrl(&info, placeholderArtistImageSmallUrl) + e.setMediumImageUrl(&info, placeholderArtistImageMediumUrl) + e.setLargeImageUrl(&info, placeholderArtistImageLargeUrl) + + log.Trace(ctx, "ArtistInfo collected", "artist", artist.Name, "info", info) + + return &info, nil +} + +func (e *externalInfo) callArtistInfo(ctx context.Context, artist *model.Artist, includeNotPresent bool, + wg *sync.WaitGroup, info *model.ArtistInfo) { + if e.lfm != nil { + log.Debug(ctx, "Calling Last.FM ArtistGetInfo", "artist", artist.Name) + wg.Add(1) + go func() { + start := time.Now() + defer wg.Done() + lfmArtist, err := e.lfm.ArtistGetInfo(nil, artist.Name) + if err != nil { + log.Error(ctx, "Error calling Last.FM", "artist", artist.Name, err) + } else { + log.Debug(ctx, "Got info from Last.FM", "artist", artist.Name, "info", lfmArtist.Bio.Summary, "elapsed", time.Since(start)) + } + e.setBio(info, lfmArtist.Bio.Summary) + e.setSimilar(ctx, info, lfmArtist.Similar.Artists, includeNotPresent) + }() + } +} + +func (e *externalInfo) callArtistImages(ctx context.Context, artist *model.Artist, wg *sync.WaitGroup, info *model.ArtistInfo) { + if e.spf != nil { + log.Debug(ctx, "Calling Spotify ArtistImages", "artist", artist.Name) + wg.Add(1) + go func() { + start := time.Now() + defer wg.Done() + spfImages, err := e.spf.ArtistImages(nil, artist.Name) + if err != nil { + log.Error(ctx, "Error calling Spotify", "artist", artist.Name, err) + } else { + log.Debug(ctx, "Got images from Spotify", "artist", artist.Name, "images", spfImages, "elapsed", time.Since(start)) + } + + sort.Slice(spfImages, func(i, j int) bool { return spfImages[i].Width > spfImages[j].Width }) + + if len(spfImages) >= 1 { + e.setLargeImageUrl(info, spfImages[0].URL) + } + if len(spfImages) >= 2 { + e.setMediumImageUrl(info, spfImages[1].URL) + } + if len(spfImages) >= 3 { + e.setSmallImageUrl(info, spfImages[2].URL) + } + }() + } +} + +func (e *externalInfo) setBio(info *model.ArtistInfo, bio string) { + policy := bluemonday.UGCPolicy() + if info.Bio == "" { + bio = policy.Sanitize(bio) + bio = strings.ReplaceAll(bio, "\n", " ") + info.Bio = strings.ReplaceAll(bio, "