Refactored agents calling into its own struct

This commit is contained in:
Deluan 2021-06-08 16:57:19 -04:00
parent 6c1ba8f0d0
commit f9eec5e4dc
5 changed files with 516 additions and 190 deletions

159
core/agents/agents.go Normal file
View File

@ -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)

244
core/agents/agents_test.go Normal file
View File

@ -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
}

View File

@ -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, "<a ", "<a target='_blank' ")
}
func (e *externalMetadata) callGetImage(ctx context.Context, agent agents.ArtistImageRetriever, artist *auxArtist) {
images, err := agent.GetImages(artist.ID, artist.Name, artist.MbzArtistID)
if len(images) == 0 || err != nil {
return
}
sort.Slice(images, func(i, j int) bool { return images[i].Size > 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, "<a ", "<a target='_blank' ")
log.Debug(ctx, "Got Biography", "agent", a.AgentName(), "artist", artist.Name, "len", len(bio), "elapsed", time.Since(start))
break
}
}
}()
}
func (e *externalMetadata) callGetImage(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.ArtistImageRetriever)
if !ok {
continue
}
images, err := agent.GetImages(artist.ID, artist.Name, artist.MbzArtistID)
if len(images) == 0 || err != nil {
continue
}
log.Debug(ctx, "Got Images", "agent", a.AgentName(), "artist", artist.Name, "images", images, "elapsed", time.Since(start))
sort.Slice(images, func(i, j int) bool { return images[i].Size > 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) {

12
utils/context.go Normal file
View File

@ -0,0 +1,12 @@
package utils
import "context"
func IsCtxDone(ctx context.Context) bool {
select {
case <-ctx.Done():
return true
default:
return false
}
}

23
utils/context_test.go Normal file
View File

@ -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())
})
})