mirror of
https://github.com/navidrome/navidrome.git
synced 2025-06-09 03:42:23 +03:00
refactor: external_metadata -> external.Provider (#3903)
* tests for TopSongs Signed-off-by: Deluan <deluan@navidrome.org> * convert to Ginkgo Signed-off-by: Deluan <deluan@navidrome.org> * consolidate tests Signed-off-by: Deluan <deluan@navidrome.org> * rename external metadata -wip Signed-off-by: Deluan <deluan@navidrome.org> * rename external metadata to extdata.Provider Signed-off-by: Deluan <deluan@navidrome.org> * refactor tests - wip Signed-off-by: Deluan <deluan@navidrome.org> * refactor test helpers Signed-off-by: Deluan <deluan@navidrome.org> * remove reflection Signed-off-by: Deluan <deluan@navidrome.org> * use mock.Mock Signed-off-by: Deluan <deluan@navidrome.org> * refactor Signed-off-by: Deluan <deluan@navidrome.org> * fix Signed-off-by: Deluan <deluan@navidrome.org> * receive Agents interface in Provider constructor Signed-off-by: Deluan <deluan@navidrome.org> * use mock for Agents Signed-off-by: Deluan <deluan@navidrome.org> * tests for SimilarSongs Signed-off-by: Deluan <deluan@navidrome.org> * remove duplication Signed-off-by: Deluan <deluan@navidrome.org> * ArtistImage tests Signed-off-by: Deluan <deluan@navidrome.org> * AlbumImage tests Signed-off-by: Deluan <deluan@navidrome.org> * fix provider error handling Signed-off-by: Deluan <deluan@navidrome.org> * UpdateAlbumInfo tests - wip Signed-off-by: Deluan <deluan@navidrome.org> * UpdateAlbumInfo tests - wip Signed-off-by: Deluan <deluan@navidrome.org> * refactor Signed-off-by: Deluan <deluan@navidrome.org> * refactor Signed-off-by: Deluan <deluan@navidrome.org> * refactor Signed-off-by: Deluan <deluan@navidrome.org> * UpdateArtistInfo tests - wip Signed-off-by: Deluan <deluan@navidrome.org> * clean up Signed-off-by: Deluan <deluan@navidrome.org> * refactor Signed-off-by: Deluan <deluan@navidrome.org> * fix test descriptions Signed-off-by: Deluan <deluan@navidrome.org> * refactor Signed-off-by: Deluan <deluan@navidrome.org> * refactor: rename extdata package to external Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
6b59f5f73a
commit
58367afaea
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/navidrome/navidrome/core/agents/lastfm"
|
"github.com/navidrome/navidrome/core/agents/lastfm"
|
||||||
"github.com/navidrome/navidrome/core/agents/listenbrainz"
|
"github.com/navidrome/navidrome/core/agents/listenbrainz"
|
||||||
"github.com/navidrome/navidrome/core/artwork"
|
"github.com/navidrome/navidrome/core/artwork"
|
||||||
|
"github.com/navidrome/navidrome/core/external"
|
||||||
"github.com/navidrome/navidrome/core/ffmpeg"
|
"github.com/navidrome/navidrome/core/ffmpeg"
|
||||||
"github.com/navidrome/navidrome/core/metrics"
|
"github.com/navidrome/navidrome/core/metrics"
|
||||||
"github.com/navidrome/navidrome/core/playback"
|
"github.com/navidrome/navidrome/core/playback"
|
||||||
@ -66,8 +67,8 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
|
|||||||
fileCache := artwork.GetImageCache()
|
fileCache := artwork.GetImageCache()
|
||||||
fFmpeg := ffmpeg.New()
|
fFmpeg := ffmpeg.New()
|
||||||
agentsAgents := agents.GetAgents(dataStore)
|
agentsAgents := agents.GetAgents(dataStore)
|
||||||
externalMetadata := core.NewExternalMetadata(dataStore, agentsAgents)
|
provider := external.NewProvider(dataStore, agentsAgents)
|
||||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata)
|
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||||
transcodingCache := core.GetTranscodingCache()
|
transcodingCache := core.GetTranscodingCache()
|
||||||
mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
||||||
share := core.NewShare(dataStore)
|
share := core.NewShare(dataStore)
|
||||||
@ -80,7 +81,7 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
|
|||||||
scannerScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics)
|
scannerScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics)
|
||||||
playTracker := scrobbler.GetPlayTracker(dataStore, broker)
|
playTracker := scrobbler.GetPlayTracker(dataStore, broker)
|
||||||
playbackServer := playback.GetInstance(dataStore)
|
playbackServer := playback.GetInstance(dataStore)
|
||||||
router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, externalMetadata, scannerScanner, broker, playlists, playTracker, share, playbackServer)
|
router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, provider, scannerScanner, broker, playlists, playTracker, share, playbackServer)
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,8 +91,8 @@ func CreatePublicRouter() *public.Router {
|
|||||||
fileCache := artwork.GetImageCache()
|
fileCache := artwork.GetImageCache()
|
||||||
fFmpeg := ffmpeg.New()
|
fFmpeg := ffmpeg.New()
|
||||||
agentsAgents := agents.GetAgents(dataStore)
|
agentsAgents := agents.GetAgents(dataStore)
|
||||||
externalMetadata := core.NewExternalMetadata(dataStore, agentsAgents)
|
provider := external.NewProvider(dataStore, agentsAgents)
|
||||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata)
|
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||||
transcodingCache := core.GetTranscodingCache()
|
transcodingCache := core.GetTranscodingCache()
|
||||||
mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
mediaStreamer := core.NewMediaStreamer(dataStore, fFmpeg, transcodingCache)
|
||||||
share := core.NewShare(dataStore)
|
share := core.NewShare(dataStore)
|
||||||
@ -134,8 +135,8 @@ func CreateScanner(ctx context.Context) scanner.Scanner {
|
|||||||
fileCache := artwork.GetImageCache()
|
fileCache := artwork.GetImageCache()
|
||||||
fFmpeg := ffmpeg.New()
|
fFmpeg := ffmpeg.New()
|
||||||
agentsAgents := agents.GetAgents(dataStore)
|
agentsAgents := agents.GetAgents(dataStore)
|
||||||
externalMetadata := core.NewExternalMetadata(dataStore, agentsAgents)
|
provider := external.NewProvider(dataStore, agentsAgents)
|
||||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata)
|
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||||
broker := events.GetBroker()
|
broker := events.GetBroker()
|
||||||
playlists := core.NewPlaylists(dataStore)
|
playlists := core.NewPlaylists(dataStore)
|
||||||
@ -150,8 +151,8 @@ func CreateScanWatcher(ctx context.Context) scanner.Watcher {
|
|||||||
fileCache := artwork.GetImageCache()
|
fileCache := artwork.GetImageCache()
|
||||||
fFmpeg := ffmpeg.New()
|
fFmpeg := ffmpeg.New()
|
||||||
agentsAgents := agents.GetAgents(dataStore)
|
agentsAgents := agents.GetAgents(dataStore)
|
||||||
externalMetadata := core.NewExternalMetadata(dataStore, agentsAgents)
|
provider := external.NewProvider(dataStore, agentsAgents)
|
||||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, externalMetadata)
|
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||||
broker := events.GetBroker()
|
broker := events.GetBroker()
|
||||||
playlists := core.NewPlaylists(dataStore)
|
playlists := core.NewPlaylists(dataStore)
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/consts"
|
"github.com/navidrome/navidrome/consts"
|
||||||
"github.com/navidrome/navidrome/core"
|
"github.com/navidrome/navidrome/core/external"
|
||||||
"github.com/navidrome/navidrome/core/ffmpeg"
|
"github.com/navidrome/navidrome/core/ffmpeg"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
@ -24,15 +24,15 @@ type Artwork interface {
|
|||||||
GetOrPlaceholder(ctx context.Context, id string, size int, square bool) (io.ReadCloser, time.Time, error)
|
GetOrPlaceholder(ctx context.Context, id string, size int, square bool) (io.ReadCloser, time.Time, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewArtwork(ds model.DataStore, cache cache.FileCache, ffmpeg ffmpeg.FFmpeg, em core.ExternalMetadata) Artwork {
|
func NewArtwork(ds model.DataStore, cache cache.FileCache, ffmpeg ffmpeg.FFmpeg, provider external.Provider) Artwork {
|
||||||
return &artwork{ds: ds, cache: cache, ffmpeg: ffmpeg, em: em}
|
return &artwork{ds: ds, cache: cache, ffmpeg: ffmpeg, provider: provider}
|
||||||
}
|
}
|
||||||
|
|
||||||
type artwork struct {
|
type artwork struct {
|
||||||
ds model.DataStore
|
ds model.DataStore
|
||||||
cache cache.FileCache
|
cache cache.FileCache
|
||||||
ffmpeg ffmpeg.FFmpeg
|
ffmpeg ffmpeg.FFmpeg
|
||||||
em core.ExternalMetadata
|
provider external.Provider
|
||||||
}
|
}
|
||||||
|
|
||||||
type artworkReader interface {
|
type artworkReader interface {
|
||||||
@ -115,9 +115,9 @@ func (a *artwork) getArtworkReader(ctx context.Context, artID model.ArtworkID, s
|
|||||||
} else {
|
} else {
|
||||||
switch artID.Kind {
|
switch artID.Kind {
|
||||||
case model.KindArtistArtwork:
|
case model.KindArtistArtwork:
|
||||||
artReader, err = newArtistReader(ctx, a, artID, a.em)
|
artReader, err = newArtistReader(ctx, a, artID, a.provider)
|
||||||
case model.KindAlbumArtwork:
|
case model.KindAlbumArtwork:
|
||||||
artReader, err = newAlbumArtworkReader(ctx, a, artID, a.em)
|
artReader, err = newAlbumArtworkReader(ctx, a, artID, a.provider)
|
||||||
case model.KindMediaFileArtwork:
|
case model.KindMediaFileArtwork:
|
||||||
artReader, err = newMediafileArtworkReader(ctx, a, artID)
|
artReader, err = newMediafileArtworkReader(ctx, a, artID)
|
||||||
case model.KindPlaylistArtwork:
|
case model.KindPlaylistArtwork:
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/Masterminds/squirrel"
|
"github.com/Masterminds/squirrel"
|
||||||
"github.com/navidrome/navidrome/conf"
|
"github.com/navidrome/navidrome/conf"
|
||||||
"github.com/navidrome/navidrome/core"
|
"github.com/navidrome/navidrome/core"
|
||||||
|
"github.com/navidrome/navidrome/core/external"
|
||||||
"github.com/navidrome/navidrome/core/ffmpeg"
|
"github.com/navidrome/navidrome/core/ffmpeg"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
)
|
)
|
||||||
@ -19,14 +20,14 @@ import (
|
|||||||
type albumArtworkReader struct {
|
type albumArtworkReader struct {
|
||||||
cacheKey
|
cacheKey
|
||||||
a *artwork
|
a *artwork
|
||||||
em core.ExternalMetadata
|
provider external.Provider
|
||||||
album model.Album
|
album model.Album
|
||||||
updatedAt *time.Time
|
updatedAt *time.Time
|
||||||
imgFiles []string
|
imgFiles []string
|
||||||
rootFolder string
|
rootFolder string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAlbumArtworkReader(ctx context.Context, artwork *artwork, artID model.ArtworkID, em core.ExternalMetadata) (*albumArtworkReader, error) {
|
func newAlbumArtworkReader(ctx context.Context, artwork *artwork, artID model.ArtworkID, provider external.Provider) (*albumArtworkReader, error) {
|
||||||
al, err := artwork.ds.Album(ctx).Get(artID.ID)
|
al, err := artwork.ds.Album(ctx).Get(artID.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -37,7 +38,7 @@ func newAlbumArtworkReader(ctx context.Context, artwork *artwork, artID model.Ar
|
|||||||
}
|
}
|
||||||
a := &albumArtworkReader{
|
a := &albumArtworkReader{
|
||||||
a: artwork,
|
a: artwork,
|
||||||
em: em,
|
provider: provider,
|
||||||
album: *al,
|
album: *al,
|
||||||
updatedAt: imagesUpdateAt,
|
updatedAt: imagesUpdateAt,
|
||||||
imgFiles: imgFiles,
|
imgFiles: imgFiles,
|
||||||
@ -82,7 +83,7 @@ func (a *albumArtworkReader) fromCoverArtPriority(ctx context.Context, ffmpeg ff
|
|||||||
embedArtPath := filepath.Join(a.rootFolder, a.album.EmbedArtPath)
|
embedArtPath := filepath.Join(a.rootFolder, a.album.EmbedArtPath)
|
||||||
ff = append(ff, fromTag(ctx, embedArtPath), fromFFmpegTag(ctx, ffmpeg, embedArtPath))
|
ff = append(ff, fromTag(ctx, embedArtPath), fromFFmpegTag(ctx, ffmpeg, embedArtPath))
|
||||||
case pattern == "external":
|
case pattern == "external":
|
||||||
ff = append(ff, fromAlbumExternalSource(ctx, a.album, a.em))
|
ff = append(ff, fromAlbumExternalSource(ctx, a.album, a.provider))
|
||||||
case len(a.imgFiles) > 0:
|
case len(a.imgFiles) > 0:
|
||||||
ff = append(ff, fromExternalFile(ctx, a.imgFiles, pattern))
|
ff = append(ff, fromExternalFile(ctx, a.imgFiles, pattern))
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/Masterminds/squirrel"
|
"github.com/Masterminds/squirrel"
|
||||||
"github.com/navidrome/navidrome/conf"
|
"github.com/navidrome/navidrome/conf"
|
||||||
"github.com/navidrome/navidrome/core"
|
"github.com/navidrome/navidrome/core"
|
||||||
|
"github.com/navidrome/navidrome/core/external"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
"github.com/navidrome/navidrome/utils/str"
|
"github.com/navidrome/navidrome/utils/str"
|
||||||
@ -22,13 +23,13 @@ import (
|
|||||||
type artistReader struct {
|
type artistReader struct {
|
||||||
cacheKey
|
cacheKey
|
||||||
a *artwork
|
a *artwork
|
||||||
em core.ExternalMetadata
|
provider external.Provider
|
||||||
artist model.Artist
|
artist model.Artist
|
||||||
artistFolder string
|
artistFolder string
|
||||||
imgFiles []string
|
imgFiles []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newArtistReader(ctx context.Context, artwork *artwork, artID model.ArtworkID, em core.ExternalMetadata) (*artistReader, error) {
|
func newArtistReader(ctx context.Context, artwork *artwork, artID model.ArtworkID, provider external.Provider) (*artistReader, error) {
|
||||||
ar, err := artwork.ds.Artist(ctx).Get(artID.ID)
|
ar, err := artwork.ds.Artist(ctx).Get(artID.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -53,7 +54,7 @@ func newArtistReader(ctx context.Context, artwork *artwork, artID model.ArtworkI
|
|||||||
}
|
}
|
||||||
a := &artistReader{
|
a := &artistReader{
|
||||||
a: artwork,
|
a: artwork,
|
||||||
em: em,
|
provider: provider,
|
||||||
artist: *ar,
|
artist: *ar,
|
||||||
artistFolder: artistFolder,
|
artistFolder: artistFolder,
|
||||||
imgFiles: imgFiles,
|
imgFiles: imgFiles,
|
||||||
@ -95,7 +96,7 @@ func (a *artistReader) fromArtistArtPriority(ctx context.Context, priority strin
|
|||||||
pattern = strings.TrimSpace(pattern)
|
pattern = strings.TrimSpace(pattern)
|
||||||
switch {
|
switch {
|
||||||
case pattern == "external":
|
case pattern == "external":
|
||||||
ff = append(ff, fromArtistExternalSource(ctx, a.artist, a.em))
|
ff = append(ff, fromArtistExternalSource(ctx, a.artist, a.provider))
|
||||||
case strings.HasPrefix(pattern, "album/"):
|
case strings.HasPrefix(pattern, "album/"):
|
||||||
ff = append(ff, fromExternalFile(ctx, a.imgFiles, strings.TrimPrefix(pattern, "album/")))
|
ff = append(ff, fromExternalFile(ctx, a.imgFiles, strings.TrimPrefix(pattern, "album/")))
|
||||||
default:
|
default:
|
||||||
|
@ -17,7 +17,7 @@ import (
|
|||||||
|
|
||||||
"github.com/dhowden/tag"
|
"github.com/dhowden/tag"
|
||||||
"github.com/navidrome/navidrome/consts"
|
"github.com/navidrome/navidrome/consts"
|
||||||
"github.com/navidrome/navidrome/core"
|
"github.com/navidrome/navidrome/core/external"
|
||||||
"github.com/navidrome/navidrome/core/ffmpeg"
|
"github.com/navidrome/navidrome/core/ffmpeg"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
@ -157,9 +157,9 @@ func fromAlbumPlaceholder() sourceFunc {
|
|||||||
return r, consts.PlaceholderAlbumArt, nil
|
return r, consts.PlaceholderAlbumArt, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func fromArtistExternalSource(ctx context.Context, ar model.Artist, em core.ExternalMetadata) sourceFunc {
|
func fromArtistExternalSource(ctx context.Context, ar model.Artist, provider external.Provider) sourceFunc {
|
||||||
return func() (io.ReadCloser, string, error) {
|
return func() (io.ReadCloser, string, error) {
|
||||||
imageUrl, err := em.ArtistImage(ctx, ar.ID)
|
imageUrl, err := provider.ArtistImage(ctx, ar.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
@ -168,9 +168,9 @@ func fromArtistExternalSource(ctx context.Context, ar model.Artist, em core.Exte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fromAlbumExternalSource(ctx context.Context, al model.Album, em core.ExternalMetadata) sourceFunc {
|
func fromAlbumExternalSource(ctx context.Context, al model.Album, provider external.Provider) sourceFunc {
|
||||||
return func() (io.ReadCloser, string, error) {
|
return func() (io.ReadCloser, string, error) {
|
||||||
imageUrl, err := em.AlbumImage(ctx, al.ID)
|
imageUrl, err := provider.AlbumImage(ctx, al.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
270
core/external/extdata_helper_test.go
vendored
Normal file
270
core/external/extdata_helper_test.go
vendored
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
package external_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/core/agents"
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Shared Mock Implementations ---
|
||||||
|
|
||||||
|
// mockArtistRepo mocks model.ArtistRepository
|
||||||
|
type mockArtistRepo struct {
|
||||||
|
mock.Mock
|
||||||
|
model.ArtistRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockArtistRepo() *mockArtistRepo {
|
||||||
|
return &mockArtistRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetData sets up basic Get expectations.
|
||||||
|
func (m *mockArtistRepo) SetData(artists model.Artists) {
|
||||||
|
for _, a := range artists {
|
||||||
|
artistCopy := a
|
||||||
|
m.On("Get", artistCopy.ID).Return(&artistCopy, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get implements model.ArtistRepository.
|
||||||
|
func (m *mockArtistRepo) Get(id string) (*model.Artist, error) {
|
||||||
|
args := m.Called(id)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*model.Artist), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAll implements model.ArtistRepository.
|
||||||
|
func (m *mockArtistRepo) GetAll(options ...model.QueryOptions) (model.Artists, error) {
|
||||||
|
argsSlice := make([]interface{}, len(options))
|
||||||
|
for i, v := range options {
|
||||||
|
argsSlice[i] = v
|
||||||
|
}
|
||||||
|
args := m.Called(argsSlice...)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(model.Artists), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetError is a helper to set up a generic error for GetAll.
|
||||||
|
func (m *mockArtistRepo) SetError(hasError bool) {
|
||||||
|
if hasError {
|
||||||
|
m.On("GetAll", mock.Anything).Return(nil, errors.New("mock repo error"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByName is a helper to set up a GetAll expectation for finding by name.
|
||||||
|
func (m *mockArtistRepo) FindByName(name string, artist model.Artist) {
|
||||||
|
m.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
||||||
|
return opt.Filters != nil
|
||||||
|
})).Return(model.Artists{artist}, nil).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockMediaFileRepo mocks model.MediaFileRepository
|
||||||
|
type mockMediaFileRepo struct {
|
||||||
|
mock.Mock
|
||||||
|
model.MediaFileRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockMediaFileRepo() *mockMediaFileRepo {
|
||||||
|
return &mockMediaFileRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetData sets up basic Get expectations.
|
||||||
|
func (m *mockMediaFileRepo) SetData(mediaFiles model.MediaFiles) {
|
||||||
|
for _, mf := range mediaFiles {
|
||||||
|
mfCopy := mf
|
||||||
|
m.On("Get", mfCopy.ID).Return(&mfCopy, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get implements model.MediaFileRepository.
|
||||||
|
func (m *mockMediaFileRepo) Get(id string) (*model.MediaFile, error) {
|
||||||
|
args := m.Called(id)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*model.MediaFile), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAll implements model.MediaFileRepository.
|
||||||
|
func (m *mockMediaFileRepo) GetAll(options ...model.QueryOptions) (model.MediaFiles, error) {
|
||||||
|
argsSlice := make([]interface{}, len(options))
|
||||||
|
for i, v := range options {
|
||||||
|
argsSlice[i] = v
|
||||||
|
}
|
||||||
|
args := m.Called(argsSlice...)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(model.MediaFiles), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetError is a helper to set up a generic error for GetAll.
|
||||||
|
func (m *mockMediaFileRepo) SetError(hasError bool) {
|
||||||
|
if hasError {
|
||||||
|
m.On("GetAll", mock.Anything).Return(nil, errors.New("mock repo error"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByMBID is a helper to set up a GetAll expectation for finding by MBID.
|
||||||
|
func (m *mockMediaFileRepo) FindByMBID(mbid string, mediaFile model.MediaFile) {
|
||||||
|
m.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
||||||
|
return opt.Filters != nil
|
||||||
|
})).Return(model.MediaFiles{mediaFile}, nil).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByArtistAndTitle is a helper to set up a GetAll expectation for finding by artist/title.
|
||||||
|
func (m *mockMediaFileRepo) FindByArtistAndTitle(artistID string, title string, mediaFile model.MediaFile) {
|
||||||
|
m.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
||||||
|
return opt.Filters != nil
|
||||||
|
})).Return(model.MediaFiles{mediaFile}, nil).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockAlbumRepo mocks model.AlbumRepository
|
||||||
|
type mockAlbumRepo struct {
|
||||||
|
mock.Mock
|
||||||
|
model.AlbumRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockAlbumRepo() *mockAlbumRepo {
|
||||||
|
return &mockAlbumRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get implements model.AlbumRepository.
|
||||||
|
func (m *mockAlbumRepo) Get(id string) (*model.Album, error) {
|
||||||
|
args := m.Called(id)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*model.Album), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAll implements model.AlbumRepository.
|
||||||
|
func (m *mockAlbumRepo) GetAll(options ...model.QueryOptions) (model.Albums, error) {
|
||||||
|
argsSlice := make([]interface{}, len(options))
|
||||||
|
for i, v := range options {
|
||||||
|
argsSlice[i] = v
|
||||||
|
}
|
||||||
|
args := m.Called(argsSlice...)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(model.Albums), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockSimilarArtistAgent mocks agents implementing ArtistTopSongsRetriever and ArtistSimilarRetriever
|
||||||
|
type mockSimilarArtistAgent struct {
|
||||||
|
mock.Mock
|
||||||
|
agents.Interface // Embed to satisfy methods not explicitly mocked
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockSimilarArtistAgent) AgentName() string {
|
||||||
|
return "mockSimilar"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockSimilarArtistAgent) GetArtistTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]agents.Song, error) {
|
||||||
|
args := m.Called(ctx, id, artistName, mbid, count)
|
||||||
|
if args.Get(0) != nil {
|
||||||
|
return args.Get(0).([]agents.Song), args.Error(1)
|
||||||
|
}
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockSimilarArtistAgent) GetSimilarArtists(ctx context.Context, id, name, mbid string, limit int) ([]agents.Artist, error) {
|
||||||
|
args := m.Called(ctx, id, name, mbid, limit)
|
||||||
|
if args.Get(0) != nil {
|
||||||
|
return args.Get(0).([]agents.Artist), args.Error(1)
|
||||||
|
}
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockAgents mocks the main Agents interface used by Provider
|
||||||
|
type mockAgents struct {
|
||||||
|
mock.Mock // Embed testify mock
|
||||||
|
topSongsAgent agents.ArtistTopSongsRetriever
|
||||||
|
similarAgent agents.ArtistSimilarRetriever
|
||||||
|
imageAgent agents.ArtistImageRetriever
|
||||||
|
albumInfoAgent agents.AlbumInfoRetriever
|
||||||
|
bioAgent agents.ArtistBiographyRetriever
|
||||||
|
mbidAgent agents.ArtistMBIDRetriever
|
||||||
|
urlAgent agents.ArtistURLRetriever
|
||||||
|
agents.Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAgents) AgentName() string {
|
||||||
|
return "mockCombined"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAgents) GetSimilarArtists(ctx context.Context, id, name, mbid string, limit int) ([]agents.Artist, error) {
|
||||||
|
if m.similarAgent != nil {
|
||||||
|
return m.similarAgent.GetSimilarArtists(ctx, id, name, mbid, limit)
|
||||||
|
}
|
||||||
|
args := m.Called(ctx, id, name, mbid, limit)
|
||||||
|
if args.Get(0) != nil {
|
||||||
|
return args.Get(0).([]agents.Artist), args.Error(1)
|
||||||
|
}
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAgents) GetArtistTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]agents.Song, error) {
|
||||||
|
if m.topSongsAgent != nil {
|
||||||
|
return m.topSongsAgent.GetArtistTopSongs(ctx, id, artistName, mbid, count)
|
||||||
|
}
|
||||||
|
args := m.Called(ctx, id, artistName, mbid, count)
|
||||||
|
if args.Get(0) != nil {
|
||||||
|
return args.Get(0).([]agents.Song), args.Error(1)
|
||||||
|
}
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAgents) GetAlbumInfo(ctx context.Context, name, artist, mbid string) (*agents.AlbumInfo, error) {
|
||||||
|
if m.albumInfoAgent != nil {
|
||||||
|
return m.albumInfoAgent.GetAlbumInfo(ctx, name, artist, mbid)
|
||||||
|
}
|
||||||
|
args := m.Called(ctx, name, artist, mbid)
|
||||||
|
if args.Get(0) != nil {
|
||||||
|
return args.Get(0).(*agents.AlbumInfo), args.Error(1)
|
||||||
|
}
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAgents) GetArtistMBID(ctx context.Context, id string, name string) (string, error) {
|
||||||
|
if m.mbidAgent != nil {
|
||||||
|
return m.mbidAgent.GetArtistMBID(ctx, id, name)
|
||||||
|
}
|
||||||
|
args := m.Called(ctx, id, name)
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAgents) GetArtistURL(ctx context.Context, id, name, mbid string) (string, error) {
|
||||||
|
if m.urlAgent != nil {
|
||||||
|
return m.urlAgent.GetArtistURL(ctx, id, name, mbid)
|
||||||
|
}
|
||||||
|
args := m.Called(ctx, id, name, mbid)
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAgents) GetArtistBiography(ctx context.Context, id, name, mbid string) (string, error) {
|
||||||
|
if m.bioAgent != nil {
|
||||||
|
return m.bioAgent.GetArtistBiography(ctx, id, name, mbid)
|
||||||
|
}
|
||||||
|
args := m.Called(ctx, id, name, mbid)
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAgents) GetArtistImages(ctx context.Context, id, name, mbid string) ([]agents.ExternalImage, error) {
|
||||||
|
if m.imageAgent != nil {
|
||||||
|
return m.imageAgent.GetArtistImages(ctx, id, name, mbid)
|
||||||
|
}
|
||||||
|
args := m.Called(ctx, id, name, mbid)
|
||||||
|
if args.Get(0) != nil {
|
||||||
|
return args.Get(0).([]agents.ExternalImage), args.Error(1)
|
||||||
|
}
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
17
core/external/extdata_suite_test.go
vendored
Normal file
17
core/external/extdata_suite_test.go
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package external
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/log"
|
||||||
|
"github.com/navidrome/navidrome/tests"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExternal(t *testing.T) {
|
||||||
|
tests.Init(t, false)
|
||||||
|
log.SetLevel(log.LevelFatal)
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "External Suite")
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package core
|
package external
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -31,7 +31,7 @@ const (
|
|||||||
refreshQueueLength = 2000
|
refreshQueueLength = 2000
|
||||||
)
|
)
|
||||||
|
|
||||||
type ExternalMetadata interface {
|
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)
|
||||||
@ -40,9 +40,9 @@ type ExternalMetadata interface {
|
|||||||
AlbumImage(ctx context.Context, id string) (*url.URL, error)
|
AlbumImage(ctx context.Context, id string) (*url.URL, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type externalMetadata struct {
|
type provider struct {
|
||||||
ds model.DataStore
|
ds model.DataStore
|
||||||
ag *agents.Agents
|
ag Agents
|
||||||
artistQueue refreshQueue[auxArtist]
|
artistQueue refreshQueue[auxArtist]
|
||||||
albumQueue refreshQueue[auxAlbum]
|
albumQueue refreshQueue[auxAlbum]
|
||||||
}
|
}
|
||||||
@ -57,14 +57,24 @@ type auxArtist struct {
|
|||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExternalMetadata(ds model.DataStore, agents *agents.Agents) ExternalMetadata {
|
type Agents interface {
|
||||||
e := &externalMetadata{ds: ds, ag: agents}
|
agents.AlbumInfoRetriever
|
||||||
|
agents.ArtistBiographyRetriever
|
||||||
|
agents.ArtistMBIDRetriever
|
||||||
|
agents.ArtistImageRetriever
|
||||||
|
agents.ArtistSimilarRetriever
|
||||||
|
agents.ArtistTopSongsRetriever
|
||||||
|
agents.ArtistURLRetriever
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProvider(ds model.DataStore, agents Agents) Provider {
|
||||||
|
e := &provider{ds: ds, ag: agents}
|
||||||
e.artistQueue = newRefreshQueue(context.TODO(), e.populateArtistInfo)
|
e.artistQueue = newRefreshQueue(context.TODO(), e.populateArtistInfo)
|
||||||
e.albumQueue = newRefreshQueue(context.TODO(), e.populateAlbumInfo)
|
e.albumQueue = newRefreshQueue(context.TODO(), e.populateAlbumInfo)
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) getAlbum(ctx context.Context, id string) (auxAlbum, error) {
|
func (e *provider) getAlbum(ctx context.Context, id string) (auxAlbum, error) {
|
||||||
var entity interface{}
|
var entity interface{}
|
||||||
entity, err := model.GetEntityByID(ctx, e.ds, id)
|
entity, err := model.GetEntityByID(ctx, e.ds, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -81,10 +91,11 @@ func (e *externalMetadata) getAlbum(ctx context.Context, id string) (auxAlbum, e
|
|||||||
default:
|
default:
|
||||||
return auxAlbum{}, model.ErrNotFound
|
return auxAlbum{}, model.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return album, nil
|
return album, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) UpdateAlbumInfo(ctx context.Context, id string) (*model.Album, error) {
|
func (e *provider) UpdateAlbumInfo(ctx context.Context, id string) (*model.Album, error) {
|
||||||
album, err := e.getAlbum(ctx, id)
|
album, err := e.getAlbum(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Info(ctx, "Not found", "id", id)
|
log.Info(ctx, "Not found", "id", id)
|
||||||
@ -109,7 +120,7 @@ func (e *externalMetadata) UpdateAlbumInfo(ctx context.Context, id string) (*mod
|
|||||||
return &album.Album, nil
|
return &album.Album, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) populateAlbumInfo(ctx context.Context, album auxAlbum) (auxAlbum, error) {
|
func (e *provider) populateAlbumInfo(ctx context.Context, album auxAlbum) (auxAlbum, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
info, err := e.ag.GetAlbumInfo(ctx, album.Name, album.AlbumArtist, album.MbzAlbumID)
|
info, err := e.ag.GetAlbumInfo(ctx, album.Name, album.AlbumArtist, album.MbzAlbumID)
|
||||||
if errors.Is(err, agents.ErrNotFound) {
|
if errors.Is(err, agents.ErrNotFound) {
|
||||||
@ -155,7 +166,7 @@ func (e *externalMetadata) populateAlbumInfo(ctx context.Context, album auxAlbum
|
|||||||
return album, nil
|
return album, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) getArtist(ctx context.Context, id string) (auxArtist, error) {
|
func (e *provider) getArtist(ctx context.Context, id string) (auxArtist, error) {
|
||||||
var entity interface{}
|
var entity interface{}
|
||||||
entity, err := model.GetEntityByID(ctx, e.ds, id)
|
entity, err := model.GetEntityByID(ctx, e.ds, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -177,7 +188,7 @@ func (e *externalMetadata) getArtist(ctx context.Context, id string) (auxArtist,
|
|||||||
return artist, nil
|
return artist, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) UpdateArtistInfo(ctx context.Context, id string, similarCount int, includeNotPresent bool) (*model.Artist, error) {
|
func (e *provider) UpdateArtistInfo(ctx context.Context, id string, similarCount int, includeNotPresent bool) (*model.Artist, error) {
|
||||||
artist, err := e.refreshArtistInfo(ctx, id)
|
artist, err := e.refreshArtistInfo(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -187,7 +198,7 @@ func (e *externalMetadata) UpdateArtistInfo(ctx context.Context, id string, simi
|
|||||||
return &artist.Artist, err
|
return &artist.Artist, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) refreshArtistInfo(ctx context.Context, id string) (auxArtist, error) {
|
func (e *provider) refreshArtistInfo(ctx context.Context, id string) (auxArtist, error) {
|
||||||
artist, err := e.getArtist(ctx, id)
|
artist, err := e.getArtist(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return auxArtist{}, err
|
return auxArtist{}, err
|
||||||
@ -211,7 +222,7 @@ func (e *externalMetadata) refreshArtistInfo(ctx context.Context, id string) (au
|
|||||||
return artist, nil
|
return artist, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) populateArtistInfo(ctx context.Context, artist auxArtist) (auxArtist, error) {
|
func (e *provider) populateArtistInfo(ctx context.Context, artist auxArtist) (auxArtist, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
// Get MBID first, if it is not yet available
|
// Get MBID first, if it is not yet available
|
||||||
if artist.MbzArtistID == "" {
|
if artist.MbzArtistID == "" {
|
||||||
@ -246,7 +257,7 @@ func (e *externalMetadata) populateArtistInfo(ctx context.Context, artist auxArt
|
|||||||
return artist, nil
|
return artist, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) SimilarSongs(ctx context.Context, id string, count int) (model.MediaFiles, error) {
|
func (e *provider) SimilarSongs(ctx context.Context, id string, count int) (model.MediaFiles, error) {
|
||||||
artist, err := e.getArtist(ctx, id)
|
artist, err := e.getArtist(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -304,7 +315,7 @@ func (e *externalMetadata) SimilarSongs(ctx context.Context, id string, count in
|
|||||||
return similarSongs, nil
|
return similarSongs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) ArtistImage(ctx context.Context, id string) (*url.URL, error) {
|
func (e *provider) ArtistImage(ctx context.Context, id string) (*url.URL, error) {
|
||||||
artist, err := e.getArtist(ctx, id)
|
artist, err := e.getArtist(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -318,24 +329,35 @@ func (e *externalMetadata) ArtistImage(ctx context.Context, id string) (*url.URL
|
|||||||
|
|
||||||
imageUrl := artist.ArtistImageUrl()
|
imageUrl := artist.ArtistImageUrl()
|
||||||
if imageUrl == "" {
|
if imageUrl == "" {
|
||||||
return nil, agents.ErrNotFound
|
return nil, model.ErrNotFound
|
||||||
}
|
}
|
||||||
return url.Parse(imageUrl)
|
return url.Parse(imageUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) AlbumImage(ctx context.Context, id string) (*url.URL, error) {
|
func (e *provider) AlbumImage(ctx context.Context, id string) (*url.URL, error) {
|
||||||
album, err := e.getAlbum(ctx, id)
|
album, err := e.getAlbum(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := e.ag.GetAlbumInfo(ctx, album.Name, album.AlbumArtist, album.MbzAlbumID)
|
info, err := e.ag.GetAlbumInfo(ctx, album.Name, album.AlbumArtist, album.MbzAlbumID)
|
||||||
if errors.Is(err, agents.ErrNotFound) {
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, agents.ErrNotFound):
|
||||||
|
log.Trace(ctx, "Album not found in agent", "albumID", id, "name", album.Name, "artist", album.AlbumArtist)
|
||||||
|
return nil, model.ErrNotFound
|
||||||
|
case errors.Is(err, context.Canceled):
|
||||||
|
log.Debug(ctx, "GetAlbumInfo call canceled", err)
|
||||||
|
default:
|
||||||
|
log.Warn(ctx, "Error getting album info from agent", "albumID", id, "name", album.Name, "artist", album.AlbumArtist, err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if utils.IsCtxDone(ctx) {
|
|
||||||
log.Warn(ctx, "AlbumImage call canceled", ctx.Err())
|
if info == nil {
|
||||||
return nil, ctx.Err()
|
log.Warn(ctx, "Agent returned nil info without error", "albumID", id, "name", album.Name, "artist", album.AlbumArtist)
|
||||||
|
return nil, model.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the biggest image
|
// Return the biggest image
|
||||||
@ -346,26 +368,37 @@ func (e *externalMetadata) AlbumImage(ctx context.Context, id string) (*url.URL,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if img.URL == "" {
|
if img.URL == "" {
|
||||||
return nil, agents.ErrNotFound
|
return nil, model.ErrNotFound
|
||||||
}
|
}
|
||||||
return url.Parse(img.URL)
|
return url.Parse(img.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) TopSongs(ctx context.Context, artistName string, count int) (model.MediaFiles, error) {
|
func (e *provider) TopSongs(ctx context.Context, artistName string, count int) (model.MediaFiles, error) {
|
||||||
artist, err := e.findArtistByName(ctx, artistName)
|
artist, err := e.findArtistByName(ctx, artistName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(ctx, "Artist not found", "name", artistName, err)
|
log.Error(ctx, "Artist not found", "name", artistName, err)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return e.getMatchingTopSongs(ctx, e.ag, artist, count)
|
songs, err := e.getMatchingTopSongs(ctx, e.ag, artist, count)
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, agents.ErrNotFound):
|
||||||
|
log.Trace(ctx, "TopSongs not found", "name", artistName)
|
||||||
|
return nil, model.ErrNotFound
|
||||||
|
case errors.Is(err, context.Canceled):
|
||||||
|
log.Debug(ctx, "TopSongs call canceled", err)
|
||||||
|
default:
|
||||||
|
log.Warn(ctx, "Error getting top songs from agent", "artist", artistName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) getMatchingTopSongs(ctx context.Context, agent agents.ArtistTopSongsRetriever, artist *auxArtist, count int) (model.MediaFiles, error) {
|
return nil, err
|
||||||
songs, err := agent.GetArtistTopSongs(ctx, artist.ID, artist.Name, artist.MbzArtistID, count)
|
|
||||||
if errors.Is(err, agents.ErrNotFound) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
return songs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *provider) getMatchingTopSongs(ctx context.Context, agent agents.ArtistTopSongsRetriever, artist *auxArtist, count int) (model.MediaFiles, error) {
|
||||||
|
songs, err := agent.GetArtistTopSongs(ctx, artist.ID, artist.Name, artist.MbzArtistID, count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -386,10 +419,11 @@ func (e *externalMetadata) getMatchingTopSongs(ctx context.Context, agent agents
|
|||||||
} else {
|
} else {
|
||||||
log.Debug(ctx, "Found matching top songs", "name", artist.Name, "numSongs", len(mfs))
|
log.Debug(ctx, "Found matching top songs", "name", artist.Name, "numSongs", len(mfs))
|
||||||
}
|
}
|
||||||
|
|
||||||
return mfs, nil
|
return mfs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) findMatchingTrack(ctx context.Context, mbid string, artistID, title string) (*model.MediaFile, error) {
|
func (e *provider) findMatchingTrack(ctx context.Context, mbid string, artistID, title string) (*model.MediaFile, error) {
|
||||||
if mbid != "" {
|
if mbid != "" {
|
||||||
mfs, err := e.ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
mfs, err := e.ds.MediaFile(ctx).GetAll(model.QueryOptions{
|
||||||
Filters: squirrel.And{
|
Filters: squirrel.And{
|
||||||
@ -420,7 +454,7 @@ func (e *externalMetadata) findMatchingTrack(ctx context.Context, mbid string, a
|
|||||||
return &mfs[0], nil
|
return &mfs[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) callGetURL(ctx context.Context, agent agents.ArtistURLRetriever, artist *auxArtist) {
|
func (e *provider) callGetURL(ctx context.Context, agent agents.ArtistURLRetriever, artist *auxArtist) {
|
||||||
artisURL, err := agent.GetArtistURL(ctx, artist.ID, artist.Name, artist.MbzArtistID)
|
artisURL, err := agent.GetArtistURL(ctx, artist.ID, artist.Name, artist.MbzArtistID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@ -428,7 +462,7 @@ func (e *externalMetadata) callGetURL(ctx context.Context, agent agents.ArtistUR
|
|||||||
artist.ExternalUrl = artisURL
|
artist.ExternalUrl = artisURL
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) callGetBiography(ctx context.Context, agent agents.ArtistBiographyRetriever, artist *auxArtist) {
|
func (e *provider) callGetBiography(ctx context.Context, agent agents.ArtistBiographyRetriever, artist *auxArtist) {
|
||||||
bio, err := agent.GetArtistBiography(ctx, artist.ID, str.Clear(artist.Name), artist.MbzArtistID)
|
bio, err := agent.GetArtistBiography(ctx, artist.ID, str.Clear(artist.Name), artist.MbzArtistID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@ -438,7 +472,7 @@ func (e *externalMetadata) callGetBiography(ctx context.Context, agent agents.Ar
|
|||||||
artist.Biography = strings.ReplaceAll(bio, "<a ", "<a target='_blank' ")
|
artist.Biography = strings.ReplaceAll(bio, "<a ", "<a target='_blank' ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) callGetImage(ctx context.Context, agent agents.ArtistImageRetriever, artist *auxArtist) {
|
func (e *provider) callGetImage(ctx context.Context, agent agents.ArtistImageRetriever, artist *auxArtist) {
|
||||||
images, err := agent.GetArtistImages(ctx, artist.ID, artist.Name, artist.MbzArtistID)
|
images, err := agent.GetArtistImages(ctx, artist.ID, artist.Name, artist.MbzArtistID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@ -456,7 +490,7 @@ func (e *externalMetadata) callGetImage(ctx context.Context, agent agents.Artist
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) callGetSimilar(ctx context.Context, agent agents.ArtistSimilarRetriever, artist *auxArtist,
|
func (e *provider) callGetSimilar(ctx context.Context, agent agents.ArtistSimilarRetriever, artist *auxArtist,
|
||||||
limit int, includeNotPresent bool) {
|
limit int, includeNotPresent bool) {
|
||||||
similar, err := agent.GetSimilarArtists(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 {
|
if len(similar) == 0 || err != nil {
|
||||||
@ -471,7 +505,7 @@ func (e *externalMetadata) callGetSimilar(ctx context.Context, agent agents.Arti
|
|||||||
artist.SimilarArtists = sa
|
artist.SimilarArtists = sa
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) mapSimilarArtists(ctx context.Context, similar []agents.Artist, includeNotPresent bool) (model.Artists, error) {
|
func (e *provider) mapSimilarArtists(ctx context.Context, similar []agents.Artist, includeNotPresent bool) (model.Artists, error) {
|
||||||
var result model.Artists
|
var result model.Artists
|
||||||
var notPresent []string
|
var notPresent []string
|
||||||
|
|
||||||
@ -515,7 +549,7 @@ func (e *externalMetadata) mapSimilarArtists(ctx context.Context, similar []agen
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) findArtistByName(ctx context.Context, artistName string) (*auxArtist, error) {
|
func (e *provider) findArtistByName(ctx context.Context, artistName string) (*auxArtist, error) {
|
||||||
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,
|
||||||
@ -533,7 +567,7 @@ func (e *externalMetadata) findArtistByName(ctx context.Context, artistName stri
|
|||||||
return artist, nil
|
return artist, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *externalMetadata) loadSimilar(ctx context.Context, artist *auxArtist, count int, includeNotPresent bool) error {
|
func (e *provider) loadSimilar(ctx context.Context, artist *auxArtist, count int, includeNotPresent bool) error {
|
||||||
var ids []string
|
var ids []string
|
||||||
for _, sa := range artist.SimilarArtists {
|
for _, sa := range artist.SimilarArtists {
|
||||||
if sa.ID == "" {
|
if sa.ID == "" {
|
303
core/external/provider_albumimage_test.go
vendored
Normal file
303
core/external/provider_albumimage_test.go
vendored
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
package external_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/conf"
|
||||||
|
"github.com/navidrome/navidrome/conf/configtest"
|
||||||
|
"github.com/navidrome/navidrome/core/agents"
|
||||||
|
. "github.com/navidrome/navidrome/core/external"
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/tests"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Provider - AlbumImage", func() {
|
||||||
|
var ds *tests.MockDataStore
|
||||||
|
var provider Provider
|
||||||
|
var mockArtistRepo *mockArtistRepo
|
||||||
|
var mockAlbumRepo *mockAlbumRepo
|
||||||
|
var mockMediaFileRepo *mockMediaFileRepo
|
||||||
|
var mockAlbumAgent *mockAlbumInfoAgent
|
||||||
|
var agentsCombined *mockAgents
|
||||||
|
var ctx context.Context
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx = GinkgoT().Context()
|
||||||
|
DeferCleanup(configtest.SetupConfig())
|
||||||
|
conf.Server.Agents = "mockAlbum" // Configure mock agent
|
||||||
|
|
||||||
|
mockArtistRepo = newMockArtistRepo()
|
||||||
|
mockAlbumRepo = newMockAlbumRepo()
|
||||||
|
mockMediaFileRepo = newMockMediaFileRepo()
|
||||||
|
|
||||||
|
ds = &tests.MockDataStore{
|
||||||
|
MockedArtist: mockArtistRepo,
|
||||||
|
MockedAlbum: mockAlbumRepo,
|
||||||
|
MockedMediaFile: mockMediaFileRepo,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockAlbumAgent = newMockAlbumInfoAgent()
|
||||||
|
|
||||||
|
agentsCombined = &mockAgents{
|
||||||
|
albumInfoAgent: mockAlbumAgent,
|
||||||
|
}
|
||||||
|
|
||||||
|
provider = NewProvider(ds, agentsCombined)
|
||||||
|
|
||||||
|
// Default mocks
|
||||||
|
// Mocks for GetEntityByID sequence (initial failed lookups)
|
||||||
|
mockArtistRepo.On("Get", "album-1").Return(nil, model.ErrNotFound).Once()
|
||||||
|
mockArtistRepo.On("Get", "mf-1").Return(nil, model.ErrNotFound).Once()
|
||||||
|
mockAlbumRepo.On("Get", "mf-1").Return(nil, model.ErrNotFound).Once()
|
||||||
|
|
||||||
|
// Default mock for non-existent entities - Use Maybe() for flexibility
|
||||||
|
mockArtistRepo.On("Get", "not-found").Return(nil, model.ErrNotFound).Maybe()
|
||||||
|
mockAlbumRepo.On("Get", "not-found").Return(nil, model.ErrNotFound).Maybe()
|
||||||
|
mockMediaFileRepo.On("Get", "not-found").Return(nil, model.ErrNotFound).Maybe()
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns the largest image URL when successful", func() {
|
||||||
|
// Arrange
|
||||||
|
mockArtistRepo.On("Get", "album-1").Return(nil, model.ErrNotFound).Once() // Expect GetEntityByID sequence
|
||||||
|
mockAlbumRepo.On("Get", "album-1").Return(&model.Album{ID: "album-1", Name: "Album One", AlbumArtistID: "artist-1"}, nil).Once()
|
||||||
|
// Explicitly mock agent call for this test
|
||||||
|
mockAlbumAgent.On("GetAlbumInfo", ctx, "Album One", "", "").
|
||||||
|
Return(&agents.AlbumInfo{
|
||||||
|
Images: []agents.ExternalImage{
|
||||||
|
{URL: "http://example.com/large.jpg", Size: 1000},
|
||||||
|
{URL: "http://example.com/medium.jpg", Size: 500},
|
||||||
|
{URL: "http://example.com/small.jpg", Size: 200},
|
||||||
|
},
|
||||||
|
}, nil).Once()
|
||||||
|
|
||||||
|
expectedURL, _ := url.Parse("http://example.com/large.jpg")
|
||||||
|
imgURL, err := provider.AlbumImage(ctx, "album-1")
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(imgURL).To(Equal(expectedURL))
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "album-1") // From GetEntityByID
|
||||||
|
mockAlbumRepo.AssertCalled(GinkgoT(), "Get", "album-1")
|
||||||
|
mockArtistRepo.AssertNotCalled(GinkgoT(), "Get", "artist-1") // Artist lookup no longer happens in getAlbum
|
||||||
|
mockAlbumAgent.AssertCalled(GinkgoT(), "GetAlbumInfo", ctx, "Album One", "", "") // Expect empty artist name
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns ErrNotFound if the album is not found in the DB", func() {
|
||||||
|
// Arrange: Explicitly expect the full GetEntityByID sequence for "not-found"
|
||||||
|
mockArtistRepo.On("Get", "not-found").Return(nil, model.ErrNotFound).Once()
|
||||||
|
mockAlbumRepo.On("Get", "not-found").Return(nil, model.ErrNotFound).Once()
|
||||||
|
mockMediaFileRepo.On("Get", "not-found").Return(nil, model.ErrNotFound).Once()
|
||||||
|
|
||||||
|
imgURL, err := provider.AlbumImage(ctx, "not-found")
|
||||||
|
|
||||||
|
Expect(err).To(MatchError("data not found"))
|
||||||
|
Expect(imgURL).To(BeNil())
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "not-found")
|
||||||
|
mockAlbumRepo.AssertCalled(GinkgoT(), "Get", "not-found")
|
||||||
|
mockMediaFileRepo.AssertCalled(GinkgoT(), "Get", "not-found")
|
||||||
|
mockAlbumAgent.AssertNotCalled(GinkgoT(), "GetAlbumInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns the agent error if the agent fails", func() {
|
||||||
|
// Arrange
|
||||||
|
mockArtistRepo.On("Get", "album-1").Return(nil, model.ErrNotFound).Once() // Expect GetEntityByID sequence
|
||||||
|
mockAlbumRepo.On("Get", "album-1").Return(&model.Album{ID: "album-1", Name: "Album One", AlbumArtistID: "artist-1"}, nil).Once()
|
||||||
|
|
||||||
|
agentErr := errors.New("agent failure")
|
||||||
|
// Explicitly mock agent call for this test
|
||||||
|
mockAlbumAgent.On("GetAlbumInfo", ctx, "Album One", "", "").Return(nil, agentErr).Once() // Expect empty artist
|
||||||
|
|
||||||
|
imgURL, err := provider.AlbumImage(ctx, "album-1")
|
||||||
|
|
||||||
|
Expect(err).To(MatchError("agent failure"))
|
||||||
|
Expect(imgURL).To(BeNil())
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "album-1")
|
||||||
|
mockAlbumRepo.AssertCalled(GinkgoT(), "Get", "album-1")
|
||||||
|
mockArtistRepo.AssertNotCalled(GinkgoT(), "Get", "artist-1")
|
||||||
|
mockAlbumAgent.AssertCalled(GinkgoT(), "GetAlbumInfo", ctx, "Album One", "", "") // Expect empty artist
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns ErrNotFound if the agent returns ErrNotFound", func() {
|
||||||
|
// Arrange
|
||||||
|
mockArtistRepo.On("Get", "album-1").Return(nil, model.ErrNotFound).Once() // Expect GetEntityByID sequence
|
||||||
|
mockAlbumRepo.On("Get", "album-1").Return(&model.Album{ID: "album-1", Name: "Album One", AlbumArtistID: "artist-1"}, nil).Once()
|
||||||
|
|
||||||
|
// Explicitly mock agent call for this test
|
||||||
|
mockAlbumAgent.On("GetAlbumInfo", ctx, "Album One", "", "").Return(nil, agents.ErrNotFound).Once() // Expect empty artist
|
||||||
|
|
||||||
|
imgURL, err := provider.AlbumImage(ctx, "album-1")
|
||||||
|
|
||||||
|
Expect(err).To(MatchError("data not found"))
|
||||||
|
Expect(imgURL).To(BeNil())
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "album-1")
|
||||||
|
mockAlbumRepo.AssertCalled(GinkgoT(), "Get", "album-1")
|
||||||
|
mockAlbumAgent.AssertCalled(GinkgoT(), "GetAlbumInfo", ctx, "Album One", "", "") // Expect empty artist
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns ErrNotFound if the agent returns no images", func() {
|
||||||
|
// Arrange
|
||||||
|
mockArtistRepo.On("Get", "album-1").Return(nil, model.ErrNotFound).Once() // Expect GetEntityByID sequence
|
||||||
|
mockAlbumRepo.On("Get", "album-1").Return(&model.Album{ID: "album-1", Name: "Album One", AlbumArtistID: "artist-1"}, nil).Once()
|
||||||
|
|
||||||
|
// Explicitly mock agent call for this test
|
||||||
|
mockAlbumAgent.On("GetAlbumInfo", ctx, "Album One", "", "").
|
||||||
|
Return(&agents.AlbumInfo{Images: []agents.ExternalImage{}}, nil).Once() // Expect empty artist
|
||||||
|
|
||||||
|
imgURL, err := provider.AlbumImage(ctx, "album-1")
|
||||||
|
|
||||||
|
Expect(err).To(MatchError("data not found"))
|
||||||
|
Expect(imgURL).To(BeNil())
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "album-1")
|
||||||
|
mockAlbumRepo.AssertCalled(GinkgoT(), "Get", "album-1")
|
||||||
|
mockAlbumAgent.AssertCalled(GinkgoT(), "GetAlbumInfo", ctx, "Album One", "", "") // Expect empty artist
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns context error if context is canceled", func() {
|
||||||
|
// Arrange
|
||||||
|
cctx, cancelCtx := context.WithCancel(ctx)
|
||||||
|
// Mock the necessary DB calls *before* canceling the context
|
||||||
|
mockArtistRepo.On("Get", "album-1").Return(nil, model.ErrNotFound).Once()
|
||||||
|
mockAlbumRepo.On("Get", "album-1").Return(&model.Album{ID: "album-1", Name: "Album One", AlbumArtistID: "artist-1"}, nil).Once()
|
||||||
|
// Expect the agent call even if context is cancelled, returning the context error
|
||||||
|
mockAlbumAgent.On("GetAlbumInfo", cctx, "Album One", "", "").Return(nil, context.Canceled).Once()
|
||||||
|
// Cancel the context *before* calling the function under test
|
||||||
|
cancelCtx()
|
||||||
|
|
||||||
|
imgURL, err := provider.AlbumImage(cctx, "album-1")
|
||||||
|
|
||||||
|
Expect(err).To(MatchError("context canceled"))
|
||||||
|
Expect(imgURL).To(BeNil())
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "album-1")
|
||||||
|
mockAlbumRepo.AssertCalled(GinkgoT(), "Get", "album-1")
|
||||||
|
// Agent should now be called, verify this expectation
|
||||||
|
mockAlbumAgent.AssertCalled(GinkgoT(), "GetAlbumInfo", cctx, "Album One", "", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("derives album ID from MediaFile ID", func() {
|
||||||
|
// Arrange: Mock full GetEntityByID for "mf-1" and recursive "album-1"
|
||||||
|
mockArtistRepo.On("Get", "mf-1").Return(nil, model.ErrNotFound).Once()
|
||||||
|
mockAlbumRepo.On("Get", "mf-1").Return(nil, model.ErrNotFound).Once()
|
||||||
|
mockMediaFileRepo.On("Get", "mf-1").Return(&model.MediaFile{ID: "mf-1", Title: "Track One", ArtistID: "artist-1", AlbumID: "album-1"}, nil).Once()
|
||||||
|
mockArtistRepo.On("Get", "album-1").Return(nil, model.ErrNotFound).Once()
|
||||||
|
mockAlbumRepo.On("Get", "album-1").Return(&model.Album{ID: "album-1", Name: "Album One", AlbumArtistID: "artist-1"}, nil).Once()
|
||||||
|
|
||||||
|
// Explicitly mock agent call for this test
|
||||||
|
mockAlbumAgent.On("GetAlbumInfo", ctx, "Album One", "", "").
|
||||||
|
Return(&agents.AlbumInfo{
|
||||||
|
Images: []agents.ExternalImage{
|
||||||
|
{URL: "http://example.com/large.jpg", Size: 1000},
|
||||||
|
{URL: "http://example.com/medium.jpg", Size: 500},
|
||||||
|
{URL: "http://example.com/small.jpg", Size: 200},
|
||||||
|
},
|
||||||
|
}, nil).Once()
|
||||||
|
|
||||||
|
expectedURL, _ := url.Parse("http://example.com/large.jpg")
|
||||||
|
imgURL, err := provider.AlbumImage(ctx, "mf-1")
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(imgURL).To(Equal(expectedURL))
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "mf-1")
|
||||||
|
mockAlbumRepo.AssertCalled(GinkgoT(), "Get", "mf-1")
|
||||||
|
mockMediaFileRepo.AssertCalled(GinkgoT(), "Get", "mf-1")
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "album-1")
|
||||||
|
mockAlbumRepo.AssertCalled(GinkgoT(), "Get", "album-1")
|
||||||
|
mockArtistRepo.AssertNotCalled(GinkgoT(), "Get", "artist-1")
|
||||||
|
mockAlbumAgent.AssertCalled(GinkgoT(), "GetAlbumInfo", ctx, "Album One", "", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("handles different image orders from agent", func() {
|
||||||
|
// Arrange
|
||||||
|
mockArtistRepo.On("Get", "album-1").Return(nil, model.ErrNotFound).Once() // Expect GetEntityByID sequence
|
||||||
|
mockAlbumRepo.On("Get", "album-1").Return(&model.Album{ID: "album-1", Name: "Album One", AlbumArtistID: "artist-1"}, nil).Once()
|
||||||
|
// Explicitly mock agent call for this test
|
||||||
|
mockAlbumAgent.On("GetAlbumInfo", ctx, "Album One", "", "").
|
||||||
|
Return(&agents.AlbumInfo{
|
||||||
|
Images: []agents.ExternalImage{
|
||||||
|
{URL: "http://example.com/small.jpg", Size: 200},
|
||||||
|
{URL: "http://example.com/large.jpg", Size: 1000},
|
||||||
|
{URL: "http://example.com/medium.jpg", Size: 500},
|
||||||
|
},
|
||||||
|
}, nil).Once()
|
||||||
|
|
||||||
|
expectedURL, _ := url.Parse("http://example.com/large.jpg")
|
||||||
|
imgURL, err := provider.AlbumImage(ctx, "album-1")
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(imgURL).To(Equal(expectedURL)) // Should still pick the largest
|
||||||
|
mockAlbumAgent.AssertCalled(GinkgoT(), "GetAlbumInfo", ctx, "Album One", "", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("handles agent returning only one image", func() {
|
||||||
|
// Arrange
|
||||||
|
mockArtistRepo.On("Get", "album-1").Return(nil, model.ErrNotFound).Once() // Expect GetEntityByID sequence
|
||||||
|
mockAlbumRepo.On("Get", "album-1").Return(&model.Album{ID: "album-1", Name: "Album One", AlbumArtistID: "artist-1"}, nil).Once()
|
||||||
|
// Explicitly mock agent call for this test
|
||||||
|
mockAlbumAgent.On("GetAlbumInfo", ctx, "Album One", "", "").
|
||||||
|
Return(&agents.AlbumInfo{
|
||||||
|
Images: []agents.ExternalImage{
|
||||||
|
{URL: "http://example.com/single.jpg", Size: 700},
|
||||||
|
},
|
||||||
|
}, nil).Once()
|
||||||
|
|
||||||
|
expectedURL, _ := url.Parse("http://example.com/single.jpg")
|
||||||
|
imgURL, err := provider.AlbumImage(ctx, "album-1")
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(imgURL).To(Equal(expectedURL))
|
||||||
|
mockAlbumAgent.AssertCalled(GinkgoT(), "GetAlbumInfo", ctx, "Album One", "", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns ErrNotFound if deriving album ID fails", func() {
|
||||||
|
// Arrange: Mock full GetEntityByID for "mf-no-album" and recursive "not-found"
|
||||||
|
mockArtistRepo.On("Get", "mf-no-album").Return(nil, model.ErrNotFound).Once()
|
||||||
|
mockAlbumRepo.On("Get", "mf-no-album").Return(nil, model.ErrNotFound).Once()
|
||||||
|
mockMediaFileRepo.On("Get", "mf-no-album").Return(&model.MediaFile{ID: "mf-no-album", Title: "Track No Album", ArtistID: "artist-1", AlbumID: "not-found"}, nil).Once()
|
||||||
|
mockArtistRepo.On("Get", "not-found").Return(nil, model.ErrNotFound).Once()
|
||||||
|
mockAlbumRepo.On("Get", "not-found").Return(nil, model.ErrNotFound).Once()
|
||||||
|
mockMediaFileRepo.On("Get", "not-found").Return(nil, model.ErrNotFound).Once()
|
||||||
|
|
||||||
|
imgURL, err := provider.AlbumImage(ctx, "mf-no-album")
|
||||||
|
|
||||||
|
Expect(err).To(MatchError("data not found"))
|
||||||
|
Expect(imgURL).To(BeNil())
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "mf-no-album")
|
||||||
|
mockAlbumRepo.AssertCalled(GinkgoT(), "Get", "mf-no-album")
|
||||||
|
mockMediaFileRepo.AssertCalled(GinkgoT(), "Get", "mf-no-album")
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "not-found")
|
||||||
|
mockAlbumRepo.AssertCalled(GinkgoT(), "Get", "not-found")
|
||||||
|
mockMediaFileRepo.AssertCalled(GinkgoT(), "Get", "not-found")
|
||||||
|
mockAlbumAgent.AssertNotCalled(GinkgoT(), "GetAlbumInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// mockAlbumInfoAgent implementation
|
||||||
|
type mockAlbumInfoAgent struct {
|
||||||
|
mock.Mock
|
||||||
|
agents.AlbumInfoRetriever // Embed interface
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockAlbumInfoAgent() *mockAlbumInfoAgent {
|
||||||
|
m := new(mockAlbumInfoAgent)
|
||||||
|
m.On("AgentName").Return("mockAlbum").Maybe()
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAlbumInfoAgent) AgentName() string {
|
||||||
|
args := m.Called()
|
||||||
|
return args.String(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAlbumInfoAgent) GetAlbumInfo(ctx context.Context, name, artist, mbid string) (*agents.AlbumInfo, error) {
|
||||||
|
args := m.Called(ctx, name, artist, mbid)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(*agents.AlbumInfo), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure mockAgent implements the interface
|
||||||
|
var _ agents.AlbumInfoRetriever = (*mockAlbumInfoAgent)(nil)
|
301
core/external/provider_artistimage_test.go
vendored
Normal file
301
core/external/provider_artistimage_test.go
vendored
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
package external_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/conf"
|
||||||
|
"github.com/navidrome/navidrome/conf/configtest"
|
||||||
|
"github.com/navidrome/navidrome/core/agents"
|
||||||
|
. "github.com/navidrome/navidrome/core/external"
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/tests"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Provider - ArtistImage", func() {
|
||||||
|
var ds *tests.MockDataStore
|
||||||
|
var provider Provider
|
||||||
|
var mockArtistRepo *mockArtistRepo
|
||||||
|
var mockAlbumRepo *mockAlbumRepo
|
||||||
|
var mockMediaFileRepo *mockMediaFileRepo
|
||||||
|
var mockImageAgent *mockArtistImageAgent
|
||||||
|
var agentsCombined *mockAgents
|
||||||
|
var ctx context.Context
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
DeferCleanup(configtest.SetupConfig())
|
||||||
|
conf.Server.Agents = "mockImage" // Configure only the mock agent
|
||||||
|
ctx = GinkgoT().Context()
|
||||||
|
|
||||||
|
mockArtistRepo = newMockArtistRepo()
|
||||||
|
mockAlbumRepo = newMockAlbumRepo()
|
||||||
|
mockMediaFileRepo = newMockMediaFileRepo()
|
||||||
|
|
||||||
|
ds = &tests.MockDataStore{
|
||||||
|
MockedArtist: mockArtistRepo,
|
||||||
|
MockedAlbum: mockAlbumRepo,
|
||||||
|
MockedMediaFile: mockMediaFileRepo,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockImageAgent = newMockArtistImageAgent()
|
||||||
|
|
||||||
|
// Use the mockAgents from helper, setting the specific agent
|
||||||
|
agentsCombined = &mockAgents{
|
||||||
|
imageAgent: mockImageAgent,
|
||||||
|
}
|
||||||
|
|
||||||
|
provider = NewProvider(ds, agentsCombined)
|
||||||
|
|
||||||
|
// Default mocks for successful Get calls
|
||||||
|
mockArtistRepo.On("Get", "artist-1").Return(&model.Artist{ID: "artist-1", Name: "Artist One"}, nil).Maybe()
|
||||||
|
mockAlbumRepo.On("Get", "album-1").Return(&model.Album{ID: "album-1", Name: "Album One", AlbumArtistID: "artist-1"}, nil).Maybe()
|
||||||
|
mockMediaFileRepo.On("Get", "mf-1").Return(&model.MediaFile{ID: "mf-1", Title: "Track One", ArtistID: "artist-1"}, nil).Maybe()
|
||||||
|
// Default mock for non-existent entities
|
||||||
|
mockArtistRepo.On("Get", "not-found").Return(nil, model.ErrNotFound).Maybe()
|
||||||
|
mockAlbumRepo.On("Get", "not-found").Return(nil, model.ErrNotFound).Maybe()
|
||||||
|
mockMediaFileRepo.On("Get", "not-found").Return(nil, model.ErrNotFound).Maybe()
|
||||||
|
|
||||||
|
// Default successful image agent response
|
||||||
|
mockImageAgent.On("GetArtistImages", mock.Anything, "artist-1", "Artist One", "").
|
||||||
|
Return([]agents.ExternalImage{
|
||||||
|
{URL: "http://example.com/large.jpg", Size: 1000},
|
||||||
|
{URL: "http://example.com/medium.jpg", Size: 500},
|
||||||
|
{URL: "http://example.com/small.jpg", Size: 200},
|
||||||
|
}, nil).Maybe()
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
mockArtistRepo.AssertExpectations(GinkgoT())
|
||||||
|
mockAlbumRepo.AssertExpectations(GinkgoT())
|
||||||
|
mockMediaFileRepo.AssertExpectations(GinkgoT())
|
||||||
|
mockImageAgent.AssertExpectations(GinkgoT())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns the largest image URL when successful", func() {
|
||||||
|
// Arrange
|
||||||
|
expectedURL, _ := url.Parse("http://example.com/large.jpg")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
imgURL, err := provider.ArtistImage(ctx, "artist-1")
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(imgURL).To(Equal(expectedURL))
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1")
|
||||||
|
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns ErrNotFound if the artist is not found in the DB", func() {
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
// Act
|
||||||
|
imgURL, err := provider.ArtistImage(ctx, "not-found")
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Expect(err).To(MatchError(model.ErrNotFound))
|
||||||
|
Expect(imgURL).To(BeNil())
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "not-found")
|
||||||
|
mockImageAgent.AssertNotCalled(GinkgoT(), "GetArtistImages", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns the agent error if the agent fails", func() {
|
||||||
|
// Arrange
|
||||||
|
agentErr := errors.New("agent failure")
|
||||||
|
mockImageAgent.Mock = mock.Mock{} // Reset default expectation
|
||||||
|
mockImageAgent.On("GetArtistImages", ctx, "artist-1", "Artist One", "").Return(nil, agentErr).Once()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
imgURL, err := provider.ArtistImage(ctx, "artist-1")
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Expect(err).To(MatchError(model.ErrNotFound)) // Corrected Expectation: The provider maps agent errors (other than canceled) to ErrNotFound if no image was found/populated
|
||||||
|
Expect(imgURL).To(BeNil())
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1")
|
||||||
|
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns ErrNotFound if the agent returns ErrNotFound", func() {
|
||||||
|
// Arrange
|
||||||
|
mockImageAgent.Mock = mock.Mock{} // Reset default expectation
|
||||||
|
mockImageAgent.On("GetArtistImages", ctx, "artist-1", "Artist One", "").Return(nil, agents.ErrNotFound).Once()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
imgURL, err := provider.ArtistImage(ctx, "artist-1")
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Expect(err).To(MatchError(model.ErrNotFound))
|
||||||
|
Expect(imgURL).To(BeNil())
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1")
|
||||||
|
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns ErrNotFound if the agent returns no images", func() {
|
||||||
|
// Arrange
|
||||||
|
mockImageAgent.Mock = mock.Mock{} // Reset default expectation
|
||||||
|
mockImageAgent.On("GetArtistImages", ctx, "artist-1", "Artist One", "").Return([]agents.ExternalImage{}, nil).Once()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
imgURL, err := provider.ArtistImage(ctx, "artist-1")
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Expect(err).To(MatchError(model.ErrNotFound)) // Implementation maps empty result to ErrNotFound
|
||||||
|
Expect(imgURL).To(BeNil())
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1")
|
||||||
|
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns context error if context is canceled before agent call", func() {
|
||||||
|
// Arrange
|
||||||
|
cctx, cancelCtx := context.WithCancel(context.Background())
|
||||||
|
mockArtistRepo.Mock = mock.Mock{} // Reset default expectation for artist repo as well
|
||||||
|
mockArtistRepo.On("Get", "artist-1").Return(&model.Artist{ID: "artist-1", Name: "Artist One"}, nil).Run(func(args mock.Arguments) {
|
||||||
|
cancelCtx() // Cancel context *during* the DB call simulation
|
||||||
|
}).Once()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
imgURL, err := provider.ArtistImage(cctx, "artist-1")
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Expect(err).To(MatchError(context.Canceled))
|
||||||
|
Expect(imgURL).To(BeNil())
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("derives artist ID from MediaFile ID", func() {
|
||||||
|
// Arrange: Add mocks for the initial GetEntityByID lookups
|
||||||
|
mockArtistRepo.On("Get", "mf-1").Return(nil, model.ErrNotFound).Once()
|
||||||
|
mockAlbumRepo.On("Get", "mf-1").Return(nil, model.ErrNotFound).Once()
|
||||||
|
// Default mocks for MediaFileRepo.Get("mf-1") and ArtistRepo.Get("artist-1") handle the rest
|
||||||
|
expectedURL, _ := url.Parse("http://example.com/large.jpg")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
imgURL, err := provider.ArtistImage(ctx, "mf-1")
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(imgURL).To(Equal(expectedURL))
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "mf-1") // GetEntityByID sequence
|
||||||
|
mockAlbumRepo.AssertCalled(GinkgoT(), "Get", "mf-1") // GetEntityByID sequence
|
||||||
|
mockMediaFileRepo.AssertCalled(GinkgoT(), "Get", "mf-1")
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1") // Should be called after getting MF
|
||||||
|
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("derives artist ID from Album ID", func() {
|
||||||
|
// Arrange: Add mock for the initial GetEntityByID lookup
|
||||||
|
mockArtistRepo.On("Get", "album-1").Return(nil, model.ErrNotFound).Once()
|
||||||
|
// Default mocks for AlbumRepo.Get("album-1") and ArtistRepo.Get("artist-1") handle the rest
|
||||||
|
expectedURL, _ := url.Parse("http://example.com/large.jpg")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
imgURL, err := provider.ArtistImage(ctx, "album-1")
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(imgURL).To(Equal(expectedURL))
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "album-1") // GetEntityByID sequence
|
||||||
|
mockAlbumRepo.AssertCalled(GinkgoT(), "Get", "album-1")
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1") // Should be called after getting Album
|
||||||
|
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns ErrNotFound if derived artist is not found", func() {
|
||||||
|
// Arrange
|
||||||
|
// Add mocks for the initial GetEntityByID lookups
|
||||||
|
mockArtistRepo.On("Get", "mf-bad-artist").Return(nil, model.ErrNotFound).Once()
|
||||||
|
mockAlbumRepo.On("Get", "mf-bad-artist").Return(nil, model.ErrNotFound).Once()
|
||||||
|
mockMediaFileRepo.On("Get", "mf-bad-artist").Return(&model.MediaFile{ID: "mf-bad-artist", ArtistID: "not-found"}, nil).Once()
|
||||||
|
// Add expectation for the recursive GetEntityByID call for the MediaFileRepo
|
||||||
|
mockMediaFileRepo.On("Get", "not-found").Return(nil, model.ErrNotFound).Maybe()
|
||||||
|
// The default mocks for ArtistRepo/AlbumRepo handle the final "not-found" lookups
|
||||||
|
|
||||||
|
// Act
|
||||||
|
imgURL, err := provider.ArtistImage(ctx, "mf-bad-artist")
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Expect(err).To(MatchError(model.ErrNotFound))
|
||||||
|
Expect(imgURL).To(BeNil())
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "mf-bad-artist") // GetEntityByID sequence
|
||||||
|
mockAlbumRepo.AssertCalled(GinkgoT(), "Get", "mf-bad-artist") // GetEntityByID sequence
|
||||||
|
mockMediaFileRepo.AssertCalled(GinkgoT(), "Get", "mf-bad-artist")
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "not-found")
|
||||||
|
mockImageAgent.AssertNotCalled(GinkgoT(), "GetArtistImages", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("handles different image orders from agent", func() {
|
||||||
|
// Arrange
|
||||||
|
mockImageAgent.Mock = mock.Mock{} // Reset default expectation
|
||||||
|
mockImageAgent.On("GetArtistImages", ctx, "artist-1", "Artist One", "").
|
||||||
|
Return([]agents.ExternalImage{
|
||||||
|
{URL: "http://example.com/small.jpg", Size: 200},
|
||||||
|
{URL: "http://example.com/large.jpg", Size: 1000},
|
||||||
|
{URL: "http://example.com/medium.jpg", Size: 500},
|
||||||
|
}, nil).Once()
|
||||||
|
expectedURL, _ := url.Parse("http://example.com/large.jpg")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
imgURL, err := provider.ArtistImage(ctx, "artist-1")
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(imgURL).To(Equal(expectedURL)) // Still picks the largest
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1")
|
||||||
|
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("handles agent returning only one image", func() {
|
||||||
|
// Arrange
|
||||||
|
mockImageAgent.Mock = mock.Mock{} // Reset default expectation
|
||||||
|
mockImageAgent.On("GetArtistImages", ctx, "artist-1", "Artist One", "").
|
||||||
|
Return([]agents.ExternalImage{
|
||||||
|
{URL: "http://example.com/medium.jpg", Size: 500},
|
||||||
|
}, nil).Once()
|
||||||
|
expectedURL, _ := url.Parse("http://example.com/medium.jpg")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
imgURL, err := provider.ArtistImage(ctx, "artist-1")
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(imgURL).To(Equal(expectedURL))
|
||||||
|
mockArtistRepo.AssertCalled(GinkgoT(), "Get", "artist-1")
|
||||||
|
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// mockArtistImageAgent implementation using testify/mock
|
||||||
|
// This remains local as it's specific to testing the ArtistImage functionality
|
||||||
|
type mockArtistImageAgent struct {
|
||||||
|
mock.Mock
|
||||||
|
agents.ArtistImageRetriever // Embed interface
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor for the mock agent
|
||||||
|
func newMockArtistImageAgent() *mockArtistImageAgent {
|
||||||
|
mock := new(mockArtistImageAgent)
|
||||||
|
// Set default AgentName if needed, although usually called via mockAgents
|
||||||
|
mock.On("AgentName").Return("mockImage").Maybe()
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockArtistImageAgent) AgentName() string {
|
||||||
|
args := m.Called()
|
||||||
|
return args.String(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockArtistImageAgent) GetArtistImages(ctx context.Context, id, artistName, mbid string) ([]agents.ExternalImage, error) {
|
||||||
|
args := m.Called(ctx, id, artistName, mbid)
|
||||||
|
// Need careful type assertion for potentially nil slice
|
||||||
|
var res []agents.ExternalImage
|
||||||
|
if args.Get(0) != nil {
|
||||||
|
res = args.Get(0).([]agents.ExternalImage)
|
||||||
|
}
|
||||||
|
return res, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure mockAgent implements the interface
|
||||||
|
var _ agents.ArtistImageRetriever = (*mockArtistImageAgent)(nil)
|
198
core/external/provider_similarsongs_test.go
vendored
Normal file
198
core/external/provider_similarsongs_test.go
vendored
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
package external_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/core/agents"
|
||||||
|
. "github.com/navidrome/navidrome/core/external"
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/tests"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Provider - SimilarSongs", func() {
|
||||||
|
var ds model.DataStore
|
||||||
|
var provider Provider
|
||||||
|
var mockAgent *mockSimilarArtistAgent
|
||||||
|
var mockTopAgent agents.ArtistTopSongsRetriever
|
||||||
|
var mockSimilarAgent agents.ArtistSimilarRetriever
|
||||||
|
var agentsCombined Agents
|
||||||
|
var artistRepo *mockArtistRepo
|
||||||
|
var mediaFileRepo *mockMediaFileRepo
|
||||||
|
var ctx context.Context
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx = GinkgoT().Context()
|
||||||
|
|
||||||
|
artistRepo = newMockArtistRepo()
|
||||||
|
mediaFileRepo = newMockMediaFileRepo()
|
||||||
|
|
||||||
|
ds = &tests.MockDataStore{
|
||||||
|
MockedArtist: artistRepo,
|
||||||
|
MockedMediaFile: mediaFileRepo,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockAgent = &mockSimilarArtistAgent{}
|
||||||
|
mockTopAgent = mockAgent
|
||||||
|
mockSimilarAgent = mockAgent
|
||||||
|
|
||||||
|
agentsCombined = &mockAgents{
|
||||||
|
topSongsAgent: mockTopAgent,
|
||||||
|
similarAgent: mockSimilarAgent,
|
||||||
|
}
|
||||||
|
|
||||||
|
provider = NewProvider(ds, agentsCombined)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns similar songs from main artist and similar artists", func() {
|
||||||
|
artist1 := model.Artist{ID: "artist-1", Name: "Artist One"}
|
||||||
|
similarArtist := model.Artist{ID: "artist-3", Name: "Similar Artist"}
|
||||||
|
song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1"}
|
||||||
|
song2 := model.MediaFile{ID: "song-2", Title: "Song Two", ArtistID: "artist-1"}
|
||||||
|
song3 := model.MediaFile{ID: "song-3", Title: "Song Three", ArtistID: "artist-3"}
|
||||||
|
|
||||||
|
artistRepo.On("Get", "artist-1").Return(&artist1, nil).Maybe()
|
||||||
|
artistRepo.On("Get", "artist-3").Return(&similarArtist, nil).Maybe()
|
||||||
|
|
||||||
|
artistRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
||||||
|
return opt.Max == 1 && opt.Filters != nil
|
||||||
|
})).Return(model.Artists{artist1}, nil).Once()
|
||||||
|
|
||||||
|
similarAgentsResp := []agents.Artist{
|
||||||
|
{Name: "Similar Artist", MBID: "similar-mbid"},
|
||||||
|
}
|
||||||
|
mockAgent.On("GetSimilarArtists", mock.Anything, "artist-1", "Artist One", "", 15).
|
||||||
|
Return(similarAgentsResp, nil).Once()
|
||||||
|
|
||||||
|
artistRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
||||||
|
return opt.Max == 0 && opt.Filters != nil
|
||||||
|
})).Return(model.Artists{similarArtist}, nil).Once()
|
||||||
|
|
||||||
|
mockAgent.On("GetArtistTopSongs", mock.Anything, "artist-1", "Artist One", "", mock.Anything).
|
||||||
|
Return([]agents.Song{
|
||||||
|
{Name: "Song One", MBID: "mbid-1"},
|
||||||
|
{Name: "Song Two", MBID: "mbid-2"},
|
||||||
|
}, nil).Once()
|
||||||
|
|
||||||
|
mockAgent.On("GetArtistTopSongs", mock.Anything, "artist-3", "Similar Artist", "", mock.Anything).
|
||||||
|
Return([]agents.Song{
|
||||||
|
{Name: "Song Three", MBID: "mbid-3"},
|
||||||
|
}, nil).Once()
|
||||||
|
|
||||||
|
mediaFileRepo.FindByMBID("mbid-1", song1)
|
||||||
|
mediaFileRepo.FindByMBID("mbid-2", song2)
|
||||||
|
mediaFileRepo.FindByMBID("mbid-3", song3)
|
||||||
|
|
||||||
|
songs, err := provider.SimilarSongs(ctx, "artist-1", 3)
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(songs).To(HaveLen(3))
|
||||||
|
for _, song := range songs {
|
||||||
|
Expect(song.ID).To(BeElementOf("song-1", "song-2", "song-3"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns ErrNotFound when artist is not found", func() {
|
||||||
|
artistRepo.On("Get", "artist-unknown-artist").Return(nil, model.ErrNotFound)
|
||||||
|
mediaFileRepo.On("Get", "artist-unknown-artist").Return(nil, model.ErrNotFound)
|
||||||
|
|
||||||
|
artistRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
||||||
|
return opt.Max == 1 && opt.Filters != nil
|
||||||
|
})).Return(model.Artists{}, nil).Maybe()
|
||||||
|
|
||||||
|
songs, err := provider.SimilarSongs(ctx, "artist-unknown-artist", 5)
|
||||||
|
|
||||||
|
Expect(err).To(Equal(model.ErrNotFound))
|
||||||
|
Expect(songs).To(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns songs from main artist when GetSimilarArtists returns error", func() {
|
||||||
|
artist1 := model.Artist{ID: "artist-1", Name: "Artist One"}
|
||||||
|
song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1"}
|
||||||
|
|
||||||
|
artistRepo.On("Get", "artist-1").Return(&artist1, nil).Maybe()
|
||||||
|
artistRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
||||||
|
return opt.Max == 1 && opt.Filters != nil
|
||||||
|
})).Return(model.Artists{artist1}, nil).Maybe()
|
||||||
|
|
||||||
|
mockAgent.On("GetSimilarArtists", mock.Anything, "artist-1", "Artist One", "", 15).
|
||||||
|
Return(nil, errors.New("error getting similar artists")).Once()
|
||||||
|
|
||||||
|
artistRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
||||||
|
return opt.Max == 0 && opt.Filters != nil
|
||||||
|
})).Return(model.Artists{}, nil).Once()
|
||||||
|
|
||||||
|
mockAgent.On("GetArtistTopSongs", mock.Anything, "artist-1", "Artist One", "", mock.Anything).
|
||||||
|
Return([]agents.Song{
|
||||||
|
{Name: "Song One", MBID: "mbid-1"},
|
||||||
|
}, nil).Once()
|
||||||
|
|
||||||
|
mediaFileRepo.FindByMBID("mbid-1", song1)
|
||||||
|
|
||||||
|
songs, err := provider.SimilarSongs(ctx, "artist-1", 5)
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(songs).To(HaveLen(1))
|
||||||
|
Expect(songs[0].ID).To(Equal("song-1"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns empty list when GetArtistTopSongs returns error", func() {
|
||||||
|
artist1 := model.Artist{ID: "artist-1", Name: "Artist One"}
|
||||||
|
|
||||||
|
artistRepo.On("Get", "artist-1").Return(&artist1, nil).Maybe()
|
||||||
|
artistRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
||||||
|
return opt.Max == 1 && opt.Filters != nil
|
||||||
|
})).Return(model.Artists{artist1}, nil).Maybe()
|
||||||
|
|
||||||
|
mockAgent.On("GetSimilarArtists", mock.Anything, "artist-1", "Artist One", "", 15).
|
||||||
|
Return([]agents.Artist{}, nil).Once()
|
||||||
|
|
||||||
|
artistRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
||||||
|
return opt.Max == 0 && opt.Filters != nil
|
||||||
|
})).Return(model.Artists{}, nil).Once()
|
||||||
|
|
||||||
|
mockAgent.On("GetArtistTopSongs", mock.Anything, "artist-1", "Artist One", "", mock.Anything).
|
||||||
|
Return(nil, errors.New("error getting top songs")).Once()
|
||||||
|
|
||||||
|
songs, err := provider.SimilarSongs(ctx, "artist-1", 5)
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(songs).To(BeEmpty())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("respects count parameter", func() {
|
||||||
|
artist1 := model.Artist{ID: "artist-1", Name: "Artist One"}
|
||||||
|
song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1"}
|
||||||
|
song2 := model.MediaFile{ID: "song-2", Title: "Song Two", ArtistID: "artist-1"}
|
||||||
|
|
||||||
|
artistRepo.On("Get", "artist-1").Return(&artist1, nil).Maybe()
|
||||||
|
artistRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
||||||
|
return opt.Max == 1 && opt.Filters != nil
|
||||||
|
})).Return(model.Artists{artist1}, nil).Maybe()
|
||||||
|
|
||||||
|
mockAgent.On("GetSimilarArtists", mock.Anything, "artist-1", "Artist One", "", 15).
|
||||||
|
Return([]agents.Artist{}, nil).Once()
|
||||||
|
|
||||||
|
artistRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
||||||
|
return opt.Max == 0 && opt.Filters != nil
|
||||||
|
})).Return(model.Artists{}, nil).Once()
|
||||||
|
|
||||||
|
mockAgent.On("GetArtistTopSongs", mock.Anything, "artist-1", "Artist One", "", mock.Anything).
|
||||||
|
Return([]agents.Song{
|
||||||
|
{Name: "Song One", MBID: "mbid-1"},
|
||||||
|
{Name: "Song Two", MBID: "mbid-2"},
|
||||||
|
}, nil).Once()
|
||||||
|
|
||||||
|
mediaFileRepo.FindByMBID("mbid-1", song1)
|
||||||
|
mediaFileRepo.FindByMBID("mbid-2", song2)
|
||||||
|
|
||||||
|
songs, err := provider.SimilarSongs(ctx, "artist-1", 1)
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(songs).To(HaveLen(1))
|
||||||
|
Expect(songs[0].ID).To(BeElementOf("song-1", "song-2"))
|
||||||
|
})
|
||||||
|
})
|
193
core/external/provider_topsongs_test.go
vendored
Normal file
193
core/external/provider_topsongs_test.go
vendored
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
package external_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/core/agents"
|
||||||
|
_ "github.com/navidrome/navidrome/core/agents/lastfm"
|
||||||
|
_ "github.com/navidrome/navidrome/core/agents/listenbrainz"
|
||||||
|
_ "github.com/navidrome/navidrome/core/agents/spotify"
|
||||||
|
. "github.com/navidrome/navidrome/core/external"
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/tests"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Provider - TopSongs", func() {
|
||||||
|
var (
|
||||||
|
p Provider
|
||||||
|
artistRepo *mockArtistRepo // From provider_helper_test.go
|
||||||
|
mediaFileRepo *mockMediaFileRepo // From provider_helper_test.go
|
||||||
|
ag *mockAgents // Consolidated mock from export_test.go
|
||||||
|
ctx context.Context
|
||||||
|
)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx = GinkgoT().Context()
|
||||||
|
|
||||||
|
artistRepo = newMockArtistRepo() // Use helper mock
|
||||||
|
mediaFileRepo = newMockMediaFileRepo() // Use helper mock
|
||||||
|
|
||||||
|
// Configure tests.MockDataStore to use the testify/mock-based repos
|
||||||
|
ds := &tests.MockDataStore{
|
||||||
|
MockedArtist: artistRepo,
|
||||||
|
MockedMediaFile: mediaFileRepo,
|
||||||
|
}
|
||||||
|
|
||||||
|
ag = new(mockAgents)
|
||||||
|
|
||||||
|
p = NewProvider(ds, ag)
|
||||||
|
})
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
// Setup expectations in individual tests
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns top songs for a known artist", func() {
|
||||||
|
// Mock finding the artist
|
||||||
|
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).Once()
|
||||||
|
|
||||||
|
// Mock agent response
|
||||||
|
agentSongs := []agents.Song{
|
||||||
|
{Name: "Song One", MBID: "mbid-song-1"},
|
||||||
|
{Name: "Song Two", MBID: "mbid-song-2"},
|
||||||
|
}
|
||||||
|
ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 2).Return(agentSongs, nil).Once()
|
||||||
|
|
||||||
|
// Mock finding matching tracks
|
||||||
|
song1 := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzRecordingID: "mbid-song-1"}
|
||||||
|
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}, nil).Once()
|
||||||
|
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song2}, nil).Once()
|
||||||
|
|
||||||
|
songs, err := p.TopSongs(ctx, "Artist One", 2)
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(songs).To(HaveLen(2))
|
||||||
|
Expect(songs[0].ID).To(Equal("song-1"))
|
||||||
|
Expect(songs[1].ID).To(Equal("song-2"))
|
||||||
|
artistRepo.AssertExpectations(GinkgoT())
|
||||||
|
ag.AssertExpectations(GinkgoT())
|
||||||
|
mediaFileRepo.AssertExpectations(GinkgoT())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns nil for an unknown artist", func() {
|
||||||
|
// Mock artist not found
|
||||||
|
artistRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.Artists{}, nil).Once()
|
||||||
|
|
||||||
|
songs, err := p.TopSongs(ctx, "Unknown Artist", 5)
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred()) // TopSongs returns nil error if artist not found
|
||||||
|
Expect(songs).To(BeNil())
|
||||||
|
artistRepo.AssertExpectations(GinkgoT())
|
||||||
|
ag.AssertNotCalled(GinkgoT(), "GetArtistTopSongs", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns error when the agent returns an error", func() {
|
||||||
|
// Mock finding the artist
|
||||||
|
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).Once()
|
||||||
|
|
||||||
|
// Mock agent error
|
||||||
|
agentErr := errors.New("agent error")
|
||||||
|
ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 5).Return(nil, agentErr).Once()
|
||||||
|
|
||||||
|
songs, err := p.TopSongs(ctx, "Artist One", 5)
|
||||||
|
|
||||||
|
Expect(err).To(MatchError(agentErr))
|
||||||
|
Expect(songs).To(BeNil())
|
||||||
|
artistRepo.AssertExpectations(GinkgoT())
|
||||||
|
ag.AssertExpectations(GinkgoT())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns ErrNotFound when the agent returns ErrNotFound", func() {
|
||||||
|
// Mock finding the artist
|
||||||
|
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).Once()
|
||||||
|
|
||||||
|
// Mock agent ErrNotFound
|
||||||
|
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)
|
||||||
|
|
||||||
|
Expect(err).To(MatchError(model.ErrNotFound))
|
||||||
|
Expect(songs).To(BeNil())
|
||||||
|
artistRepo.AssertExpectations(GinkgoT())
|
||||||
|
ag.AssertExpectations(GinkgoT())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns fewer songs if count is less than available top songs", func() {
|
||||||
|
// Mock finding the artist
|
||||||
|
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).Once()
|
||||||
|
|
||||||
|
// Mock agent response (only need 1 for the test)
|
||||||
|
agentSongs := []agents.Song{{Name: "Song One", MBID: "mbid-song-1"}}
|
||||||
|
ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 1).Return(agentSongs, nil).Once()
|
||||||
|
|
||||||
|
// Mock finding matching track
|
||||||
|
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()
|
||||||
|
|
||||||
|
songs, err := p.TopSongs(ctx, "Artist One", 1)
|
||||||
|
|
||||||
|
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 fewer songs if fewer matching tracks are found", func() {
|
||||||
|
// Mock finding the artist
|
||||||
|
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).Once()
|
||||||
|
|
||||||
|
// Mock agent response
|
||||||
|
agentSongs := []agents.Song{
|
||||||
|
{Name: "Song One", MBID: "mbid-song-1"},
|
||||||
|
{Name: "Song Two", MBID: "mbid-song-2"},
|
||||||
|
}
|
||||||
|
ag.On("GetArtistTopSongs", ctx, "artist-1", "Artist One", "mbid-artist-1", 2).Return(agentSongs, nil).Once()
|
||||||
|
|
||||||
|
// Mock finding matching tracks (only find 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{}, nil).Once() // For mbid-song-2 (fails)
|
||||||
|
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{}, nil).Once() // For title fallback (fails)
|
||||||
|
|
||||||
|
songs, err := p.TopSongs(ctx, "Artist One", 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 error when context is canceled during agent call", func() {
|
||||||
|
// Mock finding the artist
|
||||||
|
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).Once()
|
||||||
|
|
||||||
|
// Setup context that will be canceled
|
||||||
|
canceledCtx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
// Mock agent call to return context canceled error
|
||||||
|
ag.On("GetArtistTopSongs", canceledCtx, "artist-1", "Artist One", "mbid-artist-1", 5).Return(nil, context.Canceled).Once()
|
||||||
|
|
||||||
|
cancel() // Cancel the context before calling
|
||||||
|
songs, err := p.TopSongs(canceledCtx, "Artist One", 5)
|
||||||
|
|
||||||
|
Expect(err).To(MatchError(context.Canceled))
|
||||||
|
Expect(songs).To(BeNil())
|
||||||
|
artistRepo.AssertExpectations(GinkgoT())
|
||||||
|
ag.AssertExpectations(GinkgoT())
|
||||||
|
})
|
||||||
|
})
|
170
core/external/provider_updatealbuminfo_test.go
vendored
Normal file
170
core/external/provider_updatealbuminfo_test.go
vendored
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package external_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/conf"
|
||||||
|
"github.com/navidrome/navidrome/core/agents"
|
||||||
|
"github.com/navidrome/navidrome/core/external"
|
||||||
|
"github.com/navidrome/navidrome/log"
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/tests"
|
||||||
|
"github.com/navidrome/navidrome/utils/gg"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log.SetLevel(log.LevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = Describe("Provider - UpdateAlbumInfo", func() {
|
||||||
|
var (
|
||||||
|
ctx context.Context
|
||||||
|
p external.Provider
|
||||||
|
ds *tests.MockDataStore
|
||||||
|
ag *mockAgents
|
||||||
|
mockAlbumRepo *tests.MockAlbumRepo
|
||||||
|
)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx = GinkgoT().Context()
|
||||||
|
ds = new(tests.MockDataStore)
|
||||||
|
ag = new(mockAgents)
|
||||||
|
p = external.NewProvider(ds, ag)
|
||||||
|
mockAlbumRepo = ds.Album(ctx).(*tests.MockAlbumRepo)
|
||||||
|
conf.Server.DevAlbumInfoTimeToLive = 1 * time.Hour
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns error when album is not found", func() {
|
||||||
|
album, err := p.UpdateAlbumInfo(ctx, "al-not-found")
|
||||||
|
|
||||||
|
Expect(err).To(MatchError(model.ErrNotFound))
|
||||||
|
Expect(album).To(BeNil())
|
||||||
|
ag.AssertNotCalled(GinkgoT(), "GetAlbumInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("populates info when album exists but has no external info", func() {
|
||||||
|
originalAlbum := &model.Album{
|
||||||
|
ID: "al-existing",
|
||||||
|
Name: "Test Album",
|
||||||
|
AlbumArtist: "Test Artist",
|
||||||
|
MbzAlbumID: "mbid-album",
|
||||||
|
}
|
||||||
|
mockAlbumRepo.SetData(model.Albums{*originalAlbum})
|
||||||
|
|
||||||
|
expectedInfo := &agents.AlbumInfo{
|
||||||
|
URL: "http://example.com/album",
|
||||||
|
Description: "Album Description",
|
||||||
|
Images: []agents.ExternalImage{
|
||||||
|
{URL: "http://example.com/large.jpg", Size: 300},
|
||||||
|
{URL: "http://example.com/medium.jpg", Size: 200},
|
||||||
|
{URL: "http://example.com/small.jpg", Size: 100},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ag.On("GetAlbumInfo", ctx, "Test Album", "Test Artist", "mbid-album").Return(expectedInfo, nil)
|
||||||
|
|
||||||
|
updatedAlbum, err := p.UpdateAlbumInfo(ctx, "al-existing")
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(updatedAlbum).NotTo(BeNil())
|
||||||
|
Expect(updatedAlbum.ID).To(Equal("al-existing"))
|
||||||
|
Expect(updatedAlbum.ExternalUrl).To(Equal("http://example.com/album"))
|
||||||
|
Expect(updatedAlbum.Description).To(Equal("Album Description"))
|
||||||
|
Expect(updatedAlbum.LargeImageUrl).To(Equal("http://example.com/large.jpg"))
|
||||||
|
Expect(updatedAlbum.MediumImageUrl).To(Equal("http://example.com/medium.jpg"))
|
||||||
|
Expect(updatedAlbum.SmallImageUrl).To(Equal("http://example.com/small.jpg"))
|
||||||
|
Expect(updatedAlbum.ExternalInfoUpdatedAt).NotTo(BeNil())
|
||||||
|
Expect(*updatedAlbum.ExternalInfoUpdatedAt).To(BeTemporally("~", time.Now(), time.Second))
|
||||||
|
|
||||||
|
ag.AssertExpectations(GinkgoT())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns cached info when album exists and info is not expired", func() {
|
||||||
|
now := time.Now()
|
||||||
|
originalAlbum := &model.Album{
|
||||||
|
ID: "al-cached",
|
||||||
|
Name: "Cached Album",
|
||||||
|
AlbumArtist: "Cached Artist",
|
||||||
|
ExternalUrl: "http://cached.com/album",
|
||||||
|
Description: "Cached Desc",
|
||||||
|
LargeImageUrl: "http://cached.com/large.jpg",
|
||||||
|
ExternalInfoUpdatedAt: gg.P(now.Add(-conf.Server.DevAlbumInfoTimeToLive / 2)),
|
||||||
|
}
|
||||||
|
mockAlbumRepo.SetData(model.Albums{*originalAlbum})
|
||||||
|
|
||||||
|
updatedAlbum, err := p.UpdateAlbumInfo(ctx, "al-cached")
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(updatedAlbum).NotTo(BeNil())
|
||||||
|
Expect(*updatedAlbum).To(Equal(*originalAlbum))
|
||||||
|
|
||||||
|
ag.AssertNotCalled(GinkgoT(), "GetAlbumInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns cached info and triggers background refresh when info is expired", func() {
|
||||||
|
now := time.Now()
|
||||||
|
expiredTime := now.Add(-conf.Server.DevAlbumInfoTimeToLive * 2)
|
||||||
|
originalAlbum := &model.Album{
|
||||||
|
ID: "al-expired",
|
||||||
|
Name: "Expired Album",
|
||||||
|
AlbumArtist: "Expired Artist",
|
||||||
|
ExternalUrl: "http://expired.com/album",
|
||||||
|
Description: "Expired Desc",
|
||||||
|
LargeImageUrl: "http://expired.com/large.jpg",
|
||||||
|
ExternalInfoUpdatedAt: gg.P(expiredTime),
|
||||||
|
}
|
||||||
|
mockAlbumRepo.SetData(model.Albums{*originalAlbum})
|
||||||
|
|
||||||
|
updatedAlbum, err := p.UpdateAlbumInfo(ctx, "al-expired")
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(updatedAlbum).NotTo(BeNil())
|
||||||
|
Expect(*updatedAlbum).To(Equal(*originalAlbum))
|
||||||
|
|
||||||
|
ag.AssertNotCalled(GinkgoT(), "GetAlbumInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns error when agent fails to get album info", func() {
|
||||||
|
originalAlbum := &model.Album{
|
||||||
|
ID: "al-agent-error",
|
||||||
|
Name: "Agent Error Album",
|
||||||
|
AlbumArtist: "Agent Error Artist",
|
||||||
|
MbzAlbumID: "mbid-agent-error",
|
||||||
|
}
|
||||||
|
mockAlbumRepo.SetData(model.Albums{*originalAlbum})
|
||||||
|
|
||||||
|
expectedErr := errors.New("agent communication failed")
|
||||||
|
ag.On("GetAlbumInfo", ctx, "Agent Error Album", "Agent Error Artist", "mbid-agent-error").Return(nil, expectedErr)
|
||||||
|
|
||||||
|
updatedAlbum, err := p.UpdateAlbumInfo(ctx, "al-agent-error")
|
||||||
|
|
||||||
|
Expect(err).To(MatchError(expectedErr))
|
||||||
|
Expect(updatedAlbum).To(BeNil())
|
||||||
|
ag.AssertExpectations(GinkgoT())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns original album when agent returns ErrNotFound", func() {
|
||||||
|
originalAlbum := &model.Album{
|
||||||
|
ID: "al-agent-notfound",
|
||||||
|
Name: "Agent NotFound Album",
|
||||||
|
AlbumArtist: "Agent NotFound Artist",
|
||||||
|
MbzAlbumID: "mbid-agent-notfound",
|
||||||
|
}
|
||||||
|
mockAlbumRepo.SetData(model.Albums{*originalAlbum})
|
||||||
|
|
||||||
|
ag.On("GetAlbumInfo", ctx, "Agent NotFound Album", "Agent NotFound Artist", "mbid-agent-notfound").Return(nil, agents.ErrNotFound)
|
||||||
|
|
||||||
|
updatedAlbum, err := p.UpdateAlbumInfo(ctx, "al-agent-notfound")
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(updatedAlbum).NotTo(BeNil())
|
||||||
|
Expect(*updatedAlbum).To(Equal(*originalAlbum))
|
||||||
|
Expect(updatedAlbum.ExternalInfoUpdatedAt).To(BeNil())
|
||||||
|
|
||||||
|
ag.AssertExpectations(GinkgoT())
|
||||||
|
})
|
||||||
|
})
|
229
core/external/provider_updateartistinfo_test.go
vendored
Normal file
229
core/external/provider_updateartistinfo_test.go
vendored
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
package external_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/conf"
|
||||||
|
"github.com/navidrome/navidrome/conf/configtest"
|
||||||
|
"github.com/navidrome/navidrome/core/agents"
|
||||||
|
"github.com/navidrome/navidrome/core/external"
|
||||||
|
"github.com/navidrome/navidrome/log"
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/tests"
|
||||||
|
"github.com/navidrome/navidrome/utils/gg"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log.SetLevel(log.LevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = Describe("Provider - UpdateArtistInfo", func() {
|
||||||
|
var (
|
||||||
|
ctx context.Context
|
||||||
|
p external.Provider
|
||||||
|
ds *tests.MockDataStore
|
||||||
|
ag *mockAgents
|
||||||
|
mockArtistRepo *tests.MockArtistRepo
|
||||||
|
)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
DeferCleanup(configtest.SetupConfig())
|
||||||
|
conf.Server.DevArtistInfoTimeToLive = 1 * time.Hour
|
||||||
|
ctx = GinkgoT().Context()
|
||||||
|
ds = new(tests.MockDataStore)
|
||||||
|
ag = new(mockAgents)
|
||||||
|
p = external.NewProvider(ds, ag)
|
||||||
|
mockArtistRepo = ds.Artist(ctx).(*tests.MockArtistRepo)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns error when artist is not found", func() {
|
||||||
|
artist, err := p.UpdateArtistInfo(ctx, "ar-not-found", 10, false)
|
||||||
|
|
||||||
|
Expect(err).To(MatchError(model.ErrNotFound))
|
||||||
|
Expect(artist).To(BeNil())
|
||||||
|
ag.AssertNotCalled(GinkgoT(), "GetArtistMBID")
|
||||||
|
ag.AssertNotCalled(GinkgoT(), "GetArtistImages")
|
||||||
|
ag.AssertNotCalled(GinkgoT(), "GetArtistBiography")
|
||||||
|
ag.AssertNotCalled(GinkgoT(), "GetArtistURL")
|
||||||
|
ag.AssertNotCalled(GinkgoT(), "GetSimilarArtists")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("populates info when artist exists but has no external info", func() {
|
||||||
|
originalArtist := &model.Artist{
|
||||||
|
ID: "ar-existing",
|
||||||
|
Name: "Test Artist",
|
||||||
|
}
|
||||||
|
mockArtistRepo.SetData(model.Artists{*originalArtist})
|
||||||
|
|
||||||
|
expectedMBID := "mbid-artist-123"
|
||||||
|
expectedBio := "Artist Bio"
|
||||||
|
expectedURL := "http://artist.url"
|
||||||
|
expectedImages := []agents.ExternalImage{
|
||||||
|
{URL: "http://large.jpg", Size: 300},
|
||||||
|
{URL: "http://medium.jpg", Size: 200},
|
||||||
|
{URL: "http://small.jpg", Size: 100},
|
||||||
|
}
|
||||||
|
rawSimilar := []agents.Artist{
|
||||||
|
{Name: "Similar Artist 1", MBID: "mbid-similar-1"},
|
||||||
|
{Name: "Similar Artist 2", MBID: "mbid-similar-2"},
|
||||||
|
{Name: "Similar Artist 3", MBID: "mbid-similar-3"},
|
||||||
|
}
|
||||||
|
similarInDS := model.Artist{ID: "ar-similar-2", Name: "Similar Artist 2"}
|
||||||
|
|
||||||
|
ag.On("GetArtistMBID", ctx, "ar-existing", "Test Artist").Return(expectedMBID, nil).Once()
|
||||||
|
ag.On("GetArtistImages", ctx, "ar-existing", "Test Artist", expectedMBID).Return(expectedImages, nil).Once()
|
||||||
|
ag.On("GetArtistBiography", ctx, "ar-existing", "Test Artist", expectedMBID).Return(expectedBio, nil).Once()
|
||||||
|
ag.On("GetArtistURL", ctx, "ar-existing", "Test Artist", expectedMBID).Return(expectedURL, nil).Once()
|
||||||
|
ag.On("GetSimilarArtists", ctx, "ar-existing", "Test Artist", expectedMBID, 100).Return(rawSimilar, nil).Once()
|
||||||
|
|
||||||
|
mockArtistRepo.SetData(model.Artists{*originalArtist, similarInDS})
|
||||||
|
|
||||||
|
updatedArtist, err := p.UpdateArtistInfo(ctx, "ar-existing", 10, false)
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(updatedArtist).NotTo(BeNil())
|
||||||
|
Expect(updatedArtist.ID).To(Equal("ar-existing"))
|
||||||
|
Expect(updatedArtist.MbzArtistID).To(Equal(expectedMBID))
|
||||||
|
Expect(updatedArtist.Biography).To(Equal("Artist Bio"))
|
||||||
|
Expect(updatedArtist.ExternalUrl).To(Equal(expectedURL))
|
||||||
|
Expect(updatedArtist.LargeImageUrl).To(Equal("http://large.jpg"))
|
||||||
|
Expect(updatedArtist.MediumImageUrl).To(Equal("http://medium.jpg"))
|
||||||
|
Expect(updatedArtist.SmallImageUrl).To(Equal("http://small.jpg"))
|
||||||
|
Expect(updatedArtist.ExternalInfoUpdatedAt).NotTo(BeNil())
|
||||||
|
Expect(*updatedArtist.ExternalInfoUpdatedAt).To(BeTemporally("~", time.Now(), time.Second))
|
||||||
|
|
||||||
|
Expect(updatedArtist.SimilarArtists).To(HaveLen(1))
|
||||||
|
Expect(updatedArtist.SimilarArtists[0].ID).To(Equal("ar-similar-2"))
|
||||||
|
Expect(updatedArtist.SimilarArtists[0].Name).To(Equal("Similar Artist 2"))
|
||||||
|
|
||||||
|
ag.AssertExpectations(GinkgoT())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns cached info when artist exists and info is not expired", func() {
|
||||||
|
now := time.Now()
|
||||||
|
originalArtist := &model.Artist{
|
||||||
|
ID: "ar-cached",
|
||||||
|
Name: "Cached Artist",
|
||||||
|
MbzArtistID: "mbid-cached",
|
||||||
|
ExternalUrl: "http://cached.url",
|
||||||
|
Biography: "Cached Bio",
|
||||||
|
LargeImageUrl: "http://cached_large.jpg",
|
||||||
|
ExternalInfoUpdatedAt: gg.P(now.Add(-conf.Server.DevArtistInfoTimeToLive / 2)),
|
||||||
|
SimilarArtists: model.Artists{
|
||||||
|
{ID: "ar-similar-present", Name: "Similar Present"},
|
||||||
|
{ID: "ar-similar-absent", Name: "Similar Absent"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
similarInDS := model.Artist{ID: "ar-similar-present", Name: "Similar Present Updated"}
|
||||||
|
mockArtistRepo.SetData(model.Artists{*originalArtist, similarInDS})
|
||||||
|
|
||||||
|
updatedArtist, err := p.UpdateArtistInfo(ctx, "ar-cached", 5, false)
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(updatedArtist).NotTo(BeNil())
|
||||||
|
Expect(updatedArtist.ID).To(Equal(originalArtist.ID))
|
||||||
|
Expect(updatedArtist.Name).To(Equal(originalArtist.Name))
|
||||||
|
Expect(updatedArtist.MbzArtistID).To(Equal(originalArtist.MbzArtistID))
|
||||||
|
Expect(updatedArtist.ExternalUrl).To(Equal(originalArtist.ExternalUrl))
|
||||||
|
Expect(updatedArtist.Biography).To(Equal(originalArtist.Biography))
|
||||||
|
Expect(updatedArtist.LargeImageUrl).To(Equal(originalArtist.LargeImageUrl))
|
||||||
|
Expect(updatedArtist.ExternalInfoUpdatedAt).To(Equal(originalArtist.ExternalInfoUpdatedAt))
|
||||||
|
|
||||||
|
Expect(updatedArtist.SimilarArtists).To(HaveLen(1))
|
||||||
|
Expect(updatedArtist.SimilarArtists[0].ID).To(Equal(similarInDS.ID))
|
||||||
|
Expect(updatedArtist.SimilarArtists[0].Name).To(Equal(similarInDS.Name))
|
||||||
|
|
||||||
|
ag.AssertNotCalled(GinkgoT(), "GetArtistMBID")
|
||||||
|
ag.AssertNotCalled(GinkgoT(), "GetArtistImages")
|
||||||
|
ag.AssertNotCalled(GinkgoT(), "GetArtistBiography")
|
||||||
|
ag.AssertNotCalled(GinkgoT(), "GetArtistURL")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns cached info and triggers background refresh when info is expired", func() {
|
||||||
|
now := time.Now()
|
||||||
|
expiredTime := now.Add(-conf.Server.DevArtistInfoTimeToLive * 2)
|
||||||
|
originalArtist := &model.Artist{
|
||||||
|
ID: "ar-expired",
|
||||||
|
Name: "Expired Artist",
|
||||||
|
ExternalInfoUpdatedAt: gg.P(expiredTime),
|
||||||
|
SimilarArtists: model.Artists{
|
||||||
|
{ID: "ar-exp-similar", Name: "Expired Similar"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
similarInDS := model.Artist{ID: "ar-exp-similar", Name: "Expired Similar Updated"}
|
||||||
|
mockArtistRepo.SetData(model.Artists{*originalArtist, similarInDS})
|
||||||
|
|
||||||
|
updatedArtist, err := p.UpdateArtistInfo(ctx, "ar-expired", 5, false)
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(updatedArtist).NotTo(BeNil())
|
||||||
|
Expect(updatedArtist.ID).To(Equal(originalArtist.ID))
|
||||||
|
Expect(updatedArtist.Name).To(Equal(originalArtist.Name))
|
||||||
|
Expect(updatedArtist.ExternalInfoUpdatedAt).To(Equal(originalArtist.ExternalInfoUpdatedAt))
|
||||||
|
|
||||||
|
Expect(updatedArtist.SimilarArtists).To(HaveLen(1))
|
||||||
|
Expect(updatedArtist.SimilarArtists[0].ID).To(Equal(similarInDS.ID))
|
||||||
|
Expect(updatedArtist.SimilarArtists[0].Name).To(Equal(similarInDS.Name))
|
||||||
|
|
||||||
|
ag.AssertNotCalled(GinkgoT(), "GetArtistMBID")
|
||||||
|
ag.AssertNotCalled(GinkgoT(), "GetArtistImages")
|
||||||
|
ag.AssertNotCalled(GinkgoT(), "GetArtistBiography")
|
||||||
|
ag.AssertNotCalled(GinkgoT(), "GetArtistURL")
|
||||||
|
})
|
||||||
|
|
||||||
|
It("includes non-present similar artists when includeNotPresent is true", func() {
|
||||||
|
now := time.Now()
|
||||||
|
originalArtist := &model.Artist{
|
||||||
|
ID: "ar-similar-test",
|
||||||
|
Name: "Similar Test Artist",
|
||||||
|
ExternalInfoUpdatedAt: gg.P(now.Add(-conf.Server.DevArtistInfoTimeToLive / 2)),
|
||||||
|
SimilarArtists: model.Artists{
|
||||||
|
{ID: "ar-sim-present", Name: "Similar Present"},
|
||||||
|
{ID: "", Name: "Similar Absent Raw"},
|
||||||
|
{ID: "ar-sim-absent-lookup", Name: "Similar Absent Lookup"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
similarInDS := model.Artist{ID: "ar-sim-present", Name: "Similar Present Updated"}
|
||||||
|
mockArtistRepo.SetData(model.Artists{*originalArtist, similarInDS})
|
||||||
|
|
||||||
|
updatedArtist, err := p.UpdateArtistInfo(ctx, "ar-similar-test", 5, true)
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(updatedArtist).NotTo(BeNil())
|
||||||
|
|
||||||
|
Expect(updatedArtist.SimilarArtists).To(HaveLen(3))
|
||||||
|
Expect(updatedArtist.SimilarArtists[0].ID).To(Equal(similarInDS.ID))
|
||||||
|
Expect(updatedArtist.SimilarArtists[0].Name).To(Equal(similarInDS.Name))
|
||||||
|
Expect(updatedArtist.SimilarArtists[1].ID).To(BeEmpty())
|
||||||
|
Expect(updatedArtist.SimilarArtists[1].Name).To(Equal("Similar Absent Raw"))
|
||||||
|
Expect(updatedArtist.SimilarArtists[2].ID).To(BeEmpty())
|
||||||
|
Expect(updatedArtist.SimilarArtists[2].Name).To(Equal("Similar Absent Lookup"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("updates ArtistInfo even if an optional agent call fails", func() {
|
||||||
|
originalArtist := &model.Artist{
|
||||||
|
ID: "ar-agent-fail",
|
||||||
|
Name: "Agent Fail Artist",
|
||||||
|
}
|
||||||
|
mockArtistRepo.SetData(model.Artists{*originalArtist})
|
||||||
|
|
||||||
|
expectedErr := errors.New("agent MBID failed")
|
||||||
|
ag.On("GetArtistMBID", ctx, "ar-agent-fail", "Agent Fail Artist").Return("", expectedErr).Once()
|
||||||
|
ag.On("GetArtistImages", ctx, "ar-agent-fail", "Agent Fail Artist", mock.Anything).Return(nil, nil).Maybe()
|
||||||
|
ag.On("GetArtistBiography", ctx, "ar-agent-fail", "Agent Fail Artist", mock.Anything).Return("", nil).Maybe()
|
||||||
|
ag.On("GetArtistURL", ctx, "ar-agent-fail", "Agent Fail Artist", mock.Anything).Return("", nil).Maybe()
|
||||||
|
ag.On("GetSimilarArtists", ctx, "ar-agent-fail", "Agent Fail Artist", mock.Anything, 100).Return(nil, nil).Maybe()
|
||||||
|
|
||||||
|
updatedArtist, err := p.UpdateArtistInfo(ctx, "ar-agent-fail", 10, false)
|
||||||
|
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(updatedArtist).NotTo(BeNil())
|
||||||
|
Expect(updatedArtist.ID).To(Equal("ar-agent-fail"))
|
||||||
|
ag.AssertExpectations(GinkgoT())
|
||||||
|
})
|
||||||
|
})
|
@ -3,6 +3,7 @@ package core
|
|||||||
import (
|
import (
|
||||||
"github.com/google/wire"
|
"github.com/google/wire"
|
||||||
"github.com/navidrome/navidrome/core/agents"
|
"github.com/navidrome/navidrome/core/agents"
|
||||||
|
"github.com/navidrome/navidrome/core/external"
|
||||||
"github.com/navidrome/navidrome/core/ffmpeg"
|
"github.com/navidrome/navidrome/core/ffmpeg"
|
||||||
"github.com/navidrome/navidrome/core/metrics"
|
"github.com/navidrome/navidrome/core/metrics"
|
||||||
"github.com/navidrome/navidrome/core/playback"
|
"github.com/navidrome/navidrome/core/playback"
|
||||||
@ -13,11 +14,12 @@ var Set = wire.NewSet(
|
|||||||
NewMediaStreamer,
|
NewMediaStreamer,
|
||||||
GetTranscodingCache,
|
GetTranscodingCache,
|
||||||
NewArchiver,
|
NewArchiver,
|
||||||
NewExternalMetadata,
|
|
||||||
NewPlayers,
|
NewPlayers,
|
||||||
NewShare,
|
NewShare,
|
||||||
NewPlaylists,
|
NewPlaylists,
|
||||||
agents.GetAgents,
|
agents.GetAgents,
|
||||||
|
external.NewProvider,
|
||||||
|
wire.Bind(new(external.Agents), new(*agents.Agents)),
|
||||||
ffmpeg.New,
|
ffmpeg.New,
|
||||||
scrobbler.GetPlayTracker,
|
scrobbler.GetPlayTracker,
|
||||||
playback.GetInstance,
|
playback.GetInstance,
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/navidrome/navidrome/conf"
|
"github.com/navidrome/navidrome/conf"
|
||||||
"github.com/navidrome/navidrome/core"
|
"github.com/navidrome/navidrome/core"
|
||||||
"github.com/navidrome/navidrome/core/artwork"
|
"github.com/navidrome/navidrome/core/artwork"
|
||||||
|
"github.com/navidrome/navidrome/core/external"
|
||||||
"github.com/navidrome/navidrome/core/playback"
|
"github.com/navidrome/navidrome/core/playback"
|
||||||
"github.com/navidrome/navidrome/core/scrobbler"
|
"github.com/navidrome/navidrome/core/scrobbler"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
@ -35,7 +36,7 @@ type Router struct {
|
|||||||
streamer core.MediaStreamer
|
streamer core.MediaStreamer
|
||||||
archiver core.Archiver
|
archiver core.Archiver
|
||||||
players core.Players
|
players core.Players
|
||||||
externalMetadata core.ExternalMetadata
|
provider external.Provider
|
||||||
playlists core.Playlists
|
playlists core.Playlists
|
||||||
scanner scanner.Scanner
|
scanner scanner.Scanner
|
||||||
broker events.Broker
|
broker events.Broker
|
||||||
@ -45,7 +46,7 @@ type Router struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreamer, archiver core.Archiver,
|
func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreamer, archiver core.Archiver,
|
||||||
players core.Players, externalMetadata core.ExternalMetadata, scanner scanner.Scanner, broker events.Broker,
|
players core.Players, provider external.Provider, scanner scanner.Scanner, broker events.Broker,
|
||||||
playlists core.Playlists, scrobbler scrobbler.PlayTracker, share core.Share, playback playback.PlaybackServer,
|
playlists core.Playlists, scrobbler scrobbler.PlayTracker, share core.Share, playback playback.PlaybackServer,
|
||||||
) *Router {
|
) *Router {
|
||||||
r := &Router{
|
r := &Router{
|
||||||
@ -54,7 +55,7 @@ func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreame
|
|||||||
streamer: streamer,
|
streamer: streamer,
|
||||||
archiver: archiver,
|
archiver: archiver,
|
||||||
players: players,
|
players: players,
|
||||||
externalMetadata: externalMetadata,
|
provider: provider,
|
||||||
playlists: playlists,
|
playlists: playlists,
|
||||||
scanner: scanner,
|
scanner: scanner,
|
||||||
broker: broker,
|
broker: broker,
|
||||||
|
@ -210,7 +210,7 @@ func (api *Router) GetAlbumInfo(r *http.Request) (*responses.Subsonic, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
album, err := api.externalMetadata.UpdateAlbumInfo(ctx, id)
|
album, err := api.provider.UpdateAlbumInfo(ctx, id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -278,7 +278,7 @@ func (api *Router) getArtistInfo(r *http.Request) (*responses.ArtistInfoBase, *m
|
|||||||
count := p.IntOr("count", 20)
|
count := p.IntOr("count", 20)
|
||||||
includeNotPresent := p.BoolOr("includeNotPresent", false)
|
includeNotPresent := p.BoolOr("includeNotPresent", false)
|
||||||
|
|
||||||
artist, err := api.externalMetadata.UpdateArtistInfo(ctx, id, count, includeNotPresent)
|
artist, err := api.provider.UpdateArtistInfo(ctx, id, count, includeNotPresent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@ -343,7 +343,7 @@ func (api *Router) GetSimilarSongs(r *http.Request) (*responses.Subsonic, error)
|
|||||||
}
|
}
|
||||||
count := p.IntOr("count", 50)
|
count := p.IntOr("count", 50)
|
||||||
|
|
||||||
songs, err := api.externalMetadata.SimilarSongs(ctx, id, count)
|
songs, err := api.provider.SimilarSongs(ctx, id, count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -377,8 +377,8 @@ func (api *Router) GetTopSongs(r *http.Request) (*responses.Subsonic, error) {
|
|||||||
}
|
}
|
||||||
count := p.IntOr("count", 50)
|
count := p.IntOr("count", 50)
|
||||||
|
|
||||||
songs, err := api.externalMetadata.TopSongs(ctx, artist, count)
|
songs, err := api.provider.TopSongs(ctx, artist, count)
|
||||||
if err != nil {
|
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,56 +10,56 @@ import (
|
|||||||
|
|
||||||
func CreateMockAlbumRepo() *MockAlbumRepo {
|
func CreateMockAlbumRepo() *MockAlbumRepo {
|
||||||
return &MockAlbumRepo{
|
return &MockAlbumRepo{
|
||||||
data: make(map[string]*model.Album),
|
Data: make(map[string]*model.Album),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MockAlbumRepo struct {
|
type MockAlbumRepo struct {
|
||||||
model.AlbumRepository
|
model.AlbumRepository
|
||||||
data map[string]*model.Album
|
Data map[string]*model.Album
|
||||||
all model.Albums
|
All model.Albums
|
||||||
err bool
|
Err bool
|
||||||
Options model.QueryOptions
|
Options model.QueryOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockAlbumRepo) SetError(err bool) {
|
func (m *MockAlbumRepo) SetError(err bool) {
|
||||||
m.err = err
|
m.Err = err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockAlbumRepo) SetData(albums model.Albums) {
|
func (m *MockAlbumRepo) SetData(albums model.Albums) {
|
||||||
m.data = make(map[string]*model.Album, len(albums))
|
m.Data = make(map[string]*model.Album, len(albums))
|
||||||
m.all = albums
|
m.All = albums
|
||||||
for i, a := range m.all {
|
for i, a := range m.All {
|
||||||
m.data[a.ID] = &m.all[i]
|
m.Data[a.ID] = &m.All[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockAlbumRepo) Exists(id string) (bool, error) {
|
func (m *MockAlbumRepo) Exists(id string) (bool, error) {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return false, errors.New("unexpected error")
|
return false, errors.New("unexpected error")
|
||||||
}
|
}
|
||||||
_, found := m.data[id]
|
_, found := m.Data[id]
|
||||||
return found, nil
|
return found, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockAlbumRepo) Get(id string) (*model.Album, error) {
|
func (m *MockAlbumRepo) Get(id string) (*model.Album, error) {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return nil, errors.New("unexpected error")
|
return nil, errors.New("unexpected error")
|
||||||
}
|
}
|
||||||
if d, ok := m.data[id]; ok {
|
if d, ok := m.Data[id]; ok {
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
return nil, model.ErrNotFound
|
return nil, model.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockAlbumRepo) Put(al *model.Album) error {
|
func (m *MockAlbumRepo) Put(al *model.Album) error {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return errors.New("unexpected error")
|
return errors.New("unexpected error")
|
||||||
}
|
}
|
||||||
if al.ID == "" {
|
if al.ID == "" {
|
||||||
al.ID = id.NewRandom()
|
al.ID = id.NewRandom()
|
||||||
}
|
}
|
||||||
m.data[al.ID] = al
|
m.Data[al.ID] = al
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,17 +67,17 @@ func (m *MockAlbumRepo) GetAll(qo ...model.QueryOptions) (model.Albums, error) {
|
|||||||
if len(qo) > 0 {
|
if len(qo) > 0 {
|
||||||
m.Options = qo[0]
|
m.Options = qo[0]
|
||||||
}
|
}
|
||||||
if m.err {
|
if m.Err {
|
||||||
return nil, errors.New("unexpected error")
|
return nil, errors.New("unexpected error")
|
||||||
}
|
}
|
||||||
return m.all, nil
|
return m.All, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockAlbumRepo) IncPlayCount(id string, timestamp time.Time) error {
|
func (m *MockAlbumRepo) IncPlayCount(id string, timestamp time.Time) error {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return errors.New("unexpected error")
|
return errors.New("unexpected error")
|
||||||
}
|
}
|
||||||
if d, ok := m.data[id]; ok {
|
if d, ok := m.Data[id]; ok {
|
||||||
d.PlayCount++
|
d.PlayCount++
|
||||||
d.PlayDate = ×tamp
|
d.PlayDate = ×tamp
|
||||||
return nil
|
return nil
|
||||||
@ -85,15 +85,15 @@ func (m *MockAlbumRepo) IncPlayCount(id string, timestamp time.Time) error {
|
|||||||
return model.ErrNotFound
|
return model.ErrNotFound
|
||||||
}
|
}
|
||||||
func (m *MockAlbumRepo) CountAll(...model.QueryOptions) (int64, error) {
|
func (m *MockAlbumRepo) CountAll(...model.QueryOptions) (int64, error) {
|
||||||
return int64(len(m.all)), nil
|
return int64(len(m.All)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockAlbumRepo) GetTouchedAlbums(libID int) (model.AlbumCursor, error) {
|
func (m *MockAlbumRepo) GetTouchedAlbums(libID int) (model.AlbumCursor, error) {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return nil, errors.New("unexpected error")
|
return nil, errors.New("unexpected error")
|
||||||
}
|
}
|
||||||
return func(yield func(model.Album, error) bool) {
|
return func(yield func(model.Album, error) bool) {
|
||||||
for _, a := range m.data {
|
for _, a := range m.Data {
|
||||||
if a.ID == "error" {
|
if a.ID == "error" {
|
||||||
if !yield(*a, errors.New("error")) {
|
if !yield(*a, errors.New("error")) {
|
||||||
break
|
break
|
||||||
@ -110,4 +110,11 @@ func (m *MockAlbumRepo) GetTouchedAlbums(libID int) (model.AlbumCursor, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockAlbumRepo) UpdateExternalInfo(album *model.Album) error {
|
||||||
|
if m.Err {
|
||||||
|
return errors.New("unexpected error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var _ model.AlbumRepository = (*MockAlbumRepo)(nil)
|
var _ model.AlbumRepository = (*MockAlbumRepo)(nil)
|
||||||
|
@ -10,61 +10,61 @@ import (
|
|||||||
|
|
||||||
func CreateMockArtistRepo() *MockArtistRepo {
|
func CreateMockArtistRepo() *MockArtistRepo {
|
||||||
return &MockArtistRepo{
|
return &MockArtistRepo{
|
||||||
data: make(map[string]*model.Artist),
|
Data: make(map[string]*model.Artist),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MockArtistRepo struct {
|
type MockArtistRepo struct {
|
||||||
model.ArtistRepository
|
model.ArtistRepository
|
||||||
data map[string]*model.Artist
|
Data map[string]*model.Artist
|
||||||
err bool
|
Err bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockArtistRepo) SetError(err bool) {
|
func (m *MockArtistRepo) SetError(err bool) {
|
||||||
m.err = err
|
m.Err = err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockArtistRepo) SetData(artists model.Artists) {
|
func (m *MockArtistRepo) SetData(artists model.Artists) {
|
||||||
m.data = make(map[string]*model.Artist)
|
m.Data = make(map[string]*model.Artist)
|
||||||
for i, a := range artists {
|
for i, a := range artists {
|
||||||
m.data[a.ID] = &artists[i]
|
m.Data[a.ID] = &artists[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockArtistRepo) Exists(id string) (bool, error) {
|
func (m *MockArtistRepo) Exists(id string) (bool, error) {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return false, errors.New("Error!")
|
return false, errors.New("Error!")
|
||||||
}
|
}
|
||||||
_, found := m.data[id]
|
_, found := m.Data[id]
|
||||||
return found, nil
|
return found, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockArtistRepo) Get(id string) (*model.Artist, error) {
|
func (m *MockArtistRepo) Get(id string) (*model.Artist, error) {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return nil, errors.New("Error!")
|
return nil, errors.New("Error!")
|
||||||
}
|
}
|
||||||
if d, ok := m.data[id]; ok {
|
if d, ok := m.Data[id]; ok {
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
return nil, model.ErrNotFound
|
return nil, model.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockArtistRepo) Put(ar *model.Artist, columsToUpdate ...string) error {
|
func (m *MockArtistRepo) Put(ar *model.Artist, columsToUpdate ...string) error {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return errors.New("error")
|
return errors.New("error")
|
||||||
}
|
}
|
||||||
if ar.ID == "" {
|
if ar.ID == "" {
|
||||||
ar.ID = id.NewRandom()
|
ar.ID = id.NewRandom()
|
||||||
}
|
}
|
||||||
m.data[ar.ID] = ar
|
m.Data[ar.ID] = ar
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockArtistRepo) IncPlayCount(id string, timestamp time.Time) error {
|
func (m *MockArtistRepo) IncPlayCount(id string, timestamp time.Time) error {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return errors.New("error")
|
return errors.New("error")
|
||||||
}
|
}
|
||||||
if d, ok := m.data[id]; ok {
|
if d, ok := m.Data[id]; ok {
|
||||||
d.PlayCount++
|
d.PlayCount++
|
||||||
d.PlayDate = ×tamp
|
d.PlayDate = ×tamp
|
||||||
return nil
|
return nil
|
||||||
@ -72,4 +72,26 @@ func (m *MockArtistRepo) IncPlayCount(id string, timestamp time.Time) error {
|
|||||||
return model.ErrNotFound
|
return model.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockArtistRepo) GetAll(options ...model.QueryOptions) (model.Artists, error) {
|
||||||
|
if m.Err {
|
||||||
|
return nil, errors.New("mock repo error")
|
||||||
|
}
|
||||||
|
var allArtists model.Artists
|
||||||
|
for _, artist := range m.Data {
|
||||||
|
allArtists = append(allArtists, *artist)
|
||||||
|
}
|
||||||
|
// Apply Max=1 if present (simple simulation for findArtistByName)
|
||||||
|
if len(options) > 0 && options[0].Max == 1 && len(allArtists) > 0 {
|
||||||
|
return allArtists[:1], nil
|
||||||
|
}
|
||||||
|
return allArtists, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockArtistRepo) UpdateExternalInfo(artist *model.Artist) error {
|
||||||
|
if m.Err {
|
||||||
|
return errors.New("mock repo error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var _ model.ArtistRepository = (*MockArtistRepo)(nil)
|
var _ model.ArtistRepository = (*MockArtistRepo)(nil)
|
||||||
|
@ -6,12 +6,12 @@ import (
|
|||||||
|
|
||||||
type MockedGenreRepo struct {
|
type MockedGenreRepo struct {
|
||||||
Error error
|
Error error
|
||||||
data map[string]model.Genre
|
Data map[string]model.Genre
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *MockedGenreRepo) init() {
|
func (r *MockedGenreRepo) init() {
|
||||||
if r.data == nil {
|
if r.Data == nil {
|
||||||
r.data = make(map[string]model.Genre)
|
r.Data = make(map[string]model.Genre)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ func (r *MockedGenreRepo) GetAll(...model.QueryOptions) (model.Genres, error) {
|
|||||||
r.init()
|
r.init()
|
||||||
|
|
||||||
var all model.Genres
|
var all model.Genres
|
||||||
for _, g := range r.data {
|
for _, g := range r.Data {
|
||||||
all = append(all, g)
|
all = append(all, g)
|
||||||
}
|
}
|
||||||
return all, nil
|
return all, nil
|
||||||
@ -33,6 +33,6 @@ func (r *MockedGenreRepo) Put(g *model.Genre) error {
|
|||||||
return r.Error
|
return r.Error
|
||||||
}
|
}
|
||||||
r.init()
|
r.init()
|
||||||
r.data[g.ID] = *g
|
r.Data[g.ID] = *g
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,14 @@ import (
|
|||||||
|
|
||||||
type MockLibraryRepo struct {
|
type MockLibraryRepo struct {
|
||||||
model.LibraryRepository
|
model.LibraryRepository
|
||||||
data map[int]model.Library
|
Data map[int]model.Library
|
||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockLibraryRepo) SetData(data model.Libraries) {
|
func (m *MockLibraryRepo) SetData(data model.Libraries) {
|
||||||
m.data = make(map[int]model.Library)
|
m.Data = make(map[int]model.Library)
|
||||||
for _, d := range data {
|
for _, d := range data {
|
||||||
m.data[d.ID] = d
|
m.Data[d.ID] = d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,14 +22,14 @@ func (m *MockLibraryRepo) GetAll(...model.QueryOptions) (model.Libraries, error)
|
|||||||
if m.Err != nil {
|
if m.Err != nil {
|
||||||
return nil, m.Err
|
return nil, m.Err
|
||||||
}
|
}
|
||||||
return maps.Values(m.data), nil
|
return maps.Values(m.Data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockLibraryRepo) GetPath(id int) (string, error) {
|
func (m *MockLibraryRepo) GetPath(id int) (string, error) {
|
||||||
if m.Err != nil {
|
if m.Err != nil {
|
||||||
return "", m.Err
|
return "", m.Err
|
||||||
}
|
}
|
||||||
if lib, ok := m.data[id]; ok {
|
if lib, ok := m.Data[id]; ok {
|
||||||
return lib.Path, nil
|
return lib.Path, nil
|
||||||
}
|
}
|
||||||
return "", model.ErrNotFound
|
return "", model.ErrNotFound
|
||||||
|
@ -14,40 +14,40 @@ import (
|
|||||||
|
|
||||||
func CreateMockMediaFileRepo() *MockMediaFileRepo {
|
func CreateMockMediaFileRepo() *MockMediaFileRepo {
|
||||||
return &MockMediaFileRepo{
|
return &MockMediaFileRepo{
|
||||||
data: make(map[string]*model.MediaFile),
|
Data: make(map[string]*model.MediaFile),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MockMediaFileRepo struct {
|
type MockMediaFileRepo struct {
|
||||||
model.MediaFileRepository
|
model.MediaFileRepository
|
||||||
data map[string]*model.MediaFile
|
Data map[string]*model.MediaFile
|
||||||
err bool
|
Err bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockMediaFileRepo) SetError(err bool) {
|
func (m *MockMediaFileRepo) SetError(err bool) {
|
||||||
m.err = err
|
m.Err = err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockMediaFileRepo) SetData(mfs model.MediaFiles) {
|
func (m *MockMediaFileRepo) SetData(mfs model.MediaFiles) {
|
||||||
m.data = make(map[string]*model.MediaFile)
|
m.Data = make(map[string]*model.MediaFile)
|
||||||
for i, mf := range mfs {
|
for i, mf := range mfs {
|
||||||
m.data[mf.ID] = &mfs[i]
|
m.Data[mf.ID] = &mfs[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockMediaFileRepo) Exists(id string) (bool, error) {
|
func (m *MockMediaFileRepo) Exists(id string) (bool, error) {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return false, errors.New("error")
|
return false, errors.New("error")
|
||||||
}
|
}
|
||||||
_, found := m.data[id]
|
_, found := m.Data[id]
|
||||||
return found, nil
|
return found, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockMediaFileRepo) Get(id string) (*model.MediaFile, error) {
|
func (m *MockMediaFileRepo) Get(id string) (*model.MediaFile, error) {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return nil, errors.New("error")
|
return nil, errors.New("error")
|
||||||
}
|
}
|
||||||
if d, ok := m.data[id]; ok {
|
if d, ok := m.Data[id]; ok {
|
||||||
// Intentionally clone the file and remove participants. This should
|
// Intentionally clone the file and remove participants. This should
|
||||||
// catch any caller that actually means to call GetWithParticipants
|
// catch any caller that actually means to call GetWithParticipants
|
||||||
res := *d
|
res := *d
|
||||||
@ -58,52 +58,52 @@ func (m *MockMediaFileRepo) Get(id string) (*model.MediaFile, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockMediaFileRepo) GetWithParticipants(id string) (*model.MediaFile, error) {
|
func (m *MockMediaFileRepo) GetWithParticipants(id string) (*model.MediaFile, error) {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return nil, errors.New("error")
|
return nil, errors.New("error")
|
||||||
}
|
}
|
||||||
if d, ok := m.data[id]; ok {
|
if d, ok := m.Data[id]; ok {
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
return nil, model.ErrNotFound
|
return nil, model.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockMediaFileRepo) GetAll(...model.QueryOptions) (model.MediaFiles, error) {
|
func (m *MockMediaFileRepo) GetAll(...model.QueryOptions) (model.MediaFiles, error) {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return nil, errors.New("error")
|
return nil, errors.New("error")
|
||||||
}
|
}
|
||||||
values := slices.Collect(maps.Values(m.data))
|
values := slices.Collect(maps.Values(m.Data))
|
||||||
return slice.Map(values, func(p *model.MediaFile) model.MediaFile {
|
return slice.Map(values, func(p *model.MediaFile) model.MediaFile {
|
||||||
return *p
|
return *p
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockMediaFileRepo) Put(mf *model.MediaFile) error {
|
func (m *MockMediaFileRepo) Put(mf *model.MediaFile) error {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return errors.New("error")
|
return errors.New("error")
|
||||||
}
|
}
|
||||||
if mf.ID == "" {
|
if mf.ID == "" {
|
||||||
mf.ID = id.NewRandom()
|
mf.ID = id.NewRandom()
|
||||||
}
|
}
|
||||||
m.data[mf.ID] = mf
|
m.Data[mf.ID] = mf
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockMediaFileRepo) Delete(id string) error {
|
func (m *MockMediaFileRepo) Delete(id string) error {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return errors.New("error")
|
return errors.New("error")
|
||||||
}
|
}
|
||||||
if _, ok := m.data[id]; !ok {
|
if _, ok := m.Data[id]; !ok {
|
||||||
return model.ErrNotFound
|
return model.ErrNotFound
|
||||||
}
|
}
|
||||||
delete(m.data, id)
|
delete(m.Data, id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockMediaFileRepo) IncPlayCount(id string, timestamp time.Time) error {
|
func (m *MockMediaFileRepo) IncPlayCount(id string, timestamp time.Time) error {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return errors.New("error")
|
return errors.New("error")
|
||||||
}
|
}
|
||||||
if d, ok := m.data[id]; ok {
|
if d, ok := m.Data[id]; ok {
|
||||||
d.PlayCount++
|
d.PlayCount++
|
||||||
d.PlayDate = ×tamp
|
d.PlayDate = ×tamp
|
||||||
return nil
|
return nil
|
||||||
@ -112,12 +112,12 @@ func (m *MockMediaFileRepo) IncPlayCount(id string, timestamp time.Time) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockMediaFileRepo) FindByAlbum(artistId string) (model.MediaFiles, error) {
|
func (m *MockMediaFileRepo) FindByAlbum(artistId string) (model.MediaFiles, error) {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return nil, errors.New("error")
|
return nil, errors.New("error")
|
||||||
}
|
}
|
||||||
var res = make(model.MediaFiles, len(m.data))
|
var res = make(model.MediaFiles, len(m.Data))
|
||||||
i := 0
|
i := 0
|
||||||
for _, a := range m.data {
|
for _, a := range m.Data {
|
||||||
if a.AlbumID == artistId {
|
if a.AlbumID == artistId {
|
||||||
res[i] = *a
|
res[i] = *a
|
||||||
i++
|
i++
|
||||||
@ -128,17 +128,17 @@ func (m *MockMediaFileRepo) FindByAlbum(artistId string) (model.MediaFiles, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockMediaFileRepo) GetMissingAndMatching(libId int) (model.MediaFileCursor, error) {
|
func (m *MockMediaFileRepo) GetMissingAndMatching(libId int) (model.MediaFileCursor, error) {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return nil, errors.New("error")
|
return nil, errors.New("error")
|
||||||
}
|
}
|
||||||
var res model.MediaFiles
|
var res model.MediaFiles
|
||||||
for _, a := range m.data {
|
for _, a := range m.Data {
|
||||||
if a.LibraryID == libId && a.Missing {
|
if a.LibraryID == libId && a.Missing {
|
||||||
res = append(res, *a)
|
res = append(res, *a)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, a := range m.data {
|
for _, a := range m.Data {
|
||||||
if a.LibraryID == libId && !(*a).Missing && slices.IndexFunc(res, func(mediaFile model.MediaFile) bool {
|
if a.LibraryID == libId && !(*a).Missing && slices.IndexFunc(res, func(mediaFile model.MediaFile) bool {
|
||||||
return mediaFile.PID == a.PID
|
return mediaFile.PID == a.PID
|
||||||
}) != -1 {
|
}) != -1 {
|
||||||
|
@ -5,12 +5,12 @@ import "github.com/navidrome/navidrome/model"
|
|||||||
type MockedPropertyRepo struct {
|
type MockedPropertyRepo struct {
|
||||||
model.PropertyRepository
|
model.PropertyRepository
|
||||||
Error error
|
Error error
|
||||||
data map[string]string
|
Data map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MockedPropertyRepo) init() {
|
func (p *MockedPropertyRepo) init() {
|
||||||
if p.data == nil {
|
if p.Data == nil {
|
||||||
p.data = make(map[string]string)
|
p.Data = make(map[string]string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ func (p *MockedPropertyRepo) Put(id string, value string) error {
|
|||||||
return p.Error
|
return p.Error
|
||||||
}
|
}
|
||||||
p.init()
|
p.init()
|
||||||
p.data[id] = value
|
p.Data[id] = value
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ func (p *MockedPropertyRepo) Get(id string) (string, error) {
|
|||||||
return "", p.Error
|
return "", p.Error
|
||||||
}
|
}
|
||||||
p.init()
|
p.init()
|
||||||
if v, ok := p.data[id]; ok {
|
if v, ok := p.Data[id]; ok {
|
||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
return "", model.ErrNotFound
|
return "", model.ErrNotFound
|
||||||
@ -39,8 +39,8 @@ func (p *MockedPropertyRepo) Delete(id string) error {
|
|||||||
return p.Error
|
return p.Error
|
||||||
}
|
}
|
||||||
p.init()
|
p.init()
|
||||||
if _, ok := p.data[id]; ok {
|
if _, ok := p.Data[id]; ok {
|
||||||
delete(p.data, id)
|
delete(p.Data, id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return model.ErrNotFound
|
return model.ErrNotFound
|
||||||
|
@ -9,9 +9,9 @@ import (
|
|||||||
|
|
||||||
type MockedRadioRepo struct {
|
type MockedRadioRepo struct {
|
||||||
model.RadioRepository
|
model.RadioRepository
|
||||||
data map[string]*model.Radio
|
Data map[string]*model.Radio
|
||||||
all model.Radios
|
All model.Radios
|
||||||
err bool
|
Err bool
|
||||||
Options model.QueryOptions
|
Options model.QueryOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,44 +20,44 @@ func CreateMockedRadioRepo() *MockedRadioRepo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockedRadioRepo) SetError(err bool) {
|
func (m *MockedRadioRepo) SetError(err bool) {
|
||||||
m.err = err
|
m.Err = err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockedRadioRepo) CountAll(options ...model.QueryOptions) (int64, error) {
|
func (m *MockedRadioRepo) CountAll(options ...model.QueryOptions) (int64, error) {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return 0, errors.New("error")
|
return 0, errors.New("error")
|
||||||
}
|
}
|
||||||
return int64(len(m.data)), nil
|
return int64(len(m.Data)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockedRadioRepo) Delete(id string) error {
|
func (m *MockedRadioRepo) Delete(id string) error {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return errors.New("Error!")
|
return errors.New("Error!")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, found := m.data[id]
|
_, found := m.Data[id]
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
return errors.New("not found")
|
return errors.New("not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(m.data, id)
|
delete(m.Data, id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockedRadioRepo) Exists(id string) (bool, error) {
|
func (m *MockedRadioRepo) Exists(id string) (bool, error) {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return false, errors.New("Error!")
|
return false, errors.New("Error!")
|
||||||
}
|
}
|
||||||
_, found := m.data[id]
|
_, found := m.Data[id]
|
||||||
return found, nil
|
return found, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockedRadioRepo) Get(id string) (*model.Radio, error) {
|
func (m *MockedRadioRepo) Get(id string) (*model.Radio, error) {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return nil, errors.New("Error!")
|
return nil, errors.New("Error!")
|
||||||
}
|
}
|
||||||
if d, ok := m.data[id]; ok {
|
if d, ok := m.Data[id]; ok {
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
return nil, model.ErrNotFound
|
return nil, model.ErrNotFound
|
||||||
@ -67,19 +67,19 @@ func (m *MockedRadioRepo) GetAll(qo ...model.QueryOptions) (model.Radios, error)
|
|||||||
if len(qo) > 0 {
|
if len(qo) > 0 {
|
||||||
m.Options = qo[0]
|
m.Options = qo[0]
|
||||||
}
|
}
|
||||||
if m.err {
|
if m.Err {
|
||||||
return nil, errors.New("Error!")
|
return nil, errors.New("Error!")
|
||||||
}
|
}
|
||||||
return m.all, nil
|
return m.All, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockedRadioRepo) Put(radio *model.Radio) error {
|
func (m *MockedRadioRepo) Put(radio *model.Radio) error {
|
||||||
if m.err {
|
if m.Err {
|
||||||
return errors.New("error")
|
return errors.New("error")
|
||||||
}
|
}
|
||||||
if radio.ID == "" {
|
if radio.ID == "" {
|
||||||
radio.ID = id.NewRandom()
|
radio.ID = id.NewRandom()
|
||||||
}
|
}
|
||||||
m.data[radio.ID] = radio
|
m.Data[radio.ID] = radio
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type MockedScrobbleBufferRepo struct {
|
type MockedScrobbleBufferRepo struct {
|
||||||
Error error
|
Error error
|
||||||
data model.ScrobbleEntries
|
Data model.ScrobbleEntries
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateMockedScrobbleBufferRepo() *MockedScrobbleBufferRepo {
|
func CreateMockedScrobbleBufferRepo() *MockedScrobbleBufferRepo {
|
||||||
@ -20,7 +20,7 @@ func (m *MockedScrobbleBufferRepo) UserIDs(service string) ([]string, error) {
|
|||||||
return nil, m.Error
|
return nil, m.Error
|
||||||
}
|
}
|
||||||
userIds := make(map[string]struct{})
|
userIds := make(map[string]struct{})
|
||||||
for _, e := range m.data {
|
for _, e := range m.Data {
|
||||||
if e.Service == service {
|
if e.Service == service {
|
||||||
userIds[e.UserID] = struct{}{}
|
userIds[e.UserID] = struct{}{}
|
||||||
}
|
}
|
||||||
@ -36,7 +36,7 @@ func (m *MockedScrobbleBufferRepo) Enqueue(service, userId, mediaFileId string,
|
|||||||
if m.Error != nil {
|
if m.Error != nil {
|
||||||
return m.Error
|
return m.Error
|
||||||
}
|
}
|
||||||
m.data = append(m.data, model.ScrobbleEntry{
|
m.Data = append(m.Data, model.ScrobbleEntry{
|
||||||
MediaFile: model.MediaFile{ID: mediaFileId},
|
MediaFile: model.MediaFile{ID: mediaFileId},
|
||||||
Service: service,
|
Service: service,
|
||||||
UserID: userId,
|
UserID: userId,
|
||||||
@ -50,7 +50,7 @@ func (m *MockedScrobbleBufferRepo) Next(service, userId string) (*model.Scrobble
|
|||||||
if m.Error != nil {
|
if m.Error != nil {
|
||||||
return nil, m.Error
|
return nil, m.Error
|
||||||
}
|
}
|
||||||
for _, e := range m.data {
|
for _, e := range m.Data {
|
||||||
if e.Service == service && e.UserID == userId {
|
if e.Service == service && e.UserID == userId {
|
||||||
return &e, nil
|
return &e, nil
|
||||||
}
|
}
|
||||||
@ -63,13 +63,13 @@ func (m *MockedScrobbleBufferRepo) Dequeue(entry *model.ScrobbleEntry) error {
|
|||||||
return m.Error
|
return m.Error
|
||||||
}
|
}
|
||||||
newData := model.ScrobbleEntries{}
|
newData := model.ScrobbleEntries{}
|
||||||
for _, e := range m.data {
|
for _, e := range m.Data {
|
||||||
if e.Service == entry.Service && e.UserID == entry.UserID && e.PlayTime == entry.PlayTime && e.MediaFile.ID == entry.MediaFile.ID {
|
if e.Service == entry.Service && e.UserID == entry.UserID && e.PlayTime == entry.PlayTime && e.MediaFile.ID == entry.MediaFile.ID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
newData = append(newData, e)
|
newData = append(newData, e)
|
||||||
}
|
}
|
||||||
m.data = newData
|
m.Data = newData
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,5 +77,5 @@ func (m *MockedScrobbleBufferRepo) Length() (int64, error) {
|
|||||||
if m.Error != nil {
|
if m.Error != nil {
|
||||||
return 0, m.Error
|
return 0, m.Error
|
||||||
}
|
}
|
||||||
return int64(len(m.data)), nil
|
return int64(len(m.Data)), nil
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,12 @@ import "github.com/navidrome/navidrome/model"
|
|||||||
type MockedUserPropsRepo struct {
|
type MockedUserPropsRepo struct {
|
||||||
model.UserPropsRepository
|
model.UserPropsRepository
|
||||||
Error error
|
Error error
|
||||||
data map[string]string
|
Data map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *MockedUserPropsRepo) init() {
|
func (p *MockedUserPropsRepo) init() {
|
||||||
if p.data == nil {
|
if p.Data == nil {
|
||||||
p.data = make(map[string]string)
|
p.Data = make(map[string]string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ func (p *MockedUserPropsRepo) Put(userId, key string, value string) error {
|
|||||||
return p.Error
|
return p.Error
|
||||||
}
|
}
|
||||||
p.init()
|
p.init()
|
||||||
p.data[userId+key] = value
|
p.Data[userId+key] = value
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ func (p *MockedUserPropsRepo) Get(userId, key string) (string, error) {
|
|||||||
return "", p.Error
|
return "", p.Error
|
||||||
}
|
}
|
||||||
p.init()
|
p.init()
|
||||||
if v, ok := p.data[userId+key]; ok {
|
if v, ok := p.Data[userId+key]; ok {
|
||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
return "", model.ErrNotFound
|
return "", model.ErrNotFound
|
||||||
@ -39,8 +39,8 @@ func (p *MockedUserPropsRepo) Delete(userId, key string) error {
|
|||||||
return p.Error
|
return p.Error
|
||||||
}
|
}
|
||||||
p.init()
|
p.init()
|
||||||
if _, ok := p.data[userId+key]; ok {
|
if _, ok := p.Data[userId+key]; ok {
|
||||||
delete(p.data, userId+key)
|
delete(p.Data, userId+key)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return model.ErrNotFound
|
return model.ErrNotFound
|
||||||
|
Loading…
x
Reference in New Issue
Block a user