From f9eec5e4dc6faad9247e208b5e7540feb8f98ce5 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 8 Jun 2021 16:57:19 -0400 Subject: [PATCH] Refactored agents calling into its own struct --- core/agents/agents.go | 159 ++++++++++++++++++++++ core/agents/agents_test.go | 244 +++++++++++++++++++++++++++++++++ core/external_metadata.go | 268 +++++++++++-------------------------- utils/context.go | 12 ++ utils/context_test.go | 23 ++++ 5 files changed, 516 insertions(+), 190 deletions(-) create mode 100644 core/agents/agents.go create mode 100644 core/agents/agents_test.go create mode 100644 utils/context.go create mode 100644 utils/context_test.go diff --git a/core/agents/agents.go b/core/agents/agents.go new file mode 100644 index 00000000..edc2af0c --- /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 00000000..61982a76 --- /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 c8efca13..5b61a960 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 00000000..c6f1ef7f --- /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 00000000..be9b5eba --- /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()) + }) +})