mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
feat: enhance plugin manager to support metrics recording
Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
b90ecb9754
commit
7b1126201e
@ -61,12 +61,12 @@ func CreateNativeAPIRouter(ctx context.Context) *nativeapi.Router {
|
||||
fileCache := artwork.GetImageCache()
|
||||
fFmpeg := ffmpeg.New()
|
||||
broker := events.GetBroker()
|
||||
manager := plugins.GetManager(dataStore, broker)
|
||||
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
|
||||
manager := plugins.GetManager(dataStore, broker, metricsMetrics)
|
||||
agentsAgents := agents.GetAgents(dataStore, manager)
|
||||
provider := external.NewProvider(dataStore, agentsAgents)
|
||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
|
||||
modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics)
|
||||
watcher := scanner.GetWatcher(dataStore, modelScanner)
|
||||
library := core.NewLibrary(dataStore, modelScanner, watcher, broker)
|
||||
@ -81,7 +81,8 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
|
||||
fileCache := artwork.GetImageCache()
|
||||
fFmpeg := ffmpeg.New()
|
||||
broker := events.GetBroker()
|
||||
manager := plugins.GetManager(dataStore, broker)
|
||||
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
|
||||
manager := plugins.GetManager(dataStore, broker, metricsMetrics)
|
||||
agentsAgents := agents.GetAgents(dataStore, manager)
|
||||
provider := external.NewProvider(dataStore, agentsAgents)
|
||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||
@ -92,7 +93,6 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
|
||||
players := core.NewPlayers(dataStore)
|
||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||
playlists := core.NewPlaylists(dataStore)
|
||||
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
|
||||
modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics)
|
||||
playTracker := scrobbler.GetPlayTracker(dataStore, broker, manager)
|
||||
playbackServer := playback.GetInstance(dataStore)
|
||||
@ -106,7 +106,8 @@ func CreatePublicRouter() *public.Router {
|
||||
fileCache := artwork.GetImageCache()
|
||||
fFmpeg := ffmpeg.New()
|
||||
broker := events.GetBroker()
|
||||
manager := plugins.GetManager(dataStore, broker)
|
||||
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
|
||||
manager := plugins.GetManager(dataStore, broker, metricsMetrics)
|
||||
agentsAgents := agents.GetAgents(dataStore, manager)
|
||||
provider := external.NewProvider(dataStore, agentsAgents)
|
||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||
@ -152,13 +153,13 @@ func CreateScanner(ctx context.Context) model.Scanner {
|
||||
fileCache := artwork.GetImageCache()
|
||||
fFmpeg := ffmpeg.New()
|
||||
broker := events.GetBroker()
|
||||
manager := plugins.GetManager(dataStore, broker)
|
||||
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
|
||||
manager := plugins.GetManager(dataStore, broker, metricsMetrics)
|
||||
agentsAgents := agents.GetAgents(dataStore, manager)
|
||||
provider := external.NewProvider(dataStore, agentsAgents)
|
||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||
playlists := core.NewPlaylists(dataStore)
|
||||
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
|
||||
modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics)
|
||||
return modelScanner
|
||||
}
|
||||
@ -169,13 +170,13 @@ func CreateScanWatcher(ctx context.Context) scanner.Watcher {
|
||||
fileCache := artwork.GetImageCache()
|
||||
fFmpeg := ffmpeg.New()
|
||||
broker := events.GetBroker()
|
||||
manager := plugins.GetManager(dataStore, broker)
|
||||
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
|
||||
manager := plugins.GetManager(dataStore, broker, metricsMetrics)
|
||||
agentsAgents := agents.GetAgents(dataStore, manager)
|
||||
provider := external.NewProvider(dataStore, agentsAgents)
|
||||
artworkArtwork := artwork.NewArtwork(dataStore, fileCache, fFmpeg, provider)
|
||||
cacheWarmer := artwork.NewCacheWarmer(artworkArtwork, fileCache)
|
||||
playlists := core.NewPlaylists(dataStore)
|
||||
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
|
||||
modelScanner := scanner.New(ctx, dataStore, cacheWarmer, broker, playlists, metricsMetrics)
|
||||
watcher := scanner.GetWatcher(dataStore, modelScanner)
|
||||
return watcher
|
||||
@ -192,13 +193,14 @@ func getPluginManager() *plugins.Manager {
|
||||
sqlDB := db.Db()
|
||||
dataStore := persistence.New(sqlDB)
|
||||
broker := events.GetBroker()
|
||||
manager := plugins.GetManager(dataStore, broker)
|
||||
metricsMetrics := metrics.GetPrometheusInstance(dataStore)
|
||||
manager := plugins.GetManager(dataStore, broker, metricsMetrics)
|
||||
return manager
|
||||
}
|
||||
|
||||
// wire_injectors.go:
|
||||
|
||||
var allProviders = wire.NewSet(core.Set, artwork.Set, server.New, subsonic.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, scanner.New, scanner.GetWatcher, metrics.GetPrometheusInstance, db.Db, plugins.GetManager, wire.Bind(new(agents.PluginLoader), new(*plugins.Manager)), wire.Bind(new(scrobbler.PluginLoader), new(*plugins.Manager)), wire.Bind(new(nativeapi.PluginManager), new(*plugins.Manager)), wire.Bind(new(core.Watcher), new(scanner.Watcher)))
|
||||
var allProviders = wire.NewSet(core.Set, artwork.Set, server.New, subsonic.New, nativeapi.New, public.New, persistence.New, lastfm.NewRouter, listenbrainz.NewRouter, events.GetBroker, scanner.New, scanner.GetWatcher, metrics.GetPrometheusInstance, db.Db, plugins.GetManager, wire.Bind(new(agents.PluginLoader), new(*plugins.Manager)), wire.Bind(new(scrobbler.PluginLoader), new(*plugins.Manager)), wire.Bind(new(nativeapi.PluginManager), new(*plugins.Manager)), wire.Bind(new(plugins.PluginMetricsRecorder), new(metrics.Metrics)), wire.Bind(new(core.Watcher), new(scanner.Watcher)))
|
||||
|
||||
func GetPluginManager(ctx context.Context) *plugins.Manager {
|
||||
manager := getPluginManager()
|
||||
|
||||
@ -45,6 +45,7 @@ var allProviders = wire.NewSet(
|
||||
wire.Bind(new(agents.PluginLoader), new(*plugins.Manager)),
|
||||
wire.Bind(new(scrobbler.PluginLoader), new(*plugins.Manager)),
|
||||
wire.Bind(new(nativeapi.PluginManager), new(*plugins.Manager)),
|
||||
wire.Bind(new(plugins.PluginMetricsRecorder), new(metrics.Metrics)),
|
||||
wire.Bind(new(core.Watcher), new(scanner.Watcher)),
|
||||
)
|
||||
|
||||
|
||||
@ -315,7 +315,7 @@ func (c *insightsCollector) hasSmartPlaylists(ctx context.Context) (bool, error)
|
||||
// collectPlugins collects information about installed plugins
|
||||
func (c *insightsCollector) collectPlugins(_ context.Context) map[string]insights.PluginInfo {
|
||||
// TODO Fix import/inject cycles
|
||||
manager := plugins.GetManager(c.ds, events.GetBroker())
|
||||
manager := plugins.GetManager(c.ds, events.GetBroker(), nil)
|
||||
info := manager.GetPluginInfo()
|
||||
|
||||
result := make(map[string]insights.PluginInfo, len(info))
|
||||
|
||||
@ -35,6 +35,12 @@ const (
|
||||
// SubsonicRouter is an http.Handler that serves Subsonic API requests.
|
||||
type SubsonicRouter = http.Handler
|
||||
|
||||
// PluginMetricsRecorder is an interface for recording plugin metrics.
|
||||
// This is satisfied by core/metrics.Metrics but defined here to avoid import cycles.
|
||||
type PluginMetricsRecorder interface {
|
||||
RecordPluginRequest(ctx context.Context, plugin, method string, ok bool, elapsed int64)
|
||||
}
|
||||
|
||||
// Manager manages loading and lifecycle of WebAssembly plugins.
|
||||
// It implements both agents.PluginLoader and scrobbler.PluginLoader interfaces.
|
||||
type Manager struct {
|
||||
@ -56,15 +62,17 @@ type Manager struct {
|
||||
subsonicRouter SubsonicRouter
|
||||
ds model.DataStore
|
||||
broker events.Broker
|
||||
metrics PluginMetricsRecorder
|
||||
}
|
||||
|
||||
// GetManager returns a singleton instance of the plugin manager.
|
||||
// The manager is not started automatically; call Start() to begin loading plugins.
|
||||
func GetManager(ds model.DataStore, broker events.Broker) *Manager {
|
||||
func GetManager(ds model.DataStore, broker events.Broker, m PluginMetricsRecorder) *Manager {
|
||||
return singleton.GetInstance(func() *Manager {
|
||||
return &Manager{
|
||||
ds: ds,
|
||||
broker: broker,
|
||||
metrics: m,
|
||||
plugins: make(map[string]*plugin),
|
||||
}
|
||||
})
|
||||
|
||||
@ -12,6 +12,12 @@ import (
|
||||
)
|
||||
|
||||
var errFunctionNotFound = errors.New("function not found")
|
||||
var errNotImplemented = errors.New("function not implemented")
|
||||
|
||||
// notImplementedCode is the standard return code from plugin PDKs
|
||||
// indicating a function exists but is not implemented by this plugin.
|
||||
// The plugin returns -2 as int32, which becomes 0xFFFFFFFE as uint32.
|
||||
const notImplementedCode uint32 = 0xFFFFFFFE
|
||||
|
||||
// callPluginFunctionNoInput is a helper to call a plugin function with no input and output.
|
||||
func callPluginFunctionNoInput(ctx context.Context, plugin *plugin, funcName string) error {
|
||||
@ -54,15 +60,26 @@ func callPluginFunction[I any, O any](ctx context.Context, plugin *plugin, funcN
|
||||
startCall := time.Now()
|
||||
exit, output, err := p.CallWithContext(ctx, funcName, inputBytes)
|
||||
if err != nil {
|
||||
elapsed := time.Since(startCall).Milliseconds()
|
||||
// If context was cancelled, return that error instead of the plugin error
|
||||
if ctx.Err() != nil {
|
||||
log.Debug(ctx, "Plugin call cancelled", "plugin", plugin.name, "function", funcName, "pluginDuration", time.Since(startCall))
|
||||
return result, ctx.Err()
|
||||
}
|
||||
if plugin.metrics != nil {
|
||||
plugin.metrics.RecordPluginRequest(ctx, plugin.name, funcName, false, elapsed)
|
||||
}
|
||||
log.Trace(ctx, "Plugin call failed", "plugin", plugin.name, "function", funcName, "pluginDuration", time.Since(startCall), "navidromeDuration", startCall.Sub(start), err)
|
||||
return result, fmt.Errorf("plugin call failed: %w", err)
|
||||
}
|
||||
if exit != 0 {
|
||||
elapsed := time.Since(startCall).Milliseconds()
|
||||
if plugin.metrics != nil {
|
||||
plugin.metrics.RecordPluginRequest(ctx, plugin.name, funcName, false, elapsed)
|
||||
}
|
||||
if exit == notImplementedCode {
|
||||
return result, fmt.Errorf("%w: %s", errNotImplemented, funcName)
|
||||
}
|
||||
return result, fmt.Errorf("plugin call exited with code %d", exit)
|
||||
}
|
||||
|
||||
@ -73,6 +90,12 @@ func callPluginFunction[I any, O any](ctx context.Context, plugin *plugin, funcN
|
||||
}
|
||||
}
|
||||
|
||||
// Record metrics for successful calls (or JSON unmarshal failures)
|
||||
if plugin.metrics != nil {
|
||||
elapsed := time.Since(startCall).Milliseconds()
|
||||
plugin.metrics.RecordPluginRequest(ctx, plugin.name, funcName, err == nil, elapsed)
|
||||
}
|
||||
|
||||
log.Trace(ctx, "Plugin call succeeded", "plugin", plugin.name, "function", funcName, "pluginDuration", time.Since(startCall), "navidromeDuration", startCall.Sub(start))
|
||||
return result, err
|
||||
}
|
||||
|
||||
131
plugins/manager_call_test.go
Normal file
131
plugins/manager_call_test.go
Normal file
@ -0,0 +1,131 @@
|
||||
//go:build !windows
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/navidrome/navidrome/core/agents"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// mockMetricsRecorder tracks calls to RecordPluginRequest for testing
|
||||
type mockMetricsRecorder struct {
|
||||
mu sync.Mutex
|
||||
calls []metricsCall
|
||||
}
|
||||
|
||||
type metricsCall struct {
|
||||
plugin string
|
||||
method string
|
||||
ok bool
|
||||
elapsed int64
|
||||
}
|
||||
|
||||
func (m *mockMetricsRecorder) RecordPluginRequest(_ context.Context, plugin, method string, ok bool, elapsed int64) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.calls = append(m.calls, metricsCall{plugin: plugin, method: method, ok: ok, elapsed: elapsed})
|
||||
}
|
||||
|
||||
func (m *mockMetricsRecorder) getCalls() []metricsCall {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return append([]metricsCall{}, m.calls...)
|
||||
}
|
||||
|
||||
func (m *mockMetricsRecorder) reset() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.calls = nil
|
||||
}
|
||||
|
||||
var _ = Describe("callPluginFunction metrics", Ordered, func() {
|
||||
var (
|
||||
metricsManager *Manager
|
||||
metricsRecorder *mockMetricsRecorder
|
||||
agent agents.Interface
|
||||
)
|
||||
|
||||
BeforeAll(func() {
|
||||
metricsRecorder = &mockMetricsRecorder{}
|
||||
|
||||
// Create a manager with the metrics recorder
|
||||
metricsManager, _ = createTestManagerWithPluginsAndMetrics(
|
||||
nil,
|
||||
metricsRecorder,
|
||||
"test-metadata-agent"+PackageExtension,
|
||||
)
|
||||
|
||||
var ok bool
|
||||
agent, ok = metricsManager.LoadMediaAgent("test-metadata-agent")
|
||||
Expect(ok).To(BeTrue())
|
||||
})
|
||||
|
||||
BeforeEach(func() {
|
||||
metricsRecorder.reset()
|
||||
})
|
||||
|
||||
It("records metrics for successful plugin calls", func() {
|
||||
retriever := agent.(agents.ArtistBiographyRetriever)
|
||||
_, err := retriever.GetArtistBiography(GinkgoT().Context(), "artist-1", "Test Artist", "mbid")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
calls := metricsRecorder.getCalls()
|
||||
Expect(calls).To(HaveLen(1))
|
||||
Expect(calls[0].plugin).To(Equal("test-metadata-agent"))
|
||||
Expect(calls[0].method).To(Equal(FuncGetArtistBiography))
|
||||
Expect(calls[0].ok).To(BeTrue())
|
||||
Expect(calls[0].elapsed).To(BeNumerically(">=", 0))
|
||||
})
|
||||
|
||||
It("records metrics for failed plugin calls (error returned)", func() {
|
||||
// Create a manager with error config to force plugin errors
|
||||
errorRecorder := &mockMetricsRecorder{}
|
||||
errorManager, _ := createTestManagerWithPluginsAndMetrics(
|
||||
map[string]map[string]string{
|
||||
"test-metadata-agent": {"error": "simulated error"},
|
||||
},
|
||||
errorRecorder,
|
||||
"test-metadata-agent"+PackageExtension,
|
||||
)
|
||||
|
||||
errorAgent, ok := errorManager.LoadMediaAgent("test-metadata-agent")
|
||||
Expect(ok).To(BeTrue())
|
||||
|
||||
retriever := errorAgent.(agents.ArtistBiographyRetriever)
|
||||
_, err := retriever.GetArtistBiography(GinkgoT().Context(), "artist-1", "Test Artist", "mbid")
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
calls := errorRecorder.getCalls()
|
||||
Expect(calls).To(HaveLen(1))
|
||||
Expect(calls[0].plugin).To(Equal("test-metadata-agent"))
|
||||
Expect(calls[0].method).To(Equal(FuncGetArtistBiography))
|
||||
Expect(calls[0].ok).To(BeFalse())
|
||||
})
|
||||
|
||||
It("records metrics for not-implemented functions", func() {
|
||||
// Use partial metadata agent that doesn't implement GetArtistMBID
|
||||
partialRecorder := &mockMetricsRecorder{}
|
||||
partialManager, _ := createTestManagerWithPluginsAndMetrics(
|
||||
nil,
|
||||
partialRecorder,
|
||||
"partial-metadata-agent"+PackageExtension,
|
||||
)
|
||||
|
||||
partialAgent, ok := partialManager.LoadMediaAgent("partial-metadata-agent")
|
||||
Expect(ok).To(BeTrue())
|
||||
|
||||
retriever := partialAgent.(agents.ArtistMBIDRetriever)
|
||||
_, err := retriever.GetArtistMBID(GinkgoT().Context(), "artist-1", "Test Artist")
|
||||
Expect(err).To(MatchError(errNotImplemented))
|
||||
|
||||
calls := partialRecorder.getCalls()
|
||||
Expect(calls).To(HaveLen(1))
|
||||
Expect(calls[0].plugin).To(Equal("partial-metadata-agent"))
|
||||
Expect(calls[0].method).To(Equal(FuncGetArtistMBID))
|
||||
Expect(calls[0].ok).To(BeFalse())
|
||||
})
|
||||
})
|
||||
@ -317,6 +317,7 @@ func (m *Manager) loadPluginWithConfig(name, ndpPath, configJSON string) error {
|
||||
compiled: compiled,
|
||||
capabilities: capabilities,
|
||||
closers: closers,
|
||||
metrics: m.metrics,
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ type plugin struct {
|
||||
compiled *extism.CompiledPlugin
|
||||
capabilities []Capability // Auto-detected capabilities based on exported functions
|
||||
closers []io.Closer // Cleanup functions to call on unload
|
||||
metrics PluginMetricsRecorder
|
||||
}
|
||||
|
||||
// instance creates a new plugin instance for the given context.
|
||||
|
||||
@ -187,3 +187,74 @@ var _ = Describe("MetadataAgent error handling", Ordered, func() {
|
||||
Expect(err.Error()).To(ContainSubstring("simulated plugin error"))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("MetadataAgent partial implementation", Ordered, func() {
|
||||
// Tests the "not implemented" code path when a plugin only implements some methods
|
||||
var (
|
||||
partialManager *Manager
|
||||
partialAgent agents.Interface
|
||||
)
|
||||
|
||||
BeforeAll(func() {
|
||||
// Create manager with the partial metadata agent plugin
|
||||
partialManager, _ = createTestManagerWithPlugins(nil, "partial-metadata-agent"+PackageExtension)
|
||||
|
||||
// Load the agent
|
||||
var ok bool
|
||||
partialAgent, ok = partialManager.LoadMediaAgent("partial-metadata-agent")
|
||||
Expect(ok).To(BeTrue())
|
||||
})
|
||||
|
||||
It("returns data from implemented method (GetArtistBiography)", func() {
|
||||
retriever := partialAgent.(agents.ArtistBiographyRetriever)
|
||||
bio, err := retriever.GetArtistBiography(GinkgoT().Context(), "artist-1", "Test Artist", "mbid")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(bio).To(Equal("Partial agent biography for Test Artist"))
|
||||
})
|
||||
|
||||
It("returns ErrNotFound for unimplemented method (GetArtistMBID)", func() {
|
||||
retriever := partialAgent.(agents.ArtistMBIDRetriever)
|
||||
_, err := retriever.GetArtistMBID(GinkgoT().Context(), "artist-1", "Test Artist")
|
||||
Expect(err).To(MatchError(errNotImplemented))
|
||||
})
|
||||
|
||||
It("returns ErrNotFound for unimplemented method (GetArtistURL)", func() {
|
||||
retriever := partialAgent.(agents.ArtistURLRetriever)
|
||||
_, err := retriever.GetArtistURL(GinkgoT().Context(), "artist-1", "Test Artist", "mbid")
|
||||
Expect(err).To(MatchError(errNotImplemented))
|
||||
})
|
||||
|
||||
It("returns ErrNotFound for unimplemented method (GetArtistImages)", func() {
|
||||
retriever := partialAgent.(agents.ArtistImageRetriever)
|
||||
_, err := retriever.GetArtistImages(GinkgoT().Context(), "artist-1", "Test Artist", "mbid")
|
||||
Expect(err).To(MatchError(errNotImplemented))
|
||||
})
|
||||
|
||||
It("returns ErrNotFound for unimplemented method (GetSimilarArtists)", func() {
|
||||
retriever := partialAgent.(agents.ArtistSimilarRetriever)
|
||||
_, err := retriever.GetSimilarArtists(GinkgoT().Context(), "artist-1", "Test Artist", "mbid", 5)
|
||||
Expect(err).To(MatchError(errNotImplemented))
|
||||
|
||||
})
|
||||
|
||||
It("returns ErrNotFound for unimplemented method (GetArtistTopSongs)", func() {
|
||||
retriever := partialAgent.(agents.ArtistTopSongsRetriever)
|
||||
_, err := retriever.GetArtistTopSongs(GinkgoT().Context(), "artist-1", "Test Artist", "mbid", 5)
|
||||
Expect(err).To(MatchError(errNotImplemented))
|
||||
|
||||
})
|
||||
|
||||
It("returns ErrNotFound for unimplemented method (GetAlbumInfo)", func() {
|
||||
retriever := partialAgent.(agents.AlbumInfoRetriever)
|
||||
_, err := retriever.GetAlbumInfo(GinkgoT().Context(), "Album", "Artist", "mbid")
|
||||
Expect(err).To(MatchError(errNotImplemented))
|
||||
|
||||
})
|
||||
|
||||
It("returns ErrNotFound for unimplemented method (GetAlbumImages)", func() {
|
||||
retriever := partialAgent.(agents.AlbumImageRetriever)
|
||||
_, err := retriever.GetAlbumImages(GinkgoT().Context(), "Album", "Artist", "mbid")
|
||||
Expect(err).To(MatchError(errNotImplemented))
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
@ -61,6 +61,13 @@ func createTestManager(pluginConfig map[string]map[string]string) (*Manager, str
|
||||
// and specified plugins. It creates a temp directory, copies the specified plugins, and starts the manager.
|
||||
// Returns the manager and temp directory path.
|
||||
func createTestManagerWithPlugins(pluginConfig map[string]map[string]string, plugins ...string) (*Manager, string) {
|
||||
return createTestManagerWithPluginsAndMetrics(pluginConfig, nil, plugins...)
|
||||
}
|
||||
|
||||
// createTestManagerWithPluginsAndMetrics creates a new plugin Manager with the given plugin config,
|
||||
// metrics recorder, and specified plugins. It creates a temp directory, copies the specified plugins, and starts the manager.
|
||||
// Returns the manager and temp directory path.
|
||||
func createTestManagerWithPluginsAndMetrics(pluginConfig map[string]map[string]string, metrics PluginMetricsRecorder, plugins ...string) (*Manager, string) {
|
||||
// Create temp directory
|
||||
tmpDir, err := os.MkdirTemp("", "plugins-test-*")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
@ -115,6 +122,7 @@ func createTestManagerWithPlugins(pluginConfig map[string]map[string]string, plu
|
||||
manager := &Manager{
|
||||
plugins: make(map[string]*plugin),
|
||||
ds: dataStore,
|
||||
metrics: metrics,
|
||||
subsonicRouter: http.NotFoundHandler(), // Stub router for tests
|
||||
}
|
||||
err = manager.Start(GinkgoT().Context())
|
||||
|
||||
16
plugins/testdata/partial-metadata-agent/go.mod
vendored
Normal file
16
plugins/testdata/partial-metadata-agent/go.mod
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
module partial-metadata-agent
|
||||
|
||||
go 1.25
|
||||
|
||||
require github.com/navidrome/navidrome/plugins/pdk/go v0.0.0
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/extism/go-pdk v1.1.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/navidrome/navidrome/plugins/pdk/go => ../../pdk/go
|
||||
14
plugins/testdata/partial-metadata-agent/go.sum
vendored
Normal file
14
plugins/testdata/partial-metadata-agent/go.sum
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
|
||||
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
23
plugins/testdata/partial-metadata-agent/main.go
vendored
Normal file
23
plugins/testdata/partial-metadata-agent/main.go
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
// Test plugin that only implements some metadata methods.
|
||||
// Used to test the "not implemented" code path (-2 return code).
|
||||
// Build with: tinygo build -o ../partial-metadata-agent.wasm -target wasip1 -buildmode=c-shared .
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/navidrome/navidrome/plugins/pdk/go/metadata"
|
||||
)
|
||||
|
||||
func init() {
|
||||
metadata.Register(&partialMetadataAgent{})
|
||||
}
|
||||
|
||||
// partialMetadataAgent only implements GetArtistBiography.
|
||||
// All other methods will return NotImplementedCode (-2).
|
||||
type partialMetadataAgent struct{}
|
||||
|
||||
// GetArtistBiography is the only method we implement.
|
||||
func (t *partialMetadataAgent) GetArtistBiography(input metadata.ArtistRequest) (*metadata.ArtistBiographyResponse, error) {
|
||||
return &metadata.ArtistBiographyResponse{Biography: "Partial agent biography for " + input.Name}, nil
|
||||
}
|
||||
|
||||
func main() {}
|
||||
6
plugins/testdata/partial-metadata-agent/manifest.json
vendored
Normal file
6
plugins/testdata/partial-metadata-agent/manifest.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Partial Metadata Agent",
|
||||
"author": "Navidrome Test",
|
||||
"version": "1.0.0",
|
||||
"description": "A test plugin that only implements some metadata methods"
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user