From e9e09a748071fa5a3ef0cfc3efc135d3fbcacf53 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 20 Oct 2020 13:38:44 -0400 Subject: [PATCH] Add dedicated SimilarArtists call --- core/external_info.go | 110 ++++++++++++------ core/get_entity.go | 28 +++++ core/lastfm/client.go | 36 ++++-- core/lastfm/client_test.go | 42 ++++++- core/lastfm/responses.go | 13 ++- core/lastfm/responses_test.go | 13 +++ model/artist_info.go | 1 - server/subsonic/api.go | 2 + server/subsonic/browsing.go | 55 ++++++--- server/subsonic/helpers.go | 21 ---- ... SimilarSongs with data should match .JSON | 1 + ...s SimilarSongs with data should match .XML | 1 + ...milarSongs without data should match .JSON | 1 + ...imilarSongs without data should match .XML | 1 + ...SimilarSongs2 with data should match .JSON | 1 + ... SimilarSongs2 with data should match .XML | 1 + ...ilarSongs2 without data should match .JSON | 1 + ...milarSongs2 without data should match .XML | 1 + server/subsonic/responses/responses.go | 16 ++- server/subsonic/responses/responses_test.go | 58 +++++++++ server/subsonic/stream.go | 2 +- tests/fixtures/lastfm.artist.getsimilar.json | 1 + 22 files changed, 316 insertions(+), 90 deletions(-) create mode 100644 core/get_entity.go create mode 100644 server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs with data should match .JSON create mode 100644 server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs with data should match .XML create mode 100644 server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs without data should match .JSON create mode 100644 server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs without data should match .XML create mode 100644 server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs2 with data should match .JSON create mode 100644 server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs2 with data should match .XML create mode 100644 server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs2 without data should match .JSON create mode 100644 server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs2 without data should match .XML create mode 100644 tests/fixtures/lastfm.artist.getsimilar.json diff --git a/core/external_info.go b/core/external_info.go index b8249928162..04db104d8e4 100644 --- a/core/external_info.go +++ b/core/external_info.go @@ -19,11 +19,14 @@ const placeholderArtistImageMediumUrl = "https://lastfm.freetls.fastly.net/i/u/1 const placeholderArtistImageLargeUrl = "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png" type ExternalInfo interface { - ArtistInfo(ctx context.Context, artistId string, includeNotPresent bool, count int) (*model.ArtistInfo, error) + 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) } type LastFMClient interface { ArtistGetInfo(ctx context.Context, name string) (*lastfm.Artist, error) + ArtistGetSimilar(ctx context.Context, name string, limit int) ([]lastfm.Artist, error) } type SpotifyClient interface { @@ -40,20 +43,86 @@ type externalInfo struct { spf SpotifyClient } -func (e *externalInfo) ArtistInfo(ctx context.Context, artistId string, - includeNotPresent bool, count int) (*model.ArtistInfo, error) { - info := model.ArtistInfo{ID: artistId} +func (e *externalInfo) getArtist(ctx context.Context, id string) (artist *model.Artist, err error) { + var entity interface{} + entity, err = GetEntityByID(ctx, e.ds, id) + if err != nil { + return nil, err + } + + switch v := entity.(type) { + case *model.Artist: + artist = v + case *model.MediaFile: + artist = &model.Artist{ + ID: v.ArtistID, + Name: v.Artist, + } + case *model.Album: + artist = &model.Artist{ + ID: v.AlbumArtistID, + Name: v.Artist, + } + default: + err = model.ErrNotFound + } + return +} + +func (e *externalInfo) SimilarSongs(ctx context.Context, id string, count int) (model.MediaFiles, error) { + // TODO + // Get Similar Artists + // Get `count` songs from all similar artists, sorted randomly + return nil, nil +} + +func (e *externalInfo) SimilarArtists(ctx context.Context, id string, includeNotPresent bool, count int) (model.Artists, error) { + artist, err := e.getArtist(ctx, id) + if err != nil { + return nil, err + } + + var result model.Artists + var notPresent []string + + similar, err := e.lfm.ArtistGetSimilar(ctx, artist.Name, count) + if err != nil { + return nil, err + } - artist, err := e.ds.Artist(ctx).Get(artistId) + // First select artists that are present. + for _, s := range similar { + sa, err := e.ds.Artist(ctx).FindByName(s.Name) + if err != nil { + notPresent = append(notPresent, s.Name) + continue + } + result = append(result, *sa) + } + + // Then fill up with non-present artists + if includeNotPresent { + for _, s := range notPresent { + sa := model.Artist{ID: "-1", Name: s} + result = append(result, sa) + } + } + + return result, nil +} + +func (e *externalInfo) ArtistInfo(ctx context.Context, id string) (*model.ArtistInfo, error) { + artist, err := e.getArtist(ctx, id) if err != nil { return nil, err } - info.Name = artist.Name + + info := model.ArtistInfo{ID: artist.ID, Name: artist.Name} // TODO Load from local: artist.jpg/png/webp, artist.json (with the remaining info) var wg sync.WaitGroup - e.callArtistInfo(ctx, artist, includeNotPresent, &wg, &info) + e.callArtistInfo(ctx, artist, &wg, &info) e.callArtistImages(ctx, artist, &wg, &info) wg.Wait() @@ -68,7 +137,7 @@ func (e *externalInfo) ArtistInfo(ctx context.Context, artistId string, return &info, nil } -func (e *externalInfo) callArtistInfo(ctx context.Context, artist *model.Artist, includeNotPresent bool, +func (e *externalInfo) callArtistInfo(ctx context.Context, artist *model.Artist, wg *sync.WaitGroup, info *model.ArtistInfo) { if e.lfm != nil { log.Debug(ctx, "Calling Last.FM ArtistGetInfo", "artist", artist.Name) @@ -85,7 +154,6 @@ func (e *externalInfo) callArtistInfo(ctx context.Context, artist *model.Artist, e.setBio(info, lfmArtist.Bio.Summary) e.setLastFMUrl(info, lfmArtist.URL) e.setMbzID(info, lfmArtist.MBID) - e.setSimilar(ctx, info, lfmArtist.Similar.Artists, includeNotPresent) }() } } @@ -157,27 +225,3 @@ func (e *externalInfo) setLargeImageUrl(info *model.ArtistInfo, url string) { info.LargeImageUrl = url } } - -func (e *externalInfo) setSimilar(ctx context.Context, info *model.ArtistInfo, artists []lastfm.Artist, includeNotPresent bool) { - if len(info.Similar) == 0 { - var notPresent []string - - // First select artists that are present. - for _, s := range artists { - sa, err := e.ds.Artist(ctx).FindByName(s.Name) - if err != nil { - notPresent = append(notPresent, s.Name) - continue - } - info.Similar = append(info.Similar, *sa) - } - - // Then fill up with non-present artists - if includeNotPresent { - for _, s := range notPresent { - sa := model.Artist{ID: "-1", Name: s} - info.Similar = append(info.Similar, sa) - } - } - } -} diff --git a/core/get_entity.go b/core/get_entity.go new file mode 100644 index 00000000000..6d5e253fdfd --- /dev/null +++ b/core/get_entity.go @@ -0,0 +1,28 @@ +package core + +import ( + "context" + + "github.com/deluan/navidrome/model" +) + +// TODO: Should the type be encoded in the ID? +func GetEntityByID(ctx context.Context, ds model.DataStore, id string) (interface{}, error) { + ar, err := ds.Artist(ctx).Get(id) + if err == nil { + return ar, nil + } + al, err := ds.Album(ctx).Get(id) + if err == nil { + return al, nil + } + pls, err := ds.Playlist(ctx).Get(id) + if err == nil { + return pls, nil + } + mf, err := ds.MediaFile(ctx).Get(id) + if err == nil { + return mf, nil + } + return nil, err +} diff --git a/core/lastfm/client.go b/core/lastfm/client.go index 98c0a265ea9..553a0849498 100644 --- a/core/lastfm/client.go +++ b/core/lastfm/client.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "net/http" "net/url" + "strconv" ) const ( @@ -27,14 +28,10 @@ type Client struct { hc HttpClient } -// TODO SimilarArtists() -func (c *Client) ArtistGetInfo(ctx context.Context, name string) (*Artist, error) { - params := url.Values{} - params.Add("method", "artist.getInfo") +func (c *Client) makeRequest(params url.Values) (*Response, error) { params.Add("format", "json") params.Add("api_key", c.apiKey) - params.Add("artist", name) - params.Add("lang", c.lang) + req, _ := http.NewRequest("GET", apiBaseUrl, nil) req.URL.RawQuery = params.Encode() @@ -55,7 +52,32 @@ func (c *Client) ArtistGetInfo(ctx context.Context, name string) (*Artist, error var response Response err = json.Unmarshal(data, &response) - return &response.Artist, err + + return &response, err +} + +func (c *Client) ArtistGetInfo(ctx context.Context, name string) (*Artist, error) { + params := url.Values{} + params.Add("method", "artist.getInfo") + params.Add("artist", name) + params.Add("lang", c.lang) + response, err := c.makeRequest(params) + if err != nil { + return nil, err + } + return &response.Artist, nil +} + +func (c *Client) ArtistGetSimilar(ctx context.Context, name string, limit int) ([]Artist, error) { + params := url.Values{} + params.Add("method", "artist.getSimilar") + params.Add("artist", name) + params.Add("limit", strconv.Itoa(limit)) + response, err := c.makeRequest(params) + if err != nil { + return nil, err + } + return response.SimilarArtists.Artists, nil } func (c *Client) parseError(data []byte) error { diff --git a/core/lastfm/client_test.go b/core/lastfm/client_test.go index 8c8abd911f8..6d9d5ca3d67 100644 --- a/core/lastfm/client_test.go +++ b/core/lastfm/client_test.go @@ -21,7 +21,7 @@ var _ = Describe("Client", func() { client = NewClient("API_KEY", "pt", httpClient) }) - Describe("ArtistInfo", func() { + Describe("ArtistGetInfo", func() { It("returns an artist for a successful response", func() { f, _ := os.Open("tests/fixtures/lastfm.artist.getinfo.json") httpClient.res = http.Response{Body: f, StatusCode: 200} @@ -60,6 +60,46 @@ var _ = Describe("Client", func() { }) }) + + Describe("ArtistGetSimilar", func() { + It("returns an artist for a successful response", func() { + f, _ := os.Open("tests/fixtures/lastfm.artist.getsimilar.json") + httpClient.res = http.Response{Body: f, StatusCode: 200} + + artists, err := client.ArtistGetSimilar(context.TODO(), "U2", 2) + Expect(err).To(BeNil()) + Expect(len(artists)).To(Equal(2)) + Expect(httpClient.savedRequest.URL.String()).To(Equal(apiBaseUrl + "?api_key=API_KEY&artist=U2&format=json&limit=2&method=artist.getSimilar")) + }) + + 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.ArtistGetSimilar(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.ArtistGetSimilar(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.ArtistGetSimilar(context.TODO(), "U2", 2) + Expect(err).To(MatchError("invalid character '<' looking for beginning of value")) + }) + + }) }) type fakeHttpClient struct { diff --git a/core/lastfm/responses.go b/core/lastfm/responses.go index 65958c54e86..6a09ab04068 100644 --- a/core/lastfm/responses.go +++ b/core/lastfm/responses.go @@ -1,7 +1,8 @@ package lastfm type Response struct { - Artist Artist `json:"artist"` + Artist Artist `json:"artist"` + SimilarArtists SimilarArtists `json:"similarartists"` } type Artist struct { @@ -14,15 +15,17 @@ type Artist struct { Listeners string `json:"listeners"` Plays string `json:"plays"` } `json:"stats"` - Similar struct { - Artists []Artist `json:"artist"` - } `json:"similar"` - Tags struct { + Similar SimilarArtists `json:"similar"` + Tags struct { Tag []ArtistTag `json:"tag"` } `json:"tags"` Bio ArtistBio `json:"bio"` } +type SimilarArtists struct { + Artists []Artist `json:"artist"` +} + type ArtistImage struct { URL string `json:"#text"` Size string `json:"size"` diff --git a/core/lastfm/responses_test.go b/core/lastfm/responses_test.go index b41700567af..c6f801827e9 100644 --- a/core/lastfm/responses_test.go +++ b/core/lastfm/responses_test.go @@ -28,6 +28,19 @@ var _ = Describe("LastFM responses", func() { }) }) + Describe("SimilarArtists", func() { + It("parses the response correctly", func() { + var resp Response + body, _ := ioutil.ReadFile("tests/fixtures/lastfm.artist.getsimilar.json") + err := json.Unmarshal(body, &resp) + Expect(err).To(BeNil()) + + Expect(resp.SimilarArtists.Artists).To(HaveLen(2)) + Expect(resp.SimilarArtists.Artists[0].Name).To(Equal("Passengers")) + Expect(resp.SimilarArtists.Artists[1].Name).To(Equal("INXS")) + }) + }) + Describe("Error", func() { It("parses the error response correctly", func() { var error Error diff --git a/model/artist_info.go b/model/artist_info.go index f8eac3cd381..a2c1793197d 100644 --- a/model/artist_info.go +++ b/model/artist_info.go @@ -5,7 +5,6 @@ type ArtistInfo struct { Name string MBID string Biography string - Similar []Artist SmallImageUrl string MediumImageUrl string LargeImageUrl string diff --git a/server/subsonic/api.go b/server/subsonic/api.go index 018c6a419fd..c3eec02f4f4 100644 --- a/server/subsonic/api.go +++ b/server/subsonic/api.go @@ -79,6 +79,8 @@ func (api *Router) routes() http.Handler { H(withPlayer, "getArtistInfo", c.GetArtistInfo) H(withPlayer, "getArtistInfo2", c.GetArtistInfo2) H(withPlayer, "getTopSongs", c.GetTopSongs) + H(withPlayer, "getSimilarSongs", c.GetSimilarSongs) + H(withPlayer, "getSimilarSongs2", c.GetSimilarSongs2) }) r.Group(func(r chi.Router) { c := initAlbumListController(api) diff --git a/server/subsonic/browsing.go b/server/subsonic/browsing.go index be5db10aadb..9eef6a471a8 100644 --- a/server/subsonic/browsing.go +++ b/server/subsonic/browsing.go @@ -109,7 +109,7 @@ func (c *BrowsingController) GetMusicDirectory(w http.ResponseWriter, r *http.Re id := utils.ParamString(r, "id") ctx := r.Context() - entity, err := getEntityByID(ctx, c.ds, id) + entity, err := core.GetEntityByID(ctx, c.ds, id) switch { case err == model.ErrNotFound: log.Error(r, "Requested ID not found ", "id", id) @@ -241,26 +241,12 @@ func (c *BrowsingController) GetArtistInfo(w http.ResponseWriter, r *http.Reques count := utils.ParamInt(r, "count", 20) includeNotPresent := utils.ParamBool(r, "includeNotPresent", false) - entity, err := getEntityByID(ctx, c.ds, id) + info, err := c.ei.ArtistInfo(ctx, id) if err != nil { return nil, err } - switch v := entity.(type) { - case *model.MediaFile: - id = v.ArtistID - case *model.Album: - id = v.AlbumArtistID - case *model.Artist: - id = v.ID - default: - err = model.ErrNotFound - } - if err != nil { - return nil, err - } - - info, err := c.ei.ArtistInfo(ctx, id, includeNotPresent, count) + similar, err := c.ei.SimilarArtists(ctx, id, includeNotPresent, count) if err != nil { return nil, err } @@ -273,7 +259,7 @@ func (c *BrowsingController) GetArtistInfo(w http.ResponseWriter, r *http.Reques response.ArtistInfo.LargeImageUrl = info.LargeImageUrl response.ArtistInfo.LastFmUrl = info.LastFMUrl response.ArtistInfo.MusicBrainzID = info.MBID - for _, s := range info.Similar { + for _, s := range similar { similar := responses.Artist{} similar.Id = s.ID similar.Name = s.Name @@ -306,6 +292,39 @@ func (c *BrowsingController) GetArtistInfo2(w http.ResponseWriter, r *http.Reque return response, nil } +func (c *BrowsingController) GetSimilarSongs(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { + ctx := r.Context() + id, err := requiredParamString(r, "id", "id parameter required") + if err != nil { + return nil, err + } + count := utils.ParamInt(r, "count", 20) + + songs, err := c.ei.SimilarSongs(ctx, id, count) + if err != nil { + return nil, err + } + + response := newResponse() + response.SimilarSongs = &responses.SimilarSongs{ + Song: childrenFromMediaFiles(ctx, songs), + } + return response, nil +} + +func (c *BrowsingController) GetSimilarSongs2(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { + res, err := c.GetSimilarSongs(w, r) + if err != nil { + return nil, err + } + + response := newResponse() + response.SimilarSongs2 = &responses.SimilarSongs2{ + Song: res.SimilarSongs2.Song, + } + return response, nil +} + // TODO Integrate with Last.FM func (c *BrowsingController) GetTopSongs(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { response := newResponse() diff --git a/server/subsonic/helpers.go b/server/subsonic/helpers.go index b93fea44eb7..4eb817fc7e2 100644 --- a/server/subsonic/helpers.go +++ b/server/subsonic/helpers.go @@ -258,24 +258,3 @@ func childrenFromAlbums(ctx context.Context, als model.Albums) []responses.Child } return children } - -// TODO: Should the type be encoded in the ID? -func getEntityByID(ctx context.Context, ds model.DataStore, id string) (interface{}, error) { - ar, err := ds.Artist(ctx).Get(id) - if err == nil { - return ar, nil - } - al, err := ds.Album(ctx).Get(id) - if err == nil { - return al, nil - } - pls, err := ds.Playlist(ctx).Get(id) - if err == nil { - return pls, nil - } - mf, err := ds.MediaFile(ctx).Get(id) - if err == nil { - return mf, nil - } - return nil, err -} diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs with data should match .JSON b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs with data should match .JSON new file mode 100644 index 00000000000..49f25fc9162 --- /dev/null +++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs with data should match .JSON @@ -0,0 +1 @@ +{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","similarSongs":{"song":[{"id":"1","isDir":false,"title":"title","isVideo":false}]}} diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs with data should match .XML b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs with data should match .XML new file mode 100644 index 00000000000..29e13bb60de --- /dev/null +++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs with data should match .XML @@ -0,0 +1 @@ + diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs without data should match .JSON b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs without data should match .JSON new file mode 100644 index 00000000000..3ae1c3e8ff2 --- /dev/null +++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs without data should match .JSON @@ -0,0 +1 @@ +{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","similarSongs":{}} diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs without data should match .XML b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs without data should match .XML new file mode 100644 index 00000000000..b8506c7d423 --- /dev/null +++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs without data should match .XML @@ -0,0 +1 @@ + diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs2 with data should match .JSON b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs2 with data should match .JSON new file mode 100644 index 00000000000..0aea25e17af --- /dev/null +++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs2 with data should match .JSON @@ -0,0 +1 @@ +{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","similarSongs2":{"song":[{"id":"1","isDir":false,"title":"title","isVideo":false}]}} diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs2 with data should match .XML b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs2 with data should match .XML new file mode 100644 index 00000000000..d9d1db3a0d8 --- /dev/null +++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs2 with data should match .XML @@ -0,0 +1 @@ + diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs2 without data should match .JSON b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs2 without data should match .JSON new file mode 100644 index 00000000000..0293c5afa7d --- /dev/null +++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs2 without data should match .JSON @@ -0,0 +1 @@ +{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","similarSongs2":{}} diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs2 without data should match .XML b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs2 without data should match .XML new file mode 100644 index 00000000000..834a853b4f3 --- /dev/null +++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses SimilarSongs2 without data should match .XML @@ -0,0 +1 @@ + diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go index 0f1c4a9ab7a..af840de6350 100644 --- a/server/subsonic/responses/responses.go +++ b/server/subsonic/responses/responses.go @@ -36,9 +36,11 @@ type Subsonic struct { ArtistWithAlbumsID3 *ArtistWithAlbumsID3 `xml:"artist,omitempty" json:"artist,omitempty"` AlbumWithSongsID3 *AlbumWithSongsID3 `xml:"album,omitempty" json:"album,omitempty"` - ArtistInfo *ArtistInfo `xml:"artistInfo,omitempty" json:"artistInfo,omitempty"` - ArtistInfo2 *ArtistInfo2 `xml:"artistInfo2,omitempty" json:"artistInfo2,omitempty"` - TopSongs *TopSongs `xml:"topSongs,omitempty" json:"topSongs,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"` + SimilarSongs2 *SimilarSongs2 `xml:"similarSongs2,omitempty" json:"similarSongs2,omitempty"` + TopSongs *TopSongs `xml:"topSongs,omitempty" json:"topSongs,omitempty"` PlayQueue *PlayQueue `xml:"playQueue,omitempty" json:"playQueue,omitempty"` Bookmarks *Bookmarks `xml:"bookmarks,omitempty" json:"bookmarks,omitempty"` @@ -298,6 +300,14 @@ type ArtistInfo2 struct { SimilarArtist []ArtistID3 `xml:"similarArtist,omitempty" json:"similarArtist,omitempty"` } +type SimilarSongs struct { + Song []Child `xml:"song,omitempty" json:"song,omitempty"` +} + +type SimilarSongs2 struct { + Song []Child `xml:"song,omitempty" json:"song,omitempty"` +} + type TopSongs struct { Song []Child `xml:"song,omitempty" json:"song,omitempty"` } diff --git a/server/subsonic/responses/responses_test.go b/server/subsonic/responses/responses_test.go index 43dd303dda5..cfe92275ef7 100644 --- a/server/subsonic/responses/responses_test.go +++ b/server/subsonic/responses/responses_test.go @@ -361,6 +361,64 @@ var _ = Describe("Responses", func() { }) }) + Describe("SimilarSongs", func() { + BeforeEach(func() { + response.SimilarSongs = &SimilarSongs{} + }) + + 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() { + child := make([]Child, 1) + child[0] = Child{Id: "1", Title: "title", IsDir: false} + response.SimilarSongs.Song = child + }) + It("should match .XML", func() { + Expect(xml.Marshal(response)).To(MatchSnapshot()) + }) + It("should match .JSON", func() { + Expect(json.Marshal(response)).To(MatchSnapshot()) + }) + }) + }) + + Describe("SimilarSongs2", func() { + BeforeEach(func() { + response.SimilarSongs2 = &SimilarSongs2{} + }) + + 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() { + child := make([]Child, 1) + child[0] = Child{Id: "1", Title: "title", IsDir: false} + response.SimilarSongs2.Song = child + }) + It("should match .XML", func() { + Expect(xml.Marshal(response)).To(MatchSnapshot()) + }) + It("should match .JSON", func() { + Expect(json.Marshal(response)).To(MatchSnapshot()) + }) + }) + }) + Describe("PlayQueue", func() { BeforeEach(func() { response.PlayQueue = &PlayQueue{} diff --git a/server/subsonic/stream.go b/server/subsonic/stream.go index 32e4d3ffb62..8f1857767f2 100644 --- a/server/subsonic/stream.go +++ b/server/subsonic/stream.go @@ -80,7 +80,7 @@ func (c *StreamController) Download(w http.ResponseWriter, r *http.Request) (*re return nil, err } - entity, err := getEntityByID(ctx, c.ds, id) + entity, err := core.GetEntityByID(ctx, c.ds, id) if err != nil { return nil, err } diff --git a/tests/fixtures/lastfm.artist.getsimilar.json b/tests/fixtures/lastfm.artist.getsimilar.json new file mode 100644 index 00000000000..cca4f162460 --- /dev/null +++ b/tests/fixtures/lastfm.artist.getsimilar.json @@ -0,0 +1 @@ +{"similarartists":{"artist":[{"name":"Passengers","mbid":"e110c11f-1c94-4471-a350-c38f46b29389","match":"1","url":"https://www.last.fm/music/Passengers","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"},{"#text":"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png","size":"mega"},{"#text":"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png","size":""}],"streamable":"0"},{"name":"INXS","mbid":"481bf5f9-2e7c-4c44-b08a-05b32bc7c00d","match":"0.511468","url":"https://www.last.fm/music/INXS","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"},{"#text":"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png","size":"mega"},{"#text":"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png","size":""}],"streamable":"0"}],"@attr":{"artist":"U2"}}}