diff --git a/core/external_info.go b/core/external_info.go index e6a88ddb..7c173f9f 100644 --- a/core/external_info.go +++ b/core/external_info.go @@ -25,6 +25,7 @@ type ExternalInfo interface { ArtistInfo(ctx context.Context, id string) (*model.ArtistInfo, error) SimilarArtists(ctx context.Context, id string, includeNotPresent bool, count int) (model.Artists, error) SimilarSongs(ctx context.Context, id string, count int) (model.MediaFiles, error) + TopSongs(ctx context.Context, artist string, count int) (model.MediaFiles, error) } func NewExternalInfo(ds model.DataStore, lfm *lastfm.Client, spf *spotify.Client) ExternalInfo { @@ -136,6 +137,32 @@ func (e *externalInfo) similarArtists(ctx context.Context, artist *model.Artist, return result, nil } +func (e *externalInfo) TopSongs(ctx context.Context, artist string, count int) (model.MediaFiles, error) { + if e.lfm == nil { + log.Warn(ctx, "Last.FM client not configured") + return nil, model.ErrNotAvailable + } + log.Debug(ctx, "Calling Last.FM ArtistGetTopTracks", "artist", artist) + tracks, err := e.lfm.ArtistGetTopTracks(ctx, artist, count) + if err != nil { + return nil, err + } + var songs model.MediaFiles + for _, t := range tracks { + mfs, err := e.ds.MediaFile(ctx).GetAll(model.QueryOptions{ + Filters: squirrel.And{ + squirrel.Like{"artist": artist}, + squirrel.Like{"title": t.Name}, + }, + }) + if err != nil || len(mfs) == 0 { + continue + } + songs = append(songs, mfs[0]) + } + return songs, nil +} + func (e *externalInfo) ArtistInfo(ctx context.Context, id string) (*model.ArtistInfo, error) { artist, err := e.getArtist(ctx, id) if err != nil { diff --git a/core/lastfm/client.go b/core/lastfm/client.go index 553a0849..d0df3d5a 100644 --- a/core/lastfm/client.go +++ b/core/lastfm/client.go @@ -80,6 +80,18 @@ func (c *Client) ArtistGetSimilar(ctx context.Context, name string, limit int) ( return response.SimilarArtists.Artists, nil } +func (c *Client) ArtistGetTopTracks(ctx context.Context, name string, limit int) ([]Track, error) { + params := url.Values{} + params.Add("method", "artist.getTopTracks") + params.Add("artist", name) + params.Add("limit", strconv.Itoa(limit)) + response, err := c.makeRequest(params) + if err != nil { + return nil, err + } + return response.TopTracks.Track, nil +} + func (c *Client) parseError(data []byte) error { var e Error err := json.Unmarshal(data, &e) diff --git a/core/lastfm/client_test.go b/core/lastfm/client_test.go index 6d9d5ca3..bb88802c 100644 --- a/core/lastfm/client_test.go +++ b/core/lastfm/client_test.go @@ -98,7 +98,45 @@ var _ = Describe("Client", func() { _, err := client.ArtistGetSimilar(context.TODO(), "U2", 2) Expect(err).To(MatchError("invalid character '<' looking for beginning of value")) }) + }) + Describe("ArtistGetTopTracks", func() { + It("returns top tracks for a successful response", func() { + f, _ := os.Open("tests/fixtures/lastfm.artist.gettoptracks.json") + httpClient.res = http.Response{Body: f, StatusCode: 200} + + tracks, err := client.ArtistGetTopTracks(context.TODO(), "U2", 2) + Expect(err).To(BeNil()) + Expect(len(tracks)).To(Equal(2)) + Expect(httpClient.savedRequest.URL.String()).To(Equal(apiBaseUrl + "?api_key=API_KEY&artist=U2&format=json&limit=2&method=artist.getTopTracks")) + }) + + It("fails if Last.FM returns an error", func() { + httpClient.res = http.Response{ + Body: ioutil.NopCloser(bytes.NewBufferString(`{"error":3,"message":"Invalid Method - No method with that name in this package"}`)), + StatusCode: 400, + } + + _, err := client.ArtistGetTopTracks(context.TODO(), "U2", 2) + Expect(err).To(MatchError("last.fm error(3): Invalid Method - No method with that name in this package")) + }) + + It("fails if HttpClient.Do() returns error", func() { + httpClient.err = errors.New("generic error") + + _, err := client.ArtistGetTopTracks(context.TODO(), "U2", 2) + Expect(err).To(MatchError("generic error")) + }) + + It("fails if returned body is not a valid JSON", func() { + httpClient.res = http.Response{ + Body: ioutil.NopCloser(bytes.NewBufferString(`NOT_VALID_JSON`)), + StatusCode: 200, + } + + _, err := client.ArtistGetTopTracks(context.TODO(), "U2", 2) + Expect(err).To(MatchError("invalid character '<' looking for beginning of value")) + }) }) }) diff --git a/core/lastfm/responses.go b/core/lastfm/responses.go index 6a09ab04..ebfe8f87 100644 --- a/core/lastfm/responses.go +++ b/core/lastfm/responses.go @@ -3,6 +3,7 @@ package lastfm type Response struct { Artist Artist `json:"artist"` SimilarArtists SimilarArtists `json:"similarartists"` + TopTracks TopTracks `json:"toptracks"` } type Artist struct { @@ -42,6 +43,15 @@ type ArtistBio struct { Content string `json:"content"` } +type Track struct { + Name string `json:"name"` + MBID string `json:"mbid"` +} + +type TopTracks struct { + Track []Track `json:"track"` +} + type Error struct { Code int `json:"error"` Message string `json:"message"` diff --git a/core/lastfm/responses_test.go b/core/lastfm/responses_test.go index c6f80182..d0a4031c 100644 --- a/core/lastfm/responses_test.go +++ b/core/lastfm/responses_test.go @@ -41,6 +41,21 @@ var _ = Describe("LastFM responses", func() { }) }) + Describe("TopTracks", func() { + It("parses the response correctly", func() { + var resp Response + body, _ := ioutil.ReadFile("tests/fixtures/lastfm.artist.gettoptracks.json") + err := json.Unmarshal(body, &resp) + Expect(err).To(BeNil()) + + Expect(resp.TopTracks.Track).To(HaveLen(2)) + Expect(resp.TopTracks.Track[0].Name).To(Equal("Beautiful Day")) + Expect(resp.TopTracks.Track[0].MBID).To(Equal("f7f264d0-a89b-4682-9cd7-a4e7c37637af")) + Expect(resp.TopTracks.Track[1].Name).To(Equal("With or Without You")) + Expect(resp.TopTracks.Track[1].MBID).To(Equal("6b9a509f-6907-4a6e-9345-2f12da09ba4b")) + }) + }) + Describe("Error", func() { It("parses the error response correctly", func() { var error Error diff --git a/server/subsonic/browsing.go b/server/subsonic/browsing.go index b02025bd..1428a520 100644 --- a/server/subsonic/browsing.go +++ b/server/subsonic/browsing.go @@ -298,7 +298,7 @@ func (c *BrowsingController) GetSimilarSongs(w http.ResponseWriter, r *http.Requ if err != nil { return nil, err } - count := utils.ParamInt(r, "count", 20) + count := utils.ParamInt(r, "count", 50) songs, err := c.ei.SimilarSongs(ctx, id, count) if err != nil { @@ -325,10 +325,23 @@ func (c *BrowsingController) GetSimilarSongs2(w http.ResponseWriter, r *http.Req return response, nil } -// TODO Integrate with Last.FM func (c *BrowsingController) GetTopSongs(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { + ctx := r.Context() + artist, err := requiredParamString(r, "artist", "artist parameter required") + if err != nil { + return nil, err + } + count := utils.ParamInt(r, "count", 50) + + songs, err := c.ei.TopSongs(ctx, artist, count) + if err != nil { + return nil, err + } + response := newResponse() - response.TopSongs = &responses.TopSongs{} + response.TopSongs = &responses.TopSongs{ + Song: childrenFromMediaFiles(ctx, songs), + } return response, nil } diff --git a/tests/fixtures/lastfm.artist.gettoptracks.json b/tests/fixtures/lastfm.artist.gettoptracks.json new file mode 100644 index 00000000..7ed558d1 --- /dev/null +++ b/tests/fixtures/lastfm.artist.gettoptracks.json @@ -0,0 +1 @@ +{"toptracks":{"track":[{"name":"Beautiful Day","playcount":"6309776","listeners":"1037970","mbid":"f7f264d0-a89b-4682-9cd7-a4e7c37637af","url":"https://www.last.fm/music/U2/_/Beautiful+Day","streamable":"0","artist":{"name":"U2","mbid":"a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432","url":"https://www.last.fm/music/U2"},"image":[{"#text":"https://lastfm.freetls.fastly.net/i/u/34s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"small"},{"#text":"https://lastfm.freetls.fastly.net/i/u/64s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"medium"},{"#text":"https://lastfm.freetls.fastly.net/i/u/174s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"large"},{"#text":"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png","size":"extralarge"}],"@attr":{"rank":"1"}},{"name":"With or Without You","playcount":"6779665","listeners":"1022929","mbid":"6b9a509f-6907-4a6e-9345-2f12da09ba4b","url":"https://www.last.fm/music/U2/_/With+or+Without+You","streamable":"0","artist":{"name":"U2","mbid":"a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432","url":"https://www.last.fm/music/U2"},"image":[{"#text":"https://lastfm.freetls.fastly.net/i/u/34s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"small"},{"#text":"https://lastfm.freetls.fastly.net/i/u/64s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"medium"},{"#text":"https://lastfm.freetls.fastly.net/i/u/174s/2a96cbd8b46e442fc41c2b86b821562f.png","size":"large"},{"#text":"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png","size":"extralarge"}],"@attr":{"rank":"2"}}],"@attr":{"artist":"U2","page":"1","perPage":"2","totalPages":"166117","total":"332234"}}}