diff --git a/conf/configuration.go b/conf/configuration.go index abd84f59..93247d46 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -243,7 +243,7 @@ func init() { viper.SetDefault("ignoredarticles", "The El La Los Las Le Les Os As O A") viper.SetDefault("indexgroups", "A B C D E F G H I J K L M N O P Q R S T U V W X-Z(XYZ) [Unknown]([)") viper.SetDefault("probecommand", "ffmpeg %s -f ffmetadata") - viper.SetDefault("coverartpriority", "embedded, cover.*, folder.*, front.*") + viper.SetDefault("coverartpriority", "cover.*, folder.*, front.*, embedded, external") viper.SetDefault("coverjpegquality", 75) viper.SetDefault("uiwelcomemessage", "") viper.SetDefault("enablegravatar", false) diff --git a/consts/consts.go b/consts/consts.go index 72a0d8ca..3c38ad42 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -49,6 +49,7 @@ const ( ServerReadHeaderTimeout = 3 * time.Second ArtistInfoTimeToLive = 24 * time.Hour + AlbumInfoTimeToLive = 7 * 24 * time.Hour I18nFolder = "i18n" SkipScanFile = ".ndignore" diff --git a/core/agents/agents.go b/core/agents/agents.go index aa551a8b..f31fbd94 100644 --- a/core/agents/agents.go +++ b/core/agents/agents.go @@ -41,7 +41,7 @@ func (a *Agents) AgentName() string { return "agents" } -func (a *Agents) GetMBID(ctx context.Context, id string, name string) (string, error) { +func (a *Agents) GetArtistMBID(ctx context.Context, id string, name string) (string, error) { start := time.Now() for _, ag := range a.agents { if utils.IsCtxDone(ctx) { @@ -51,7 +51,7 @@ func (a *Agents) GetMBID(ctx context.Context, id string, name string) (string, e if !ok { continue } - mbid, err := agent.GetMBID(ctx, id, name) + mbid, err := agent.GetArtistMBID(ctx, id, name) if mbid != "" && err == nil { log.Debug(ctx, "Got MBID", "agent", ag.AgentName(), "artist", name, "mbid", mbid, "elapsed", time.Since(start)) return mbid, nil @@ -60,7 +60,7 @@ func (a *Agents) GetMBID(ctx context.Context, id string, name string) (string, e return "", ErrNotFound } -func (a *Agents) GetURL(ctx context.Context, id, name, mbid string) (string, error) { +func (a *Agents) GetArtistURL(ctx context.Context, id, name, mbid string) (string, error) { start := time.Now() for _, ag := range a.agents { if utils.IsCtxDone(ctx) { @@ -70,7 +70,7 @@ func (a *Agents) GetURL(ctx context.Context, id, name, mbid string) (string, err if !ok { continue } - url, err := agent.GetURL(ctx, id, name, mbid) + url, err := agent.GetArtistURL(ctx, id, name, mbid) if url != "" && err == nil { log.Debug(ctx, "Got External Url", "agent", ag.AgentName(), "artist", name, "url", url, "elapsed", time.Since(start)) return url, nil @@ -79,7 +79,7 @@ func (a *Agents) GetURL(ctx context.Context, id, name, mbid string) (string, err return "", ErrNotFound } -func (a *Agents) GetBiography(ctx context.Context, id, name, mbid string) (string, error) { +func (a *Agents) GetArtistBiography(ctx context.Context, id, name, mbid string) (string, error) { start := time.Now() for _, ag := range a.agents { if utils.IsCtxDone(ctx) { @@ -89,7 +89,7 @@ func (a *Agents) GetBiography(ctx context.Context, id, name, mbid string) (strin if !ok { continue } - bio, err := agent.GetBiography(ctx, id, name, mbid) + bio, err := agent.GetArtistBiography(ctx, id, name, mbid) if bio != "" && err == nil { log.Debug(ctx, "Got Biography", "agent", ag.AgentName(), "artist", name, "len", len(bio), "elapsed", time.Since(start)) return bio, nil @@ -98,7 +98,7 @@ func (a *Agents) GetBiography(ctx context.Context, id, name, mbid string) (strin return "", ErrNotFound } -func (a *Agents) GetSimilar(ctx context.Context, id, name, mbid string, limit int) ([]Artist, error) { +func (a *Agents) GetSimilarArtists(ctx context.Context, id, name, mbid string, limit int) ([]Artist, error) { start := time.Now() for _, ag := range a.agents { if utils.IsCtxDone(ctx) { @@ -108,7 +108,7 @@ func (a *Agents) GetSimilar(ctx context.Context, id, name, mbid string, limit in if !ok { continue } - similar, err := agent.GetSimilar(ctx, id, name, mbid, limit) + similar, err := agent.GetSimilarArtists(ctx, id, name, mbid, limit) if len(similar) > 0 && err == nil { if log.CurrentLevel() >= log.LevelTrace { log.Debug(ctx, "Got Similar Artists", "agent", ag.AgentName(), "artist", name, "similar", similar, "elapsed", time.Since(start)) @@ -121,7 +121,7 @@ func (a *Agents) GetSimilar(ctx context.Context, id, name, mbid string, limit in return nil, ErrNotFound } -func (a *Agents) GetImages(ctx context.Context, id, name, mbid string) ([]ArtistImage, error) { +func (a *Agents) GetArtistImages(ctx context.Context, id, name, mbid string) ([]ExternalImage, error) { start := time.Now() for _, ag := range a.agents { if utils.IsCtxDone(ctx) { @@ -131,7 +131,7 @@ func (a *Agents) GetImages(ctx context.Context, id, name, mbid string) ([]Artist if !ok { continue } - images, err := agent.GetImages(ctx, id, name, mbid) + images, err := agent.GetArtistImages(ctx, id, name, mbid) if len(images) > 0 && err == nil { log.Debug(ctx, "Got Images", "agent", ag.AgentName(), "artist", name, "images", images, "elapsed", time.Since(start)) return images, nil @@ -140,7 +140,7 @@ func (a *Agents) GetImages(ctx context.Context, id, name, mbid string) ([]Artist return nil, ErrNotFound } -func (a *Agents) GetTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]Song, error) { +func (a *Agents) GetArtistTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]Song, error) { start := time.Now() for _, ag := range a.agents { if utils.IsCtxDone(ctx) { @@ -150,7 +150,7 @@ func (a *Agents) GetTopSongs(ctx context.Context, id, artistName, mbid string, c if !ok { continue } - songs, err := agent.GetTopSongs(ctx, id, artistName, mbid, count) + songs, err := agent.GetArtistTopSongs(ctx, id, artistName, mbid, count) if len(songs) > 0 && err == nil { log.Debug(ctx, "Got Top Songs", "agent", ag.AgentName(), "artist", artistName, "songs", songs, "elapsed", time.Since(start)) return songs, nil @@ -159,6 +159,26 @@ func (a *Agents) GetTopSongs(ctx context.Context, id, artistName, mbid string, c return nil, ErrNotFound } +func (a *Agents) GetAlbumInfo(ctx context.Context, name, artist, mbid string) (*AlbumInfo, error) { + start := time.Now() + for _, ag := range a.agents { + if utils.IsCtxDone(ctx) { + break + } + agent, ok := ag.(AlbumInfoRetriever) + if !ok { + continue + } + album, err := agent.GetAlbumInfo(ctx, name, artist, mbid) + if err == nil { + log.Debug(ctx, "Got Album Info", "agent", ag.AgentName(), "album", name, "artist", artist, + "mbid", mbid, "elapsed", time.Since(start)) + return album, nil + } + } + return nil, ErrNotFound +} + var _ Interface = (*Agents)(nil) var _ ArtistMBIDRetriever = (*Agents)(nil) var _ ArtistURLRetriever = (*Agents)(nil) @@ -166,3 +186,4 @@ var _ ArtistBiographyRetriever = (*Agents)(nil) var _ ArtistSimilarRetriever = (*Agents)(nil) var _ ArtistImageRetriever = (*Agents)(nil) var _ ArtistTopSongsRetriever = (*Agents)(nil) +var _ AlbumInfoRetriever = (*Agents)(nil) diff --git a/core/agents/agents_test.go b/core/agents/agents_test.go index 86c7a483..485343b4 100644 --- a/core/agents/agents_test.go +++ b/core/agents/agents_test.go @@ -30,9 +30,9 @@ var _ = Describe("Agents", func() { ag = New(ds) }) - It("calls the placeholder GetImages", func() { + It("calls the placeholder GetArtistImages", func() { mfRepo.SetData(model.MediaFiles{{ID: "1", Title: "One", MbzReleaseTrackID: "111"}, {ID: "2", Title: "Two", MbzReleaseTrackID: "222"}}) - songs, err := ag.GetTopSongs(ctx, "123", "John Doe", "mb123", 2) + songs, err := ag.GetArtistTopSongs(ctx, "123", "John Doe", "mb123", 2) Expect(err).ToNot(HaveOccurred()) Expect(songs).To(ConsistOf([]Song{{Name: "One", MBID: "111"}, {Name: "Two", MBID: "222"}})) }) @@ -56,66 +56,66 @@ var _ = Describe("Agents", func() { Expect(ag.AgentName()).To(Equal("agents")) }) - Describe("GetMBID", func() { + Describe("GetArtistMBID", func() { It("returns on first match", func() { - Expect(ag.GetMBID(ctx, "123", "test")).To(Equal("mbid")) + Expect(ag.GetArtistMBID(ctx, "123", "test")).To(Equal("mbid")) Expect(mock.Args).To(ConsistOf("123", "test")) }) It("skips the agent if it returns an error", func() { mock.Err = errors.New("error") - _, err := ag.GetMBID(ctx, "123", "test") + _, err := ag.GetArtistMBID(ctx, "123", "test") Expect(err).To(MatchError(ErrNotFound)) Expect(mock.Args).To(ConsistOf("123", "test")) }) It("interrupts if the context is canceled", func() { cancel() - _, err := ag.GetMBID(ctx, "123", "test") + _, err := ag.GetArtistMBID(ctx, "123", "test") Expect(err).To(MatchError(ErrNotFound)) Expect(mock.Args).To(BeEmpty()) }) }) - Describe("GetURL", func() { + Describe("GetArtistURL", func() { It("returns on first match", func() { - Expect(ag.GetURL(ctx, "123", "test", "mb123")).To(Equal("url")) + Expect(ag.GetArtistURL(ctx, "123", "test", "mb123")).To(Equal("url")) Expect(mock.Args).To(ConsistOf("123", "test", "mb123")) }) It("skips the agent if it returns an error", func() { mock.Err = errors.New("error") - _, err := ag.GetURL(ctx, "123", "test", "mb123") + _, err := ag.GetArtistURL(ctx, "123", "test", "mb123") Expect(err).To(MatchError(ErrNotFound)) Expect(mock.Args).To(ConsistOf("123", "test", "mb123")) }) It("interrupts if the context is canceled", func() { cancel() - _, err := ag.GetURL(ctx, "123", "test", "mb123") + _, err := ag.GetArtistURL(ctx, "123", "test", "mb123") Expect(err).To(MatchError(ErrNotFound)) Expect(mock.Args).To(BeEmpty()) }) }) - Describe("GetBiography", func() { + Describe("GetArtistBiography", func() { It("returns on first match", func() { - Expect(ag.GetBiography(ctx, "123", "test", "mb123")).To(Equal("bio")) + Expect(ag.GetArtistBiography(ctx, "123", "test", "mb123")).To(Equal("bio")) Expect(mock.Args).To(ConsistOf("123", "test", "mb123")) }) It("skips the agent if it returns an error", func() { mock.Err = errors.New("error") - _, err := ag.GetBiography(ctx, "123", "test", "mb123") + _, err := ag.GetArtistBiography(ctx, "123", "test", "mb123") Expect(err).To(MatchError(ErrNotFound)) Expect(mock.Args).To(ConsistOf("123", "test", "mb123")) }) It("interrupts if the context is canceled", func() { cancel() - _, err := ag.GetBiography(ctx, "123", "test", "mb123") + _, err := ag.GetArtistBiography(ctx, "123", "test", "mb123") Expect(err).To(MatchError(ErrNotFound)) Expect(mock.Args).To(BeEmpty()) }) }) - Describe("GetImages", func() { + Describe("GetArtistImages", func() { It("returns on first match", func() { - Expect(ag.GetImages(ctx, "123", "test", "mb123")).To(Equal([]ArtistImage{{ + Expect(ag.GetArtistImages(ctx, "123", "test", "mb123")).To(Equal([]ExternalImage{{ URL: "imageUrl", Size: 100, }})) @@ -123,21 +123,21 @@ var _ = Describe("Agents", func() { }) It("skips the agent if it returns an error", func() { mock.Err = errors.New("error") - _, err := ag.GetImages(ctx, "123", "test", "mb123") + _, err := ag.GetArtistImages(ctx, "123", "test", "mb123") Expect(err).To(MatchError("not found")) Expect(mock.Args).To(ConsistOf("123", "test", "mb123")) }) It("interrupts if the context is canceled", func() { cancel() - _, err := ag.GetImages(ctx, "123", "test", "mb123") + _, err := ag.GetArtistImages(ctx, "123", "test", "mb123") Expect(err).To(MatchError(ErrNotFound)) Expect(mock.Args).To(BeEmpty()) }) }) - Describe("GetSimilar", func() { + Describe("GetSimilarArtists", func() { It("returns on first match", func() { - Expect(ag.GetSimilar(ctx, "123", "test", "mb123", 1)).To(Equal([]Artist{{ + Expect(ag.GetSimilarArtists(ctx, "123", "test", "mb123", 1)).To(Equal([]Artist{{ Name: "Joe Dohn", MBID: "mbid321", }})) @@ -145,21 +145,21 @@ var _ = Describe("Agents", func() { }) It("skips the agent if it returns an error", func() { mock.Err = errors.New("error") - _, err := ag.GetSimilar(ctx, "123", "test", "mb123", 1) + _, err := ag.GetSimilarArtists(ctx, "123", "test", "mb123", 1) Expect(err).To(MatchError(ErrNotFound)) Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 1)) }) It("interrupts if the context is canceled", func() { cancel() - _, err := ag.GetSimilar(ctx, "123", "test", "mb123", 1) + _, err := ag.GetSimilarArtists(ctx, "123", "test", "mb123", 1) Expect(err).To(MatchError(ErrNotFound)) Expect(mock.Args).To(BeEmpty()) }) }) - Describe("GetTopSongs", func() { + Describe("GetArtistTopSongs", func() { It("returns on first match", func() { - Expect(ag.GetTopSongs(ctx, "123", "test", "mb123", 2)).To(Equal([]Song{{ + Expect(ag.GetArtistTopSongs(ctx, "123", "test", "mb123", 2)).To(Equal([]Song{{ Name: "A Song", MBID: "mbid444", }})) @@ -167,13 +167,49 @@ var _ = Describe("Agents", func() { }) It("skips the agent if it returns an error", func() { mock.Err = errors.New("error") - _, err := ag.GetTopSongs(ctx, "123", "test", "mb123", 2) + _, err := ag.GetArtistTopSongs(ctx, "123", "test", "mb123", 2) Expect(err).To(MatchError(ErrNotFound)) Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 2)) }) It("interrupts if the context is canceled", func() { cancel() - _, err := ag.GetTopSongs(ctx, "123", "test", "mb123", 2) + _, err := ag.GetArtistTopSongs(ctx, "123", "test", "mb123", 2) + Expect(err).To(MatchError(ErrNotFound)) + Expect(mock.Args).To(BeEmpty()) + }) + }) + + Describe("GetAlbumInfo", func() { + It("returns meaningful data", func() { + Expect(ag.GetAlbumInfo(ctx, "album", "artist", "mbid")).To(Equal(&AlbumInfo{ + Name: "A Song", + MBID: "mbid444", + Description: "A Description", + URL: "External URL", + Images: []ExternalImage{ + { + Size: 174, + URL: "https://lastfm.freetls.fastly.net/i/u/174s/00000000000000000000000000000000.png", + }, { + Size: 64, + URL: "https://lastfm.freetls.fastly.net/i/u/64s/00000000000000000000000000000000.png", + }, { + Size: 34, + URL: "https://lastfm.freetls.fastly.net/i/u/34s/00000000000000000000000000000000.png", + }, + }, + })) + Expect(mock.Args).To(ConsistOf("album", "artist", "mbid")) + }) + It("skips the agent if it returns an error", func() { + mock.Err = errors.New("error") + _, err := ag.GetAlbumInfo(ctx, "album", "artist", "mbid") + Expect(err).To(MatchError(ErrNotFound)) + Expect(mock.Args).To(ConsistOf("album", "artist", "mbid")) + }) + It("interrupts if the context is canceled", func() { + cancel() + _, err := ag.GetAlbumInfo(ctx, "album", "artist", "mbid") Expect(err).To(MatchError(ErrNotFound)) Expect(mock.Args).To(BeEmpty()) }) @@ -190,7 +226,7 @@ func (a *mockAgent) AgentName() string { return "fake" } -func (a *mockAgent) GetMBID(_ context.Context, id string, name string) (string, error) { +func (a *mockAgent) GetArtistMBID(_ context.Context, id string, name string) (string, error) { a.Args = []interface{}{id, name} if a.Err != nil { return "", a.Err @@ -198,7 +234,7 @@ func (a *mockAgent) GetMBID(_ context.Context, id string, name string) (string, return "mbid", nil } -func (a *mockAgent) GetURL(_ context.Context, id, name, mbid string) (string, error) { +func (a *mockAgent) GetArtistURL(_ context.Context, id, name, mbid string) (string, error) { a.Args = []interface{}{id, name, mbid} if a.Err != nil { return "", a.Err @@ -206,7 +242,7 @@ func (a *mockAgent) GetURL(_ context.Context, id, name, mbid string) (string, er return "url", nil } -func (a *mockAgent) GetBiography(_ context.Context, id, name, mbid string) (string, error) { +func (a *mockAgent) GetArtistBiography(_ context.Context, id, name, mbid string) (string, error) { a.Args = []interface{}{id, name, mbid} if a.Err != nil { return "", a.Err @@ -214,18 +250,18 @@ func (a *mockAgent) GetBiography(_ context.Context, id, name, mbid string) (stri return "bio", nil } -func (a *mockAgent) GetImages(_ context.Context, id, name, mbid string) ([]ArtistImage, error) { +func (a *mockAgent) GetArtistImages(_ context.Context, id, name, mbid string) ([]ExternalImage, error) { a.Args = []interface{}{id, name, mbid} if a.Err != nil { return nil, a.Err } - return []ArtistImage{{ + return []ExternalImage{{ URL: "imageUrl", Size: 100, }}, nil } -func (a *mockAgent) GetSimilar(_ context.Context, id, name, mbid string, limit int) ([]Artist, error) { +func (a *mockAgent) GetSimilarArtists(_ context.Context, id, name, mbid string, limit int) ([]Artist, error) { a.Args = []interface{}{id, name, mbid, limit} if a.Err != nil { return nil, a.Err @@ -236,7 +272,7 @@ func (a *mockAgent) GetSimilar(_ context.Context, id, name, mbid string, limit i }}, nil } -func (a *mockAgent) GetTopSongs(_ context.Context, id, artistName, mbid string, count int) ([]Song, error) { +func (a *mockAgent) GetArtistTopSongs(_ context.Context, id, artistName, mbid string, count int) ([]Song, error) { a.Args = []interface{}{id, artistName, mbid, count} if a.Err != nil { return nil, a.Err @@ -246,3 +282,28 @@ func (a *mockAgent) GetTopSongs(_ context.Context, id, artistName, mbid string, MBID: "mbid444", }}, nil } + +func (a *mockAgent) GetAlbumInfo(ctx context.Context, name, artist, mbid string) (*AlbumInfo, error) { + a.Args = []interface{}{name, artist, mbid} + if a.Err != nil { + return nil, a.Err + } + return &AlbumInfo{ + Name: "A Song", + MBID: "mbid444", + Description: "A Description", + URL: "External URL", + Images: []ExternalImage{ + { + Size: 174, + URL: "https://lastfm.freetls.fastly.net/i/u/174s/00000000000000000000000000000000.png", + }, { + Size: 64, + URL: "https://lastfm.freetls.fastly.net/i/u/64s/00000000000000000000000000000000.png", + }, { + Size: 34, + URL: "https://lastfm.freetls.fastly.net/i/u/34s/00000000000000000000000000000000.png", + }, + }, + }, nil +} diff --git a/core/agents/interfaces.go b/core/agents/interfaces.go index 9c9f8085..d4071c9f 100644 --- a/core/agents/interfaces.go +++ b/core/agents/interfaces.go @@ -13,12 +13,20 @@ type Interface interface { AgentName() string } +type AlbumInfo struct { + Name string + MBID string + Description string + URL string + Images []ExternalImage +} + type Artist struct { Name string MBID string } -type ArtistImage struct { +type ExternalImage struct { URL string Size int } @@ -32,28 +40,32 @@ var ( ErrNotFound = errors.New("not found") ) +type AlbumInfoRetriever interface { + GetAlbumInfo(ctx context.Context, name, artist, mbid string) (*AlbumInfo, error) +} + type ArtistMBIDRetriever interface { - GetMBID(ctx context.Context, id string, name string) (string, error) + GetArtistMBID(ctx context.Context, id string, name string) (string, error) } type ArtistURLRetriever interface { - GetURL(ctx context.Context, id, name, mbid string) (string, error) + GetArtistURL(ctx context.Context, id, name, mbid string) (string, error) } type ArtistBiographyRetriever interface { - GetBiography(ctx context.Context, id, name, mbid string) (string, error) + GetArtistBiography(ctx context.Context, id, name, mbid string) (string, error) } type ArtistSimilarRetriever interface { - GetSimilar(ctx context.Context, id, name, mbid string, limit int) ([]Artist, error) + GetSimilarArtists(ctx context.Context, id, name, mbid string, limit int) ([]Artist, error) } type ArtistImageRetriever interface { - GetImages(ctx context.Context, id, name, mbid string) ([]ArtistImage, error) + GetArtistImages(ctx context.Context, id, name, mbid string) ([]ExternalImage, error) } type ArtistTopSongsRetriever interface { - GetTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]Song, error) + GetArtistTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]Song, error) } var Map map[string]Constructor diff --git a/core/agents/lastfm/agent.go b/core/agents/lastfm/agent.go index e6f10ac0..2da6cb6b 100644 --- a/core/agents/lastfm/agent.go +++ b/core/agents/lastfm/agent.go @@ -4,6 +4,8 @@ import ( "context" "errors" "net/http" + "regexp" + "strconv" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/consts" @@ -48,7 +50,54 @@ func (l *lastfmAgent) AgentName() string { return lastFMAgentName } -func (l *lastfmAgent) GetMBID(ctx context.Context, id string, name string) (string, error) { +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, "") if err != nil { return "", err @@ -59,7 +108,7 @@ func (l *lastfmAgent) GetMBID(ctx context.Context, id string, name string) (stri return a.MBID, nil } -func (l *lastfmAgent) GetURL(ctx context.Context, id, name, mbid string) (string, error) { +func (l *lastfmAgent) GetArtistURL(ctx context.Context, id, name, mbid string) (string, error) { a, err := l.callArtistGetInfo(ctx, name, mbid) if err != nil { return "", err @@ -70,7 +119,7 @@ func (l *lastfmAgent) GetURL(ctx context.Context, id, name, mbid string) (string return a.URL, nil } -func (l *lastfmAgent) GetBiography(ctx context.Context, id, name, mbid string) (string, error) { +func (l *lastfmAgent) GetArtistBiography(ctx context.Context, id, name, mbid string) (string, error) { a, err := l.callArtistGetInfo(ctx, name, mbid) if err != nil { return "", err @@ -81,7 +130,7 @@ func (l *lastfmAgent) GetBiography(ctx context.Context, id, name, mbid string) ( return a.Bio.Summary, nil } -func (l *lastfmAgent) GetSimilar(ctx context.Context, id, name, mbid string, limit int) ([]agents.Artist, error) { +func (l *lastfmAgent) GetSimilarArtists(ctx context.Context, id, name, mbid string, limit int) ([]agents.Artist, error) { resp, err := l.callArtistGetSimilar(ctx, name, mbid, limit) if err != nil { return nil, err @@ -99,7 +148,7 @@ func (l *lastfmAgent) GetSimilar(ctx context.Context, id, name, mbid string, lim return res, nil } -func (l *lastfmAgent) GetTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]agents.Song, error) { +func (l *lastfmAgent) GetArtistTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]agents.Song, error) { resp, err := l.callArtistGetTopTracks(ctx, artistName, mbid, count) if err != nil { return nil, err @@ -117,6 +166,27 @@ func (l *lastfmAgent) GetTopSongs(ctx context.Context, id, artistName, mbid stri return res, nil } +func (l *lastfmAgent) callAlbumGetInfo(ctx context.Context, name, artist, mbid string) (*Album, error) { + 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) { a, err := l.client.ArtistGetInfo(ctx, name, mbid) var lfErr *lastFMError diff --git a/core/agents/lastfm/agent_test.go b/core/agents/lastfm/agent_test.go index 1e399c8b..4f0aeef1 100644 --- a/core/agents/lastfm/agent_test.go +++ b/core/agents/lastfm/agent_test.go @@ -43,7 +43,7 @@ var _ = Describe("lastfmAgent", func() { }) }) - Describe("GetBiography", func() { + Describe("GetArtistBiography", func() { var agent *lastfmAgent var httpClient *tests.FakeHttpClient BeforeEach(func() { @@ -56,52 +56,52 @@ var _ = Describe("lastfmAgent", func() { It("returns the biography", func() { f, _ := os.Open("tests/fixtures/lastfm.artist.getinfo.json") httpClient.Res = http.Response{Body: f, StatusCode: 200} - Expect(agent.GetBiography(ctx, "123", "U2", "mbid-1234")).To(Equal("U2 é uma das mais importantes bandas de rock de todos os tempos. Formada em 1976 em Dublin, composta por Bono (vocalista e guitarrista), The Edge (guitarrista, pianista e backing vocal), Adam Clayton (baixista), Larry Mullen, Jr. (baterista e percussionista).\n\nDesde a década de 80, U2 é uma das bandas mais populares no mundo. Seus shows são únicos e um verdadeiro festival de efeitos especiais, além de serem um dos que mais arrecadam anualmente. Read more on Last.fm")) + Expect(agent.GetArtistBiography(ctx, "123", "U2", "mbid-1234")).To(Equal("U2 é uma das mais importantes bandas de rock de todos os tempos. Formada em 1976 em Dublin, composta por Bono (vocalista e guitarrista), The Edge (guitarrista, pianista e backing vocal), Adam Clayton (baixista), Larry Mullen, Jr. (baterista e percussionista).\n\nDesde a década de 80, U2 é uma das bandas mais populares no mundo. Seus shows são únicos e um verdadeiro festival de efeitos especiais, além de serem um dos que mais arrecadam anualmente. Read more on Last.fm")) Expect(httpClient.RequestCount).To(Equal(1)) Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234")) }) - It("returns an error if Last.FM call fails", func() { + It("returns an error if Last.fm call fails", func() { httpClient.Err = errors.New("error") - _, err := agent.GetBiography(ctx, "123", "U2", "mbid-1234") + _, err := agent.GetArtistBiography(ctx, "123", "U2", "mbid-1234") Expect(err).To(HaveOccurred()) Expect(httpClient.RequestCount).To(Equal(1)) Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234")) }) - It("returns an error if Last.FM call returns an error", func() { + It("returns an error if Last.fm call returns an error", func() { httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError3)), StatusCode: 200} - _, err := agent.GetBiography(ctx, "123", "U2", "mbid-1234") + _, err := agent.GetArtistBiography(ctx, "123", "U2", "mbid-1234") Expect(err).To(HaveOccurred()) Expect(httpClient.RequestCount).To(Equal(1)) Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234")) }) - It("returns an error if Last.FM call returns an error 6 and mbid is empty", func() { + It("returns an error if Last.fm call returns an error 6 and mbid is empty", func() { httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError6)), StatusCode: 200} - _, err := agent.GetBiography(ctx, "123", "U2", "") + _, err := agent.GetArtistBiography(ctx, "123", "U2", "") Expect(err).To(HaveOccurred()) Expect(httpClient.RequestCount).To(Equal(1)) }) - Context("MBID non existent in Last.FM", func() { + Context("MBID non existent in Last.fm", func() { It("calls again when the response is artist == [unknown]", func() { f, _ := os.Open("tests/fixtures/lastfm.artist.getinfo.unknown.json") httpClient.Res = http.Response{Body: f, StatusCode: 200} - _, _ = agent.GetBiography(ctx, "123", "U2", "mbid-1234") + _, _ = agent.GetArtistBiography(ctx, "123", "U2", "mbid-1234") Expect(httpClient.RequestCount).To(Equal(2)) Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(BeEmpty()) }) It("calls again when last.fm returns an error 6", func() { httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError6)), StatusCode: 200} - _, _ = agent.GetBiography(ctx, "123", "U2", "mbid-1234") + _, _ = agent.GetArtistBiography(ctx, "123", "U2", "mbid-1234") Expect(httpClient.RequestCount).To(Equal(2)) Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(BeEmpty()) }) }) }) - Describe("GetSimilar", func() { + Describe("GetSimilarArtists", func() { var agent *lastfmAgent var httpClient *tests.FakeHttpClient BeforeEach(func() { @@ -114,7 +114,7 @@ var _ = Describe("lastfmAgent", func() { It("returns similar artists", func() { f, _ := os.Open("tests/fixtures/lastfm.artist.getsimilar.json") httpClient.Res = http.Response{Body: f, StatusCode: 200} - Expect(agent.GetSimilar(ctx, "123", "U2", "mbid-1234", 2)).To(Equal([]agents.Artist{ + Expect(agent.GetSimilarArtists(ctx, "123", "U2", "mbid-1234", 2)).To(Equal([]agents.Artist{ {Name: "Passengers", MBID: "e110c11f-1c94-4471-a350-c38f46b29389"}, {Name: "INXS", MBID: "481bf5f9-2e7c-4c44-b08a-05b32bc7c00d"}, })) @@ -122,47 +122,47 @@ var _ = Describe("lastfmAgent", func() { Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234")) }) - It("returns an error if Last.FM call fails", func() { + It("returns an error if Last.fm call fails", func() { httpClient.Err = errors.New("error") - _, err := agent.GetSimilar(ctx, "123", "U2", "mbid-1234", 2) + _, err := agent.GetSimilarArtists(ctx, "123", "U2", "mbid-1234", 2) Expect(err).To(HaveOccurred()) Expect(httpClient.RequestCount).To(Equal(1)) Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234")) }) - It("returns an error if Last.FM call returns an error", func() { + It("returns an error if Last.fm call returns an error", func() { httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError3)), StatusCode: 200} - _, err := agent.GetSimilar(ctx, "123", "U2", "mbid-1234", 2) + _, err := agent.GetSimilarArtists(ctx, "123", "U2", "mbid-1234", 2) Expect(err).To(HaveOccurred()) Expect(httpClient.RequestCount).To(Equal(1)) Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234")) }) - It("returns an error if Last.FM call returns an error 6 and mbid is empty", func() { + It("returns an error if Last.fm call returns an error 6 and mbid is empty", func() { httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError6)), StatusCode: 200} - _, err := agent.GetSimilar(ctx, "123", "U2", "", 2) + _, err := agent.GetSimilarArtists(ctx, "123", "U2", "", 2) Expect(err).To(HaveOccurred()) Expect(httpClient.RequestCount).To(Equal(1)) }) - Context("MBID non existent in Last.FM", func() { + Context("MBID non existent in Last.fm", func() { It("calls again when the response is artist == [unknown]", func() { f, _ := os.Open("tests/fixtures/lastfm.artist.getsimilar.unknown.json") httpClient.Res = http.Response{Body: f, StatusCode: 200} - _, _ = agent.GetSimilar(ctx, "123", "U2", "mbid-1234", 2) + _, _ = agent.GetSimilarArtists(ctx, "123", "U2", "mbid-1234", 2) Expect(httpClient.RequestCount).To(Equal(2)) Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(BeEmpty()) }) It("calls again when last.fm returns an error 6", func() { httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError6)), StatusCode: 200} - _, _ = agent.GetSimilar(ctx, "123", "U2", "mbid-1234", 2) + _, _ = agent.GetSimilarArtists(ctx, "123", "U2", "mbid-1234", 2) Expect(httpClient.RequestCount).To(Equal(2)) Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(BeEmpty()) }) }) }) - Describe("GetTopSongs", func() { + Describe("GetArtistTopSongs", func() { var agent *lastfmAgent var httpClient *tests.FakeHttpClient BeforeEach(func() { @@ -175,7 +175,7 @@ var _ = Describe("lastfmAgent", func() { It("returns top songs", func() { f, _ := os.Open("tests/fixtures/lastfm.artist.gettoptracks.json") httpClient.Res = http.Response{Body: f, StatusCode: 200} - Expect(agent.GetTopSongs(ctx, "123", "U2", "mbid-1234", 2)).To(Equal([]agents.Song{ + Expect(agent.GetArtistTopSongs(ctx, "123", "U2", "mbid-1234", 2)).To(Equal([]agents.Song{ {Name: "Beautiful Day", MBID: "f7f264d0-a89b-4682-9cd7-a4e7c37637af"}, {Name: "With or Without You", MBID: "6b9a509f-6907-4a6e-9345-2f12da09ba4b"}, })) @@ -183,40 +183,40 @@ var _ = Describe("lastfmAgent", func() { Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234")) }) - It("returns an error if Last.FM call fails", func() { + It("returns an error if Last.fm call fails", func() { httpClient.Err = errors.New("error") - _, err := agent.GetTopSongs(ctx, "123", "U2", "mbid-1234", 2) + _, err := agent.GetArtistTopSongs(ctx, "123", "U2", "mbid-1234", 2) Expect(err).To(HaveOccurred()) Expect(httpClient.RequestCount).To(Equal(1)) Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234")) }) - It("returns an error if Last.FM call returns an error", func() { + It("returns an error if Last.fm call returns an error", func() { httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError3)), StatusCode: 200} - _, err := agent.GetTopSongs(ctx, "123", "U2", "mbid-1234", 2) + _, err := agent.GetArtistTopSongs(ctx, "123", "U2", "mbid-1234", 2) Expect(err).To(HaveOccurred()) Expect(httpClient.RequestCount).To(Equal(1)) Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234")) }) - It("returns an error if Last.FM call returns an error 6 and mbid is empty", func() { + It("returns an error if Last.fm call returns an error 6 and mbid is empty", func() { httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError6)), StatusCode: 200} - _, err := agent.GetTopSongs(ctx, "123", "U2", "", 2) + _, err := agent.GetArtistTopSongs(ctx, "123", "U2", "", 2) Expect(err).To(HaveOccurred()) Expect(httpClient.RequestCount).To(Equal(1)) }) - Context("MBID non existent in Last.FM", func() { + Context("MBID non existent in Last.fm", func() { It("calls again when the response is artist == [unknown]", func() { f, _ := os.Open("tests/fixtures/lastfm.artist.gettoptracks.unknown.json") httpClient.Res = http.Response{Body: f, StatusCode: 200} - _, _ = agent.GetTopSongs(ctx, "123", "U2", "mbid-1234", 2) + _, _ = agent.GetArtistTopSongs(ctx, "123", "U2", "mbid-1234", 2) Expect(httpClient.RequestCount).To(Equal(2)) Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(BeEmpty()) }) It("calls again when last.fm returns an error 6", func() { httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError6)), StatusCode: 200} - _, _ = agent.GetTopSongs(ctx, "123", "U2", "mbid-1234", 2) + _, _ = agent.GetArtistTopSongs(ctx, "123", "U2", "mbid-1234", 2) Expect(httpClient.RequestCount).To(Equal(2)) Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(BeEmpty()) }) @@ -350,4 +350,89 @@ var _ = Describe("lastfmAgent", func() { }) }) + Describe("GetAlbumInfo", func() { + var agent *lastfmAgent + var httpClient *tests.FakeHttpClient + BeforeEach(func() { + httpClient = &tests.FakeHttpClient{} + client := NewClient("API_KEY", "SECRET", "pt", httpClient) + agent = lastFMConstructor(ds) + agent.client = client + }) + + It("returns the biography", func() { + f, _ := os.Open("tests/fixtures/lastfm.album.getinfo.json") + httpClient.Res = http.Response{Body: f, StatusCode: 200} + Expect(agent.GetAlbumInfo(ctx, "Believe", "Cher", "03c91c40-49a6-44a7-90e7-a700edf97a62")).To(Equal(&agents.AlbumInfo{ + Name: "Believe", + MBID: "03c91c40-49a6-44a7-90e7-a700edf97a62", + Description: "Believe is the twenty-third studio album by American singer-actress Cher, released on November 10, 1998 by Warner Bros. Records. The RIAA certified it Quadruple Platinum on December 23, 1999, recognizing four million shipments in the United States; Worldwide, the album has sold more than 20 million copies, making it the biggest-selling album of her career. In 1999 the album received three Grammy Awards nominations including \"Record of the Year\", \"Best Pop Album\" and winning \"Best Dance Recording\" for the single \"Believe\". It was released by Warner Bros. Records at the end of 1998. The album was executive produced by Rob Read more on Last.fm.", + URL: "https://www.last.fm/music/Cher/Believe", + Images: []agents.ExternalImage{ + { + URL: "https://lastfm.freetls.fastly.net/i/u/34s/3b54885952161aaea4ce2965b2db1638.png", + Size: 34, + }, + { + URL: "https://lastfm.freetls.fastly.net/i/u/64s/3b54885952161aaea4ce2965b2db1638.png", + Size: 64, + }, + { + URL: "https://lastfm.freetls.fastly.net/i/u/174s/3b54885952161aaea4ce2965b2db1638.png", + Size: 174, + }, + { + URL: "https://lastfm.freetls.fastly.net/i/u/300x300/3b54885952161aaea4ce2965b2db1638.png", + Size: 300, + }, + }, + })) + Expect(httpClient.RequestCount).To(Equal(1)) + Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("03c91c40-49a6-44a7-90e7-a700edf97a62")) + }) + + It("returns empty images if no images are available", func() { + f, _ := os.Open("tests/fixtures/lastfm.album.getinfo.empty_urls.json") + httpClient.Res = http.Response{Body: f, StatusCode: 200} + Expect(agent.GetAlbumInfo(ctx, "The Definitive Less Damage And More Joy", "The Jesus and Mary Chain", "")).To(Equal(&agents.AlbumInfo{ + Name: "The Definitive Less Damage And More Joy", + URL: "https://www.last.fm/music/The+Jesus+and+Mary+Chain/The+Definitive+Less+Damage+And+More+Joy", + Images: []agents.ExternalImage{}, + })) + Expect(httpClient.RequestCount).To(Equal(1)) + Expect(httpClient.SavedRequest.URL.Query().Get("album")).To(Equal("The Definitive Less Damage And More Joy")) + }) + + It("returns an error if Last.fm call fails", func() { + httpClient.Err = errors.New("error") + _, err := agent.GetAlbumInfo(ctx, "123", "U2", "mbid-1234") + Expect(err).To(HaveOccurred()) + Expect(httpClient.RequestCount).To(Equal(1)) + Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234")) + }) + + It("returns an error if Last.fm call returns an error", func() { + httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError3)), StatusCode: 200} + _, err := agent.GetAlbumInfo(ctx, "123", "U2", "mbid-1234") + Expect(err).To(HaveOccurred()) + Expect(httpClient.RequestCount).To(Equal(1)) + Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(Equal("mbid-1234")) + }) + + It("returns an error if Last.fm call returns an error 6 and mbid is empty", func() { + httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError6)), StatusCode: 200} + _, err := agent.GetAlbumInfo(ctx, "123", "U2", "") + Expect(err).To(HaveOccurred()) + Expect(httpClient.RequestCount).To(Equal(1)) + }) + + Context("MBID non existent in Last.fm", func() { + It("calls again when last.fm returns an error 6", func() { + httpClient.Res = http.Response{Body: io.NopCloser(bytes.NewBufferString(lastfmError6)), StatusCode: 200} + _, _ = agent.GetAlbumInfo(ctx, "123", "U2", "mbid-1234") + Expect(httpClient.RequestCount).To(Equal(2)) + Expect(httpClient.SavedRequest.URL.Query().Get("mbid")).To(BeEmpty()) + }) + }) + }) }) diff --git a/core/agents/lastfm/client.go b/core/agents/lastfm/client.go index 2757c96f..42df84a8 100644 --- a/core/agents/lastfm/client.go +++ b/core/agents/lastfm/client.go @@ -45,6 +45,20 @@ type Client struct { hc httpDoer } +func (c *Client) AlbumGetInfo(ctx context.Context, name string, artist string, mbid string) (*Album, error) { + params := url.Values{} + params.Add("method", "album.getInfo") + params.Add("album", name) + params.Add("artist", artist) + params.Add("mbid", mbid) + params.Add("lang", c.lang) + response, err := c.makeRequest(ctx, http.MethodGet, params, false) + if err != nil { + return nil, err + } + return &response.Album, nil +} + func (c *Client) ArtistGetInfo(ctx context.Context, name string, mbid string) (*Artist, error) { params := url.Values{} params.Add("method", "artist.getInfo") diff --git a/core/agents/lastfm/client_test.go b/core/agents/lastfm/client_test.go index e08955fa..fedb1ff2 100644 --- a/core/agents/lastfm/client_test.go +++ b/core/agents/lastfm/client_test.go @@ -25,6 +25,18 @@ var _ = Describe("Client", func() { client = NewClient("API_KEY", "SECRET", "pt", httpClient) }) + Describe("AlbumGetInfo", func() { + It("returns an album on successful response", func() { + f, _ := os.Open("tests/fixtures/lastfm.album.getinfo.json") + httpClient.Res = http.Response{Body: f, StatusCode: 200} + + album, err := client.AlbumGetInfo(context.Background(), "Believe", "U2", "mbid-1234") + Expect(err).To(BeNil()) + Expect(album.Name).To(Equal("Believe")) + Expect(httpClient.SavedRequest.URL.String()).To(Equal(apiBaseUrl + "?album=Believe&api_key=API_KEY&artist=U2&format=json&lang=pt&mbid=mbid-1234&method=album.getInfo")) + }) + }) + Describe("ArtistGetInfo", func() { It("returns an artist for a successful response", func() { f, _ := os.Open("tests/fixtures/lastfm.artist.getinfo.json") @@ -36,7 +48,7 @@ var _ = Describe("Client", func() { Expect(httpClient.SavedRequest.URL.String()).To(Equal(apiBaseUrl + "?api_key=API_KEY&artist=U2&format=json&lang=pt&mbid=123&method=artist.getInfo")) }) - It("fails if Last.FM returns an http status != 200", func() { + It("fails if Last.fm returns an http status != 200", func() { httpClient.Res = http.Response{ Body: io.NopCloser(bytes.NewBufferString(`Internal Server Error`)), StatusCode: 500, @@ -46,7 +58,7 @@ var _ = Describe("Client", func() { Expect(err).To(MatchError("last.fm http status: (500)")) }) - It("fails if Last.FM returns an http status != 200", func() { + It("fails if Last.fm returns an http status != 200", func() { httpClient.Res = http.Response{ Body: io.NopCloser(bytes.NewBufferString(`{"error":3,"message":"Invalid Method - No method with that name in this package"}`)), StatusCode: 400, @@ -56,7 +68,7 @@ var _ = Describe("Client", func() { Expect(err).To(MatchError(&lastFMError{Code: 3, Message: "Invalid Method - No method with that name in this package"})) }) - It("fails if Last.FM returns an error", func() { + It("fails if Last.fm returns an error", func() { httpClient.Res = http.Response{ Body: io.NopCloser(bytes.NewBufferString(`{"error":6,"message":"The artist you supplied could not be found"}`)), StatusCode: 200, diff --git a/core/agents/lastfm/responses.go b/core/agents/lastfm/responses.go index 06914ec5..1ceebe76 100644 --- a/core/agents/lastfm/responses.go +++ b/core/agents/lastfm/responses.go @@ -4,6 +4,7 @@ type Response struct { Artist Artist `json:"artist"` SimilarArtists SimilarArtists `json:"similarartists"` TopTracks TopTracks `json:"toptracks"` + Album Album `json:"album"` Error int `json:"error"` Message string `json:"message"` Token string `json:"token"` @@ -12,12 +13,20 @@ type Response struct { Scrobbles Scrobbles `json:"scrobbles"` } +type Album struct { + Name string `json:"name"` + MBID string `json:"mbid"` + URL string `json:"url"` + Image []ExternalImage `json:"image"` + Description Description `json:"wiki"` +} + type Artist struct { - Name string `json:"name"` - MBID string `json:"mbid"` - URL string `json:"url"` - Image []ArtistImage `json:"image"` - Bio ArtistBio `json:"bio"` + Name string `json:"name"` + MBID string `json:"mbid"` + URL string `json:"url"` + Image []ExternalImage `json:"image"` + Bio Description `json:"bio"` } type SimilarArtists struct { @@ -29,12 +38,12 @@ type Attr struct { Artist string `json:"artist"` } -type ArtistImage struct { +type ExternalImage struct { URL string `json:"#text"` Size string `json:"size"` } -type ArtistBio struct { +type Description struct { Published string `json:"published"` Summary string `json:"summary"` Content string `json:"content"` diff --git a/core/agents/local_agent.go b/core/agents/local_agent.go index 05d0fb3d..ce8f9f07 100644 --- a/core/agents/local_agent.go +++ b/core/agents/local_agent.go @@ -21,7 +21,7 @@ func (p *localAgent) AgentName() string { return LocalAgentName } -func (p *localAgent) GetTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]Song, error) { +func (p *localAgent) GetArtistTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]Song, error) { top, err := p.ds.MediaFile(ctx).GetAll(model.QueryOptions{ Sort: "playCount", Order: "desc", diff --git a/core/agents/spotify/spotify.go b/core/agents/spotify/spotify.go index aa751672..72939470 100644 --- a/core/agents/spotify/spotify.go +++ b/core/agents/spotify/spotify.go @@ -44,7 +44,7 @@ func (s *spotifyAgent) AgentName() string { return spotifyAgentName } -func (s *spotifyAgent) GetImages(ctx context.Context, id, name, mbid string) ([]agents.ArtistImage, error) { +func (s *spotifyAgent) GetArtistImages(ctx context.Context, id, name, mbid string) ([]agents.ExternalImage, error) { a, err := s.searchArtist(ctx, name) if err != nil { if errors.Is(err, model.ErrNotFound) { @@ -55,9 +55,9 @@ func (s *spotifyAgent) GetImages(ctx context.Context, id, name, mbid string) ([] return nil, err } - var res []agents.ArtistImage + var res []agents.ExternalImage for _, img := range a.Images { - res = append(res, agents.ArtistImage{ + res = append(res, agents.ExternalImage{ URL: img.URL, Size: img.Width, }) diff --git a/core/artwork/artwork.go b/core/artwork/artwork.go index 5fee7aa3..a23b1980 100644 --- a/core/artwork/artwork.go +++ b/core/artwork/artwork.go @@ -98,7 +98,7 @@ func (a *artwork) getArtworkReader(ctx context.Context, artID model.ArtworkID, s case model.KindArtistArtwork: artReader, err = newArtistReader(ctx, a, artID, a.em) case model.KindAlbumArtwork: - artReader, err = newAlbumArtworkReader(ctx, a, artID) + artReader, err = newAlbumArtworkReader(ctx, a, artID, a.em) case model.KindMediaFileArtwork: artReader, err = newMediafileArtworkReader(ctx, a, artID) case model.KindPlaylistArtwork: diff --git a/core/artwork/artwork_internal_test.go b/core/artwork/artwork_internal_test.go index 55d5083e..8807001f 100644 --- a/core/artwork/artwork_internal_test.go +++ b/core/artwork/artwork_internal_test.go @@ -49,7 +49,7 @@ var _ = Describe("Artwork", func() { Describe("albumArtworkReader", func() { Context("ID not found", func() { It("returns ErrNotFound if album is not in the DB", func() { - _, err := newAlbumArtworkReader(ctx, aw, model.MustParseArtworkID("al-NOT_FOUND")) + _, err := newAlbumArtworkReader(ctx, aw, model.MustParseArtworkID("al-NOT_FOUND"), nil) Expect(err).To(MatchError(model.ErrNotFound)) }) }) @@ -61,7 +61,7 @@ var _ = Describe("Artwork", func() { }) }) It("returns embed cover", func() { - aw, err := newAlbumArtworkReader(ctx, aw, alOnlyEmbed.CoverArtID()) + aw, err := newAlbumArtworkReader(ctx, aw, alOnlyEmbed.CoverArtID(), nil) Expect(err).ToNot(HaveOccurred()) _, path, err := aw.Reader(ctx) Expect(err).ToNot(HaveOccurred()) @@ -69,7 +69,7 @@ var _ = Describe("Artwork", func() { }) It("returns placeholder if embed path is not available", func() { ffmpeg.Error = errors.New("not available") - aw, err := newAlbumArtworkReader(ctx, aw, alEmbedNotFound.CoverArtID()) + aw, err := newAlbumArtworkReader(ctx, aw, alEmbedNotFound.CoverArtID(), nil) Expect(err).ToNot(HaveOccurred()) _, path, err := aw.Reader(ctx) Expect(err).ToNot(HaveOccurred()) @@ -84,14 +84,14 @@ var _ = Describe("Artwork", func() { }) }) It("returns external cover", func() { - aw, err := newAlbumArtworkReader(ctx, aw, alOnlyExternal.CoverArtID()) + aw, err := newAlbumArtworkReader(ctx, aw, alOnlyExternal.CoverArtID(), nil) Expect(err).ToNot(HaveOccurred()) _, path, err := aw.Reader(ctx) Expect(err).ToNot(HaveOccurred()) Expect(path).To(Equal("tests/fixtures/front.png")) }) It("returns placeholder if external file is not available", func() { - aw, err := newAlbumArtworkReader(ctx, aw, alExternalNotFound.CoverArtID()) + aw, err := newAlbumArtworkReader(ctx, aw, alExternalNotFound.CoverArtID(), nil) Expect(err).ToNot(HaveOccurred()) _, path, err := aw.Reader(ctx) Expect(err).ToNot(HaveOccurred()) @@ -107,7 +107,7 @@ var _ = Describe("Artwork", func() { DescribeTable("CoverArtPriority", func(priority string, expected string) { conf.Server.CoverArtPriority = priority - aw, err := newAlbumArtworkReader(ctx, aw, alMultipleCovers.CoverArtID()) + aw, err := newAlbumArtworkReader(ctx, aw, alMultipleCovers.CoverArtID(), nil) Expect(err).ToNot(HaveOccurred()) _, path, err := aw.Reader(ctx) Expect(err).ToNot(HaveOccurred()) @@ -122,7 +122,7 @@ var _ = Describe("Artwork", func() { Describe("mediafileArtworkReader", func() { Context("ID not found", func() { It("returns ErrNotFound if mediafile is not in the DB", func() { - _, err := newAlbumArtworkReader(ctx, aw, alMultipleCovers.CoverArtID()) + _, err := newAlbumArtworkReader(ctx, aw, alMultipleCovers.CoverArtID(), nil) Expect(err).To(MatchError(model.ErrNotFound)) }) }) diff --git a/core/artwork/reader_album.go b/core/artwork/reader_album.go index 3666acd6..54174e65 100644 --- a/core/artwork/reader_album.go +++ b/core/artwork/reader_album.go @@ -7,6 +7,7 @@ import ( "time" "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/ffmpeg" "github.com/navidrome/navidrome/model" ) @@ -14,16 +15,18 @@ import ( type albumArtworkReader struct { cacheKey a *artwork + em core.ExternalMetadata album model.Album } -func newAlbumArtworkReader(ctx context.Context, artwork *artwork, artID model.ArtworkID) (*albumArtworkReader, error) { +func newAlbumArtworkReader(ctx context.Context, artwork *artwork, artID model.ArtworkID, em core.ExternalMetadata) (*albumArtworkReader, error) { al, err := artwork.ds.Album(ctx).Get(artID.ID) if err != nil { return nil, err } a := &albumArtworkReader{ a: artwork, + em: em, album: *al, } a.cacheKey.artID = artID @@ -36,21 +39,22 @@ func (a *albumArtworkReader) LastUpdated() time.Time { } func (a *albumArtworkReader) Reader(ctx context.Context) (io.ReadCloser, string, error) { - var ff = fromCoverArtPriority(ctx, a.a.ffmpeg, conf.Server.CoverArtPriority, a.album) + var ff = a.fromCoverArtPriority(ctx, a.a.ffmpeg, conf.Server.CoverArtPriority) ff = append(ff, fromAlbumPlaceholder()) return selectImageReader(ctx, a.artID, ff...) } -func fromCoverArtPriority(ctx context.Context, ffmpeg ffmpeg.FFmpeg, priority string, al model.Album) []sourceFunc { +func (a *albumArtworkReader) fromCoverArtPriority(ctx context.Context, ffmpeg ffmpeg.FFmpeg, priority string) []sourceFunc { var ff []sourceFunc for _, pattern := range strings.Split(strings.ToLower(priority), ",") { pattern = strings.TrimSpace(pattern) - if pattern == "embedded" { - ff = append(ff, fromTag(al.EmbedArtPath), fromFFmpegTag(ctx, ffmpeg, al.EmbedArtPath)) - continue - } - if al.ImageFiles != "" { - ff = append(ff, fromExternalFile(ctx, al.ImageFiles, pattern)) + switch { + case pattern == "embedded": + ff = append(ff, fromTag(a.album.EmbedArtPath), fromFFmpegTag(ctx, ffmpeg, a.album.EmbedArtPath)) + case pattern == "external": + ff = append(ff, fromAlbumExternalSource(ctx, a.album, a.em)) + case a.album.ImageFiles != "": + ff = append(ff, fromExternalFile(ctx, a.album.ImageFiles, pattern)) } } return ff diff --git a/core/artwork/reader_artist.go b/core/artwork/reader_artist.go index c261c06c..6464779f 100644 --- a/core/artwork/reader_artist.go +++ b/core/artwork/reader_artist.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "io/fs" - "net/http" "os" "path/filepath" "strings" @@ -81,7 +80,7 @@ func (a *artistReader) Reader(ctx context.Context) (io.ReadCloser, string, error return selectImageReader(ctx, a.artID, fromArtistFolder(ctx, a.artistFolder, "artist.*"), fromExternalFile(ctx, a.files, "artist.*"), - fromExternalSource(ctx, a.artist, a.em), + fromArtistExternalSource(ctx, a.artist, a.em), fromArtistPlaceholder(), ) } @@ -106,24 +105,3 @@ func fromArtistFolder(ctx context.Context, artistFolder string, pattern string) return f, filePath, err } } - -func fromExternalSource(ctx context.Context, ar model.Artist, em core.ExternalMetadata) sourceFunc { - return func() (io.ReadCloser, string, error) { - imageUrl, err := em.ArtistImage(ctx, ar.ID) - if err != nil { - return nil, "", err - } - - hc := http.Client{Timeout: 5 * time.Second} - req, _ := http.NewRequestWithContext(ctx, http.MethodGet, imageUrl.String(), nil) - resp, err := hc.Do(req) - if err != nil { - return nil, "", err - } - if resp.StatusCode != http.StatusOK { - resp.Body.Close() - return nil, "", fmt.Errorf("error retrieveing cover from %s: %s", imageUrl, resp.Status) - } - return resp.Body, imageUrl.String(), nil - } -} diff --git a/core/artwork/sources.go b/core/artwork/sources.go index ee557d16..04336e5d 100644 --- a/core/artwork/sources.go +++ b/core/artwork/sources.go @@ -5,6 +5,8 @@ import ( "context" "fmt" "io" + "net/http" + "net/url" "os" "path/filepath" "reflect" @@ -14,6 +16,7 @@ import ( "github.com/dhowden/tag" "github.com/navidrome/navidrome/consts" + "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/ffmpeg" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" @@ -138,3 +141,39 @@ func fromArtistPlaceholder() sourceFunc { return r, consts.PlaceholderArtistArt, nil } } + +func fromArtistExternalSource(ctx context.Context, ar model.Artist, em core.ExternalMetadata) sourceFunc { + return func() (io.ReadCloser, string, error) { + imageUrl, err := em.ArtistImage(ctx, ar.ID) + if err != nil { + return nil, "", err + } + + return fromURL(ctx, imageUrl) + } +} + +func fromAlbumExternalSource(ctx context.Context, al model.Album, em core.ExternalMetadata) sourceFunc { + return func() (io.ReadCloser, string, error) { + imageUrl, err := em.AlbumImage(ctx, al.ID) + if err != nil { + return nil, "", err + } + + return fromURL(ctx, imageUrl) + } +} + +func fromURL(ctx context.Context, imageUrl *url.URL) (io.ReadCloser, string, error) { + hc := http.Client{Timeout: 5 * time.Second} + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, imageUrl.String(), nil) + resp, err := hc.Do(req) + if err != nil { + return nil, "", err + } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, "", fmt.Errorf("error retrieveing artwork from %s: %s", imageUrl, resp.Status) + } + return resp.Body, imageUrl.String(), nil +} diff --git a/core/external_metadata.go b/core/external_metadata.go index 21dc4b32..2b07798d 100644 --- a/core/external_metadata.go +++ b/core/external_metadata.go @@ -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 } diff --git a/db/migration/20230117180400_add_album_info.go b/db/migration/20230117180400_add_album_info.go new file mode 100644 index 00000000..995150e3 --- /dev/null +++ b/db/migration/20230117180400_add_album_info.go @@ -0,0 +1,33 @@ +package migrations + +import ( + "database/sql" + + "github.com/pressly/goose" +) + +func init() { + goose.AddMigration(upAddAlbumInfo, downAddAlbumInfo) +} + +func upAddAlbumInfo(tx *sql.Tx) error { + _, err := tx.Exec(` +alter table album + add description varchar(255) default '' not null; +alter table album + add small_image_url varchar(255) default '' not null; +alter table album + add medium_image_url varchar(255) default '' not null; +alter table album + add large_image_url varchar(255) default '' not null; +alter table album + add external_url varchar(255) default '' not null; +alter table album + add external_info_updated_at datetime; +`) + return err +} + +func downAddAlbumInfo(tx *sql.Tx) error { + return nil +} diff --git a/model/album.go b/model/album.go index 5cc79121..17124748 100644 --- a/model/album.go +++ b/model/album.go @@ -10,38 +10,44 @@ import ( type Album struct { Annotations `structs:"-"` - ID string `structs:"id" json:"id" orm:"column(id)"` - Name string `structs:"name" json:"name"` - EmbedArtPath string `structs:"embed_art_path" json:"embedArtPath"` - ArtistID string `structs:"artist_id" json:"artistId" orm:"column(artist_id)"` - Artist string `structs:"artist" json:"artist"` - AlbumArtistID string `structs:"album_artist_id" json:"albumArtistId" orm:"column(album_artist_id)"` - AlbumArtist string `structs:"album_artist" json:"albumArtist"` - AllArtistIDs string `structs:"all_artist_ids" json:"allArtistIds" orm:"column(all_artist_ids)"` - MaxYear int `structs:"max_year" json:"maxYear"` - MinYear int `structs:"min_year" json:"minYear"` - Compilation bool `structs:"compilation" json:"compilation"` - Comment string `structs:"comment" json:"comment,omitempty"` - SongCount int `structs:"song_count" json:"songCount"` - Duration float32 `structs:"duration" json:"duration"` - Size int64 `structs:"size" json:"size"` - Genre string `structs:"genre" json:"genre"` - Genres Genres `structs:"-" json:"genres"` - FullText string `structs:"full_text" json:"fullText"` - SortAlbumName string `structs:"sort_album_name" json:"sortAlbumName,omitempty"` - SortArtistName string `structs:"sort_artist_name" json:"sortArtistName,omitempty"` - SortAlbumArtistName string `structs:"sort_album_artist_name" json:"sortAlbumArtistName,omitempty"` - OrderAlbumName string `structs:"order_album_name" json:"orderAlbumName"` - OrderAlbumArtistName string `structs:"order_album_artist_name" json:"orderAlbumArtistName"` - CatalogNum string `structs:"catalog_num" json:"catalogNum,omitempty"` - MbzAlbumID string `structs:"mbz_album_id" json:"mbzAlbumId,omitempty" orm:"column(mbz_album_id)"` - MbzAlbumArtistID string `structs:"mbz_album_artist_id" json:"mbzAlbumArtistId,omitempty" orm:"column(mbz_album_artist_id)"` - MbzAlbumType string `structs:"mbz_album_type" json:"mbzAlbumType,omitempty"` - MbzAlbumComment string `structs:"mbz_album_comment" json:"mbzAlbumComment,omitempty"` - ImageFiles string `structs:"image_files" json:"imageFiles,omitempty"` - Paths string `structs:"paths" json:"paths,omitempty"` - CreatedAt time.Time `structs:"created_at" json:"createdAt"` - UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"` + ID string `structs:"id" json:"id" orm:"column(id)"` + Name string `structs:"name" json:"name"` + EmbedArtPath string `structs:"embed_art_path" json:"embedArtPath"` + ArtistID string `structs:"artist_id" json:"artistId" orm:"column(artist_id)"` + Artist string `structs:"artist" json:"artist"` + AlbumArtistID string `structs:"album_artist_id" json:"albumArtistId" orm:"column(album_artist_id)"` + AlbumArtist string `structs:"album_artist" json:"albumArtist"` + AllArtistIDs string `structs:"all_artist_ids" json:"allArtistIds" orm:"column(all_artist_ids)"` + MaxYear int `structs:"max_year" json:"maxYear"` + MinYear int `structs:"min_year" json:"minYear"` + Compilation bool `structs:"compilation" json:"compilation"` + Comment string `structs:"comment" json:"comment,omitempty"` + SongCount int `structs:"song_count" json:"songCount"` + Duration float32 `structs:"duration" json:"duration"` + Size int64 `structs:"size" json:"size"` + Genre string `structs:"genre" json:"genre"` + Genres Genres `structs:"-" json:"genres"` + FullText string `structs:"full_text" json:"fullText"` + SortAlbumName string `structs:"sort_album_name" json:"sortAlbumName,omitempty"` + SortArtistName string `structs:"sort_artist_name" json:"sortArtistName,omitempty"` + SortAlbumArtistName string `structs:"sort_album_artist_name" json:"sortAlbumArtistName,omitempty"` + OrderAlbumName string `structs:"order_album_name" json:"orderAlbumName"` + OrderAlbumArtistName string `structs:"order_album_artist_name" json:"orderAlbumArtistName"` + CatalogNum string `structs:"catalog_num" json:"catalogNum,omitempty"` + MbzAlbumID string `structs:"mbz_album_id" json:"mbzAlbumId,omitempty" orm:"column(mbz_album_id)"` + MbzAlbumArtistID string `structs:"mbz_album_artist_id" json:"mbzAlbumArtistId,omitempty" orm:"column(mbz_album_artist_id)"` + MbzAlbumType string `structs:"mbz_album_type" json:"mbzAlbumType,omitempty"` + MbzAlbumComment string `structs:"mbz_album_comment" json:"mbzAlbumComment,omitempty"` + ImageFiles string `structs:"image_files" json:"imageFiles,omitempty"` + Paths string `structs:"paths" json:"paths,omitempty"` + Description string `structs:"description" json:"description,omitempty"` + SmallImageUrl string `structs:"small_image_url" json:"smallImageUrl,omitempty"` + MediumImageUrl string `structs:"medium_image_url" json:"mediumImageUrl,omitempty"` + LargeImageUrl string `structs:"large_image_url" json:"largeImageUrl,omitempty"` + ExternalUrl string `structs:"external_url" json:"externalUrl,omitempty" orm:"column(external_url)"` + ExternalInfoUpdatedAt time.Time `structs:"external_info_updated_at" json:"externalInfoUpdatedAt"` + CreatedAt time.Time `structs:"created_at" json:"createdAt"` + UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"` } func (a Album) CoverArtID() ArtworkID { diff --git a/server/initial_setup.go b/server/initial_setup.go index 0044a0a7..53aeca2b 100644 --- a/server/initial_setup.go +++ b/server/initial_setup.go @@ -92,9 +92,9 @@ func checkFfmpegInstallation() { func checkExternalCredentials() { if conf.Server.EnableExternalServices { if !conf.Server.LastFM.Enabled { - log.Info("Last.FM integration is DISABLED") + log.Info("Last.fm integration is DISABLED") } else { - log.Debug("Last.FM integration is ENABLED") + log.Debug("Last.fm integration is ENABLED") } if !conf.Server.ListenBrainz.Enabled { diff --git a/server/subsonic/api.go b/server/subsonic/api.go index d16dad4a..8906260a 100644 --- a/server/subsonic/api.go +++ b/server/subsonic/api.go @@ -83,6 +83,8 @@ func (api *Router) routes() http.Handler { h(r, "getArtist", api.GetArtist) h(r, "getAlbum", api.GetAlbum) h(r, "getSong", api.GetSong) + h(r, "getAlbumInfo", api.GetAlbumInfo) + h(r, "getAlbumInfo2", api.GetAlbumInfo) h(r, "getArtistInfo", api.GetArtistInfo) h(r, "getArtistInfo2", api.GetArtistInfo2) h(r, "getTopSongs", api.GetTopSongs) @@ -162,7 +164,6 @@ func (api *Router) routes() http.Handler { // Not Implemented (yet?) h501(r, "jukeboxControl") - h501(r, "getAlbumInfo", "getAlbumInfo2") h501(r, "getShares", "createShare", "updateShare", "deleteShare") h501(r, "getPodcasts", "getNewestPodcasts", "refreshPodcasts", "createPodcastChannel", "deletePodcastChannel", "deletePodcastEpisode", "downloadPodcastEpisode") diff --git a/server/subsonic/browsing.go b/server/subsonic/browsing.go index 65b8f175..f9c48ac2 100644 --- a/server/subsonic/browsing.go +++ b/server/subsonic/browsing.go @@ -154,6 +154,7 @@ func (api *Router) GetArtist(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetAlbum(r *http.Request) (*responses.Subsonic, error) { id := utils.ParamString(r, "id") + ctx := r.Context() album, err := api.ds.Album(ctx).Get(id) @@ -177,6 +178,32 @@ func (api *Router) GetAlbum(r *http.Request) (*responses.Subsonic, error) { return response, nil } +func (api *Router) GetAlbumInfo(r *http.Request) (*responses.Subsonic, error) { + id, err := requiredParamString(r, "id") + ctx := r.Context() + + if err != nil { + return nil, err + } + + album, err := api.externalMetadata.UpdateAlbumInfo(ctx, id) + + if err != nil { + return nil, err + } + + response := newResponse() + response.AlbumInfo = &responses.AlbumInfo{} + response.AlbumInfo.Notes = album.Description + response.AlbumInfo.SmallImageUrl = album.SmallImageUrl + response.AlbumInfo.MediumImageUrl = album.MediumImageUrl + response.AlbumInfo.LargeImageUrl = album.LargeImageUrl + response.AlbumInfo.LastFmUrl = album.ExternalUrl + response.AlbumInfo.MusicBrainzID = album.MbzAlbumID + + return response, nil +} + func (api *Router) GetSong(r *http.Request) (*responses.Subsonic, error) { id := utils.ParamString(r, "id") ctx := r.Context() @@ -397,7 +424,6 @@ func (api *Router) buildAlbum(ctx context.Context, album *model.Album, mfs model if album.Starred { dir.Starred = &album.StarredAt } - dir.Song = childrenFromMediaFiles(ctx, mfs) return dir } diff --git a/server/subsonic/responses/.snapshots/Responses AlbumInfo with data should match .JSON b/server/subsonic/responses/.snapshots/Responses AlbumInfo with data should match .JSON new file mode 100644 index 00000000..30919663 --- /dev/null +++ b/server/subsonic/responses/.snapshots/Responses AlbumInfo with data should match .JSON @@ -0,0 +1 @@ +{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","albumInfo":{"notes":"Believe is the twenty-third studio album by American singer-actress Cher...","musicBrainzId":"03c91c40-49a6-44a7-90e7-a700edf97a62","lastFmUrl":"https://www.last.fm/music/Cher/Believe","smallImageUrl":"https://lastfm.freetls.fastly.net/i/u/34s/3b54885952161aaea4ce2965b2db1638.png","mediumImageUrl":"https://lastfm.freetls.fastly.net/i/u/64s/3b54885952161aaea4ce2965b2db1638.png","largeImageUrl":"https://lastfm.freetls.fastly.net/i/u/174s/3b54885952161aaea4ce2965b2db1638.png"}} diff --git a/server/subsonic/responses/.snapshots/Responses AlbumInfo with data should match .XML b/server/subsonic/responses/.snapshots/Responses AlbumInfo with data should match .XML new file mode 100644 index 00000000..a2f86147 --- /dev/null +++ b/server/subsonic/responses/.snapshots/Responses AlbumInfo with data should match .XML @@ -0,0 +1 @@ +Believe is the twenty-third studio album by American singer-actress Cher...03c91c40-49a6-44a7-90e7-a700edf97a62https://www.last.fm/music/Cher/Believehttps://lastfm.freetls.fastly.net/i/u/34s/3b54885952161aaea4ce2965b2db1638.pnghttps://lastfm.freetls.fastly.net/i/u/64s/3b54885952161aaea4ce2965b2db1638.pnghttps://lastfm.freetls.fastly.net/i/u/174s/3b54885952161aaea4ce2965b2db1638.png diff --git a/server/subsonic/responses/.snapshots/Responses AlbumInfo without data should match .JSON b/server/subsonic/responses/.snapshots/Responses AlbumInfo without data should match .JSON new file mode 100644 index 00000000..07e7bfa8 --- /dev/null +++ b/server/subsonic/responses/.snapshots/Responses AlbumInfo without data should match .JSON @@ -0,0 +1 @@ +{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","albumInfo":{}} diff --git a/server/subsonic/responses/.snapshots/Responses AlbumInfo without data should match .XML b/server/subsonic/responses/.snapshots/Responses AlbumInfo without data should match .XML new file mode 100644 index 00000000..3dff7bbe --- /dev/null +++ b/server/subsonic/responses/.snapshots/Responses AlbumInfo without data should match .XML @@ -0,0 +1 @@ + diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go index 7956b041..a2009cf2 100644 --- a/server/subsonic/responses/responses.go +++ b/server/subsonic/responses/responses.go @@ -37,6 +37,7 @@ type Subsonic struct { ArtistWithAlbumsID3 *ArtistWithAlbumsID3 `xml:"artist,omitempty" json:"artist,omitempty"` AlbumWithSongsID3 *AlbumWithSongsID3 `xml:"album,omitempty" json:"album,omitempty"` + AlbumInfo *AlbumInfo `xml:"albumInfo,omitempty" json:"albumInfo,omitempty"` ArtistInfo *ArtistInfo `xml:"artistInfo,omitempty" json:"artistInfo,omitempty"` ArtistInfo2 *ArtistInfo2 `xml:"artistInfo2,omitempty" json:"artistInfo2,omitempty"` SimilarSongs *SimilarSongs `xml:"similarSongs,omitempty" json:"similarSongs,omitempty"` @@ -296,6 +297,15 @@ type Genres struct { Genre []Genre `xml:"genre,omitempty" json:"genre,omitempty"` } +type AlbumInfo struct { + Notes string `xml:"notes,omitempty" json:"notes,omitempty"` + MusicBrainzID string `xml:"musicBrainzId,omitempty" json:"musicBrainzId,omitempty"` + LastFmUrl string `xml:"lastFmUrl,omitempty" json:"lastFmUrl,omitempty"` + SmallImageUrl string `xml:"smallImageUrl,omitempty" json:"smallImageUrl,omitempty"` + MediumImageUrl string `xml:"mediumImageUrl,omitempty" json:"mediumImageUrl,omitempty"` + LargeImageUrl string `xml:"largeImageUrl,omitempty" json:"largeImageUrl,omitempty"` +} + type ArtistInfoBase struct { Biography string `xml:"biography,omitempty" json:"biography,omitempty"` MusicBrainzID string `xml:"musicBrainzId,omitempty" json:"musicBrainzId,omitempty"` diff --git a/server/subsonic/responses/responses_test.go b/server/subsonic/responses/responses_test.go index c133e5d7..9e769032 100644 --- a/server/subsonic/responses/responses_test.go +++ b/server/subsonic/responses/responses_test.go @@ -335,6 +335,39 @@ var _ = Describe("Responses", func() { }) }) + Describe("AlbumInfo", func() { + BeforeEach(func() { + response.AlbumInfo = &AlbumInfo{} + }) + + Context("without data", func() { + It("should match .XML", func() { + Expect(xml.Marshal(response)).To(MatchSnapshot()) + }) + It("should match .JSON", func() { + Expect(json.Marshal(response)).To(MatchSnapshot()) + }) + }) + + Context("with data", func() { + BeforeEach(func() { + response.AlbumInfo.SmallImageUrl = "https://lastfm.freetls.fastly.net/i/u/34s/3b54885952161aaea4ce2965b2db1638.png" + response.AlbumInfo.MediumImageUrl = "https://lastfm.freetls.fastly.net/i/u/64s/3b54885952161aaea4ce2965b2db1638.png" + response.AlbumInfo.LargeImageUrl = "https://lastfm.freetls.fastly.net/i/u/174s/3b54885952161aaea4ce2965b2db1638.png" + response.AlbumInfo.LastFmUrl = "https://www.last.fm/music/Cher/Believe" + response.AlbumInfo.MusicBrainzID = "03c91c40-49a6-44a7-90e7-a700edf97a62" + response.AlbumInfo.Notes = "Believe is the twenty-third studio album by American singer-actress Cher..." + }) + + It("should match .XML", func() { + Expect(xml.Marshal(response)).To(MatchSnapshot()) + }) + It("should match .JSON", func() { + Expect(json.Marshal(response)).To(MatchSnapshot()) + }) + }) + }) + Describe("ArtistInfo", func() { BeforeEach(func() { response.ArtistInfo = &ArtistInfo{} diff --git a/tests/fixtures/lastfm.album.getinfo.empty_urls.json b/tests/fixtures/lastfm.album.getinfo.empty_urls.json new file mode 100644 index 00000000..9daad07d --- /dev/null +++ b/tests/fixtures/lastfm.album.getinfo.empty_urls.json @@ -0,0 +1 @@ +{"album":{"artist":"The Jesus and Mary Chain","listeners":"2","image":[{"size":"small","#text":""},{"size":"medium","#text":""},{"size":"large","#text":""},{"size":"extralarge","#text":""},{"size":"mega","#text":""},{"size":"","#text":""}],"mbid":"","tags":"","name":"The Definitive Less Damage And More Joy","playcount":"2","url":"https:\/\/www.last.fm\/music\/The+Jesus+and+Mary+Chain\/The+Definitive+Less+Damage+And+More+Joy"}} diff --git a/tests/fixtures/lastfm.album.getinfo.json b/tests/fixtures/lastfm.album.getinfo.json new file mode 100644 index 00000000..b9069283 --- /dev/null +++ b/tests/fixtures/lastfm.album.getinfo.json @@ -0,0 +1 @@ +{"album":{"artist":"Cher","mbid":"03c91c40-49a6-44a7-90e7-a700edf97a62","tags":{"tag":[{"url":"https://www.last.fm/tag/pop","name":"pop"},{"url":"https://www.last.fm/tag/dance","name":"dance"},{"url":"https://www.last.fm/tag/90s","name":"90s"},{"url":"https://www.last.fm/tag/1998","name":"1998"},{"url":"https://www.last.fm/tag/cher","name":"cher"}]},"name":"Believe","userplaycount":0,"image":[{"size":"small","#text":"https://lastfm.freetls.fastly.net/i/u/34s/3b54885952161aaea4ce2965b2db1638.png"},{"size":"medium","#text":"https://lastfm.freetls.fastly.net/i/u/64s/3b54885952161aaea4ce2965b2db1638.png"},{"size":"large","#text":"https://lastfm.freetls.fastly.net/i/u/174s/3b54885952161aaea4ce2965b2db1638.png"},{"size":"extralarge","#text":"https://lastfm.freetls.fastly.net/i/u/300x300/3b54885952161aaea4ce2965b2db1638.png"},{"size":"mega","#text":"https://lastfm.freetls.fastly.net/i/u/300x300/3b54885952161aaea4ce2965b2db1638.png"},{"size":"","#text":"https://lastfm.freetls.fastly.net/i/u/300x300/3b54885952161aaea4ce2965b2db1638.png"}],"tracks":{"track":[{"streamable":{"fulltrack":"0","#text":"0"},"duration":238,"url":"https://www.last.fm/music/Cher/Believe/Believe","name":"Believe","@attr":{"rank":1},"artist":{"url":"https://www.last.fm/music/Cher","name":"Cher","mbid":"bfcc6d75-a6a5-4bc6-8282-47aec8531818"}},{"streamable":{"fulltrack":"0","#text":"0"},"duration":228,"url":"https://www.last.fm/music/Cher/Believe/The+Power","name":"The Power","@attr":{"rank":2},"artist":{"url":"https://www.last.fm/music/Cher","name":"Cher","mbid":"bfcc6d75-a6a5-4bc6-8282-47aec8531818"}},{"streamable":{"fulltrack":"0","#text":"0"},"duration":272,"url":"https://www.last.fm/music/Cher/Believe/Runaway","name":"Runaway","@attr":{"rank":3},"artist":{"url":"https://www.last.fm/music/Cher","name":"Cher","mbid":"bfcc6d75-a6a5-4bc6-8282-47aec8531818"}},{"streamable":{"fulltrack":"0","#text":"0"},"duration":237,"url":"https://www.last.fm/music/Cher/Believe/All+or+Nothing","name":"All or Nothing","@attr":{"rank":4},"artist":{"url":"https://www.last.fm/music/Cher","name":"Cher","mbid":"bfcc6d75-a6a5-4bc6-8282-47aec8531818"}},{"streamable":{"fulltrack":"0","#text":"0"},"duration":224,"url":"https://www.last.fm/music/Cher/Believe/Strong+Enough","name":"Strong Enough","@attr":{"rank":5},"artist":{"url":"https://www.last.fm/music/Cher","name":"Cher","mbid":"bfcc6d75-a6a5-4bc6-8282-47aec8531818"}},{"streamable":{"fulltrack":"0","#text":"0"},"duration":null,"url":"https://www.last.fm/music/Cher/Believe/Dov%27e+L%27amore","name":"Dov'e L'amore","@attr":{"rank":6},"artist":{"url":"https://www.last.fm/music/Cher","name":"Cher","mbid":"bfcc6d75-a6a5-4bc6-8282-47aec8531818"}},{"streamable":{"fulltrack":"0","#text":"0"},"duration":272,"url":"https://www.last.fm/music/Cher/Believe/Takin%27+Back+My+Heart","name":"Takin' Back My Heart","@attr":{"rank":7},"artist":{"url":"https://www.last.fm/music/Cher","name":"Cher","mbid":"bfcc6d75-a6a5-4bc6-8282-47aec8531818"}},{"streamable":{"fulltrack":"0","#text":"0"},"duration":304,"url":"https://www.last.fm/music/Cher/Believe/Taxi+Taxi","name":"Taxi Taxi","@attr":{"rank":8},"artist":{"url":"https://www.last.fm/music/Cher","name":"Cher","mbid":"bfcc6d75-a6a5-4bc6-8282-47aec8531818"}},{"streamable":{"fulltrack":"0","#text":"0"},"duration":263,"url":"https://www.last.fm/music/Cher/Believe/Love+Is+the+Groove","name":"Love Is the Groove","@attr":{"rank":9},"artist":{"url":"https://www.last.fm/music/Cher","name":"Cher","mbid":"bfcc6d75-a6a5-4bc6-8282-47aec8531818"}},{"streamable":{"fulltrack":"0","#text":"0"},"duration":233,"url":"https://www.last.fm/music/Cher/Believe/We+All+Sleep+Alone","name":"We All Sleep Alone","@attr":{"rank":10},"artist":{"url":"https://www.last.fm/music/Cher","name":"Cher","mbid":"bfcc6d75-a6a5-4bc6-8282-47aec8531818"}}]},"listeners":"597578","playcount":"4419891","url":"https://www.last.fm/music/Cher/Believe","wiki":{"published":"03 Mar 2010, 16:48","summary":"Believe is the twenty-third studio album by American singer-actress Cher, released on November 10, 1998 by Warner Bros. Records. The RIAA certified it Quadruple Platinum on December 23, 1999, recognizing four million shipments in the United States; Worldwide, the album has sold more than 20 million copies, making it the biggest-selling album of her career. In 1999 the album received three Grammy Awards nominations including \"Record of the Year\", \"Best Pop Album\" and winning \"Best Dance Recording\" for the single \"Believe\". It was released by Warner Bros. Records at the end of 1998. The album was executive produced by Rob Read more on Last.fm.","content":"Believe is the twenty-third studio album by American singer-actress Cher, released on November 10, 1998 by Warner Bros. Records. The RIAA certified it Quadruple Platinum on December 23, 1999, recognizing four million shipments in the United States; Worldwide, the album has sold more than 20 million copies, making it the biggest-selling album of her career. In 1999 the album received three Grammy Awards nominations including \"Record of the Year\", \"Best Pop Album\" and winning \"Best Dance Recording\" for the single \"Believe\".\n\nIt was released by Warner Bros. Records at the end of 1998. The album was executive produced by Rob Dickens. Upon its debut, critical reception was generally positive. Believe became Cher's most commercially-successful release, reached number one and Top 10 all over the world. In the United States, the album was released on November 10, 1998, and reached number four on the Billboard 200 chart, where it was certified four times platinum.\n\nThe album featured a change in Cher's music; in addition, Believe presented a vocally stronger Cher and a massive use of vocoder and Auto-Tune. In 1999, the album received 3 Grammy Awards nominations for \"Record of the Year\", \"Best Pop Album\" and winning \"Best Dance Recording\". Throughout 1999 and into 2000 Cher was nominated and winning many awards for the album including a Billboard Music Award for \"Female Vocalist of the Year\", Lifelong Contribution Awards and a Star on the Walk of Fame shared with former Sonny Bono. The boost in Cher's popularity led to a very successful Do You Believe? Tour.\n\nThe album was dedicated to Sonny Bono, Cher's former husband who died earlier that year from a skiing accident.\n\nCher also recorded a cover version of \"Love Is in the Air\" during early sessions for this album. Although never officially released, the song has leaked on file sharing networks.\n\nSingles\n\n\n\"Believe\"\n\"Strong Enough\"\n\"All or Nothing\"\n\"Dov'è L'Amore\" Read more on Last.fm. User-contributed text is available under the Creative Commons By-SA License; additional terms may apply."}}} \ No newline at end of file diff --git a/ui/src/album/AlbumDetails.js b/ui/src/album/AlbumDetails.js index 2bf3c4e1..3ab8c5ae 100644 --- a/ui/src/album/AlbumDetails.js +++ b/ui/src/album/AlbumDetails.js @@ -1,4 +1,4 @@ -import React, { useMemo, useCallback } from 'react' +import React, { useMemo, useCallback, useState, useEffect } from 'react' import { Card, CardContent, @@ -91,6 +91,13 @@ const useStyles = makeStyles( float: 'left', wordBreak: 'break-word', }, + notes: { + display: 'inline-block', + marginTop: '1em', + float: 'left', + wordBreak: 'break-word', + cursor: 'pointer', + }, pointerCursor: { cursor: 'pointer', }, @@ -211,6 +218,29 @@ const AlbumDetails = (props) => { const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('lg')) const classes = useStyles() const [isLightboxOpen, setLightboxOpen] = React.useState(false) + const [expanded, setExpanded] = useState(false) + const [albumInfo, setAlbumInfo] = useState() + + let notes = + albumInfo?.notes?.replace(new RegExp('<.*>', 'g'), '') || record.notes + + if (notes !== undefined) { + notes += '..' + } + + useEffect(() => { + subsonic + .getAlbumInfo(record.id) + .then((resp) => resp.json['subsonic-response']) + .then((data) => { + if (data.status === 'ok') { + setAlbumInfo(data.albumInfo) + } + }) + .catch((e) => { + console.error('error on album page', e) + }) + }, [record]) const imageUrl = subsonic.getCoverArtUrl(record, 300) const fullImageUrl = subsonic.getCoverArtUrl(record) @@ -277,11 +307,38 @@ const AlbumDetails = (props) => { )} + {isDesktop && ( + + setExpanded(!expanded)} + > + + + + )} {isDesktop && record['comment'] && } {!isDesktop && record['comment'] && } + {!isDesktop && ( +
+ + setExpanded(!expanded)} + > + + + +
+ )} {isLightboxOpen && ( { return httpClient(url('getArtistInfo', id)) } +const getAlbumInfo = (id) => { + return httpClient(url('getAlbumInfo', id)) +} + const streamUrl = (id) => { return baseUrl(url('stream', id, { ts: true })) } @@ -79,5 +83,6 @@ export default { getScanStatus, getCoverArtUrl, streamUrl, + getAlbumInfo, getArtistInfo, }