diff --git a/core/agents/agents.go b/core/agents/agents.go
new file mode 100644
index 000000000..edc2af0cb
--- /dev/null
+++ b/core/agents/agents.go
@@ -0,0 +1,159 @@
+package agents
+
+import (
+ "context"
+ "strings"
+ "time"
+
+ "github.com/navidrome/navidrome/conf"
+ "github.com/navidrome/navidrome/log"
+ "github.com/navidrome/navidrome/utils"
+)
+
+type Agents struct {
+ ctx context.Context
+ agents []Interface
+}
+
+func NewAgents(ctx context.Context) *Agents {
+ order := strings.Split(conf.Server.Agents, ",")
+ order = append(order, PlaceholderAgentName)
+ var res []Interface
+ for _, name := range order {
+ init, ok := Map[name]
+ if !ok {
+ log.Error(ctx, "Agent not available. Check configuration", "name", name)
+ continue
+ }
+
+ res = append(res, init(ctx))
+ }
+
+ return &Agents{ctx: ctx, agents: res}
+}
+
+func (a *Agents) AgentName() string {
+ return "agents"
+}
+
+func (a *Agents) GetMBID(id string, name string) (string, error) {
+ start := time.Now()
+ for _, ag := range a.agents {
+ if utils.IsCtxDone(a.ctx) {
+ break
+ }
+ agent, ok := ag.(ArtistMBIDRetriever)
+ if !ok {
+ continue
+ }
+ mbid, err := agent.GetMBID(id, name)
+ if mbid != "" && err == nil {
+ log.Debug(a.ctx, "Got MBID", "agent", ag.AgentName(), "artist", name, "mbid", mbid, "elapsed", time.Since(start))
+ return mbid, err
+ }
+ }
+ return "", ErrNotFound
+}
+
+func (a *Agents) GetURL(id, name, mbid string) (string, error) {
+ start := time.Now()
+ for _, ag := range a.agents {
+ if utils.IsCtxDone(a.ctx) {
+ break
+ }
+ agent, ok := ag.(ArtistURLRetriever)
+ if !ok {
+ continue
+ }
+ url, err := agent.GetURL(id, name, mbid)
+ if url != "" && err == nil {
+ log.Debug(a.ctx, "Got External Url", "agent", ag.AgentName(), "artist", name, "url", url, "elapsed", time.Since(start))
+ return url, err
+ }
+ }
+ return "", ErrNotFound
+}
+
+func (a *Agents) GetBiography(id, name, mbid string) (string, error) {
+ start := time.Now()
+ for _, ag := range a.agents {
+ if utils.IsCtxDone(a.ctx) {
+ break
+ }
+ agent, ok := ag.(ArtistBiographyRetriever)
+ if !ok {
+ continue
+ }
+ bio, err := agent.GetBiography(id, name, mbid)
+ if bio != "" && err == nil {
+ log.Debug(a.ctx, "Got Biography", "agent", ag.AgentName(), "artist", name, "len", len(bio), "elapsed", time.Since(start))
+ return bio, err
+ }
+ }
+ return "", ErrNotFound
+}
+
+func (a *Agents) GetSimilar(id, name, mbid string, limit int) ([]Artist, error) {
+ start := time.Now()
+ for _, ag := range a.agents {
+ if utils.IsCtxDone(a.ctx) {
+ break
+ }
+ agent, ok := ag.(ArtistSimilarRetriever)
+ if !ok {
+ continue
+ }
+ similar, err := agent.GetSimilar(id, name, mbid, limit)
+ if len(similar) >= 0 && err == nil {
+ log.Debug(a.ctx, "Got Similar Artists", "agent", ag.AgentName(), "artist", name, "similar", similar, "elapsed", time.Since(start))
+ return similar, err
+ }
+ }
+ return nil, ErrNotFound
+}
+
+func (a *Agents) GetImages(id, name, mbid string) ([]ArtistImage, error) {
+ start := time.Now()
+ for _, ag := range a.agents {
+ if utils.IsCtxDone(a.ctx) {
+ break
+ }
+ agent, ok := ag.(ArtistImageRetriever)
+ if !ok {
+ continue
+ }
+ images, err := agent.GetImages(id, name, mbid)
+ if len(images) > 0 && err == nil {
+ log.Debug(a.ctx, "Got Images", "agent", ag.AgentName(), "artist", name, "images", images, "elapsed", time.Since(start))
+ return images, err
+ }
+ }
+ return nil, ErrNotFound
+}
+
+func (a *Agents) GetTopSongs(id, artistName, mbid string, count int) ([]Song, error) {
+ start := time.Now()
+ for _, ag := range a.agents {
+ if utils.IsCtxDone(a.ctx) {
+ break
+ }
+ agent, ok := ag.(ArtistTopSongsRetriever)
+ if !ok {
+ continue
+ }
+ songs, err := agent.GetTopSongs(id, artistName, mbid, count)
+ if len(songs) > 0 && err == nil {
+ log.Debug(a.ctx, "Got Top Songs", "agent", ag.AgentName(), "artist", artistName, "songs", songs, "elapsed", time.Since(start))
+ return songs, err
+ }
+ }
+ return nil, ErrNotFound
+}
+
+var _ Interface = (*Agents)(nil)
+var _ ArtistMBIDRetriever = (*Agents)(nil)
+var _ ArtistURLRetriever = (*Agents)(nil)
+var _ ArtistBiographyRetriever = (*Agents)(nil)
+var _ ArtistSimilarRetriever = (*Agents)(nil)
+var _ ArtistImageRetriever = (*Agents)(nil)
+var _ ArtistTopSongsRetriever = (*Agents)(nil)
diff --git a/core/agents/agents_test.go b/core/agents/agents_test.go
new file mode 100644
index 000000000..61982a764
--- /dev/null
+++ b/core/agents/agents_test.go
@@ -0,0 +1,244 @@
+package agents
+
+import (
+ "context"
+ "errors"
+
+ "github.com/navidrome/navidrome/conf"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("Agents", func() {
+ var ctx context.Context
+ var cancel context.CancelFunc
+ BeforeEach(func() {
+ ctx, cancel = context.WithCancel(context.Background())
+ })
+
+ Describe("Placeholder", func() {
+ var ag *Agents
+ BeforeEach(func() {
+ conf.Server.Agents = ""
+ ag = NewAgents(ctx)
+ })
+
+ It("calls the placeholder GetBiography", func() {
+ Expect(ag.GetBiography("123", "John Doe", "mb123")).To(Equal(placeholderBiography))
+ })
+ It("calls the placeholder GetImages", func() {
+ images, err := ag.GetImages("123", "John Doe", "mb123")
+ Expect(err).ToNot(HaveOccurred())
+ Expect(images).To(HaveLen(3))
+ for _, i := range images {
+ Expect(i.URL).To(BeElementOf(placeholderArtistImageSmallUrl, placeholderArtistImageMediumUrl, placeholderArtistImageLargeUrl))
+ }
+ })
+ })
+
+ Describe("Agents", func() {
+ var ag *Agents
+ var mock *mockAgent
+ BeforeEach(func() {
+ mock = &mockAgent{}
+ Register("fake", func(ctx context.Context) Interface {
+ return mock
+ })
+ Register("empty", func(ctx context.Context) Interface {
+ return struct {
+ Interface
+ }{}
+ })
+ conf.Server.Agents = "empty,fake"
+ ag = NewAgents(ctx)
+ Expect(ag.AgentName()).To(Equal("agents"))
+ })
+
+ Describe("GetMBID", func() {
+ It("returns on first match", func() {
+ Expect(ag.GetMBID("123", "test")).To(Equal("mbid"))
+ Expect(mock.Args).To(ConsistOf("123", "test"))
+ })
+ It("skips the agent if it returns an error", func() {
+ mock.Err = errors.New("error")
+ _, err := ag.GetMBID("123", "test")
+ Expect(err).To(MatchError(ErrNotFound))
+ Expect(mock.Args).To(ConsistOf("123", "test"))
+ })
+ It("interrupts if the context is canceled", func() {
+ cancel()
+ _, err := ag.GetMBID("123", "test")
+ Expect(err).To(MatchError(ErrNotFound))
+ Expect(mock.Args).To(BeEmpty())
+ })
+ })
+
+ Describe("GetURL", func() {
+ It("returns on first match", func() {
+ Expect(ag.GetURL("123", "test", "mb123")).To(Equal("url"))
+ Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
+ })
+ It("skips the agent if it returns an error", func() {
+ mock.Err = errors.New("error")
+ _, err := ag.GetURL("123", "test", "mb123")
+ Expect(err).To(MatchError(ErrNotFound))
+ Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
+ })
+ It("interrupts if the context is canceled", func() {
+ cancel()
+ _, err := ag.GetURL("123", "test", "mb123")
+ Expect(err).To(MatchError(ErrNotFound))
+ Expect(mock.Args).To(BeEmpty())
+ })
+ })
+
+ Describe("GetBiography", func() {
+ It("returns on first match", func() {
+ Expect(ag.GetBiography("123", "test", "mb123")).To(Equal("bio"))
+ Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
+ })
+ It("skips the agent if it returns an error", func() {
+ mock.Err = errors.New("error")
+ Expect(ag.GetBiography("123", "test", "mb123")).To(Equal(placeholderBiography))
+ Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
+ })
+ It("interrupts if the context is canceled", func() {
+ cancel()
+ _, err := ag.GetBiography("123", "test", "mb123")
+ Expect(err).To(MatchError(ErrNotFound))
+ Expect(mock.Args).To(BeEmpty())
+ })
+ })
+
+ Describe("GetImages", func() {
+ It("returns on first match", func() {
+ Expect(ag.GetImages("123", "test", "mb123")).To(Equal([]ArtistImage{{
+ URL: "imageUrl",
+ Size: 100,
+ }}))
+ Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
+ })
+ It("skips the agent if it returns an error", func() {
+ mock.Err = errors.New("error")
+ Expect(ag.GetImages("123", "test", "mb123")).To(HaveLen(3))
+ Expect(mock.Args).To(ConsistOf("123", "test", "mb123"))
+ })
+ It("interrupts if the context is canceled", func() {
+ cancel()
+ _, err := ag.GetImages("123", "test", "mb123")
+ Expect(err).To(MatchError(ErrNotFound))
+ Expect(mock.Args).To(BeEmpty())
+ })
+ })
+
+ Describe("GetSimilar", func() {
+ It("returns on first match", func() {
+ Expect(ag.GetSimilar("123", "test", "mb123", 1)).To(Equal([]Artist{{
+ Name: "Joe Dohn",
+ MBID: "mbid321",
+ }}))
+ Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 1))
+ })
+ It("skips the agent if it returns an error", func() {
+ mock.Err = errors.New("error")
+ _, err := ag.GetSimilar("123", "test", "mb123", 1)
+ Expect(err).To(MatchError(ErrNotFound))
+ Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 1))
+ })
+ It("interrupts if the context is canceled", func() {
+ cancel()
+ _, err := ag.GetSimilar("123", "test", "mb123", 1)
+ Expect(err).To(MatchError(ErrNotFound))
+ Expect(mock.Args).To(BeEmpty())
+ })
+ })
+
+ Describe("GetTopSongs", func() {
+ It("returns on first match", func() {
+ Expect(ag.GetTopSongs("123", "test", "mb123", 2)).To(Equal([]Song{{
+ Name: "A Song",
+ MBID: "mbid444",
+ }}))
+ Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 2))
+ })
+ It("skips the agent if it returns an error", func() {
+ mock.Err = errors.New("error")
+ _, err := ag.GetTopSongs("123", "test", "mb123", 2)
+ Expect(err).To(MatchError(ErrNotFound))
+ Expect(mock.Args).To(ConsistOf("123", "test", "mb123", 2))
+ })
+ It("interrupts if the context is canceled", func() {
+ cancel()
+ _, err := ag.GetTopSongs("123", "test", "mb123", 2)
+ Expect(err).To(MatchError(ErrNotFound))
+ Expect(mock.Args).To(BeEmpty())
+ })
+ })
+ })
+})
+
+type mockAgent struct {
+ Args []interface{}
+ Err error
+}
+
+func (a *mockAgent) AgentName() string {
+ return "fake"
+}
+
+func (a *mockAgent) GetMBID(id string, name string) (string, error) {
+ a.Args = []interface{}{id, name}
+ if a.Err != nil {
+ return "", a.Err
+ }
+ return "mbid", nil
+}
+
+func (a *mockAgent) GetURL(id, name, mbid string) (string, error) {
+ a.Args = []interface{}{id, name, mbid}
+ if a.Err != nil {
+ return "", a.Err
+ }
+ return "url", nil
+}
+
+func (a *mockAgent) GetBiography(id, name, mbid string) (string, error) {
+ a.Args = []interface{}{id, name, mbid}
+ if a.Err != nil {
+ return "", a.Err
+ }
+ return "bio", nil
+}
+
+func (a *mockAgent) GetImages(id, name, mbid string) ([]ArtistImage, error) {
+ a.Args = []interface{}{id, name, mbid}
+ if a.Err != nil {
+ return nil, a.Err
+ }
+ return []ArtistImage{{
+ URL: "imageUrl",
+ Size: 100,
+ }}, nil
+}
+
+func (a *mockAgent) GetSimilar(id, name, mbid string, limit int) ([]Artist, error) {
+ a.Args = []interface{}{id, name, mbid, limit}
+ if a.Err != nil {
+ return nil, a.Err
+ }
+ return []Artist{{
+ Name: "Joe Dohn",
+ MBID: "mbid321",
+ }}, nil
+}
+
+func (a *mockAgent) GetTopSongs(id, artistName, mbid string, count int) ([]Song, error) {
+ a.Args = []interface{}{id, artistName, mbid, count}
+ if a.Err != nil {
+ return nil, a.Err
+ }
+ return []Song{{
+ Name: "A Song",
+ MBID: "mbid444",
+ }}, nil
+}
diff --git a/core/external_metadata.go b/core/external_metadata.go
index c8efca137..5b61a9606 100644
--- a/core/external_metadata.go
+++ b/core/external_metadata.go
@@ -9,9 +9,10 @@ import (
"github.com/Masterminds/squirrel"
"github.com/microcosm-cc/bluemonday"
- "github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/core/agents"
+ _ "github.com/navidrome/navidrome/core/agents/lastfm"
+ _ "github.com/navidrome/navidrome/core/agents/spotify"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils"
@@ -41,23 +42,6 @@ func NewExternalMetadata(ds model.DataStore) ExternalMetadata {
return &externalMetadata{ds: ds}
}
-func (e *externalMetadata) initAgents(ctx context.Context) []agents.Interface {
- order := strings.Split(conf.Server.Agents, ",")
- order = append(order, agents.PlaceholderAgentName)
- var res []agents.Interface
- for _, name := range order {
- init, ok := agents.Map[name]
- if !ok {
- log.Error(ctx, "Agent not available. Check configuration", "name", name)
- continue
- }
-
- res = append(res, init(ctx))
- }
-
- return res
-}
-
func (e *externalMetadata) getArtist(ctx context.Context, id string) (*auxArtist, error) {
var entity interface{}
entity, err := GetEntityByID(ctx, e.ds, id)
@@ -122,22 +106,25 @@ func (e *externalMetadata) UpdateArtistInfo(ctx context.Context, id string, simi
}
func (e *externalMetadata) refreshArtistInfo(ctx context.Context, artist *auxArtist) error {
- allAgents := e.initAgents(ctx)
+ ag := agents.NewAgents(ctx)
// Get MBID first, if it is not yet available
if artist.MbzArtistID == "" {
- e.callGetMBID(ctx, allAgents, artist)
+ mbid, err := ag.GetMBID(artist.ID, artist.Name)
+ if mbid != "" && err == nil {
+ artist.MbzArtistID = mbid
+ }
}
// Call all registered agents and collect information
- wg := &sync.WaitGroup{}
- e.callGetBiography(ctx, allAgents, artist, wg)
- e.callGetURL(ctx, allAgents, artist, wg)
- e.callGetImage(ctx, allAgents, artist, wg)
- e.callGetSimilar(ctx, allAgents, artist, maxSimilarArtists, true, wg)
- wg.Wait()
+ callParallel([]func(){
+ func() { e.callGetBiography(ctx, ag, artist) },
+ func() { e.callGetURL(ctx, ag, artist) },
+ func() { e.callGetImage(ctx, ag, artist) },
+ func() { e.callGetSimilar(ctx, ag, artist, maxSimilarArtists, true) },
+ })
- if isDone(ctx) {
+ if utils.IsCtxDone(ctx) {
log.Warn(ctx, "ArtistInfo update canceled", ctx.Err())
return ctx.Err()
}
@@ -152,18 +139,27 @@ func (e *externalMetadata) refreshArtistInfo(ctx context.Context, artist *auxArt
return nil
}
+func callParallel(fs []func()) {
+ wg := &sync.WaitGroup{}
+ wg.Add(len(fs))
+ for _, f := range fs {
+ go func(f func()) {
+ f()
+ wg.Done()
+ }(f)
+ }
+ wg.Wait()
+}
+
func (e *externalMetadata) SimilarSongs(ctx context.Context, id string, count int) (model.MediaFiles, error) {
- allAgents := e.initAgents(ctx)
+ ag := agents.NewAgents(ctx)
artist, err := e.getArtist(ctx, id)
if err != nil {
return nil, err
}
- wg := &sync.WaitGroup{}
- e.callGetSimilar(ctx, allAgents, artist, 15, false, wg)
- wg.Wait()
-
- if isDone(ctx) {
+ e.callGetSimilar(ctx, ag, artist, 15, false)
+ if utils.IsCtxDone(ctx) {
log.Warn(ctx, "SimilarSongs call canceled", ctx.Err())
return nil, ctx.Err()
}
@@ -173,13 +169,13 @@ func (e *externalMetadata) SimilarSongs(ctx context.Context, id string, count in
weightedSongs := utils.NewWeightedRandomChooser()
for _, a := range artists {
- if isDone(ctx) {
+ if utils.IsCtxDone(ctx) {
log.Warn(ctx, "SimilarSongs call canceled", ctx.Err())
return nil, ctx.Err()
}
topCount := utils.MaxInt(count, 20)
- topSongs, err := e.getMatchingTopSongs(ctx, allAgents, &auxArtist{Name: a.Name, Artist: a}, topCount)
+ topSongs, err := e.getMatchingTopSongs(ctx, ag, &auxArtist{Name: a.Name, Artist: a}, topCount)
if err != nil {
log.Warn(ctx, "Error getting artist's top songs", "artist", a.Name, err)
continue
@@ -206,18 +202,18 @@ func (e *externalMetadata) SimilarSongs(ctx context.Context, id string, count in
}
func (e *externalMetadata) TopSongs(ctx context.Context, artistName string, count int) (model.MediaFiles, error) {
- allAgents := e.initAgents(ctx)
+ ag := agents.NewAgents(ctx)
artist, err := e.findArtistByName(ctx, artistName)
if err != nil {
log.Error(ctx, "Artist not found", "name", artistName, err)
return nil, nil
}
- return e.getMatchingTopSongs(ctx, allAgents, artist, count)
+ return e.getMatchingTopSongs(ctx, ag, artist, count)
}
-func (e *externalMetadata) getMatchingTopSongs(ctx context.Context, allAgents []agents.Interface, artist *auxArtist, count int) (model.MediaFiles, error) {
- songs, err := e.callGetTopSongs(ctx, allAgents, artist, 50)
+func (e *externalMetadata) getMatchingTopSongs(ctx context.Context, agent agents.ArtistTopSongsRetriever, artist *auxArtist, count int) (model.MediaFiles, error) {
+ songs, err := agent.GetTopSongs(artist.ID, artist.Name, artist.MbzArtistID, count)
if err != nil {
return nil, err
}
@@ -261,162 +257,54 @@ func (e *externalMetadata) findMatchingTrack(ctx context.Context, mbid string, a
return &mfs[0], nil
}
-func isDone(ctx context.Context) bool {
- select {
- case <-ctx.Done():
- return true
- default:
- return false
+func (e *externalMetadata) callGetURL(ctx context.Context, agent agents.ArtistURLRetriever, artist *auxArtist) {
+ url, err := agent.GetURL(artist.ID, artist.Name, artist.MbzArtistID)
+ if url == "" || err != nil {
+ return
+ }
+ artist.ExternalUrl = url
+}
+
+func (e *externalMetadata) callGetBiography(ctx context.Context, agent agents.ArtistBiographyRetriever, artist *auxArtist) {
+ bio, err := agent.GetBiography(artist.ID, clearName(artist.Name), artist.MbzArtistID)
+ if bio == "" || err != nil {
+ return
+ }
+ policy := bluemonday.UGCPolicy()
+ bio = policy.Sanitize(bio)
+ bio = strings.ReplaceAll(bio, "\n", " ")
+ artist.Biography = strings.ReplaceAll(bio, " images[j].Size })
+
+ if len(images) >= 1 {
+ artist.LargeImageUrl = images[0].URL
+ }
+ if len(images) >= 2 {
+ artist.MediumImageUrl = images[1].URL
+ }
+ if len(images) >= 3 {
+ artist.SmallImageUrl = images[2].URL
}
}
-func (e *externalMetadata) callGetMBID(ctx context.Context, allAgents []agents.Interface, artist *auxArtist) {
- start := time.Now()
- for _, a := range allAgents {
- if isDone(ctx) {
- break
- }
- agent, ok := a.(agents.ArtistMBIDRetriever)
- if !ok {
- continue
- }
- mbid, err := agent.GetMBID(artist.ID, artist.Name)
- if mbid != "" && err == nil {
- artist.MbzArtistID = mbid
- log.Debug(ctx, "Got MBID", "agent", a.AgentName(), "artist", artist.Name, "mbid", mbid, "elapsed", time.Since(start))
- break
- }
+func (e *externalMetadata) callGetSimilar(ctx context.Context, agent agents.ArtistSimilarRetriever, artist *auxArtist,
+ limit int, includeNotPresent bool) {
+ similar, err := agent.GetSimilar(artist.ID, artist.Name, artist.MbzArtistID, limit)
+ if len(similar) == 0 || err != nil {
+ return
}
-}
-
-func (e *externalMetadata) callGetTopSongs(ctx context.Context, allAgents []agents.Interface, artist *auxArtist,
- count int) ([]agents.Song, error) {
- start := time.Now()
- for _, a := range allAgents {
- if isDone(ctx) {
- break
- }
- agent, ok := a.(agents.ArtistTopSongsRetriever)
- if !ok {
- continue
- }
- songs, err := agent.GetTopSongs(artist.ID, artist.Name, artist.MbzArtistID, count)
- if len(songs) > 0 && err == nil {
- log.Debug(ctx, "Got Top Songs", "agent", a.AgentName(), "artist", artist.Name, "songs", songs, "elapsed", time.Since(start))
- return songs, err
- }
+ sa, err := e.mapSimilarArtists(ctx, similar, includeNotPresent)
+ if err != nil {
+ return
}
- return nil, nil
-}
-
-func (e *externalMetadata) callGetURL(ctx context.Context, allAgents []agents.Interface, artist *auxArtist, wg *sync.WaitGroup) {
- wg.Add(1)
- go func() {
- defer wg.Done()
- start := time.Now()
- for _, a := range allAgents {
- if isDone(ctx) {
- break
- }
- agent, ok := a.(agents.ArtistURLRetriever)
- if !ok {
- continue
- }
- url, err := agent.GetURL(artist.ID, artist.Name, artist.MbzArtistID)
- if url != "" && err == nil {
- artist.ExternalUrl = url
- log.Debug(ctx, "Got External Url", "agent", a.AgentName(), "artist", artist.Name, "url", url, "elapsed", time.Since(start))
- break
- }
- }
- }()
-}
-
-func (e *externalMetadata) callGetBiography(ctx context.Context, allAgents []agents.Interface, artist *auxArtist, wg *sync.WaitGroup) {
- wg.Add(1)
- go func() {
- defer wg.Done()
- start := time.Now()
- for _, a := range allAgents {
- if isDone(ctx) {
- break
- }
- agent, ok := a.(agents.ArtistBiographyRetriever)
- if !ok {
- continue
- }
- bio, err := agent.GetBiography(artist.ID, clearName(artist.Name), artist.MbzArtistID)
- if bio != "" && err == nil {
- policy := bluemonday.UGCPolicy()
- bio = policy.Sanitize(bio)
- bio = strings.ReplaceAll(bio, "\n", " ")
- artist.Biography = strings.ReplaceAll(bio, " images[j].Size })
- if len(images) >= 1 {
- artist.LargeImageUrl = images[0].URL
- }
- if len(images) >= 2 {
- artist.MediumImageUrl = images[1].URL
- }
- if len(images) >= 3 {
- artist.SmallImageUrl = images[2].URL
- }
- break
- }
- }()
-}
-
-func (e *externalMetadata) callGetSimilar(ctx context.Context, allAgents []agents.Interface, artist *auxArtist, limit int, includeNotPresent bool, wg *sync.WaitGroup) {
- wg.Add(1)
- go func() {
- defer wg.Done()
- start := time.Now()
- for _, a := range allAgents {
- if isDone(ctx) {
- break
- }
- agent, ok := a.(agents.ArtistSimilarRetriever)
- if !ok {
- continue
- }
- similar, err := agent.GetSimilar(artist.ID, artist.Name, artist.MbzArtistID, limit)
- if len(similar) == 0 || err != nil {
- continue
- }
- sa, err := e.mapSimilarArtists(ctx, similar, includeNotPresent)
- if err != nil {
- continue
- }
- log.Debug(ctx, "Got Similar Artists", "agent", a.AgentName(), "artist", artist.Name, "similar", similar, "elapsed", time.Since(start))
- artist.SimilarArtists = sa
- break
- }
- }()
+ artist.SimilarArtists = sa
}
func (e *externalMetadata) mapSimilarArtists(ctx context.Context, similar []agents.Artist, includeNotPresent bool) (model.Artists, error) {
diff --git a/utils/context.go b/utils/context.go
new file mode 100644
index 000000000..c6f1ef7f3
--- /dev/null
+++ b/utils/context.go
@@ -0,0 +1,12 @@
+package utils
+
+import "context"
+
+func IsCtxDone(ctx context.Context) bool {
+ select {
+ case <-ctx.Done():
+ return true
+ default:
+ return false
+ }
+}
diff --git a/utils/context_test.go b/utils/context_test.go
new file mode 100644
index 000000000..be9b5eba9
--- /dev/null
+++ b/utils/context_test.go
@@ -0,0 +1,23 @@
+package utils_test
+
+import (
+ "context"
+
+ "github.com/navidrome/navidrome/utils"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("IsCtxDone", func() {
+ It("returns false if the context is not done", func() {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ Expect(utils.IsCtxDone(ctx)).To(BeFalse())
+ })
+
+ It("returns true if the context is done", func() {
+ ctx, cancel := context.WithCancel(context.Background())
+ cancel()
+ Expect(utils.IsCtxDone(ctx)).To(BeTrue())
+ })
+})