refactor(plugins): implement plugin function call helper and refactor MetadataAgent methods

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-12-22 10:44:30 -05:00
parent 6d13416c15
commit 81e9c5b4e9
6 changed files with 268 additions and 312 deletions

View File

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

View File

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

View File

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

View File

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

100
plugins/metadata_types.go Normal file
View File

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

View File

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