mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
feat(subsonic): Implement OpenSubsonic topSongsByArtistId extension (#5853)
* implement topSongsByArtistId extension * do not look up by artist name if empty --------- Co-authored-by: Deluan Quintão <deluan@navidrome.org>
This commit is contained in:
parent
810b14ed57
commit
b40b41584a
@ -171,7 +171,7 @@ func (n *noopProvider) UpdateArtistInfo(context.Context, string, int, bool) (*mo
|
|||||||
func (n *noopProvider) SimilarSongs(context.Context, string, int) (model.MediaFiles, error) {
|
func (n *noopProvider) SimilarSongs(context.Context, string, int) (model.MediaFiles, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (n *noopProvider) TopSongs(context.Context, string, int) (model.MediaFiles, error) {
|
func (n *noopProvider) TopSongs(context.Context, string, string, int) (model.MediaFiles, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (n *noopProvider) ArtistImage(context.Context, string) (*url.URL, error) {
|
func (n *noopProvider) ArtistImage(context.Context, string) (*url.URL, error) {
|
||||||
|
|||||||
34
core/external/provider.go
vendored
34
core/external/provider.go
vendored
@ -34,7 +34,7 @@ type Provider interface {
|
|||||||
UpdateAlbumInfo(ctx context.Context, id string) (*model.Album, error)
|
UpdateAlbumInfo(ctx context.Context, id string) (*model.Album, error)
|
||||||
UpdateArtistInfo(ctx context.Context, id string, count int, includeNotPresent bool) (*model.Artist, 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)
|
SimilarSongs(ctx context.Context, id string, count int) (model.MediaFiles, error)
|
||||||
TopSongs(ctx context.Context, artist string, count int) (model.MediaFiles, error)
|
TopSongs(ctx context.Context, artist, artistId string, count int) (model.MediaFiles, error)
|
||||||
ArtistImage(ctx context.Context, id string) (*url.URL, error)
|
ArtistImage(ctx context.Context, id string) (*url.URL, error)
|
||||||
AlbumImage(ctx context.Context, id string) (*url.URL, error)
|
AlbumImage(ctx context.Context, id string) (*url.URL, error)
|
||||||
}
|
}
|
||||||
@ -440,11 +440,16 @@ func (e *provider) AlbumImage(ctx context.Context, id string) (*url.URL, error)
|
|||||||
return url.Parse(img.URL)
|
return url.Parse(img.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *provider) TopSongs(ctx context.Context, artistName string, count int) (model.MediaFiles, error) {
|
func (e *provider) TopSongs(ctx context.Context, artistName, id string, count int) (model.MediaFiles, error) {
|
||||||
artist, err := e.findArtistByName(ctx, artistName)
|
artist, err := e.findArtist(ctx, artistName, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(ctx, "Artist not found", "name", artistName, err)
|
if errors.Is(err, model.ErrNotFound) {
|
||||||
return nil, nil
|
log.Error(ctx, "Artist not found", "name", artistName, "id", id, err)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error(ctx, "Failure occurred when trying to fetch artist", "name", artistName, "id", id, err)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
songs, err := e.getMatchingTopSongs(ctx, e.ag, artist, count)
|
songs, err := e.getMatchingTopSongs(ctx, e.ag, artist, count)
|
||||||
@ -713,7 +718,24 @@ func (e *provider) loadArtistsByName(ctx context.Context, similar []agents.Artis
|
|||||||
return matches, nil
|
return matches, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *provider) findArtistByName(ctx context.Context, artistName string) (*auxArtist, error) {
|
func (e *provider) findArtist(ctx context.Context, artistName, id string) (*auxArtist, error) {
|
||||||
|
if id != "" {
|
||||||
|
artist, err := e.ds.Artist(ctx).Get(id)
|
||||||
|
if err == nil {
|
||||||
|
return &auxArtist{Artist: *artist}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, model.ErrNotFound) {
|
||||||
|
log.Warn(ctx, "Could not find artist by id", "id", id, err)
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if artistName == "" {
|
||||||
|
return nil, model.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
artists, err := e.ds.Artist(ctx).GetAll(model.QueryOptions{
|
artists, err := e.ds.Artist(ctx).GetAll(model.QueryOptions{
|
||||||
Filters: squirrel.Like{"artist.name": artistName},
|
Filters: squirrel.Like{"artist.name": artistName},
|
||||||
Max: 1,
|
Max: 1,
|
||||||
|
|||||||
84
core/external/provider_topsongs_test.go
vendored
84
core/external/provider_topsongs_test.go
vendored
@ -65,7 +65,7 @@ var _ = Describe("Provider - TopSongs", func() {
|
|||||||
song2 := model.MediaFile{ID: "song-2", Title: "Song Two", ArtistID: "artist-1", MbzRecordingID: "mbid-song-2"}
|
song2 := model.MediaFile{ID: "song-2", Title: "Song Two", ArtistID: "artist-1", MbzRecordingID: "mbid-song-2"}
|
||||||
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1, song2}, nil).Once()
|
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1, song2}, nil).Once()
|
||||||
|
|
||||||
songs, err := p.TopSongs(ctx, "Artist One", 2)
|
songs, err := p.TopSongs(ctx, "Artist One", "", 2)
|
||||||
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(songs).To(HaveLen(2))
|
Expect(songs).To(HaveLen(2))
|
||||||
@ -76,6 +76,59 @@ var _ = Describe("Provider - TopSongs", func() {
|
|||||||
mediaFileRepo.AssertExpectations(GinkgoT())
|
mediaFileRepo.AssertExpectations(GinkgoT())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Context("artist id and/or name", func() {
|
||||||
|
artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
agentSongs := []agents.Song{{Name: "Song One", MBID: "mbid-song-1"}}
|
||||||
|
ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 2).Return(agentSongs, nil).Once()
|
||||||
|
|
||||||
|
song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzRecordingID: "mbid-song-1"}
|
||||||
|
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1}, nil).Once()
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns top songs for a known artist by id only", func() {
|
||||||
|
artistRepo.On("Get", "artist-1").Return(&artist1, nil).Once()
|
||||||
|
|
||||||
|
songs, err := p.TopSongs(ctx, "", "artist-1", 2)
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(songs).To(HaveLen(1))
|
||||||
|
Expect(songs[0].ID).To(Equal("song-1"))
|
||||||
|
artistRepo.AssertExpectations(GinkgoT())
|
||||||
|
ag.AssertExpectations(GinkgoT())
|
||||||
|
mediaFileRepo.AssertExpectations(GinkgoT())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns top songs for a known artist by id, falling back to name", func() {
|
||||||
|
artistRepo.On("Get", "fake-id").Return(nil, model.ErrNotFound).Once()
|
||||||
|
artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil).Once()
|
||||||
|
|
||||||
|
songs, err := p.TopSongs(ctx, "Artist One", "fake-id", 2)
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(songs).To(HaveLen(1))
|
||||||
|
Expect(songs[0].ID).To(Equal("song-1"))
|
||||||
|
artistRepo.AssertExpectations(GinkgoT())
|
||||||
|
ag.AssertExpectations(GinkgoT())
|
||||||
|
mediaFileRepo.AssertExpectations(GinkgoT())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
It("bails if lookup by id returns a fatal error", func() {
|
||||||
|
artistRepo.On("Get", "artist-1").Return(nil, model.ErrInvalidAuth).Once()
|
||||||
|
songs, err := p.TopSongs(ctx, "Artist One", "artist-1", 2)
|
||||||
|
Expect(songs).To(BeEmpty())
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("bails if lookup by name returns a fatal error", func() {
|
||||||
|
artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(nil, model.ErrInvalidAuth).Once()
|
||||||
|
songs, err := p.TopSongs(ctx, "Artist One", "", 2)
|
||||||
|
Expect(songs).To(BeEmpty())
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
It("backfills name and MBID onto an unnamed primary credit (the queried artist) and matches", func() {
|
It("backfills name and MBID onto an unnamed primary credit (the queried artist) and matches", func() {
|
||||||
artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}
|
artist1 := model.Artist{ID: "artist-1", Name: "Artist One", MbzArtistID: "mbid-artist-1"}
|
||||||
artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil)
|
artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{artist1}, nil)
|
||||||
@ -96,7 +149,7 @@ var _ = Describe("Provider - TopSongs", func() {
|
|||||||
}
|
}
|
||||||
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{track}, nil)
|
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{track}, nil)
|
||||||
|
|
||||||
songs, err := p.TopSongs(ctx, "Artist One", 1)
|
songs, err := p.TopSongs(ctx, "Artist One", "", 1)
|
||||||
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(songs).To(HaveLen(1))
|
Expect(songs).To(HaveLen(1))
|
||||||
@ -116,7 +169,8 @@ var _ = Describe("Provider - TopSongs", func() {
|
|||||||
ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 1).Return(agentSongs, nil).Once()
|
ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 1).Return(agentSongs, nil).Once()
|
||||||
|
|
||||||
// Library track is credited to Artist One (the queried artist) under a same title. If the
|
// Library track is credited to Artist One (the queried artist) under a same title. If the
|
||||||
// queried MBID were wrongly stamped onto the "Artist Two" credit, that mismatched name+MBID
|
// queried MBID were wrongly stamped onto the "Artist Two" credit, that mismatched name+MBID artistRepo.On("Get", "fake-id").Return(nil, model.ErrNotFound).Once()
|
||||||
|
|
||||||
// could mis-resolve. With the guard, "Artist Two" stays MBID-less and does not match One's track.
|
// could mis-resolve. With the guard, "Artist Two" stays MBID-less and does not match One's track.
|
||||||
track := model.MediaFile{
|
track := model.MediaFile{
|
||||||
ID: "one-track", Title: "Collab Song", ArtistID: "artist-1",
|
ID: "one-track", Title: "Collab Song", ArtistID: "artist-1",
|
||||||
@ -126,7 +180,7 @@ var _ = Describe("Provider - TopSongs", func() {
|
|||||||
}
|
}
|
||||||
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{track}, nil)
|
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{track}, nil)
|
||||||
|
|
||||||
songs, err := p.TopSongs(ctx, "Artist One", 1)
|
songs, err := p.TopSongs(ctx, "Artist One", "", 1)
|
||||||
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
// "Artist Two" (named, MBID-less, not in the library) does not resolve to One's track.
|
// "Artist Two" (named, MBID-less, not in the library) does not resolve to One's track.
|
||||||
@ -137,7 +191,7 @@ var _ = Describe("Provider - TopSongs", func() {
|
|||||||
// Mock artist not found
|
// Mock artist not found
|
||||||
artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{}, nil).Once()
|
artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{}, nil).Once()
|
||||||
|
|
||||||
songs, err := p.TopSongs(ctx, "Unknown Artist", 5)
|
songs, err := p.TopSongs(ctx, "Unknown Artist", "", 5)
|
||||||
|
|
||||||
Expect(err).ToNot(HaveOccurred()) // TopSongs returns nil error if artist not found
|
Expect(err).ToNot(HaveOccurred()) // TopSongs returns nil error if artist not found
|
||||||
Expect(songs).To(BeNil())
|
Expect(songs).To(BeNil())
|
||||||
@ -154,7 +208,7 @@ var _ = Describe("Provider - TopSongs", func() {
|
|||||||
agentErr := errors.New("agent error")
|
agentErr := errors.New("agent error")
|
||||||
ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 5).Return(nil, agentErr).Once()
|
ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 5).Return(nil, agentErr).Once()
|
||||||
|
|
||||||
songs, err := p.TopSongs(ctx, "Artist One", 5)
|
songs, err := p.TopSongs(ctx, "Artist One", "", 5)
|
||||||
|
|
||||||
Expect(err).To(MatchError(agentErr))
|
Expect(err).To(MatchError(agentErr))
|
||||||
Expect(songs).To(BeNil())
|
Expect(songs).To(BeNil())
|
||||||
@ -170,7 +224,7 @@ var _ = Describe("Provider - TopSongs", func() {
|
|||||||
// Mock agent ErrNotFound
|
// Mock agent ErrNotFound
|
||||||
ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 5).Return(nil, agents.ErrNotFound).Once()
|
ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 5).Return(nil, agents.ErrNotFound).Once()
|
||||||
|
|
||||||
songs, err := p.TopSongs(ctx, "Artist One", 5)
|
songs, err := p.TopSongs(ctx, "Artist One", "", 5)
|
||||||
|
|
||||||
Expect(err).To(MatchError(model.ErrNotFound))
|
Expect(err).To(MatchError(model.ErrNotFound))
|
||||||
Expect(songs).To(BeNil())
|
Expect(songs).To(BeNil())
|
||||||
@ -191,7 +245,7 @@ var _ = Describe("Provider - TopSongs", func() {
|
|||||||
song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzRecordingID: "mbid-song-1"}
|
song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzRecordingID: "mbid-song-1"}
|
||||||
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1}, nil).Once()
|
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1}, nil).Once()
|
||||||
|
|
||||||
songs, err := p.TopSongs(ctx, "Artist One", 1)
|
songs, err := p.TopSongs(ctx, "Artist One", "", 1)
|
||||||
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(songs).To(HaveLen(1))
|
Expect(songs).To(HaveLen(1))
|
||||||
@ -220,7 +274,7 @@ var _ = Describe("Provider - TopSongs", func() {
|
|||||||
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1}, nil).Once() // bulk MBID query
|
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1}, nil).Once() // bulk MBID query
|
||||||
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{}, nil).Once() // title track-fetch for song2: no match
|
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{}, nil).Once() // title track-fetch for song2: no match
|
||||||
|
|
||||||
songs, err := p.TopSongs(ctx, "Artist One", 2)
|
songs, err := p.TopSongs(ctx, "Artist One", "", 2)
|
||||||
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(songs).To(HaveLen(1))
|
Expect(songs).To(HaveLen(1))
|
||||||
@ -242,7 +296,7 @@ var _ = Describe("Provider - TopSongs", func() {
|
|||||||
ag.On("GetArtistTopSongs", canceledCtx, "artist-1", "Artist One", "mbid-artist-1", 5).Return(nil, context.Canceled).Once()
|
ag.On("GetArtistTopSongs", canceledCtx, "artist-1", "Artist One", "mbid-artist-1", 5).Return(nil, context.Canceled).Once()
|
||||||
|
|
||||||
cancel() // Cancel the context before calling
|
cancel() // Cancel the context before calling
|
||||||
songs, err := p.TopSongs(canceledCtx, "Artist One", 5)
|
songs, err := p.TopSongs(canceledCtx, "Artist One", "", 5)
|
||||||
|
|
||||||
Expect(err).To(MatchError(context.Canceled))
|
Expect(err).To(MatchError(context.Canceled))
|
||||||
Expect(songs).To(BeNil())
|
Expect(songs).To(BeNil())
|
||||||
@ -276,7 +330,7 @@ var _ = Describe("Provider - TopSongs", func() {
|
|||||||
}
|
}
|
||||||
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1, song2}, nil).Once()
|
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1, song2}, nil).Once()
|
||||||
|
|
||||||
songs, err := p.TopSongs(ctx, "Artist One", 2)
|
songs, err := p.TopSongs(ctx, "Artist One", "", 2)
|
||||||
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(songs).To(HaveLen(2))
|
Expect(songs).To(HaveLen(2))
|
||||||
@ -313,7 +367,7 @@ var _ = Describe("Provider - TopSongs", func() {
|
|||||||
}
|
}
|
||||||
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song2}, nil).Once()
|
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song2}, nil).Once()
|
||||||
|
|
||||||
songs, err := p.TopSongs(ctx, "Artist One", 2)
|
songs, err := p.TopSongs(ctx, "Artist One", "", 2)
|
||||||
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(songs).To(HaveLen(2))
|
Expect(songs).To(HaveLen(2))
|
||||||
@ -341,7 +395,7 @@ var _ = Describe("Provider - TopSongs", func() {
|
|||||||
song2 := model.MediaFile{ID: "song-2", Title: "Song Two", ArtistID: "artist-1", MbzRecordingID: "mbid-song-2"}
|
song2 := model.MediaFile{ID: "song-2", Title: "Song Two", ArtistID: "artist-1", MbzRecordingID: "mbid-song-2"}
|
||||||
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1, song2}, nil).Once()
|
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1, song2}, nil).Once()
|
||||||
|
|
||||||
songs, err := p.TopSongs(ctx, "Artist One", 1)
|
songs, err := p.TopSongs(ctx, "Artist One", "", 1)
|
||||||
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(songs).To(HaveLen(1))
|
Expect(songs).To(HaveLen(1))
|
||||||
@ -369,7 +423,7 @@ var _ = Describe("Provider - TopSongs", func() {
|
|||||||
song2 := model.MediaFile{ID: "song-2", Title: "Song Two", ArtistID: "artist-1"}
|
song2 := model.MediaFile{ID: "song-2", Title: "Song Two", ArtistID: "artist-1"}
|
||||||
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1, song2}, nil).Once()
|
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1, song2}, nil).Once()
|
||||||
|
|
||||||
songs, err := p.TopSongs(ctx, "Artist One", 2)
|
songs, err := p.TopSongs(ctx, "Artist One", "", 2)
|
||||||
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(songs).To(HaveLen(2))
|
Expect(songs).To(HaveLen(2))
|
||||||
@ -397,7 +451,7 @@ var _ = Describe("Provider - TopSongs", func() {
|
|||||||
song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzRecordingID: "mbid-song-1"}
|
song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzRecordingID: "mbid-song-1"}
|
||||||
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1}, nil).Once()
|
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1}, nil).Once()
|
||||||
|
|
||||||
songs, err := p.TopSongs(ctx, "Artist One", 1)
|
songs, err := p.TopSongs(ctx, "Artist One", "", 1)
|
||||||
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
Expect(songs).To(HaveLen(1))
|
Expect(songs).To(HaveLen(1))
|
||||||
|
|||||||
@ -381,13 +381,14 @@ func (api *Router) GetSimilarSongs2(r *http.Request) (*responses.Subsonic, error
|
|||||||
func (api *Router) GetTopSongs(r *http.Request) (*responses.Subsonic, error) {
|
func (api *Router) GetTopSongs(r *http.Request) (*responses.Subsonic, error) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
p := req.Params(r)
|
p := req.Params(r)
|
||||||
|
id, idErr := p.String("id")
|
||||||
artist, err := p.String("artist")
|
artist, err := p.String("artist")
|
||||||
if err != nil {
|
if err != nil && idErr != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
count := p.IntOr("count", 50)
|
count := p.IntOr("count", 50)
|
||||||
|
|
||||||
songs, err := api.provider.TopSongs(ctx, artist, count)
|
songs, err := api.provider.TopSongs(ctx, artist, id, count)
|
||||||
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -353,7 +353,7 @@ func (n noopProvider) SimilarSongs(context.Context, string, int) (model.MediaFil
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n noopProvider) TopSongs(context.Context, string, int) (model.MediaFiles, error) {
|
func (n noopProvider) TopSongs(context.Context, string, string, int) (model.MediaFiles, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,15 @@ var _ = Describe("Browsing Endpoints", func() {
|
|||||||
setupTestDB()
|
setupTestDB()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
getBeatlesId := func() string {
|
||||||
|
artists, err := ds.Artist(ctx).GetAll(model.QueryOptions{
|
||||||
|
Filters: squirrel.Eq{"name": "The Beatles"},
|
||||||
|
})
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(artists).ToNot(BeEmpty())
|
||||||
|
return artists[0].ID
|
||||||
|
}
|
||||||
|
|
||||||
Describe("getMusicFolders", func() {
|
Describe("getMusicFolders", func() {
|
||||||
It("returns the configured music library", func() {
|
It("returns the configured music library", func() {
|
||||||
resp := doReq("getMusicFolders")
|
resp := doReq("getMusicFolders")
|
||||||
@ -85,12 +94,7 @@ var _ = Describe("Browsing Endpoints", func() {
|
|||||||
|
|
||||||
Describe("getMusicDirectory", func() {
|
Describe("getMusicDirectory", func() {
|
||||||
It("returns an artist directory with its albums as children", func() {
|
It("returns an artist directory with its albums as children", func() {
|
||||||
artists, err := ds.Artist(ctx).GetAll(model.QueryOptions{
|
beatlesID := getBeatlesId()
|
||||||
Filters: squirrel.Eq{"name": "The Beatles"},
|
|
||||||
})
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
|
||||||
Expect(artists).ToNot(BeEmpty())
|
|
||||||
beatlesID := artists[0].ID
|
|
||||||
|
|
||||||
resp := doReq("getMusicDirectory", "id", beatlesID)
|
resp := doReq("getMusicDirectory", "id", beatlesID)
|
||||||
|
|
||||||
@ -125,12 +129,7 @@ var _ = Describe("Browsing Endpoints", func() {
|
|||||||
|
|
||||||
Describe("getArtist", func() {
|
Describe("getArtist", func() {
|
||||||
It("returns artist with albums in ID3 format", func() {
|
It("returns artist with albums in ID3 format", func() {
|
||||||
artists, err := ds.Artist(ctx).GetAll(model.QueryOptions{
|
beatlesID := getBeatlesId()
|
||||||
Filters: squirrel.Eq{"name": "The Beatles"},
|
|
||||||
})
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
|
||||||
Expect(artists).ToNot(BeEmpty())
|
|
||||||
beatlesID := artists[0].ID
|
|
||||||
|
|
||||||
resp := doReq("getArtist", "id", beatlesID)
|
resp := doReq("getArtist", "id", beatlesID)
|
||||||
|
|
||||||
@ -141,12 +140,7 @@ var _ = Describe("Browsing Endpoints", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
It("returns album names for the artist", func() {
|
It("returns album names for the artist", func() {
|
||||||
artists, err := ds.Artist(ctx).GetAll(model.QueryOptions{
|
beatlesID := getBeatlesId()
|
||||||
Filters: squirrel.Eq{"name": "The Beatles"},
|
|
||||||
})
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
|
||||||
Expect(artists).ToNot(BeEmpty())
|
|
||||||
beatlesID := artists[0].ID
|
|
||||||
|
|
||||||
resp := doReq("getArtist", "id", beatlesID)
|
resp := doReq("getArtist", "id", beatlesID)
|
||||||
|
|
||||||
@ -381,13 +375,7 @@ var _ = Describe("Browsing Endpoints", func() {
|
|||||||
|
|
||||||
Describe("getArtistInfo", func() {
|
Describe("getArtistInfo", func() {
|
||||||
It("returns artist info for a valid artist", func() {
|
It("returns artist info for a valid artist", func() {
|
||||||
artists, err := ds.Artist(ctx).GetAll(model.QueryOptions{
|
beatlesID := getBeatlesId()
|
||||||
Filters: squirrel.Eq{"name": "The Beatles"},
|
|
||||||
})
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
|
||||||
Expect(artists).ToNot(BeEmpty())
|
|
||||||
beatlesID := artists[0].ID
|
|
||||||
|
|
||||||
resp := doReq("getArtistInfo", "id", beatlesID)
|
resp := doReq("getArtistInfo", "id", beatlesID)
|
||||||
|
|
||||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
@ -397,13 +385,7 @@ var _ = Describe("Browsing Endpoints", func() {
|
|||||||
|
|
||||||
Describe("getArtistInfo2", func() {
|
Describe("getArtistInfo2", func() {
|
||||||
It("returns artist info2 for a valid artist", func() {
|
It("returns artist info2 for a valid artist", func() {
|
||||||
artists, err := ds.Artist(ctx).GetAll(model.QueryOptions{
|
beatlesID := getBeatlesId()
|
||||||
Filters: squirrel.Eq{"name": "The Beatles"},
|
|
||||||
})
|
|
||||||
Expect(err).ToNot(HaveOccurred())
|
|
||||||
Expect(artists).ToNot(BeEmpty())
|
|
||||||
beatlesID := artists[0].ID
|
|
||||||
|
|
||||||
resp := doReq("getArtistInfo2", "id", beatlesID)
|
resp := doReq("getArtistInfo2", "id", beatlesID)
|
||||||
|
|
||||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
@ -426,6 +408,28 @@ var _ = Describe("Browsing Endpoints", func() {
|
|||||||
Expect(resp.TopSongs).ToNot(BeNil())
|
Expect(resp.TopSongs).ToNot(BeNil())
|
||||||
Expect(resp.TopSongs.Song).To(BeEmpty())
|
Expect(resp.TopSongs.Song).To(BeEmpty())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("returns an error if no arguments are specified", func() {
|
||||||
|
resp := doReq("getTopSongs")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
||||||
|
Expect(resp.TopSongs).To(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns a response only by id", func() {
|
||||||
|
beatlesID := getBeatlesId()
|
||||||
|
resp := doReq("getTopSongs", "id", beatlesID)
|
||||||
|
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TopSongs).ToNot(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns a response when both id and name are included", func() {
|
||||||
|
beatlesID := getBeatlesId()
|
||||||
|
resp := doReq("getTopSongs", "id", beatlesID, "artist", "The Beatles")
|
||||||
|
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TopSongs).ToNot(BeNil())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Describe("getSimilarSongs", func() {
|
Describe("getSimilarSongs", func() {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ func (api *Router) GetOpenSubsonicExtensions(_ *http.Request) (*responses.Subson
|
|||||||
{Name: "indexBasedQueue", Versions: []int32{1}},
|
{Name: "indexBasedQueue", Versions: []int32{1}},
|
||||||
{Name: "transcoding", Versions: []int32{1}},
|
{Name: "transcoding", Versions: []int32{1}},
|
||||||
{Name: "playbackReport", Versions: []int32{1}},
|
{Name: "playbackReport", Versions: []int32{1}},
|
||||||
|
{Name: "topSongsByArtistId", Versions: []int32{1}},
|
||||||
}
|
}
|
||||||
if api.sonic != nil && api.sonic.HasProvider() {
|
if api.sonic != nil && api.sonic.HasProvider() {
|
||||||
extensions = append(extensions, responses.OpenSubsonicExtension{
|
extensions = append(extensions, responses.OpenSubsonicExtension{
|
||||||
|
|||||||
@ -55,13 +55,14 @@ var _ = Describe("GetOpenSubsonicExtensions", func() {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(*response.Subsonic.OpenSubsonicExtensions).To(SatisfyAll(
|
Expect(*response.Subsonic.OpenSubsonicExtensions).To(SatisfyAll(
|
||||||
HaveLen(6),
|
HaveLen(7),
|
||||||
ContainElement(responses.OpenSubsonicExtension{Name: "transcodeOffset", Versions: []int32{1}}),
|
ContainElement(responses.OpenSubsonicExtension{Name: "transcodeOffset", Versions: []int32{1}}),
|
||||||
ContainElement(responses.OpenSubsonicExtension{Name: "formPost", Versions: []int32{1}}),
|
ContainElement(responses.OpenSubsonicExtension{Name: "formPost", Versions: []int32{1}}),
|
||||||
ContainElement(responses.OpenSubsonicExtension{Name: "songLyrics", Versions: []int32{1, 2}}),
|
ContainElement(responses.OpenSubsonicExtension{Name: "songLyrics", Versions: []int32{1, 2}}),
|
||||||
ContainElement(responses.OpenSubsonicExtension{Name: "indexBasedQueue", Versions: []int32{1}}),
|
ContainElement(responses.OpenSubsonicExtension{Name: "indexBasedQueue", Versions: []int32{1}}),
|
||||||
ContainElement(responses.OpenSubsonicExtension{Name: "transcoding", Versions: []int32{1}}),
|
ContainElement(responses.OpenSubsonicExtension{Name: "transcoding", Versions: []int32{1}}),
|
||||||
ContainElement(responses.OpenSubsonicExtension{Name: "playbackReport", Versions: []int32{1}}),
|
ContainElement(responses.OpenSubsonicExtension{Name: "playbackReport", Versions: []int32{1}}),
|
||||||
|
ContainElement(responses.OpenSubsonicExtension{Name: "topSongsByArtistId", Versions: []int32{1}}),
|
||||||
))
|
))
|
||||||
Expect(*response.Subsonic.OpenSubsonicExtensions).NotTo(
|
Expect(*response.Subsonic.OpenSubsonicExtensions).NotTo(
|
||||||
ContainElement(responses.OpenSubsonicExtension{Name: "sonicSimilarity", Versions: []int32{1}}),
|
ContainElement(responses.OpenSubsonicExtension{Name: "sonicSimilarity", Versions: []int32{1}}),
|
||||||
@ -85,7 +86,7 @@ var _ = Describe("GetOpenSubsonicExtensions", func() {
|
|||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(*response.Subsonic.OpenSubsonicExtensions).To(SatisfyAll(
|
Expect(*response.Subsonic.OpenSubsonicExtensions).To(SatisfyAll(
|
||||||
HaveLen(7),
|
HaveLen(8),
|
||||||
ContainElement(responses.OpenSubsonicExtension{Name: "transcodeOffset", Versions: []int32{1}}),
|
ContainElement(responses.OpenSubsonicExtension{Name: "transcodeOffset", Versions: []int32{1}}),
|
||||||
ContainElement(responses.OpenSubsonicExtension{Name: "formPost", Versions: []int32{1}}),
|
ContainElement(responses.OpenSubsonicExtension{Name: "formPost", Versions: []int32{1}}),
|
||||||
ContainElement(responses.OpenSubsonicExtension{Name: "songLyrics", Versions: []int32{1, 2}}),
|
ContainElement(responses.OpenSubsonicExtension{Name: "songLyrics", Versions: []int32{1, 2}}),
|
||||||
@ -93,6 +94,7 @@ var _ = Describe("GetOpenSubsonicExtensions", func() {
|
|||||||
ContainElement(responses.OpenSubsonicExtension{Name: "transcoding", Versions: []int32{1}}),
|
ContainElement(responses.OpenSubsonicExtension{Name: "transcoding", Versions: []int32{1}}),
|
||||||
ContainElement(responses.OpenSubsonicExtension{Name: "playbackReport", Versions: []int32{1}}),
|
ContainElement(responses.OpenSubsonicExtension{Name: "playbackReport", Versions: []int32{1}}),
|
||||||
ContainElement(responses.OpenSubsonicExtension{Name: "sonicSimilarity", Versions: []int32{1}}),
|
ContainElement(responses.OpenSubsonicExtension{Name: "sonicSimilarity", Versions: []int32{1}}),
|
||||||
|
ContainElement(responses.OpenSubsonicExtension{Name: "topSongsByArtistId", Versions: []int32{1}}),
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user