mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
refactor(plugins): implement plugin function call helper and refactor MetadataAgent methods
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
8cd3785ac4
commit
40ce71294e
@ -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)
|
||||
|
||||
@ -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"))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
100
plugins/metadata_types.go
Normal 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"`
|
||||
}
|
||||
@ -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{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user