From 81e9c5b4e9d605066893f8a61a3db2edad57ca5e Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 22 Dec 2025 10:44:30 -0500 Subject: [PATCH] refactor(plugins): implement plugin function call helper and refactor MetadataAgent methods Signed-off-by: Deluan --- plugins/manager.go | 69 +++++--- plugins/manager_test.go | 43 +++++ plugins/metadata_agent.go | 280 +++------------------------------ plugins/metadata_agent_test.go | 87 ++++++---- plugins/metadata_types.go | 100 ++++++++++++ plugins/watcher_test.go | 1 + 6 files changed, 268 insertions(+), 312 deletions(-) create mode 100644 plugins/metadata_types.go diff --git a/plugins/manager.go b/plugins/manager.go index 93fe5c505..3b98fdc65 100644 --- a/plugins/manager.go +++ b/plugins/manager.go @@ -55,6 +55,12 @@ type pluginInstance struct { capabilities []Capability // Auto-detected capabilities based on exported functions } +func (p *pluginInstance) create() (*extism.Plugin, error) { + return p.compiled.Instance(context.Background(), extism.PluginInstanceConfig{ + ModuleConfig: wazero.NewModuleConfig().WithSysWalltime().WithRandSource(rand.Reader), + }) +} + // GetManager returns a singleton instance of the plugin manager. // The manager is not started automatically; call Start() to begin loading plugins. func GetManager() *Manager { @@ -174,14 +180,11 @@ func (m *Manager) LoadMediaAgent(name string) (agents.Interface, bool) { return nil, false } - // Create a new plugin instance for this agent - agent, err := m.createMetadataAgent(instance) - if err != nil { - log.Error("Failed to create metadata agent from plugin", "plugin", name, err) - return nil, false - } - - return agent, true + // Create a new metadata agent adapter for this plugin + return &MetadataAgent{ + name: instance.name, + plugin: instance, + }, true } // LoadScrobbler loads and returns a scrobbler plugin by name. @@ -340,19 +343,6 @@ func (m *Manager) getPluginConfig(name string) map[string]string { return conf.Server.PluginConfig[name] } -// createMetadataAgent creates a new MetadataAgent from a plugin instance -func (m *Manager) createMetadataAgent(instance *pluginInstance) (*MetadataAgent, error) { - // Create a new plugin instance from the compiled plugin - plugin, err := instance.compiled.Instance(m.ctx, extism.PluginInstanceConfig{ - ModuleConfig: wazero.NewModuleConfig().WithSysWalltime().WithRandSource(rand.Reader), - }) - if err != nil { - return nil, err - } - - return NewMetadataAgent(instance.name, plugin), nil -} - // UnloadPlugin removes a plugin from the manager and closes its resources. // Returns an error if the plugin is not found. func (m *Manager) UnloadPlugin(name string) error { @@ -430,6 +420,43 @@ func (m *Manager) ReloadPlugin(name string) error { return nil } +// callPluginFunction is a helper to call a plugin function with input and output types. +// It handles JSON marshalling/unmarshalling and error checking. +func callPluginFunction[I any, O any](ctx context.Context, plugin *pluginInstance, funcName string, input I) (O, error) { + var result O + + // Create plugin instance + p, err := plugin.create() + if err != nil { + return result, fmt.Errorf("failed to create plugin: %w", err) + } + defer p.Close(ctx) + + if !p.FunctionExists(funcName) { + return result, fmt.Errorf("%s does not exist", funcName) + } + + inputBytes, err := json.Marshal(input) + if err != nil { + return result, fmt.Errorf("failed to marshal input: %w", err) + } + + exit, output, err := p.Call(funcName, inputBytes) + if err != nil { + log.Debug(ctx, "Plugin call failed", "p", plugin.name, "function", funcName, err) + return result, fmt.Errorf("plugin call failed: %w", err) + } + if exit != 0 { + return result, fmt.Errorf("plugin call exited with code %d", exit) + } + + err = json.Unmarshal(output, &result) + if err != nil { + log.Debug(ctx, "Plugin call failed", "p", plugin.name, "function", funcName, err) + } + return result, err +} + // Verify interface implementations at compile time var ( _ agents.PluginLoader = (*Manager)(nil) diff --git a/plugins/manager_test.go b/plugins/manager_test.go index 149ce312d..b7aba6206 100644 --- a/plugins/manager_test.go +++ b/plugins/manager_test.go @@ -2,12 +2,15 @@ package plugins import ( "context" + "fmt" "os" "path/filepath" "runtime" + "sync" "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf/configtest" + "github.com/navidrome/navidrome/core/agents" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -37,6 +40,7 @@ var _ = Describe("Manager", func() { DeferCleanup(configtest.SetupConfig()) conf.Server.Plugins.Enabled = true conf.Server.Plugins.Folder = tmpDir + conf.Server.CacheFolder = filepath.Join(tmpDir, "cache") // Create a fresh manager for each test manager = &Manager{ @@ -157,4 +161,43 @@ var _ = Describe("Manager", func() { Expect(names).ToNot(ContainElement("fail-reload")) }) }) + + It("can call the plugin concurrently", func() { + copyTestPlugin("new-plugin") + err := manager.LoadPlugin("new-plugin") + Expect(err).ToNot(HaveOccurred()) + + const concurrency = 100 + errs := make(chan error, concurrency) + bios := make(chan string, concurrency) + + g := sync.WaitGroup{} + g.Add(concurrency) + for i := range concurrency { + go func(i int) { + defer g.Done() + a, ok := manager.LoadMediaAgent("new-plugin") + Expect(ok).To(BeTrue()) + agent := a.(agents.ArtistBiographyRetriever) + bio, err := agent.GetArtistBiography(ctx, fmt.Sprintf("artist-%d", i), fmt.Sprintf("Artist %d", i), "") + if err != nil { + errs <- err + return + } + bios <- bio + }(i) + } + g.Wait() + + // Collect results + for range concurrency { + select { + case err := <-errs: + Expect(err).ToNot(HaveOccurred()) + case bio := <-bios: + Expect(bio).To(ContainSubstring("Biography for Artist")) + } + } + }) + }) diff --git a/plugins/metadata_agent.go b/plugins/metadata_agent.go index 99179f917..4fe617128 100644 --- a/plugins/metadata_agent.go +++ b/plugins/metadata_agent.go @@ -2,11 +2,9 @@ package plugins import ( "context" - "encoding/json" + "errors" - extism "github.com/extism/go-sdk" "github.com/navidrome/navidrome/core/agents" - "github.com/navidrome/navidrome/log" ) // Export function names (snake_case as per design) @@ -25,15 +23,7 @@ const ( // the agents interfaces for metadata retrieval. type MetadataAgent struct { name string - plugin *extism.Plugin -} - -// NewMetadataAgent creates a new MetadataAgent wrapping the given plugin. -func NewMetadataAgent(name string, plugin *extism.Plugin) *MetadataAgent { - return &MetadataAgent{ - name: name, - plugin: plugin, - } + plugin *pluginInstance } // AgentName returns the plugin name @@ -41,120 +31,14 @@ func (a *MetadataAgent) AgentName() string { return a.name } -// Close closes the plugin instance -func (a *MetadataAgent) Close() error { - if a.plugin != nil { - return a.plugin.Close(context.Background()) - } - return nil -} - -// --- Input/Output JSON structures --- - -type artistMBIDInput struct { - ID string `json:"id"` - Name string `json:"name"` -} - -type artistMBIDOutput struct { - MBID string `json:"mbid"` -} - -type artistInput struct { - ID string `json:"id"` - Name string `json:"name"` - MBID string `json:"mbid,omitempty"` -} - -type artistURLOutput struct { - URL string `json:"url"` -} - -type artistBiographyOutput struct { - Biography string `json:"biography"` -} - -type similarArtistsInput struct { - ID string `json:"id"` - Name string `json:"name"` - MBID string `json:"mbid,omitempty"` - Limit int `json:"limit"` -} - -type similarArtistsOutput struct { - Artists []struct { - Name string `json:"name"` - MBID string `json:"mbid,omitempty"` - } `json:"artists"` -} - -type artistImagesOutput struct { - Images []struct { - URL string `json:"url"` - Size int `json:"size"` - } `json:"images"` -} - -type topSongsInput struct { - ID string `json:"id"` - Name string `json:"name"` - MBID string `json:"mbid,omitempty"` - Count int `json:"count"` -} - -type topSongsOutput struct { - Songs []struct { - Name string `json:"name"` - MBID string `json:"mbid,omitempty"` - } `json:"songs"` -} - -type albumInput struct { - Name string `json:"name"` - Artist string `json:"artist"` - MBID string `json:"mbid,omitempty"` -} - -type albumInfoOutput struct { - Name string `json:"name"` - MBID string `json:"mbid"` - Description string `json:"description"` - URL string `json:"url"` -} - -type albumImagesOutput struct { - Images []struct { - URL string `json:"url"` - Size int `json:"size"` - } `json:"images"` -} - // --- Interface implementations --- // GetArtistMBID retrieves the MusicBrainz ID for an artist func (a *MetadataAgent) GetArtistMBID(ctx context.Context, id string, name string) (string, error) { - if !a.plugin.FunctionExists(FuncGetArtistMBID) { - return "", agents.ErrNotFound - } - input := artistMBIDInput{ID: id, Name: name} - inputBytes, err := json.Marshal(input) + result, err := callPluginFunction[artistMBIDInput, artistMBIDOutput](ctx, a.plugin, FuncGetArtistMBID, input) if err != nil { - return "", err - } - - exit, output, err := a.plugin.Call(FuncGetArtistMBID, inputBytes) - if err != nil { - log.Debug(ctx, "Plugin call failed", "plugin", a.name, "function", FuncGetArtistMBID, err) - return "", agents.ErrNotFound - } - if exit != 0 { - return "", agents.ErrNotFound - } - - var result artistMBIDOutput - if err := json.Unmarshal(output, &result); err != nil { - return "", err + return "", errors.Join(agents.ErrNotFound, err) } if result.MBID == "" { @@ -166,61 +50,23 @@ func (a *MetadataAgent) GetArtistMBID(ctx context.Context, id string, name strin // GetArtistURL retrieves the external URL for an artist func (a *MetadataAgent) GetArtistURL(ctx context.Context, id, name, mbid string) (string, error) { - if !a.plugin.FunctionExists(FuncGetArtistURL) { - return "", agents.ErrNotFound - } - input := artistInput{ID: id, Name: name, MBID: mbid} - inputBytes, err := json.Marshal(input) + result, err := callPluginFunction[artistInput, artistURLOutput](ctx, a.plugin, FuncGetArtistURL, input) if err != nil { - return "", err + return "", errors.Join(agents.ErrNotFound, err) } - - exit, output, err := a.plugin.Call(FuncGetArtistURL, inputBytes) - if err != nil { - log.Debug(ctx, "Plugin call failed", "plugin", a.name, "function", FuncGetArtistURL, err) - return "", agents.ErrNotFound - } - if exit != 0 { - return "", agents.ErrNotFound - } - - var result artistURLOutput - if err := json.Unmarshal(output, &result); err != nil { - return "", err - } - if result.URL == "" { return "", agents.ErrNotFound } - return result.URL, nil } // GetArtistBiography retrieves the biography for an artist func (a *MetadataAgent) GetArtistBiography(ctx context.Context, id, name, mbid string) (string, error) { - if !a.plugin.FunctionExists(FuncGetArtistBiography) { - return "", agents.ErrNotFound - } - input := artistInput{ID: id, Name: name, MBID: mbid} - inputBytes, err := json.Marshal(input) + result, err := callPluginFunction[artistInput, artistBiographyOutput](ctx, a.plugin, FuncGetArtistBiography, input) if err != nil { - return "", err - } - - exit, output, err := a.plugin.Call(FuncGetArtistBiography, inputBytes) - if err != nil { - log.Debug(ctx, "Plugin call failed", "plugin", a.name, "function", FuncGetArtistBiography, err) - return "", agents.ErrNotFound - } - if exit != 0 { - return "", agents.ErrNotFound - } - - var result artistBiographyOutput - if err := json.Unmarshal(output, &result); err != nil { - return "", err + return "", errors.Join(agents.ErrNotFound, err) } if result.Biography == "" { @@ -232,28 +78,10 @@ func (a *MetadataAgent) GetArtistBiography(ctx context.Context, id, name, mbid s // GetSimilarArtists retrieves similar artists func (a *MetadataAgent) GetSimilarArtists(ctx context.Context, id, name, mbid string, limit int) ([]agents.Artist, error) { - if !a.plugin.FunctionExists(FuncGetSimilarArtists) { - return nil, agents.ErrNotFound - } - input := similarArtistsInput{ID: id, Name: name, MBID: mbid, Limit: limit} - inputBytes, err := json.Marshal(input) + result, err := callPluginFunction[similarArtistsInput, similarArtistsOutput](ctx, a.plugin, FuncGetSimilarArtists, input) if err != nil { - return nil, err - } - - exit, output, err := a.plugin.Call(FuncGetSimilarArtists, inputBytes) - if err != nil { - log.Debug(ctx, "Plugin call failed", "plugin", a.name, "function", FuncGetSimilarArtists, err) - return nil, agents.ErrNotFound - } - if exit != 0 { - return nil, agents.ErrNotFound - } - - var result similarArtistsOutput - if err := json.Unmarshal(output, &result); err != nil { - return nil, err + return nil, errors.Join(agents.ErrNotFound, err) } if len(result.Artists) == 0 { @@ -270,28 +98,10 @@ func (a *MetadataAgent) GetSimilarArtists(ctx context.Context, id, name, mbid st // GetArtistImages retrieves images for an artist func (a *MetadataAgent) GetArtistImages(ctx context.Context, id, name, mbid string) ([]agents.ExternalImage, error) { - if !a.plugin.FunctionExists(FuncGetArtistImages) { - return nil, agents.ErrNotFound - } - input := artistInput{ID: id, Name: name, MBID: mbid} - inputBytes, err := json.Marshal(input) + result, err := callPluginFunction[artistInput, artistImagesOutput](ctx, a.plugin, FuncGetArtistImages, input) if err != nil { - return nil, err - } - - exit, output, err := a.plugin.Call(FuncGetArtistImages, inputBytes) - if err != nil { - log.Debug(ctx, "Plugin call failed", "plugin", a.name, "function", FuncGetArtistImages, err) - return nil, agents.ErrNotFound - } - if exit != 0 { - return nil, agents.ErrNotFound - } - - var result artistImagesOutput - if err := json.Unmarshal(output, &result); err != nil { - return nil, err + return nil, errors.Join(agents.ErrNotFound, err) } if len(result.Images) == 0 { @@ -308,28 +118,10 @@ func (a *MetadataAgent) GetArtistImages(ctx context.Context, id, name, mbid stri // GetArtistTopSongs retrieves top songs for an artist func (a *MetadataAgent) GetArtistTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]agents.Song, error) { - if !a.plugin.FunctionExists(FuncGetArtistTopSongs) { - return nil, agents.ErrNotFound - } - input := topSongsInput{ID: id, Name: artistName, MBID: mbid, Count: count} - inputBytes, err := json.Marshal(input) + result, err := callPluginFunction[topSongsInput, topSongsOutput](ctx, a.plugin, FuncGetArtistTopSongs, input) if err != nil { - return nil, err - } - - exit, output, err := a.plugin.Call(FuncGetArtistTopSongs, inputBytes) - if err != nil { - log.Debug(ctx, "Plugin call failed", "plugin", a.name, "function", FuncGetArtistTopSongs, err) - return nil, agents.ErrNotFound - } - if exit != 0 { - return nil, agents.ErrNotFound - } - - var result topSongsOutput - if err := json.Unmarshal(output, &result); err != nil { - return nil, err + return nil, errors.Join(agents.ErrNotFound, err) } if len(result.Songs) == 0 { @@ -346,28 +138,10 @@ func (a *MetadataAgent) GetArtistTopSongs(ctx context.Context, id, artistName, m // GetAlbumInfo retrieves album information func (a *MetadataAgent) GetAlbumInfo(ctx context.Context, name, artist, mbid string) (*agents.AlbumInfo, error) { - if !a.plugin.FunctionExists(FuncGetAlbumInfo) { - return nil, agents.ErrNotFound - } - input := albumInput{Name: name, Artist: artist, MBID: mbid} - inputBytes, err := json.Marshal(input) + result, err := callPluginFunction[albumInput, albumInfoOutput](ctx, a.plugin, FuncGetAlbumInfo, input) if err != nil { - return nil, err - } - - exit, output, err := a.plugin.Call(FuncGetAlbumInfo, inputBytes) - if err != nil { - log.Debug(ctx, "Plugin call failed", "plugin", a.name, "function", FuncGetAlbumInfo, err) - return nil, agents.ErrNotFound - } - if exit != 0 { - return nil, agents.ErrNotFound - } - - var result albumInfoOutput - if err := json.Unmarshal(output, &result); err != nil { - return nil, err + return nil, errors.Join(agents.ErrNotFound, err) } return &agents.AlbumInfo{ @@ -380,28 +154,10 @@ func (a *MetadataAgent) GetAlbumInfo(ctx context.Context, name, artist, mbid str // GetAlbumImages retrieves images for an album func (a *MetadataAgent) GetAlbumImages(ctx context.Context, name, artist, mbid string) ([]agents.ExternalImage, error) { - if !a.plugin.FunctionExists(FuncGetAlbumImages) { - return nil, agents.ErrNotFound - } - input := albumInput{Name: name, Artist: artist, MBID: mbid} - inputBytes, err := json.Marshal(input) + result, err := callPluginFunction[albumInput, albumImagesOutput](ctx, a.plugin, FuncGetAlbumImages, input) if err != nil { - return nil, err - } - - exit, output, err := a.plugin.Call(FuncGetAlbumImages, inputBytes) - if err != nil { - log.Debug(ctx, "Plugin call failed", "plugin", a.name, "function", FuncGetAlbumImages, err) - return nil, agents.ErrNotFound - } - if exit != 0 { - return nil, agents.ErrNotFound - } - - var result albumImagesOutput - if err := json.Unmarshal(output, &result); err != nil { - return nil, err + return nil, errors.Join(agents.ErrNotFound, err) } if len(result.Images) == 0 { diff --git a/plugins/metadata_agent_test.go b/plugins/metadata_agent_test.go index 0a0a35c99..2826c1141 100644 --- a/plugins/metadata_agent_test.go +++ b/plugins/metadata_agent_test.go @@ -2,48 +2,69 @@ package plugins import ( "context" + "os" "path/filepath" "runtime" - extism "github.com/extism/go-sdk" + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/conf/configtest" + "github.com/navidrome/navidrome/core/agents" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("MetadataAgent", func() { var ( - agent *MetadataAgent - ctx context.Context + manager *Manager + agent agents.Interface + ctx context.Context + testdataDir string + tmpDir string ) BeforeEach(func() { ctx = GinkgoT().Context() - // Load the test plugin + // Get testdata directory _, currentFile, _, ok := runtime.Caller(0) Expect(ok).To(BeTrue()) - testdataDir := filepath.Join(filepath.Dir(currentFile), "testdata") - wasmPath := filepath.Join(testdataDir, "test-plugin.wasm") + testdataDir = filepath.Join(filepath.Dir(currentFile), "testdata") - manifest := extism.Manifest{ - Wasm: []extism.Wasm{ - extism.WasmFile{Path: wasmPath}, - }, - AllowedHosts: []string{"test.example.com"}, - } - - plugin, err := extism.NewPlugin(ctx, manifest, extism.PluginConfig{ - EnableWasi: true, - }, nil) + // Create temp dir for plugins + var err error + tmpDir, err = os.MkdirTemp("", "metadata-agent-test-*") Expect(err).ToNot(HaveOccurred()) - agent = NewMetadataAgent("test-plugin", plugin) - }) + // Copy test plugin to temp dir + srcPath := filepath.Join(testdataDir, "test-plugin.wasm") + destPath := filepath.Join(tmpDir, "test-plugin.wasm") + data, err := os.ReadFile(srcPath) + Expect(err).ToNot(HaveOccurred()) + err = os.WriteFile(destPath, data, 0600) + Expect(err).ToNot(HaveOccurred()) - AfterEach(func() { - if agent != nil { - _ = agent.Close() + // Setup config + DeferCleanup(configtest.SetupConfig()) + conf.Server.Plugins.Enabled = true + conf.Server.Plugins.Folder = tmpDir + conf.Server.CacheFolder = filepath.Join(tmpDir, "cache") + + // Create and start the manager + manager = &Manager{ + plugins: make(map[string]*pluginInstance), } + err = manager.Start(ctx) + Expect(err).ToNot(HaveOccurred()) + + // Load the agent via manager + var ok2 bool + agent, ok2 = manager.LoadMediaAgent("test-plugin") + Expect(ok2).To(BeTrue()) + + DeferCleanup(func() { + _ = manager.Stop() + _ = os.RemoveAll(tmpDir) + }) }) Describe("AgentName", func() { @@ -54,7 +75,8 @@ var _ = Describe("MetadataAgent", func() { Describe("GetArtistMBID", func() { It("returns the MBID from the plugin", func() { - mbid, err := agent.GetArtistMBID(ctx, "artist-1", "The Beatles") + retriever := agent.(agents.ArtistMBIDRetriever) + mbid, err := retriever.GetArtistMBID(ctx, "artist-1", "The Beatles") Expect(err).ToNot(HaveOccurred()) Expect(mbid).To(Equal("test-mbid-The Beatles")) }) @@ -62,7 +84,8 @@ var _ = Describe("MetadataAgent", func() { Describe("GetArtistURL", func() { It("returns the URL from the plugin", func() { - url, err := agent.GetArtistURL(ctx, "artist-1", "The Beatles", "some-mbid") + retriever := agent.(agents.ArtistURLRetriever) + url, err := retriever.GetArtistURL(ctx, "artist-1", "The Beatles", "some-mbid") Expect(err).ToNot(HaveOccurred()) Expect(url).To(Equal("https://test.example.com/artist/The Beatles")) }) @@ -70,7 +93,8 @@ var _ = Describe("MetadataAgent", func() { Describe("GetArtistBiography", func() { It("returns the biography from the plugin", func() { - bio, err := agent.GetArtistBiography(ctx, "artist-1", "The Beatles", "some-mbid") + retriever := agent.(agents.ArtistBiographyRetriever) + bio, err := retriever.GetArtistBiography(ctx, "artist-1", "The Beatles", "some-mbid") Expect(err).ToNot(HaveOccurred()) Expect(bio).To(Equal("Biography for The Beatles")) }) @@ -78,7 +102,8 @@ var _ = Describe("MetadataAgent", func() { Describe("GetArtistImages", func() { It("returns images from the plugin", func() { - images, err := agent.GetArtistImages(ctx, "artist-1", "The Beatles", "some-mbid") + retriever := agent.(agents.ArtistImageRetriever) + images, err := retriever.GetArtistImages(ctx, "artist-1", "The Beatles", "some-mbid") Expect(err).ToNot(HaveOccurred()) Expect(images).To(HaveLen(2)) Expect(images[0].URL).To(Equal("https://test.example.com/images/The Beatles/large.jpg")) @@ -90,7 +115,8 @@ var _ = Describe("MetadataAgent", func() { Describe("GetSimilarArtists", func() { It("returns similar artists from the plugin", func() { - artists, err := agent.GetSimilarArtists(ctx, "artist-1", "The Beatles", "some-mbid", 3) + retriever := agent.(agents.ArtistSimilarRetriever) + artists, err := retriever.GetSimilarArtists(ctx, "artist-1", "The Beatles", "some-mbid", 3) Expect(err).ToNot(HaveOccurred()) Expect(artists).To(HaveLen(3)) Expect(artists[0].Name).To(Equal("The Beatles Similar A")) @@ -101,7 +127,8 @@ var _ = Describe("MetadataAgent", func() { Describe("GetArtistTopSongs", func() { It("returns top songs from the plugin", func() { - songs, err := agent.GetArtistTopSongs(ctx, "artist-1", "The Beatles", "some-mbid", 3) + retriever := agent.(agents.ArtistTopSongsRetriever) + songs, err := retriever.GetArtistTopSongs(ctx, "artist-1", "The Beatles", "some-mbid", 3) Expect(err).ToNot(HaveOccurred()) Expect(songs).To(HaveLen(3)) Expect(songs[0].Name).To(Equal("The Beatles Song 1")) @@ -112,7 +139,8 @@ var _ = Describe("MetadataAgent", func() { Describe("GetAlbumInfo", func() { It("returns album info from the plugin", func() { - info, err := agent.GetAlbumInfo(ctx, "Abbey Road", "The Beatles", "album-mbid") + retriever := agent.(agents.AlbumInfoRetriever) + info, err := retriever.GetAlbumInfo(ctx, "Abbey Road", "The Beatles", "album-mbid") Expect(err).ToNot(HaveOccurred()) Expect(info.Name).To(Equal("Abbey Road")) Expect(info.MBID).To(Equal("test-album-mbid-Abbey Road")) @@ -123,7 +151,8 @@ var _ = Describe("MetadataAgent", func() { Describe("GetAlbumImages", func() { It("returns album images from the plugin", func() { - images, err := agent.GetAlbumImages(ctx, "Abbey Road", "The Beatles", "album-mbid") + retriever := agent.(agents.AlbumImageRetriever) + images, err := retriever.GetAlbumImages(ctx, "Abbey Road", "The Beatles", "album-mbid") Expect(err).ToNot(HaveOccurred()) Expect(images).To(HaveLen(1)) Expect(images[0].URL).To(Equal("https://test.example.com/albums/Abbey Road/cover.jpg")) diff --git a/plugins/metadata_types.go b/plugins/metadata_types.go new file mode 100644 index 000000000..b69681fac --- /dev/null +++ b/plugins/metadata_types.go @@ -0,0 +1,100 @@ +package plugins + +// --- Input/Output JSON structures for MetadataAgent plugin calls --- + +// artistMBIDInput is the input for GetArtistMBID +type artistMBIDInput struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// artistMBIDOutput is the output for GetArtistMBID +type artistMBIDOutput struct { + MBID string `json:"mbid"` +} + +// artistInput is the common input for artist-related functions +type artistInput struct { + ID string `json:"id"` + Name string `json:"name"` + MBID string `json:"mbid,omitempty"` +} + +// artistURLOutput is the output for GetArtistURL +type artistURLOutput struct { + URL string `json:"url"` +} + +// artistBiographyOutput is the output for GetArtistBiography +type artistBiographyOutput struct { + Biography string `json:"biography"` +} + +// similarArtistsInput is the input for GetSimilarArtists +type similarArtistsInput struct { + ID string `json:"id"` + Name string `json:"name"` + MBID string `json:"mbid,omitempty"` + Limit int `json:"limit"` +} + +// artistRef is a reference to an artist with name and optional MBID +type artistRef struct { + Name string `json:"name"` + MBID string `json:"mbid,omitempty"` +} + +// similarArtistsOutput is the output for GetSimilarArtists +type similarArtistsOutput struct { + Artists []artistRef `json:"artists"` +} + +// imageInfo represents an image with URL and size +type imageInfo struct { + URL string `json:"url"` + Size int `json:"size"` +} + +// artistImagesOutput is the output for GetArtistImages +type artistImagesOutput struct { + Images []imageInfo `json:"images"` +} + +// topSongsInput is the input for GetArtistTopSongs +type topSongsInput struct { + ID string `json:"id"` + Name string `json:"name"` + MBID string `json:"mbid,omitempty"` + Count int `json:"count"` +} + +// songRef is a reference to a song with name and optional MBID +type songRef struct { + Name string `json:"name"` + MBID string `json:"mbid,omitempty"` +} + +// topSongsOutput is the output for GetArtistTopSongs +type topSongsOutput struct { + Songs []songRef `json:"songs"` +} + +// albumInput is the common input for album-related functions +type albumInput struct { + Name string `json:"name"` + Artist string `json:"artist"` + MBID string `json:"mbid,omitempty"` +} + +// albumInfoOutput is the output for GetAlbumInfo +type albumInfoOutput struct { + Name string `json:"name"` + MBID string `json:"mbid"` + Description string `json:"description"` + URL string `json:"url"` +} + +// albumImagesOutput is the output for GetAlbumImages +type albumImagesOutput struct { + Images []imageInfo `json:"images"` +} diff --git a/plugins/watcher_test.go b/plugins/watcher_test.go index 9f818a347..3fe179ebd 100644 --- a/plugins/watcher_test.go +++ b/plugins/watcher_test.go @@ -44,6 +44,7 @@ var _ = Describe("Watcher", func() { conf.Server.Plugins.Enabled = true conf.Server.Plugins.Folder = tmpDir conf.Server.Plugins.AutoReload = true + conf.Server.CacheFolder = filepath.Join(tmpDir, "cache") // Create a fresh manager for each test manager = &Manager{