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, "<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) {
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())
+	})
+})