* feat(plugins): add minimal test agent plugin with API definitions Signed-off-by: Deluan <deluan@navidrome.org> * feat: add plugin manager with auto-registration and unique agent names Introduced a plugin manager that scans the plugins folder for subdirectories containing plugin.wasm files and auto-registers them as agents using the directory name as the unique agent name. Updated the configuration to support plugins with enabled/folder options, and ensured the plugin manager is started as a concurrent task during server startup. The wasmAgent now returns the plugin directory name for AgentName, ensuring each plugin agent is uniquely identifiable. This enables dynamic plugin discovery and integration with the agents orchestrator. * test: add Ginkgo suite and test for plugin manager auto-registration Added a Ginkgo v2 suite bootstrap (plugins_suite_test.go) for the plugins package and a test (manager_test.go) to verify that plugins in the testdata folder are auto-registered and can be loaded as agents. The test uses a mock DataStore and asserts that the agent is registered and its AgentName matches the plugin directory. Updated go.mod and go.sum for wazero dependency required by plugin WASM support. * test(plugins): ensure test WASM plugin is always freshly built before running suite; add real-plugin Ginkgo tests. Add BeforeSuite to plugins suite to build plugins/testdata/agent/plugin.wasm using Go WASI build command, matching README instructions. Remove plugin.wasm before build to guarantee a clean build. Add full real-plugin Ginkgo/Gomega tests for wasmAgent, covering all methods and error cases. Fix manager_test.go to use pointer to Manager. This ensures plugin tests are always run against a freshly compiled WASM binary, increasing reliability and reproducibility. Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): implement persistent compilation cache for WASM agent plugins Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): implement instance pooling for wasmAgent to improve resource management Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): enhance logging for wasmAgent and plugin manager operations Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): implement HttpService for handling HTTP requests in WASM plugins Also add a sample Wikimedia plugin Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): standardize error handling in wasmAgent and MinimalAgent Signed-off-by: Deluan <deluan@navidrome.org> * refactor: clean up wikimedia plugin code Standardized error creation using 'errors.New' where formatting was not needed. Introduced a constant for HTTP request timeouts. Removed commented-out log statement. Improved code comments for clarity and accuracy. * refactor: use unified SPARQLResult struct and parser for SPARQL responses Introduced a single SPARQLResult struct to represent all possible SPARQL response fields (sitelink, wiki, comment, img). Added a parseSPARQLResult helper to unmarshal and check for empty results, simplifying all fetch functions and improving type safety and maintainability. * feat(plugins): improve error handling in HTTP request processing Signed-off-by: Deluan <deluan@navidrome.org> * fix: background plugin compilation, logging, and race safety Implemented background WASM plugin compilation with concurrency limits, proper closure capture, and global compilation cache to avoid data races. Added debug and warning logs for plugin compilation results, including elapsed time. Ensured plugin registration is correct and all tests pass. * perf: implement true lazy loading for agents Changed agent instantiation to be fully lazy. The Agents struct now stores agent names in order and only instantiates each agent on first use, caching the result. This preserves agent call order, improves server startup time, and ensures thread safety. Updated all agent methods and tests to use the new pattern. No changes to agent registration or interface. All tests pass. * fix: ensure wasm plugin instances are closed via runtime.AddCleanup Introduced runtime.AddCleanup to guarantee that the Close method of WASM plugin instances is called, even if they are garbage collected from the sync.Pool. Modified the sync.Pool.New function in manager.go to register a cleanup function for each loaded instance that implements Close. Updated agent.go to handle the pooledInstance wrapper containing the instance and its cleanup handle. Ensured cleanup.Stop() is called before explicitly closing an instance (on error or agent shutdown) to prevent double closing. This fixes a potential resource leak where instances could be GC'd from the pool without proper cleanup. * refactor: break down long functions in plugin manager and agent Refactored plugins/manager.go and plugins/agent.go to improve readability and reduce function length. Extracted pool initialization logic into newPluginPool and background compilation/agent factory logic into precompilePlugin/createAgentFactory in manager.go. Extracted pool retrieval/validation and cleanup function creation into getValidPooledInstance/createPoolCleanupFunc in agent.go. Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): rename wasmAgent to wasmArtistAgent Signed-off-by: Deluan <deluan@navidrome.org> * feat(api): add AlbumMetadataService with AlbumInfo and AlbumImages requests Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugin): rename MinimalAgent for artist metadata service Signed-off-by: Deluan <deluan@navidrome.org> * feat(api): implement wasmAlbumAgent for album metadata service with GetAlbumInfo and GetAlbumImages methods Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): simplify wasmAlbumAgent and wasmArtistAgent by using wasmBasePlugin Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): add support for ArtistMetadataService and AlbumMetadataService in plugin manager Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): enhance plugin pool creation with custom runtime and precompilation support Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): implement generic plugin pool and agent factory for improved service handling Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): reorganize plugin management Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): improve function signatures for clarity and consistency Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): implement background precompilation for plugins and agent factory creation Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): include instanceID in logging for better traceability Signed-off-by: Deluan <deluan@navidrome.org> * test(plugins): add tests for plugin pre-compilation and agent factory synchronization Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): add minimal album test agent plugin for AlbumMetadataService Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): rename fake artist and album test agent plugins for metadata services Signed-off-by: Deluan <deluan@navidrome.org> * feat(makefile): add Makefile for building plugin WASM binaries Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): add FakeMultiAgent plugin implementing Artist and Album metadata services Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): remove log statements from FakeArtistAgent and FakeMultiAgent methods Signed-off-by: Deluan <deluan@navidrome.org> * refactor: split AlbumInfoRetriever and AlbumImageRetriever, update all usages Split the AlbumInfoRetriever interface into two: AlbumInfoRetriever (for album metadata) and AlbumImageRetriever (for album images), to better separate concerns and simplify implementations. Updated all agents, providers, plugins, and tests to use the new interfaces and methods. Removed the now-unnecessary mockAlbumAgents in favor of the shared mockAgents. Fixed a missing images slice declaration in lastfm agent. All tests pass except for known ignored persistence tests. This change reduces code duplication, improves clarity, and keeps the codebase clean and organized. * feat(plugins): add Cover Art Archive AlbumMetadataService plugin for album cover images Signed-off-by: Deluan <deluan@navidrome.org> * refactor: remove wasm module pooling it was causing issues with the GC and the Close methods Signed-off-by: Deluan <deluan@navidrome.org> * refactor: rename metadata service files to adapter naming convention Signed-off-by: Deluan <deluan@navidrome.org> * refactor: unify album and artist method calls by introducing callMethod function Signed-off-by: Deluan <deluan@navidrome.org> * refactor: unify album and artist method calls by introducing callMethod function Signed-off-by: Deluan <deluan@navidrome.org> * fix: handle nil values in data redaction process Signed-off-by: Deluan <deluan@navidrome.org> * fix: add timeout for plugin compilation to prevent indefinite blocking Signed-off-by: Deluan <deluan@navidrome.org> * feat: implement ScrobblerService plugin with authorization and scrobbling capabilities Signed-off-by: Deluan <deluan@navidrome.org> * refactor: simplify generalization Signed-off-by: Deluan <deluan@navidrome.org> * fix: tests Signed-off-by: Deluan <deluan@navidrome.org> * refactor: enhance plugin management by improving scanning and loading mechanisms Signed-off-by: Deluan <deluan@navidrome.org> * refactor: update plugin creation functions to return specific interfaces for better type safety Signed-off-by: Deluan <deluan@navidrome.org> * refactor: enhance wasmBasePlugin to support specific plugin types for improved type safety Signed-off-by: Deluan <deluan@navidrome.org> * refactor: implement MediaMetadataService with combined artist and album methods Signed-off-by: Deluan <deluan@navidrome.org> * refactor: improve MediaMetadataService plugin implementation and testing structure Signed-off-by: Deluan <deluan@navidrome.org> * refactor: add tests for Adapter Media Agent and improve plugin documentation Signed-off-by: Deluan <deluan@navidrome.org> * docs: add README for Navidrome Plugin System with detailed architecture and usage guidelines Signed-off-by: Deluan <deluan@navidrome.org> * refactor: enhance agent management with plugin loading and caching Signed-off-by: Deluan <deluan@navidrome.org> * refactor: update agent discovery logic to include only local agent when no config is specified Signed-off-by: Deluan <deluan@navidrome.org> * refactor: encapsulate agent caching logic in agentCache struct\n\nReplaced direct map/mutex usage for agent caching in Agents with a dedicated agentCache struct. This improves readability, maintainability, and testability by centralizing TTL and concurrency logic. Cleaned up comments and ensured all linter and test requirements are met. Signed-off-by: Deluan <deluan@navidrome.org> * fix: correct file extension filter in goimports command Signed-off-by: Deluan <deluan@navidrome.org> * refactor: use defer to unlock the mutex Signed-off-by: Deluan <deluan@navidrome.org> * chore: move Cover Art Archive AlbumMetadataService plugins to an example folder Signed-off-by: Deluan <deluan@navidrome.org> * fix: handle errors when creating media metadata and scrobbler service plugins Signed-off-by: Deluan <deluan@navidrome.org> * fix: increase compilation timeout to one minute Signed-off-by: Deluan <deluan@navidrome.org> * feat: add configurable plugin compilation timeout Signed-off-by: Deluan <deluan@navidrome.org> * feat: implement plugin scrobbler support in PlayTracker Signed-off-by: Deluan <deluan@navidrome.org> * feat: add context management and Stop method to buffered scrobbler Signed-off-by: Deluan <deluan@navidrome.org> * feat: add username field to scrobbler requests and update logging Signed-off-by: Deluan <deluan@navidrome.org> * fix: data race in test Signed-off-by: Deluan <deluan@navidrome.org> * refactor: rename http proto files to host and update references Signed-off-by: Deluan <deluan@navidrome.org> * refactor: remove unused plugin registration methods from manager Signed-off-by: Deluan <deluan@navidrome.org> * feat: extend plugin manifests and implement plugin management commands Signed-off-by: Deluan <deluan@navidrome.org> * Update utils/files.go Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * fix for code scanning alert no. 43: Arbitrary file access during archive extraction ("Zip Slip") Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * feat: add plugin dev workflow support Added new CLI commands to improve plugin development workflow: 'plugin dev' to create symlinks from development directories to plugins folder, 'plugin refresh' to reload plugins without restarting Navidrome, enhanced 'plugin remove' to handle symlinked development plugins correctly, and updated 'plugin list' to display development plugins with '(dev)' indicator. These changes make the plugin development workflow more efficient by allowing developers to work on plugins in their own directories, link them to Navidrome without copying files, refresh plugins after changes without restart, and clean up safely. Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): implement timer service with register and cancel functionality - WIP Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): implement timer service with register and cancel functionality - WIP Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): implement timer service with register and cancel functionality - WIP Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): implement timer service with register and cancel functionality Signed-off-by: Deluan <deluan@navidrome.org> * fix: lint errors Signed-off-by: Deluan <deluan@navidrome.org> * feat(README): update documentation to include TimerCallbackService and its functionality Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): add InitService with OnInit method and initialization tracking - WIP Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): add tests for InitService and plugin initialization tracking Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): expand documentation on plugin system implementation and architecture Signed-off-by: Deluan <deluan@navidrome.org> * fix: panic Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): redirect plugins' stderr to logs Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): add safe accessor methods for TimerService Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): add plugin-specific configuration support in InitRequest and documentation Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): add TimerCallbackService plugin adapter and integration Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): rename services for consistency and clarity Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): add mutex for configuration access and clone plugin config Signed-off-by: Deluan <deluan@navidrome.org> * refactor(tests): remove configtest dependency to prevent data races in integration tests Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): remove PluginName method from WASM plugin implementations and update LoadPlugin to accept service type Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): implement instance pooling for wasmBasePlugin to improve performance - WIP Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): add wasmInstancePool for managing WASM plugin instances with TTL and max size Signed-off-by: Deluan <deluan@navidrome.org> * fix(plugins): correctly pass error to done function in wasmBasePlugin Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): rename service types to capabilities for consistency Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): simplify instance management in wasmBasePlugin by removing error handling in closure Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): update wasmBasePlugin and wasmInstancePool to return errors for better error handling Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): rename InitService to LifecycleManagement for consistency Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): fix instance ID logging in wasmBasePlugin Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): extract instance ID logging to a separate function in wasmBasePlugin, to avoid vet error Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): make timers be isolated per plugin Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): make timers be isolated per plugin Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): rename HttpServiceImpl to httpServiceImpl for consistency and improve logging Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): add config service for plugin-specific configuration management Signed-off-by: Deluan <deluan@navidrome.org> * Update plugins/manager.go Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * Update plugins/manager.go Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * feat(crontab): implement crontab service for scheduling and canceling jobs Signed-off-by: Deluan <deluan@navidrome.org> * fix(singleton): fix deadlock issue when a constructor calls GetSingleton again Signed-off-by: Deluan <deluan@navidrome.org> (+1 squashed commit) Squashed commits: [325a96ea2] fix(singleton): fix deadlock issue when a constructor calls GetSingleton again Signed-off-by: Deluan <deluan@navidrome.org> * feat(scheduler): implement Scheduler for one-time and recurring job scheduling, merging CrontabService and TimerService Signed-off-by: Deluan <deluan@navidrome.org> * fix(scheduler): race condition in the scheduleOneTime and scheduleRecurring methods when replacing jobs with the same ID Signed-off-by: Deluan <deluan@navidrome.org> * refactor(scheduler): consolidate job scheduling logic into a single helper function Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugin): rename GetInstance method to Instantiate for clarity Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): add WebSocket service for handling connections and messages Signed-off-by: Deluan <deluan@navidrome.org> * feat(crypto-ticker): add WebSocket plugin for real-time cryptocurrency price tracking Signed-off-by: Deluan <deluan@navidrome.org> * feat(websocket): enhance connection management and callback handling Signed-off-by: Deluan <deluan@navidrome.org> * feat(manager): only create one adapter instance for each adapter/capability pair Signed-off-by: Deluan <deluan@navidrome.org> * fix(websocket): ensure proper resource management by closing response body and use defer to unlocking mutexes Signed-off-by: Deluan <deluan@navidrome.org> * fix: flaky test Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugin): refactor WebSocket service integration and improve error logging Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugin): add SchedulerCallback support and improve reconnection logic Signed-off-by: Deluan <deluan@navidrome.org> * fix: test panic Signed-off-by: Deluan <deluan@navidrome.org> * docs: add crypto-ticker plugin example to README Signed-off-by: Deluan <deluan@navidrome.org> * feat(manager): add LoadAllPlugins and LoadAllMediaAgents methods with slice.Map integration Signed-off-by: Deluan <deluan@navidrome.org> * feat(api): add Timestamp field to ScrobblerNowPlayingRequest and update related methods Signed-off-by: Deluan <deluan@navidrome.org> * feat(websocket): add error field to response messages for better error handling Signed-off-by: Deluan <deluan@navidrome.org> * feat(cache): implement CacheService with string, int, float, and byte operations Signed-off-by: Deluan <deluan@navidrome.org> * feat(tests): update buffered scrobbler tests for improved scrobble verification and use RWMutex in mock repo Signed-off-by: Deluan <deluan@navidrome.org> * refactor(cache): simplify cache service implementation and remove unnecessary synchronization Signed-off-by: Deluan <deluan@navidrome.org> * feat(tests): add build step for test plugins in the test suite Signed-off-by: Deluan <deluan@navidrome.org> * wip Signed-off-by: Deluan <deluan@navidrome.org> * feat(scheduler): implement named scheduler callbacks and enhance Discord plugin integration Signed-off-by: Deluan <deluan@navidrome.org> * feat(rpc): enhance activity image processing and improve error handling in Discord integration Signed-off-by: Deluan <deluan@navidrome.org> * feat(discord): enhance activity state with artist list and add large text asset Signed-off-by: Deluan <deluan@navidrome.org> * fix tests Signed-off-by: Deluan <deluan@navidrome.org> * feat(artwork): implement ArtworkService for retrieving artwork URLs Signed-off-by: Deluan <deluan@navidrome.org> * Add playback position to scrobble NowPlaying (#4089) * test(playtracker): cover playback position * address review comment Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org> * fix merge Signed-off-by: Deluan <deluan@navidrome.org> * refactor: remove unnecessary check for empty slice in Map function Signed-off-by: Deluan <deluan@navidrome.org> * fix: update reflex.conf to include .wasm file extension Signed-off-by: Deluan <deluan@navidrome.org> * fix(scanner): normalize attribute strings and add edge case tests for PID calculation Relates to https://github.com/navidrome/navidrome/issues/4183#issuecomment-2952729458 Signed-off-by: Deluan <deluan@navidrome.org> * test(ui): fix warnings (#4187) * fix(ui): address test warnings * ignore lint error in test Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org> * refactor(server): optimize top songs lookup (#4189) * optimize top songs lookup * Optimize title matching queries * refactor: simplify top songs matching * improve error handling and logging in track loading functions Signed-off-by: Deluan <deluan@navidrome.org> * test: add cases for fallback to title matching and combined MBID/title matching Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org> * fix(ui): playlist details overflow in spotify-based themes (#4184) * test: ensure playlist details width * fix(test): simplify expectation for minWidth in NDPlaylistDetails Signed-off-by: Deluan <deluan@navidrome.org> * fix(test): test all themes Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org> * chore(deps): update TagLib to version 2.1 (#4185) * chore: update cross-taglib * fix(taglib): add logging for TagLib version Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org> * test: verify agents fallback (#4191) * build(docker): downgrade Alpine version from 3.21 to 3.19, oldest supported version. This is to reduce the image size, as we don't really need the latest. Signed-off-by: Deluan <deluan@navidrome.org> * fix tests Signed-off-by: Deluan <deluan@navidrome.org> * feat(runtime): implement pooled WASM runtime and module for better instance management Signed-off-by: Deluan <deluan@navidrome.org> * fix(discord-plugin): adjust timer delay calculation for track completion Signed-off-by: Deluan <deluan@navidrome.org> * resolve PR comments Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): implement cache cleanup by size functionality Signed-off-by: Deluan <deluan@navidrome.org> * fix(manager): return error from getCompilationCache and handle it in ScanPlugins Signed-off-by: Deluan <deluan@navidrome.org> * fix possible rce condition Signed-off-by: Deluan <deluan@navidrome.org> * feat(docs): update README to include Cache and Artwork services Signed-off-by: Deluan <deluan@navidrome.org> * feat(manager): add permissions support for host services in custom runtime - WIP Signed-off-by: Deluan <deluan@navidrome.org> * feat(manifest): add permissions field to plugin manifests - WIP Signed-off-by: Deluan <deluan@navidrome.org> * test(permissions): implement permission validation and testing for plugins - WIP Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): add unauthorized_plugin to test permission enforcement - WIP Signed-off-by: Deluan <deluan@navidrome.org> * feat(docs): add Plugin Permission System section to README - WIP Signed-off-by: Deluan <deluan@navidrome.org> * feat(manifest): add detailed reasons for permissions in plugin manifests - WIP Signed-off-by: Deluan <deluan@navidrome.org> * feat(permissions): implement granular HTTP permissions for plugins - WIP Signed-off-by: Deluan <deluan@navidrome.org> * feat(permissions): implement HTTP and WebSocket permissions for plugins - WIP Signed-off-by: Deluan <deluan@navidrome.org> * refactor Signed-off-by: Deluan <deluan@navidrome.org> * refactor: unexport all plugins package private symbols Signed-off-by: Deluan <deluan@navidrome.org> * update docs Signed-off-by: Deluan <deluan@navidrome.org> * refactor: rename plugin_lifecycle_manager Signed-off-by: Deluan <deluan@navidrome.org> * docs: add discord-rich-presence plugin example to README Signed-off-by: Deluan <deluan@navidrome.org> * feat: add support for PATCH, HEAD, and OPTIONS HTTP methods Signed-off-by: Deluan <deluan@navidrome.org> * feat: use folder names as unique identifiers for plugins Signed-off-by: Deluan <deluan@navidrome.org> * fix: read config just once, to avoid data race in tests Signed-off-by: Deluan <deluan@navidrome.org> * refactor: rename pluginName to pluginID for consistency across services Signed-off-by: Deluan <deluan@navidrome.org> * fix: use symlink name instead of folder name for plugin registration Signed-off-by: Deluan <deluan@navidrome.org> * feat: update plugin output format to include ID and enhance README with symlink usage Signed-off-by: Deluan <deluan@navidrome.org> * refactor: implement shared plugin discovery function to streamline plugin scanning and error handling Signed-off-by: Deluan <deluan@navidrome.org> * feat: show plugin permissions in `plugin info` Signed-off-by: Deluan <deluan@navidrome.org> * feat: add JSON schema for Navidrome Plugin manifest and generate corresponding Go types - WIP Signed-off-by: Deluan <deluan@navidrome.org> * feat: implement typed permissions for plugins to enhance permission handling Signed-off-by: Deluan <deluan@navidrome.org> * feat: refactor plugin permissions to use typed schema and improve validation - WIP Signed-off-by: Deluan <deluan@navidrome.org> * feat: update HTTP permissions handling to use typed schema for allowed URLs - WIP Signed-off-by: Deluan <deluan@navidrome.org> * feat: remove unused JSON schema validation for plugin manifests Signed-off-by: Deluan <deluan@navidrome.org> * feat: remove unused fields from PluginPackage struct in package.go Signed-off-by: Deluan <deluan@navidrome.org> * feat: update file permissions in tests and remove unused permission parsing function Signed-off-by: Deluan <deluan@navidrome.org> * feat: refactor test plugin creation to use typed permissions and remove legacy helper Signed-off-by: Deluan <deluan@navidrome.org> * feat: add website field to plugin manifests and update test cases Signed-off-by: Deluan <deluan@navidrome.org> * refactor: permission schema to use basePermission structure for consistency Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance host service management by adding permission checks for each service Signed-off-by: Deluan <deluan@navidrome.org> * refactor: reorganize code files Signed-off-by: Deluan <deluan@navidrome.org> * refactor: simplify custom runtime creation by removing compilation cache parameter Signed-off-by: Deluan <deluan@navidrome.org> * doc: add WebSocketService and update ConfigService for plugin-specific configuration Signed-off-by: Deluan <deluan@navidrome.org> * feat: implement WASM loading optimization to enhance plugin instance creation speed Signed-off-by: Deluan <deluan@navidrome.org> * refactor: rename custom runtime functions and update related tests for clarity Signed-off-by: Deluan <deluan@navidrome.org> * refactor: enhance plugin structure with compilation handling and error reporting Signed-off-by: Deluan <deluan@navidrome.org> * refactor: improve logging and context tracing in runtime and wasm base plugin Signed-off-by: Deluan <deluan@navidrome.org> * refactor: enhance runtime management with scoped runtime and caching improvements Signed-off-by: Deluan <deluan@navidrome.org> * refactor: implement EnsureCompiled method for improved plugin compilation handling Signed-off-by: Deluan <deluan@navidrome.org> * refactor: implement cached module management with TTL for improved performance Signed-off-by: Deluan <deluan@navidrome.org> * refactor: replace map with sync.Map Signed-off-by: Deluan <deluan@navidrome.org> * refactor: adjust time tolerance in scrobble buffer repository tests to avoid flakiness Signed-off-by: Deluan <deluan@navidrome.org> * refactor: enhance image processing with fallback mechanism for improved error handling Signed-off-by: Deluan <deluan@navidrome.org> * docs: review test plugins readme Signed-off-by: Deluan <deluan@navidrome.org> * feat: set default timeout for HTTP client to 10 seconds Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance wasm instance pool with concurrency limits and timeout settings Signed-off-by: Deluan <deluan@navidrome.org> * feat(discordrp): implement caching for processed image URLs with configurable TTL Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org> Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
57 KiB
Navidrome Plugin System
Overview
Navidrome's plugin system is a WebAssembly (WASM) based extension mechanism that enables developers to expand Navidrome's functionality without modifying the core codebase. The plugin system supports several capabilities that can be implemented by plugins:
- MetadataAgent - For fetching artist and album information, images, etc.
- Scrobbler - For implementing scrobbling functionality with external services
- SchedulerCallback - For executing code after a specified delay or on a recurring schedule
- WebSocketCallback - For interacting with WebSocket endpoints and handling WebSocket events
- LifecycleManagement - For plugin initialization and configuration (one-time
OnInit
only; not invoked per-request)
Plugin Architecture
The plugin system is built on the following key components:
1. Plugin Manager
The Manager
(implemented in plugins/manager.go
) is the core component that:
- Scans for plugins in the configured plugins directory
- Loads and compiles plugins
- Provides access to loaded plugins through capability-specific interfaces
2. Plugin Protocol
Plugins communicate with Navidrome using Protocol Buffers (protobuf) over a WASM runtime. The protocol is defined in plugins/api/api.proto
which specifies the capabilities and messages that plugins can implement.
3. Plugin Adapters
Adapters bridge between the plugin API and Navidrome's internal interfaces:
wasmMediaAgent
adaptsMetadataAgent
to the internalagents.Interface
wasmScrobblerPlugin
adaptsScrobbler
to the internalscrobbler.Scrobbler
wasmSchedulerCallback
adaptsSchedulerCallback
to the internalSchedulerCallback
- Plugin Instance Pooling: Instances are managed in an internal pool (default 8 max, 1m TTL).
- WASM Compilation & Caching: Modules are pre-compiled concurrently (max 2) and cached in
[CacheFolder]/plugins
, reducing startup time. The compilation timeout can be configured viaDevPluginCompilationTimeout
in development.
4. Host Services
Navidrome provides host services that plugins can call to access functionality like HTTP requests and scheduling.
These services are defined in plugins/host/
and implemented in corresponding host files:
- HTTP service (in
plugins/host_http.go
) for making external requests - Scheduler service (in
plugins/host_scheduler.go
) for scheduling timed events - Config service (in
plugins/host_config.go
) for accessing plugin-specific configuration - WebSocket service (in
plugins/host_websocket.go
) for WebSocket communication - Cache service (in
plugins/host_cache.go
) for TTL-based plugin caching - Artwork service (in
plugins/host_artwork.go
) for generating public artwork URLs
Available Host Services
The following host services are available to plugins:
HttpService
// HTTP methods available to plugins
service HttpService {
rpc Get(HttpRequest) returns (HttpResponse);
rpc Post(HttpRequest) returns (HttpResponse);
rpc Put(HttpRequest) returns (HttpResponse);
rpc Delete(HttpRequest) returns (HttpResponse);
rpc Patch(HttpRequest) returns (HttpResponse);
rpc Head(HttpRequest) returns (HttpResponse);
rpc Options(HttpRequest) returns (HttpResponse);
}
ConfigService
service ConfigService {
rpc GetPluginConfig(GetPluginConfigRequest) returns (GetPluginConfigResponse);
}
The ConfigService allows plugins to access plugin-specific configuration. See the config.proto file for the full API.
ArtworkService
service ArtworkService {
rpc GetArtistUrl(GetArtworkUrlRequest) returns (GetArtworkUrlResponse);
rpc GetAlbumUrl(GetArtworkUrlRequest) returns (GetArtworkUrlResponse);
rpc GetTrackUrl(GetArtworkUrlRequest) returns (GetArtworkUrlResponse);
}
Provides methods to get public URLs for artwork images:
GetArtistUrl(id string, size int) string
: Returns a public URL for an artist's artworkGetAlbumUrl(id string, size int) string
: Returns a public URL for an album's artworkGetTrackUrl(id string, size int) string
: Returns a public URL for a track's artwork
The size
parameter is optional (use 0 for original size). The URLs returned are based on the server's ShareURL configuration.
Example:
url := artwork.GetArtistUrl("123", 300) // Get artist artwork URL with size 300px
url := artwork.GetAlbumUrl("456", 0) // Get album artwork URL in original size
CacheService
service CacheService {
// Set a string value in the cache
rpc SetString(SetStringRequest) returns (SetResponse);
// Get a string value from the cache
rpc GetString(GetRequest) returns (GetStringResponse);
// Set an integer value in the cache
rpc SetInt(SetIntRequest) returns (SetResponse);
// Get an integer value from the cache
rpc GetInt(GetRequest) returns (GetIntResponse);
// Set a float value in the cache
rpc SetFloat(SetFloatRequest) returns (SetResponse);
// Get a float value from the cache
rpc GetFloat(GetRequest) returns (GetFloatResponse);
// Set a byte slice value in the cache
rpc SetBytes(SetBytesRequest) returns (SetResponse);
// Get a byte slice value from the cache
rpc GetBytes(GetRequest) returns (GetBytesResponse);
// Remove a value from the cache
rpc Remove(RemoveRequest) returns (RemoveResponse);
// Check if a key exists in the cache
rpc Has(HasRequest) returns (HasResponse);
}
The CacheService provides a TTL-based cache for plugins. Each plugin gets its own isolated cache instance. By default, cached items expire after 24 hours unless a custom TTL is specified.
Key features:
- Isolated Caches: Each plugin has its own cache namespace, so different plugins can use the same key names without conflicts
- Typed Values: Store and retrieve values with their proper types (string, int64, float64, or byte slice)
- Configurable TTL: Set custom expiration times per item, or use the default 24-hour TTL
- Type Safety: The system handles type checking, returning "not exists" if there's a type mismatch
Example usage:
// Store a string value with default TTL (24 hours)
cacheService.SetString(ctx, &cache.SetStringRequest{
Key: "user_preference",
Value: "dark_mode",
})
// Store an integer with custom TTL (5 minutes)
cacheService.SetInt(ctx, &cache.SetIntRequest{
Key: "api_call_count",
Value: 42,
TtlSeconds: 300, // 5 minutes
})
// Retrieve a value
resp, err := cacheService.GetString(ctx, &cache.GetRequest{
Key: "user_preference",
})
if err != nil {
// Handle error
}
if resp.Exists {
// Use resp.Value
} else {
// Key doesn't exist or has expired
}
// Check if a key exists
hasResp, err := cacheService.Has(ctx, &cache.HasRequest{
Key: "api_call_count",
})
if hasResp.Exists {
// Key exists and hasn't expired
}
// Remove a value
cacheService.Remove(ctx, &cache.RemoveRequest{
Key: "user_preference",
})
See the cache.proto file for the full API definition.
SchedulerService
The SchedulerService provides a unified interface for scheduling both one-time and recurring tasks. See the scheduler.proto file for the full API.
service SchedulerService {
// One-time event scheduling
rpc ScheduleOneTime(ScheduleOneTimeRequest) returns (ScheduleResponse);
// Recurring event scheduling
rpc ScheduleRecurring(ScheduleRecurringRequest) returns (ScheduleResponse);
// Cancel any scheduled job
rpc CancelSchedule(CancelRequest) returns (CancelResponse);
}
- One-time scheduling: Schedule a callback to be executed once after a specified delay.
- Recurring scheduling: Schedule a callback to be executed repeatedly according to a cron expression.
Plugins using this service must implement the SchedulerCallback
interface:
service SchedulerCallback {
rpc OnSchedulerCallback(SchedulerCallbackRequest) returns (SchedulerCallbackResponse);
}
The IsRecurring
field in the request allows plugins to differentiate between one-time and recurring callbacks.
WebSocketService
The WebSocketService enables plugins to connect to and interact with WebSocket endpoints. See the websocket.proto file for the full API.
service WebSocketService {
// Connect to a WebSocket endpoint
rpc Connect(ConnectRequest) returns (ConnectResponse);
// Send a text message
rpc SendText(SendTextRequest) returns (SendTextResponse);
// Send binary data
rpc SendBinary(SendBinaryRequest) returns (SendBinaryResponse);
// Close a connection
rpc Close(CloseRequest) returns (CloseResponse);
}
- Connect: Establish a WebSocket connection to a specified URL with optional headers
- SendText: Send text messages over an established connection
- SendBinary: Send binary data over an established connection
- Close: Close a WebSocket connection with optional close code and reason
Plugins using this service must implement the WebSocketCallback
interface to handle incoming messages and connection events:
service WebSocketCallback {
rpc OnTextMessage(OnTextMessageRequest) returns (OnTextMessageResponse);
rpc OnBinaryMessage(OnBinaryMessageRequest) returns (OnBinaryMessageResponse);
rpc OnError(OnErrorRequest) returns (OnErrorResponse);
rpc OnClose(OnCloseRequest) returns (OnCloseResponse);
}
Example usage:
// Connect to a WebSocket server
connectResp, err := websocket.Connect(ctx, &websocket.ConnectRequest{
Url: "wss://example.com/ws",
Headers: map[string]string{"Authorization": "Bearer token"},
ConnectionId: "my-connection-id",
})
if err != nil {
return err
}
// Send a text message
_, err = websocket.SendText(ctx, &websocket.SendTextRequest{
ConnectionId: "my-connection-id",
Message: "Hello WebSocket",
})
// Send binary data
_, err = websocket.SendBinary(ctx, &websocket.SendBinaryRequest{
ConnectionId: "my-connection-id",
Data: []byte{0x01, 0x02, 0x03},
})
// Close the connection when done
_, err = websocket.Close(ctx, &websocket.CloseRequest{
ConnectionId: "my-connection-id",
Code: 1000, // Normal closure
Reason: "Done",
})
Plugin Permission System
Navidrome implements a permission-based security system that controls which host services plugins can access. This system enforces security at load-time by only making authorized services available to plugins in their WebAssembly runtime environment.
How Permissions Work
The permission system follows a secure-by-default approach:
- Default Behavior: Plugins have access to no host services unless explicitly declared
- Load-time Enforcement: Only services listed in a plugin's permissions are loaded into its WASM runtime
- Runtime Security: Unauthorized services are completely unavailable - attempts to call them result in "function not exported" errors
This design ensures that even if malicious code tries to access unauthorized services, the calls will fail because the functions simply don't exist in the plugin's runtime environment.
Permission Syntax
Permissions are declared in the plugin's manifest.json
file using the permissions
field as an object:
{
"name": "my-plugin",
"author": "Plugin Developer",
"version": "1.0.0",
"description": "A plugin that fetches data and caches results",
"website": "https://github.com/plugindeveloper/my-plugin",
"capabilities": ["MetadataAgent"],
"permissions": {
"http": {
"reason": "To fetch metadata from external APIs",
"allowedUrls": {
"https://api.musicbrainz.org": ["GET"],
"https://coverartarchive.org": ["GET"]
},
"allowLocalNetwork": false
},
"cache": {
"reason": "To cache API responses and reduce rate limiting"
}
}
}
Each permission is represented as a key in the permissions object. The value must be an object containing a reason
field that explains why the permission is needed.
Important: Some permissions require additional configuration fields:
http
: RequiresallowedUrls
object mapping URL patterns to allowed HTTP methods, and optionalallowLocalNetwork
booleanwebsocket
: RequiresallowedUrls
array of WebSocket URL patterns, and optionalallowLocalNetwork
booleanconfig
,cache
,scheduler
,artwork
: Only require thereason
field
Security Benefits of Required Reasons:
- Transparency: Users can see exactly what each plugin will do with its permissions
- Security Auditing: Makes it easier to identify suspicious or overly broad permission requests
- Developer Accountability: Forces plugin authors to justify each permission they request
- Trust Building: Clear explanations help users make informed decisions about plugin installation
If no permissions are needed, use an empty permissions object: "permissions": {}
.
Available Permissions
The following permission keys correspond to host services:
Permission | Host Service | Description | Required Fields |
---|---|---|---|
http |
HttpService | Make HTTP requests (GET, POST, PUT, DELETE, etc..) | reason , allowedUrls |
websocket |
WebSocketService | Connect to and communicate via WebSockets | reason , allowedUrls |
cache |
CacheService | Store and retrieve cached data with TTL | reason |
config |
ConfigService | Access Navidrome configuration values | reason |
scheduler |
SchedulerService | Schedule one-time and recurring tasks | reason |
artwork |
ArtworkService | Generate public URLs for artwork images | reason |
HTTP Permission Structure
HTTP permissions require explicit URL whitelisting for security:
{
"http": {
"reason": "To fetch artist data from MusicBrainz and album covers from Cover Art Archive",
"allowedUrls": {
"https://musicbrainz.org/ws/2/*": ["GET"],
"https://coverartarchive.org/*": ["GET"],
"https://api.example.com/submit": ["POST"]
},
"allowLocalNetwork": false
}
}
Fields:
reason
(required): Explanation of why HTTP access is neededallowedUrls
(required): Object mapping URL patterns to allowed HTTP methodsallowLocalNetwork
(optional, default false): Whether to allow requests to localhost/private IPs
URL Pattern Matching:
- Exact URLs:
"https://api.example.com/endpoint": ["GET"]
- Wildcard paths:
"https://api.example.com/*": ["GET", "POST"]
- Subdomain wildcards:
"https://*.example.com": ["GET"]
Important: Redirect destinations must also be included in allowedUrls
if you want to follow redirects.
WebSocket Permission Structure
WebSocket permissions require explicit URL whitelisting:
{
"websocket": {
"reason": "To connect to Discord gateway for real-time Rich Presence updates",
"allowedUrls": ["wss://gateway.discord.gg", "wss://*.discord.gg"],
"allowLocalNetwork": false
}
}
Fields:
reason
(required): Explanation of why WebSocket access is neededallowedUrls
(required): Array of WebSocket URL patterns (must start withws://
orwss://
)allowLocalNetwork
(optional, default false): Whether to allow connections to localhost/private IPs
Permission Validation
The plugin system validates permissions during loading:
- Schema Validation: The manifest is validated against the JSON schema
- Permission Recognition: Unknown permission keys are silently accepted for forward compatibility
- Service Loading: Only services with corresponding permissions are made available to the plugin
Security Model
The permission system provides multiple layers of security:
1. Principle of Least Privilege
- Plugins start with zero permissions
- Only explicitly requested services are available
- No way to escalate privileges at runtime
2. Load-time Enforcement
- Unauthorized services are not loaded into the WASM runtime
- No performance overhead for permission checks during execution
- Impossible to bypass restrictions through code manipulation
3. Service Isolation
- Each plugin gets its own isolated service instances
- Plugins cannot interfere with each other's service usage
- Host services are sandboxed within the WASM environment
Best Practices for Plugin Developers
Request Minimal Permissions
// Good: No permissions if none needed
{
"permissions": {}
}
// Good: Only request what you need with clear reasoning
{
"permissions": {
"http": {
"reason": "To fetch artist biography from MusicBrainz database",
"allowedUrls": {
"https://musicbrainz.org/ws/2/artist/*": ["GET"]
},
"allowLocalNetwork": false
}
}
}
// Avoid: Requesting unnecessary permissions
{
"permissions": {
"http": {
"reason": "To fetch data",
"allowedUrls": {
"https://*": ["*"]
},
"allowLocalNetwork": true
},
"cache": {
"reason": "For caching"
},
"scheduler": {
"reason": "For scheduling"
},
"websocket": {
"reason": "For real-time updates",
"allowedUrls": ["wss://*"],
"allowLocalNetwork": true
}
}
}
Write Clear Permission Reasons
Provide specific, descriptive reasons for each permission that explain exactly what the plugin does. Good reasons should:
- Specify what data will be accessed/fetched
- Mention which external services will be contacted (if applicable)
- Explain why the permission is necessary for the plugin's functionality
- Use clear, non-technical language that users can understand
// Good: Specific and informative
{
"http": {
"reason": "To fetch album reviews from AllMusic API and artist biographies from MusicBrainz",
"allowedUrls": {
"https://www.allmusic.com/api/*": ["GET"],
"https://musicbrainz.org/ws/2/*": ["GET"]
},
"allowLocalNetwork": false
},
"cache": {
"reason": "To cache API responses for 24 hours to respect rate limits and improve performance"
}
}
// Bad: Vague and unhelpful
{
"http": {
"reason": "To make requests",
"allowedUrls": {
"https://*": ["*"]
},
"allowLocalNetwork": true
},
"cache": {
"reason": "For caching"
}
}
Handle Missing Permissions Gracefully
Your plugin should provide clear error messages when permissions are missing:
func (p *Plugin) GetArtistInfo(ctx context.Context, req *api.ArtistInfoRequest) (*api.ArtistInfoResponse, error) {
// This will fail with "function not exported" if http permission is missing
resp, err := p.httpClient.Get(ctx, &http.HttpRequest{Url: apiURL})
if err != nil {
// Check if it's a permission error
if strings.Contains(err.Error(), "not exported") {
return &api.ArtistInfoResponse{
Error: "Plugin requires 'http' permission (reason: 'To fetch artist metadata from external APIs') - please add to manifest.json",
}, nil
}
return &api.ArtistInfoResponse{Error: err.Error()}, nil
}
// ... process response
}
Troubleshooting Permissions
Common Error Messages
"function not exported in module env"
- Cause: Plugin trying to call a service without proper permission
- Solution: Add the required permission to your manifest.json
"manifest validation failed" or "missing required field"
- Cause: Plugin manifest is missing required fields (e.g.,
allowedUrls
for HTTP/WebSocket permissions) - Solution: Ensure your manifest includes all required fields for each permission type
Permission silently ignored
- Cause: Using a permission key not recognized by current Navidrome version
- Effect: The unknown permission is silently ignored (no error or warning)
- Solution: This is actually normal behavior for forward compatibility
Debugging Permission Issues
- Check the manifest: Ensure required permissions are spelled correctly and present
- Verify required fields: Check that HTTP and WebSocket permissions include
allowedUrls
and other required fields - Review logs: Check for plugin loading errors, manifest validation errors, and WASM runtime errors
- Test incrementally: Add permissions one at a time to identify which services your plugin needs
- Verify service names: Ensure permission keys match exactly:
http
,cache
,config
,scheduler
,websocket
,artwork
- Validate manifest: Use a JSON schema validator to check your manifest against the schema
Future Considerations
The permission system is designed for extensibility:
- Unknown permissions are allowed in manifests for forward compatibility
- New services can be added with corresponding permission keys
- Permission scoping could be added in the future (e.g., read-only vs. read-write access)
This ensures that plugins developed today will continue to work as the system evolves, while maintaining strong security boundaries.
Plugin System Implementation
Navidrome's plugin system is built using the following key libraries:
1. WebAssembly Runtime (Wazero)
The plugin system uses Wazero, a WebAssembly runtime written in pure Go. Wazero was chosen for several reasons:
- No CGO dependency: Unlike other WebAssembly runtimes, Wazero is implemented in pure Go, which simplifies cross-compilation and deployment.
- Performance: It provides efficient compilation and caching of WebAssembly modules.
- Security: Wazero enforces strict sandboxing, which is important for running third-party plugin code safely.
The plugin manager uses Wazero to:
- Compile and cache WebAssembly modules
- Create isolated runtime environments for each plugin
- Instantiate plugin modules when they're called
- Provide host functions that plugins can call
2. Go-plugin Framework
Navidrome builds on go-plugin, a Go plugin system over WebAssembly that provides:
- Code generation: Custom Protocol Buffer compiler plugin (
protoc-gen-go-plugin
) that generates Go code for both the host and WebAssembly plugins - Host function system: Framework for exposing host functionality to plugins safely
- Interface versioning: Built-in mechanism for handling API compatibility between the host and plugins
- Type conversion: Utilities for marshaling and unmarshaling data between Go and WebAssembly
This framework significantly simplifies plugin development by handling the low-level details of WebAssembly communication, allowing plugin developers to focus on implementing capabilities interfaces.
3. Protocol Buffers (Protobuf)
Protocol Buffers serve as the interface definition language for the plugin system. Navidrome uses:
- protoc-gen-go-plugin: A custom protobuf compiler plugin that generates Go code for both the Navidrome host and WebAssembly plugins
- Protobuf messages for structured data exchange between the host and plugins
The protobuf definitions are located in:
plugins/api/api.proto
: Core plugin capability interfacesplugins/host/http/http.proto
: HTTP service interfaceplugins/host/scheduler/scheduler.proto
: Scheduler service interfaceplugins/host/config/config.proto
: Config service interfaceplugins/host/websocket/websocket.proto
: WebSocket service interfaceplugins/host/cache/cache.proto
: Cache service interfaceplugins/host/artwork/artwork.proto
: Artwork service interface
4. Integration Architecture
The plugin system integrates these libraries through several key components:
- Plugin Manager: Manages the lifecycle of plugins, from discovery to loading
- Compilation Cache: Improves performance by caching compiled WebAssembly modules
- Host Function Bridge: Exposes Navidrome functionality to plugins through WebAssembly imports
- Capability Adapters: Convert between the plugin API and Navidrome's internal interfaces
Each plugin method call:
- Creates a new isolated plugin instance using Wazero
- Executes the method in the sandboxed environment
- Converts data between Go and WebAssembly formats using the protobuf-generated code
- Cleans up the instance after the call completes
This stateless design ensures that plugins remain isolated and can't interfere with Navidrome's core functionality or each other.
Configuration
Plugins are configured in Navidrome's main configuration via the Plugins
section:
[Plugins]
# Enable or disable plugin support
Enabled = true
# Directory where plugins are stored (defaults to [DataFolder]/plugins)
Folder = "/path/to/plugins"
By default, the plugins folder is created under [DataFolder]/plugins
with restrictive permissions (0700
) to limit access to the Navidrome user.
Plugin-specific Configuration
You can also provide plugin-specific configuration using the PluginConfig
section. Each plugin can have its own configuration map using the folder name as the key:
[PluginConfig.my-plugin-folder]
api_key = "your-api-key"
user_id = "your-user-id"
enable_feature = "true"
[PluginConfig.another-plugin-folder]
server_url = "https://example.com/api"
timeout = "30"
These configuration values are passed to plugins during initialization through the OnInit
method in the LifecycleManagement
capability.
Plugins that implement the LifecycleManagement
capability will receive their configuration as a map of string keys and values.
Plugin Directory Structure
Each plugin must be located in its own directory under the plugins folder:
plugins/
├── my-plugin/
│ ├── plugin.wasm # Compiled WebAssembly module
│ └── manifest.json # Plugin manifest defining metadata and capabilities
├── another-plugin/
│ ├── plugin.wasm
│ └── manifest.json
Note: Plugin identification has changed! Navidrome now uses the folder name as the unique identifier for plugins, not the name
field in manifest.json
. This means:
- Multiple plugins can have the same
name
in their manifest, as long as they are in different folders - Plugin loading and commands use the folder name, not the manifest name
- Folder names must be unique across all plugins in your plugins directory
This change allows you to have multiple versions or variants of the same plugin (e.g., lastfm-official
, lastfm-custom
, lastfm-dev
) that all have the same manifest name but coexist peacefully.
Example: Multiple Plugin Variants
plugins/
├── lastfm-official/
│ ├── plugin.wasm
│ └── manifest.json # {"name": "LastFM Agent", ...}
├── lastfm-custom/
│ ├── plugin.wasm
│ └── manifest.json # {"name": "LastFM Agent", ...}
└── lastfm-dev/
├── plugin.wasm
└── manifest.json # {"name": "LastFM Agent", ...}
All three plugins can have the same "name": "LastFM Agent"
in their manifest, but they are identified and loaded by their folder names:
# Load specific variants
navidrome plugin refresh lastfm-official
navidrome plugin refresh lastfm-custom
navidrome plugin refresh lastfm-dev
# Configure each variant separately
[PluginConfig.lastfm-official]
api_key = "production-key"
[PluginConfig.lastfm-dev]
api_key = "development-key"
Using Symlinks for Plugin Variants
Symlinks provide a powerful way to create multiple configurations for the same plugin without duplicating files. When you create a symlink to a plugin directory, Navidrome treats the symlink as a separate plugin with its own configuration.
Example: Discord Rich Presence with Multiple Configurations
# Create symlinks for different environments
cd /path/to/navidrome/plugins
ln -s /path/to/discord-rich-presence-plugin drp-prod
ln -s /path/to/discord-rich-presence-plugin drp-dev
ln -s /path/to/discord-rich-presence-plugin drp-test
Directory structure:
plugins/
├── drp-prod -> /path/to/discord-rich-presence-plugin/
├── drp-dev -> /path/to/discord-rich-presence-plugin/
├── drp-test -> /path/to/discord-rich-presence-plugin/
Each symlink can have its own configuration:
[PluginConfig.drp-prod]
clientid = "production-client-id"
users = "admin:prod-token"
[PluginConfig.drp-dev]
clientid = "development-client-id"
users = "admin:dev-token,testuser:test-token"
[PluginConfig.drp-test]
clientid = "test-client-id"
users = "testuser:test-token"
Key Benefits:
- Single Source: One plugin implementation serves multiple use cases
- Independent Configuration: Each symlink has its own configuration namespace
- Development Workflow: Easy to test different configurations without code changes
- Resource Sharing: All symlinks share the same compiled WASM binary
Important Notes:
- The symlink name (not the target folder name) is used as the plugin ID
- Configuration keys use the symlink name:
PluginConfig.<symlink-name>
- Each symlink appears as a separate plugin in
navidrome plugin list
- CLI commands use the symlink name:
navidrome plugin refresh drp-dev
Plugin Package Format (.ndp)
Navidrome Plugin Packages (.ndp) are ZIP archives that bundle all files needed for a plugin. They can be installed using the navidrome plugin install
command.
Package Structure
A valid .ndp file must contain:
plugin-name.ndp (ZIP file)
├── plugin.wasm # Required: The compiled WebAssembly module
├── manifest.json # Required: Plugin manifest with metadata
├── README.md # Optional: Documentation
└── LICENSE # Optional: License information
Creating a Plugin Package
To create a plugin package:
- Compile your plugin to WebAssembly (plugin.wasm)
- Create a manifest.json file with required fields
- Include any documentation files you want to bundle
- Create a ZIP archive of all files
- Rename the ZIP file to have a .ndp extension
Installing a Plugin Package
Use the Navidrome CLI to install plugins:
navidrome plugin install /path/to/plugin-name.ndp
This will extract the plugin to a directory in your configured plugins folder.
Plugin Management
Navidrome provides a command-line interface for managing plugins. To use these commands, the plugin system must be enabled in your configuration.
Available Commands
# List all installed plugins
navidrome plugin list
# Show detailed information about a plugin package or installed plugin
navidrome plugin info plugin-name-or-package.ndp
# Install a plugin from a .ndp file
navidrome plugin install /path/to/plugin.ndp
# Remove an installed plugin (use folder name)
navidrome plugin remove plugin-folder-name
# Update an existing plugin
navidrome plugin update /path/to/updated-plugin.ndp
# Reload a plugin without restarting Navidrome (use folder name)
navidrome plugin refresh plugin-folder-name
# Create a symlink to a plugin development folder
navidrome plugin dev /path/to/dev/folder
Plugin Development
The dev
and refresh
commands are particularly useful for plugin development:
Development Workflow
- Create a plugin development folder with required files (
manifest.json
andplugin.wasm
) - Run
navidrome plugin dev /path/to/your/plugin
to create a symlink in the plugins directory - Make changes to your plugin code
- Recompile the WebAssembly module
- Run
navidrome plugin refresh your-plugin-folder-name
to reload the plugin without restarting Navidrome
The dev
command creates a symlink from your development folder to the plugins directory, allowing you to edit the plugin files directly in your development environment without copying them to the plugins directory after each change.
The refresh process:
- Reloads the plugin manifest
- Recompiles the WebAssembly module
- Updates the plugin registration
- Makes the updated plugin immediately available to Navidrome
Plugin Security
Navidrome provides multiple layers of security for plugin execution:
- WebAssembly Sandbox: Plugins run in isolated WebAssembly environments with no direct system access
- Permission System: Plugins can only access host services they explicitly request in their manifest (see Plugin Permission System)
- File System Security: The plugins folder is configured with restricted permissions (0700) accessible only by the user running Navidrome
- Resource Isolation: Each plugin instance is isolated and cannot interfere with other plugins or core Navidrome functionality
The permission system ensures that plugins follow the principle of least privilege - they start with no access to host services and must explicitly declare what they need. This prevents malicious or poorly written plugins from accessing unauthorized functionality.
Always ensure you trust the source of any plugins you install, and review their requested permissions before installation.
Plugin Manifest
Capability Names Are Case-Sensitive: Entries in the capabilities
array must exactly match one of the supported capabilities: MetadataAgent
, Scrobbler
, SchedulerCallback
, WebSocketCallback
, or LifecycleManagement
.
Manifest Validation: The manifest.json
is validated against the embedded JSON schema (plugins/schema/manifest.schema.json
). Invalid manifests will be rejected during plugin discovery.
Every plugin must provide a manifest.json
file that declares metadata, capabilities, and permissions:
{
"name": "my-awesome-plugin",
"author": "Your Name",
"version": "1.0.0",
"description": "A plugin that does awesome things",
"website": "https://github.com/yourname/my-awesome-plugin",
"capabilities": [
"MetadataAgent",
"Scrobbler",
"SchedulerCallback",
"WebSocketCallback",
"LifecycleManagement"
],
"permissions": {
"http": {
"reason": "To fetch metadata from external music APIs"
},
"cache": {
"reason": "To cache API responses and reduce rate limiting"
},
"config": {
"reason": "To read API keys and service configuration"
},
"scheduler": {
"reason": "To schedule periodic data refresh tasks"
}
}
}
Required fields:
name
: Display name of the plugin (used for documentation/display purposes; folder name is used for identification)author
: The creator or organization behind the pluginversion
: Version identifier (recommended to follow semantic versioning)description
: A brief description of what the plugin doeswebsite
: Website URL for the plugin documentation, source code, or homepage (must be a valid URI)capabilities
: Array of capability types the plugin implementspermissions
: Object mapping host service names to their configurations (use empty object{}
for no permissions)
Currently supported capabilities:
MetadataAgent
- For implementing media metadata providersScrobbler
- For implementing scrobbling pluginsSchedulerCallback
- For implementing timed callbacksWebSocketCallback
- For interacting with WebSocket endpoints and handling WebSocket eventsLifecycleManagement
- For handling plugin initialization and configuration
Plugin Loading Process
- The Plugin Manager scans the plugins directory and all subdirectories
- For each subdirectory containing a
plugin.wasm
file and validmanifest.json
, the manager:- Validates the manifest and checks for supported capabilities
- Pre-compiles the WASM module in the background
- Registers the plugin using the folder name as the unique identifier in the plugin registry
- Plugins can be loaded on-demand by folder name or all at once, depending on the manager's method calls
Writing a Plugin
Requirements
- Your plugin must be compiled to WebAssembly (WASM)
- Your plugin must implement at least one of the capability interfaces defined in
api.proto
- Your plugin must be placed in its own directory with a proper
manifest.json
Plugin Registration Functions
The plugin API provides several registration functions that plugins can call during initialization to register capabilities and obtain host services. These functions should typically be called in your plugin's init()
function.
Standard Registration Functions
func RegisterMetadataAgent(agent MetadataAgent)
func RegisterScrobbler(scrobbler Scrobbler)
func RegisterSchedulerCallback(callback SchedulerCallback)
func RegisterLifecycleManagement(lifecycle LifecycleManagement)
func RegisterWebSocketCallback(callback WebSocketCallback)
These functions register plugins for the standard capability interfaces:
- RegisterMetadataAgent: Register a plugin that provides artist/album metadata and images
- RegisterScrobbler: Register a plugin that handles scrobbling to external services
- RegisterSchedulerCallback: Register a plugin that handles scheduled callbacks (single callback per plugin)
- RegisterLifecycleManagement: Register a plugin that handles initialization and configuration
- RegisterWebSocketCallback: Register a plugin that handles WebSocket events
Basic Usage Example:
type MyPlugin struct {
// plugin implementation
}
func init() {
plugin := &MyPlugin{}
// Register capabilities your plugin implements
api.RegisterScrobbler(plugin)
api.RegisterLifecycleManagement(plugin)
}
RegisterNamedSchedulerCallback
func RegisterNamedSchedulerCallback(name string, cb SchedulerCallback) scheduler.SchedulerService
This function registers a named scheduler callback and returns a scheduler service instance. Named callbacks allow a single plugin to register multiple scheduler callbacks for different purposes, each with its own identifier.
Parameters:
name
(string): A unique identifier for this scheduler callback within the plugin. This name is used to route scheduled events to the correct callback handler.cb
(SchedulerCallback): An object that implements theSchedulerCallback
interface
Returns:
scheduler.SchedulerService
: A scheduler service instance that can be used to schedule one-time or recurring tasks for this specific callback
Usage Example (from Discord Rich Presence plugin):
func init() {
// Register multiple named scheduler callbacks for different purposes
plugin.sched = api.RegisterNamedSchedulerCallback("close-activity", plugin)
plugin.rpc.sched = api.RegisterNamedSchedulerCallback("heartbeat", plugin.rpc)
}
// The plugin implements SchedulerCallback to handle "close-activity" events
func (d *DiscordRPPlugin) OnSchedulerCallback(ctx context.Context, req *api.SchedulerCallbackRequest) (*api.SchedulerCallbackResponse, error) {
log.Printf("Removing presence for user %s", req.ScheduleId)
// Handle close-activity scheduling events
return nil, d.rpc.clearActivity(ctx, req.ScheduleId)
}
// The rpc component implements SchedulerCallback to handle "heartbeat" events
func (r *discordRPC) OnSchedulerCallback(ctx context.Context, req *api.SchedulerCallbackRequest) (*api.SchedulerCallbackResponse, error) {
// Handle heartbeat scheduling events
return nil, r.sendHeartbeat(ctx, req.ScheduleId)
}
// Use the returned scheduler service to schedule tasks
func (d *DiscordRPPlugin) NowPlaying(ctx context.Context, request *api.ScrobblerNowPlayingRequest) (*api.ScrobblerNowPlayingResponse, error) {
// Schedule a one-time callback to clear activity when track ends
_, err = d.sched.ScheduleOneTime(ctx, &scheduler.ScheduleOneTimeRequest{
ScheduleId: request.Username,
DelaySeconds: request.Track.Length - request.Track.Position + 5,
})
return nil, err
}
func (r *discordRPC) connect(ctx context.Context, username string, token string) error {
// Schedule recurring heartbeats for Discord connection
_, err := r.sched.ScheduleRecurring(ctx, &scheduler.ScheduleRecurringRequest{
CronExpression: "@every 41s",
ScheduleId: username,
})
return err
}
Key Benefits:
- Multiple Schedulers: A single plugin can have multiple named scheduler callbacks for different purposes (e.g., "heartbeat", "cleanup", "refresh")
- Isolated Scheduling: Each named callback gets its own scheduler service, allowing independent scheduling management
- Clear Separation: Different callback handlers can be implemented on different objects within your plugin
- Flexible Routing: The scheduler automatically routes callbacks to the correct handler based on the registration name
Important Notes:
- The
name
parameter must be unique within your plugin, but can be the same across different plugins - The returned scheduler service is specifically tied to the named callback you registered
- Scheduled events will call the
OnSchedulerCallback
method on the object you provided during registration - You must implement the
SchedulerCallback
interface on the object you register
RegisterSchedulerCallback vs RegisterNamedSchedulerCallback
- Use
RegisterSchedulerCallback
when your plugin only needs a single scheduler callback - Use
RegisterNamedSchedulerCallback
when your plugin needs multiple scheduler callbacks for different purposes (like the Discord plugin's "heartbeat" and "close-activity" callbacks)
The named version allows better organization and separation of concerns when you have complex scheduling requirements.
Capability Interfaces
Metadata Agent
A capability fetches metadata about artists and albums. Implement this interface to add support for fetching data from external sources.
service MetadataAgent {
// Artist metadata methods
rpc GetArtistMBID(ArtistMBIDRequest) returns (ArtistMBIDResponse);
rpc GetArtistURL(ArtistURLRequest) returns (ArtistURLResponse);
rpc GetArtistBiography(ArtistBiographyRequest) returns (ArtistBiographyResponse);
rpc GetSimilarArtists(ArtistSimilarRequest) returns (ArtistSimilarResponse);
rpc GetArtistImages(ArtistImageRequest) returns (ArtistImageResponse);
rpc GetArtistTopSongs(ArtistTopSongsRequest) returns (ArtistTopSongsResponse);
// Album metadata methods
rpc GetAlbumInfo(AlbumInfoRequest) returns (AlbumInfoResponse);
rpc GetAlbumImages(AlbumImagesRequest) returns (AlbumImagesResponse);
}
Scrobbler
This capability enables scrobbling to external services. Implement this interface to add support for custom scrobblers.
service Scrobbler {
rpc IsAuthorized(ScrobblerIsAuthorizedRequest) returns (ScrobblerIsAuthorizedResponse);
rpc NowPlaying(ScrobblerNowPlayingRequest) returns (ScrobblerNowPlayingResponse);
rpc Scrobble(ScrobblerScrobbleRequest) returns (ScrobblerScrobbleResponse);
}
Scheduler Callback
This capability allows plugins to receive one-time or recurring scheduled callbacks. Implement this interface to add support for scheduled tasks. See the SchedulerService for more information.
service SchedulerCallback {
rpc OnSchedulerCallback(SchedulerCallbackRequest) returns (SchedulerCallbackResponse);
}
WebSocket Callback
This capability allows plugins to interact with WebSocket endpoints and handle WebSocket events. Implement this interface to add support for WebSocket-based communication.
service WebSocketCallback {
// Called when a text message is received
rpc OnTextMessage(OnTextMessageRequest) returns (OnTextMessageResponse);
// Called when a binary message is received
rpc OnBinaryMessage(OnBinaryMessageRequest) returns (OnBinaryMessageResponse);
// Called when an error occurs
rpc OnError(OnErrorRequest) returns (OnErrorResponse);
// Called when the connection is closed
rpc OnClose(OnCloseRequest) returns (OnCloseResponse);
}
Plugins can use the WebSocket host service to connect to WebSocket endpoints, send messages, and handle responses:
// Define a connection ID first
connectionID := "my-connection-id"
// Connect to a WebSocket server
connectResp, err := websocket.Connect(ctx, &websocket.ConnectRequest{
Url: "wss://example.com/ws",
Headers: map[string]string{"Authorization": "Bearer token"},
ConnectionId: connectionID,
})
if err != nil {
return err
}
// Send a text message
_, err = websocket.SendText(ctx, &websocket.SendTextRequest{
ConnectionId: connectionID,
Message: "Hello WebSocket",
})
// Close the connection when done
_, err = websocket.Close(ctx, &websocket.CloseRequest{
ConnectionId: connectionID,
Code: 1000, // Normal closure
Reason: "Done",
})
Host Services
Navidrome provides several host services that plugins can use to interact with external systems and access functionality. Plugins must declare permissions for each service they want to use in their manifest.json
.
HTTP Service
The HTTP service allows plugins to make HTTP requests to external APIs and services. To use this service, declare the http
permission in your manifest.
Basic Usage
{
"permissions": {
"http": {
"reason": "To fetch artist metadata from external music APIs"
}
}
}
Granular Permissions
For enhanced security, you can specify granular HTTP permissions that restrict which URLs and HTTP methods your plugin can access:
{
"permissions": {
"http": {
"reason": "To fetch album reviews from AllMusic and artist data from MusicBrainz",
"allowedUrls": {
"https://api.allmusic.com": ["GET", "POST"],
"https://*.musicbrainz.org": ["GET"],
"https://coverartarchive.org": ["GET"],
"*": ["GET"]
},
"allowLocalNetwork": false
}
}
}
Permission Fields:
-
reason
(required): Clear explanation of why HTTP access is needed -
allowedUrls
(required): Map of URL patterns to allowed HTTP methods- Must contain at least one URL pattern
- For unrestricted access, use:
{"*": ["*"]}
- Keys can be exact URLs, wildcard patterns, or
*
for any URL - Values are arrays of HTTP methods:
GET
,POST
,PUT
,DELETE
,PATCH
,HEAD
,OPTIONS
, or*
for any method - Important: Redirect destinations must also be included in this list. If a URL redirects to another URL not in
allowedUrls
, the redirect will be blocked.
-
allowLocalNetwork
(optional, default:false
): Whether to allow requests to localhost/private IPs
URL Pattern Matching:
- Exact URLs:
https://api.example.com
- Wildcard subdomains:
https://*.example.com
(matches any subdomain) - Wildcard paths:
https://example.com/api/*
(matches any path under /api/) - Global wildcard:
*
(matches any URL - use with caution)
Examples:
// Allow only GET requests to specific APIs
{
"allowedUrls": {
"https://api.last.fm": ["GET"],
"https://ws.audioscrobbler.com": ["GET"]
}
}
// Allow any method to a trusted domain, GET everywhere else
{
"allowedUrls": {
"https://my-trusted-api.com": ["*"],
"*": ["GET"]
}
}
// Handle redirects by including redirect destinations
{
"allowedUrls": {
"https://short.ly/api123": ["GET"], // Original URL
"https://api.actual-service.com": ["GET"] // Redirect destination
}
}
// Strict permissions for a secure plugin (blocks redirects by not including redirect destinations)
{
"allowedUrls": {
"https://api.musicbrainz.org/ws/2": ["GET"]
},
"allowLocalNetwork": false
}
Security Considerations
The HTTP service implements several security features:
- Local Network Protection: By default, requests to localhost and private IP ranges are blocked
- URL Filtering: Only URLs matching
allowedUrls
patterns are allowed - Method Restrictions: HTTP methods are validated against the allowed list for each URL pattern
- Redirect Security:
- Redirect destinations must also match
allowedUrls
patterns and methods - Maximum of 5 redirects per request to prevent redirect loops
- To block all redirects, simply don't include any redirect destinations in
allowedUrls
- Redirect destinations must also match
Private IP Ranges Blocked (when allowLocalNetwork: false
):
- IPv4:
10.0.0.0/8
,172.16.0.0/12
,192.168.0.0/16
,127.0.0.0/8
,169.254.0.0/16
- IPv6:
::1
,fe80::/10
,fc00::/7
- Hostnames:
localhost
Making HTTP Requests
import "github.com/navidrome/navidrome/plugins/host/http"
// GET request
resp, err := httpClient.Get(ctx, &http.HttpRequest{
Url: "https://api.example.com/data",
Headers: map[string]string{
"Authorization": "Bearer " + token,
"User-Agent": "MyPlugin/1.0",
},
TimeoutMs: 5000,
})
// POST request with body
resp, err := httpClient.Post(ctx, &http.HttpRequest{
Url: "https://api.example.com/submit",
Headers: map[string]string{
"Content-Type": "application/json",
},
Body: []byte(`{"key": "value"}`),
TimeoutMs: 10000,
})
// Handle response
if err != nil {
return &api.Response{Error: "HTTP request failed: " + err.Error()}, nil
}
if resp.Error != "" {
return &api.Response{Error: "HTTP error: " + resp.Error}, nil
}
if resp.Status != 200 {
return &api.Response{Error: fmt.Sprintf("HTTP %d: %s", resp.Status, string(resp.Body))}, nil
}
// Use response data
data := resp.Body
headers := resp.Headers
Other Host Services
Config Service
Access plugin-specific configuration:
{
"permissions": {
"config": {
"reason": "To read API keys and service endpoints from plugin configuration"
}
}
}
Cache Service
Store and retrieve data to improve performance:
{
"permissions": {
"cache": {
"reason": "To cache API responses and reduce external service calls"
}
}
}
Scheduler Service
Schedule recurring or one-time tasks:
{
"permissions": {
"scheduler": {
"reason": "To schedule periodic metadata refresh and cleanup tasks"
}
}
}
WebSocket Service
Connect to WebSocket endpoints:
{
"permissions": {
"websocket": {
"reason": "To connect to real-time music service APIs for live data",
"allowedUrls": [
"wss://api.musicservice.com/ws",
"wss://realtime.example.com"
],
"allowLocalNetwork": false
}
}
}
Artwork Service
Generate public URLs for artwork:
{
"permissions": {
"artwork": {
"reason": "To generate public URLs for album and artist images"
}
}
}
Error Handling
Plugins should use the standard error values (plugin:not_found
, plugin:not_implemented
) to indicate resource-not-found and unimplemented-method scenarios. All other errors will be propagated directly to the caller. Ensure your capability methods return errors via the response message error
fields rather than panicking or relying on transport errors.
Plugin Lifecycle and Statelessness
Important: Navidrome plugins are stateless. Each method call creates a new plugin instance which is destroyed afterward. This has several important implications:
- No in-memory persistence: Plugins cannot store state between method calls in memory
- Each call is isolated: Variables, configurations, and runtime state don't persist between calls
- No shared resources: Each plugin instance has its own memory space
This stateless design is crucial for security and stability:
- Memory leaks in one call won't affect subsequent operations
- A crashed plugin instance won't bring down the entire system
- Resource usage is more predictable and contained
When developing plugins, keep these guidelines in mind:
- Don't try to cache data in memory between calls
- Don't store authentication tokens or session data in variables
- If persistence is needed, use external storage or the host's HTTP interface
- Performance optimizations should focus on efficient per-call execution
Using Plugin Configuration
Since plugins are stateless, you can use the LifecycleManagement
interface to read configuration when your plugin is loaded and perform any necessary setup:
func (p *myPlugin) OnInit(ctx context.Context, req *api.InitRequest) (*api.InitResponse, error) {
// Access plugin configuration
apiKey := req.Config["api_key"]
if apiKey == "" {
return &api.InitResponse{Error: "Missing API key in configuration"}, nil
}
// Validate configuration
serverURL := req.Config["server_url"]
if serverURL == "" {
serverURL = "https://default-api.example.com" // Use default if not specified
}
// Perform initialization tasks (e.g., validate API key)
httpClient := &http.HttpServiceClient{}
resp, err := httpClient.Get(ctx, &http.HttpRequest{
Url: serverURL + "/validate?key=" + apiKey,
})
if err != nil {
return &api.InitResponse{Error: "Failed to validate API key: " + err.Error()}, nil
}
if resp.StatusCode != 200 {
return &api.InitResponse{Error: "Invalid API key"}, nil
}
return &api.InitResponse{}, nil
}
Remember, the OnInit
method is called only once when the plugin is loaded. It cannot store any state that needs to persist between method calls. It's primarily useful for:
- Validating required configuration
- Checking API credentials
- Verifying connectivity to external services
- Initializing any external resources
Caching
The plugin system implements a compilation cache to improve performance:
- Compiled WASM modules are cached in
[CacheFolder]/plugins
- This reduces startup time for plugins that have already been compiled
- The cache has a automatic cleanup mechanism to remove old modules.
- when the cache folder exceeds
Plugins.CacheSize
(default 100MB), the oldest modules are removed
- when the cache folder exceeds
WASM Loading Optimization
To improve performance during plugin instance creation, the system implements an optimization that avoids repeated file reads and compilation:
-
Precompilation: During plugin discovery, WASM files are read and compiled in the background, with both the MD5 hash of the file bytes and compiled modules cached in memory.
-
Optimized Runtime: After precompilation completes, plugins use an
optimizedRuntime
wrapper that overridesCompileModule
to detect when the same WASM bytes are being compiled by comparing MD5 hashes. -
Cache Hit: When the generated plugin code calls
os.ReadFile()
andCompileModule()
, the optimization calculates the MD5 hash of the incoming bytes and compares it with the cached hash. If they match, it returns the pre-compiled module directly. -
Performance Benefit: This eliminates repeated compilation while using minimal memory (16 bytes per plugin for the MD5 hash vs potentially MB of WASM bytes), significantly improving plugin instance creation speed while maintaining full compatibility with the generated API code.
-
Memory Efficiency: By storing only MD5 hashes instead of full WASM bytes, the optimization scales efficiently regardless of plugin size or count.
The optimization is transparent to plugin developers and automatically activates when plugins are successfully precompiled.
Best Practices
-
Resource Management:
- The host handles HTTP response cleanup, so no need to close response objects
- Keep plugin instances lightweight as they are created and destroyed frequently
-
Error Handling:
- Use the standard error types when appropriate
- Return descriptive error messages for debugging
- Custom errors are supported and will be propagated to the caller
-
Performance:
- Remember plugins are stateless, so don't rely on local variables for caching. Use the CacheService for caching data.
- Use efficient algorithms that work well in single-call scenarios
-
Security:
- Only request permissions you actually need (see Plugin Permission System)
- Validate inputs to prevent injection attacks
- Don't store sensitive credentials in the plugin code
- Use configuration for API keys and sensitive data
Limitations
- WASM plugins have limited access to system resources
- Plugin compilation has an initial overhead on first load, as it needs to be compiled to WebAssembly
- Subsequent calls are faster due to caching
- New plugin capabilities types require changes to the core codebase
- Stateless nature prevents certain optimizations
Troubleshooting
-
Plugin not detected:
- Ensure
plugin.wasm
andmanifest.json
exist in the plugin directory - Check that the manifest contains valid capabilities names
- Verify the manifest schema is valid (see Plugin Permission System)
- Ensure
-
Permission errors:
- "function not exported in module env": Plugin trying to use a service without proper permission
- Check that required permissions are declared in
manifest.json
- See Troubleshooting Permissions for detailed guidance
-
Compilation errors:
- Check logs for WASM compilation errors
- Verify the plugin is compatible with the current API version
-
Runtime errors:
- Look for error messages in the Navidrome logs
- Add debug logging to your plugin
- Check if the error is permission-related before debugging plugin logic