mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
* refactor: extract matchSongsToLibrary to core/matcher package Move the song-to-library matching algorithm from core/external into its own core/matcher package. The Matcher struct exposes a single public method MatchSongsToLibrary that implements a multi-phase matching algorithm (ID > MBID > ISRC > fuzzy title+artist). Includes pre-sanitization optimization for the fuzzy matching loop. No behavioral changes — the algorithm is identical to the version in core/external/provider_matching.go. * refactor: inject matcher.Matcher via Wire instead of creating it inline Add *matcher.Matcher as a dependency of external.NewProvider, wired via Google Wire. Update all provider test files to pass matcher.New(ds). This eliminates tight coupling so future consumers can reuse the matcher without depending on the external package. * refactor: remove old provider_matching files Delete core/external/provider_matching.go and its tests. All matching logic now lives in core/matcher/. * test(matcher): restore test coverage lost in extraction Port back 23 specs that existed in the old provider_matching_test.go but were dropped during the extraction. Covers specificity levels, fuzzy matching thresholds, fuzzy album matching, duration matching, and deduplication edge cases. * test(matcher): extract matchFieldInAnd/matchFieldInEq helpers The four inline mock.MatchedBy closures in setupAllPhaseExpectations all followed the same squirrel.And -> squirrel.Eq -> field-name-check pattern. Extract into two small helpers to reduce duplication and make the setup functions read as a concise list of phase expectations. * refactor(matcher): address PR #5348 review feedback - sanitizedTrack now holds *model.MediaFile instead of a value copy. Since MediaFile is a large struct (~74 fields), this avoids the per-track copy into sanitized[] and a second copy when findBestMatch assigns the winner. loadTracksByTitleAndArtist updated to iterate by index and pass &tracks[i]. - loadTracksByISRC now sorts results (starred desc, rating desc, year asc, compilation asc) so that when multiple library tracks share an ISRC the most relevant one is picked deterministically, matching the sort order already used by loadTracksByTitleAndArtist. - Restored the four worked examples (MBID Priority, ISRC Priority, Specificity Ranking, Fuzzy Title Matching) in the MatchSongsToLibrary godoc that were dropped during the extraction. - matcher_test.go: tests now enforce expectations via AssertExpectations in a DeferCleanup. The old setupAllPhaseExpectations helper was replaced with per-phase helpers (expectIDPhase/expectMBIDPhase/expectISRCPhase + allowOtherPhases) so each test deterministically verifies which matching phases fire. This surfaced (and fixes) a latent issue copilot flagged: the old .Once() expectations were not actually asserted, so tests would silently pass even when phases short-circuited unexpectedly.
445 lines
18 KiB
Go
445 lines
18 KiB
Go
package external_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/Masterminds/squirrel"
|
|
"github.com/navidrome/navidrome/core/agents"
|
|
. "github.com/navidrome/navidrome/core/external"
|
|
"github.com/navidrome/navidrome/core/matcher"
|
|
"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 *mockAgents
|
|
var artistRepo *mockArtistRepo
|
|
var mediaFileRepo *mockMediaFileRepo
|
|
var albumRepo *mockAlbumRepo
|
|
var ctx context.Context
|
|
|
|
BeforeEach(func() {
|
|
ctx = GinkgoT().Context()
|
|
|
|
artistRepo = newMockArtistRepo()
|
|
mediaFileRepo = newMockMediaFileRepo()
|
|
albumRepo = newMockAlbumRepo()
|
|
|
|
ds = &tests.MockDataStore{
|
|
MockedArtist: artistRepo,
|
|
MockedMediaFile: mediaFileRepo,
|
|
MockedAlbum: albumRepo,
|
|
}
|
|
|
|
mockAgent = &mockSimilarArtistAgent{}
|
|
mockTopAgent = mockAgent
|
|
mockSimilarAgent = mockAgent
|
|
|
|
agentsCombined = &mockAgents{
|
|
topSongsAgent: mockTopAgent,
|
|
similarAgent: mockSimilarAgent,
|
|
}
|
|
|
|
provider = NewProvider(ds, agentsCombined, matcher.New(ds))
|
|
})
|
|
|
|
Describe("dispatch by entity type", func() {
|
|
Context("when ID is a MediaFile (track)", func() {
|
|
It("calls GetSimilarSongsByTrack and returns matched songs", func() {
|
|
track := model.MediaFile{ID: "track-1", Title: "Just Can't Get Enough", Artist: "Depeche Mode", MbzRecordingID: "track-mbid"}
|
|
matchedSong := model.MediaFile{ID: "matched-1", Title: "Dreaming of Me", Artist: "Depeche Mode"}
|
|
|
|
// GetEntityByID tries Artist, Album, Playlist, then MediaFile
|
|
artistRepo.On("Get", "track-1").Return(nil, model.ErrNotFound).Once()
|
|
albumRepo.On("Get", "track-1").Return(nil, model.ErrNotFound).Once()
|
|
mediaFileRepo.On("Get", "track-1").Return(&track, nil).Once()
|
|
|
|
agentsCombined.On("GetSimilarSongsByTrack", mock.Anything, "track-1", "Just Can't Get Enough", "Depeche Mode", "track-mbid", 5).
|
|
Return([]agents.Song{
|
|
{Name: "Dreaming of Me", MBID: "", Artist: "Depeche Mode", ArtistMBID: "artist-mbid"},
|
|
}, nil).Once()
|
|
|
|
// Mock loadTracksByID - no ID matches
|
|
mediaFileRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
|
_, ok := opt.Filters.(squirrel.Eq)
|
|
return ok
|
|
})).Return(model.MediaFiles{}, nil).Once()
|
|
|
|
// Mock loadTracksByMBID - no MBID matches (empty MBID means this won't be called)
|
|
mediaFileRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
|
and, ok := opt.Filters.(squirrel.And)
|
|
if !ok || len(and) < 1 {
|
|
return false
|
|
}
|
|
eq, hasEq := and[0].(squirrel.Eq)
|
|
if !hasEq {
|
|
return false
|
|
}
|
|
_, hasMBID := eq["mbz_recording_id"]
|
|
return hasMBID
|
|
})).Return(model.MediaFiles{}, nil).Maybe()
|
|
|
|
// Mock loadTracksByTitleAndArtist - queries by artist name
|
|
mediaFileRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
|
and, ok := opt.Filters.(squirrel.And)
|
|
if !ok || len(and) < 2 {
|
|
return false
|
|
}
|
|
eq, hasEq := and[0].(squirrel.Eq)
|
|
if !hasEq {
|
|
return false
|
|
}
|
|
_, hasArtist := eq["order_artist_name"]
|
|
return hasArtist
|
|
})).Return(model.MediaFiles{matchedSong}, nil).Maybe()
|
|
|
|
songs, err := provider.SimilarSongs(ctx, "track-1", 5)
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(songs).To(HaveLen(1))
|
|
Expect(songs[0].ID).To(Equal("matched-1"))
|
|
})
|
|
|
|
It("falls back to artist-based algorithm when GetSimilarSongsByTrack returns empty", func() {
|
|
track := model.MediaFile{ID: "track-1", Title: "Track", Artist: "Artist", ArtistID: "artist-1"}
|
|
artist := model.Artist{ID: "artist-1", Name: "Artist"}
|
|
song := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzRecordingID: "mbid-1"}
|
|
|
|
// GetEntityByID for the initial call tries Artist, Album, Playlist, then MediaFile
|
|
artistRepo.On("Get", "track-1").Return(nil, model.ErrNotFound).Once()
|
|
albumRepo.On("Get", "track-1").Return(nil, model.ErrNotFound).Once()
|
|
mediaFileRepo.On("Get", "track-1").Return(&track, nil).Once()
|
|
|
|
agentsCombined.On("GetSimilarSongsByTrack", mock.Anything, "track-1", "Track", "Artist", "", mock.Anything).
|
|
Return([]agents.Song{}, nil).Once()
|
|
|
|
// Fallback calls getArtist(id) which calls GetEntityByID again - this time it finds the mediafile
|
|
// and recursively calls getArtist(v.ArtistID)
|
|
artistRepo.On("Get", "track-1").Return(nil, model.ErrNotFound).Once()
|
|
albumRepo.On("Get", "track-1").Return(nil, model.ErrNotFound).Once()
|
|
mediaFileRepo.On("Get", "track-1").Return(&track, nil).Once()
|
|
|
|
// Then it recurses with the artist-1 ID
|
|
artistRepo.On("Get", "artist-1").Return(&artist, nil).Maybe()
|
|
artistRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
|
return opt.Max == 1 && opt.Filters != nil
|
|
})).Return(model.Artists{artist}, nil).Maybe()
|
|
|
|
mockAgent.On("GetSimilarArtists", mock.Anything, "artist-1", "Artist", "", 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", "", mock.Anything).
|
|
Return([]agents.Song{{Name: "Song One", MBID: "mbid-1"}}, nil).Once()
|
|
|
|
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song}, nil).Once()
|
|
|
|
songs, err := provider.SimilarSongs(ctx, "track-1", 5)
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(songs).To(HaveLen(1))
|
|
Expect(songs[0].ID).To(Equal("song-1"))
|
|
})
|
|
})
|
|
|
|
Context("when ID is an Album", func() {
|
|
It("calls GetSimilarSongsByAlbum and returns matched songs", func() {
|
|
album := model.Album{ID: "album-1", Name: "Speak & Spell", AlbumArtist: "Depeche Mode", MbzAlbumID: "album-mbid"}
|
|
matchedSong := model.MediaFile{ID: "matched-1", Title: "New Life", Artist: "Depeche Mode", MbzRecordingID: "song-mbid"}
|
|
|
|
// GetEntityByID tries Artist, Album, Playlist, then MediaFile
|
|
artistRepo.On("Get", "album-1").Return(nil, model.ErrNotFound).Once()
|
|
albumRepo.On("Get", "album-1").Return(&album, nil).Once()
|
|
|
|
agentsCombined.On("GetSimilarSongsByAlbum", mock.Anything, "album-1", "Speak & Spell", "Depeche Mode", "album-mbid", 5).
|
|
Return([]agents.Song{
|
|
{Name: "New Life", MBID: "song-mbid", Artist: "Depeche Mode"},
|
|
}, nil).Once()
|
|
|
|
// Mock loadTracksByID - no ID matches
|
|
mediaFileRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
|
_, ok := opt.Filters.(squirrel.Eq)
|
|
return ok
|
|
})).Return(model.MediaFiles{}, nil).Once()
|
|
|
|
// Mock loadTracksByMBID - MBID match
|
|
mediaFileRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
|
and, ok := opt.Filters.(squirrel.And)
|
|
if !ok || len(and) < 1 {
|
|
return false
|
|
}
|
|
_, hasEq := and[0].(squirrel.Eq)
|
|
return hasEq
|
|
})).Return(model.MediaFiles{matchedSong}, nil).Once()
|
|
|
|
songs, err := provider.SimilarSongs(ctx, "album-1", 5)
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(songs).To(HaveLen(1))
|
|
Expect(songs[0].ID).To(Equal("matched-1"))
|
|
})
|
|
|
|
It("falls back when GetSimilarSongsByAlbum returns ErrNotFound", func() {
|
|
album := model.Album{ID: "album-1", Name: "Album", AlbumArtist: "Artist", AlbumArtistID: "artist-1"}
|
|
artist := model.Artist{ID: "artist-1", Name: "Artist"}
|
|
song := model.MediaFile{ID: "song-1", Title: "Song One", ArtistID: "artist-1", MbzRecordingID: "mbid-1"}
|
|
|
|
// GetEntityByID for the initial call tries Artist, Album, Playlist, then MediaFile
|
|
artistRepo.On("Get", "album-1").Return(nil, model.ErrNotFound).Once()
|
|
albumRepo.On("Get", "album-1").Return(&album, nil).Once()
|
|
|
|
agentsCombined.On("GetSimilarSongsByAlbum", mock.Anything, "album-1", "Album", "Artist", "", mock.Anything).
|
|
Return(nil, agents.ErrNotFound).Once()
|
|
|
|
// Fallback calls getArtist(id) which calls GetEntityByID again - this time it finds the album
|
|
// and recursively calls getArtist(v.AlbumArtistID)
|
|
artistRepo.On("Get", "album-1").Return(nil, model.ErrNotFound).Once()
|
|
albumRepo.On("Get", "album-1").Return(&album, nil).Once()
|
|
|
|
// Then it recurses with the artist-1 ID
|
|
artistRepo.On("Get", "artist-1").Return(&artist, nil).Maybe()
|
|
artistRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
|
return opt.Max == 1 && opt.Filters != nil
|
|
})).Return(model.Artists{artist}, nil).Maybe()
|
|
|
|
mockAgent.On("GetSimilarArtists", mock.Anything, "artist-1", "Artist", "", 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", "", mock.Anything).
|
|
Return([]agents.Song{{Name: "Song One", MBID: "mbid-1"}}, nil).Once()
|
|
|
|
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song}, nil).Once()
|
|
|
|
songs, err := provider.SimilarSongs(ctx, "album-1", 5)
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(songs).To(HaveLen(1))
|
|
Expect(songs[0].ID).To(Equal("song-1"))
|
|
})
|
|
})
|
|
|
|
Context("when ID is an Artist", func() {
|
|
It("calls GetSimilarSongsByArtist and returns matched songs", func() {
|
|
artist := model.Artist{ID: "artist-1", Name: "Depeche Mode", MbzArtistID: "artist-mbid"}
|
|
matchedSong := model.MediaFile{ID: "matched-1", Title: "Enjoy the Silence", Artist: "Depeche Mode", MbzRecordingID: "song-mbid"}
|
|
|
|
artistRepo.On("Get", "artist-1").Return(&artist, nil).Once()
|
|
agentsCombined.On("GetSimilarSongsByArtist", mock.Anything, "artist-1", "Depeche Mode", "artist-mbid", 5).
|
|
Return([]agents.Song{
|
|
{Name: "Enjoy the Silence", MBID: "song-mbid", Artist: "Depeche Mode"},
|
|
}, nil).Once()
|
|
|
|
// Mock loadTracksByID - no ID matches
|
|
mediaFileRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
|
_, ok := opt.Filters.(squirrel.Eq)
|
|
return ok
|
|
})).Return(model.MediaFiles{}, nil).Once()
|
|
|
|
// Mock loadTracksByMBID - MBID match
|
|
mediaFileRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
|
and, ok := opt.Filters.(squirrel.And)
|
|
if !ok || len(and) < 1 {
|
|
return false
|
|
}
|
|
_, hasEq := and[0].(squirrel.Eq)
|
|
return hasEq
|
|
})).Return(model.MediaFiles{matchedSong}, nil).Once()
|
|
|
|
songs, err := provider.SimilarSongs(ctx, "artist-1", 5)
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(songs).To(HaveLen(1))
|
|
Expect(songs[0].ID).To(Equal("matched-1"))
|
|
})
|
|
})
|
|
})
|
|
|
|
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", MbzRecordingID: "mbid-1"}
|
|
song2 := model.MediaFile{ID: "song-2", Title: "Song Two", ArtistID: "artist-1", MbzRecordingID: "mbid-2"}
|
|
song3 := model.MediaFile{ID: "song-3", Title: "Song Three", ArtistID: "artist-3", MbzRecordingID: "mbid-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()
|
|
|
|
// New similar songs by artist returns ErrNotFound to trigger fallback
|
|
agentsCombined.On("GetSimilarSongsByArtist", mock.Anything, "artist-1", "Artist One", "", mock.Anything).
|
|
Return(nil, agents.ErrNotFound).Once()
|
|
|
|
similarAgentsResp := []agents.Artist{
|
|
{Name: "Similar Artist", MBID: "similar-mbid"},
|
|
}
|
|
mockAgent.On("GetSimilarArtists", mock.Anything, "artist-1", "Artist One", "", 15).
|
|
Return(similarAgentsResp, nil).Once()
|
|
|
|
// Mock the three-phase artist lookup: ID (skipped - no IDs), MBID, then Name
|
|
// MBID lookup returns empty (no match)
|
|
artistRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
|
_, ok := opt.Filters.(squirrel.Eq)
|
|
return opt.Max == 0 && ok
|
|
})).Return(model.Artists{}, nil).Once()
|
|
// Name lookup returns the similar artist
|
|
artistRepo.On("GetAll", mock.MatchedBy(func(opt model.QueryOptions) bool {
|
|
_, ok := opt.Filters.(squirrel.Or)
|
|
return opt.Max == 0 && ok
|
|
})).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.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1, song2}, nil).Once()
|
|
mediaFileRepo.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song3}, nil).Once()
|
|
|
|
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)
|
|
albumRepo.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", MbzRecordingID: "mbid-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()
|
|
|
|
// New similar songs by artist returns ErrNotFound to trigger fallback
|
|
agentsCombined.On("GetSimilarSongsByArtist", mock.Anything, "artist-1", "Artist One", "", mock.Anything).
|
|
Return(nil, agents.ErrNotFound).Once()
|
|
|
|
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.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1}, nil).Once()
|
|
|
|
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()
|
|
|
|
// New similar songs by artist returns ErrNotFound to trigger fallback
|
|
agentsCombined.On("GetSimilarSongsByArtist", mock.Anything, "artist-1", "Artist One", "", mock.Anything).
|
|
Return(nil, agents.ErrNotFound).Once()
|
|
|
|
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", MbzRecordingID: "mbid-1"}
|
|
song2 := model.MediaFile{ID: "song-2", Title: "Song Two", ArtistID: "artist-1", MbzRecordingID: "mbid-2"}
|
|
|
|
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()
|
|
|
|
// New similar songs by artist returns ErrNotFound to trigger fallback
|
|
agentsCombined.On("GetSimilarSongsByArtist", mock.Anything, "artist-1", "Artist One", "", mock.Anything).
|
|
Return(nil, agents.ErrNotFound).Once()
|
|
|
|
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.On("GetAll", mock.AnythingOfType("model.QueryOptions")).Return(model.MediaFiles{song1, song2}, nil).Once()
|
|
|
|
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"))
|
|
})
|
|
})
|