* feat(plugins): define TaskQueue host service interface Add the TaskQueueService interface with CreateQueue, Enqueue, GetTaskStatus, and CancelTask methods plus QueueConfig struct. * feat(plugins): define TaskWorker capability for task execution callbacks * feat(plugins): add taskqueue permission to manifest schema Add TaskQueuePermission with maxConcurrency option. * feat(plugins): implement TaskQueue service with SQLite persistence and workers Per-plugin SQLite database with queues and tasks tables. Worker goroutines dequeue tasks and invoke nd_task_execute callback. Exponential backoff retries, rate limiting via delayMs, automatic cleanup of terminal tasks. * feat(plugins): require TaskWorker capability for taskqueue permission * feat(plugins): register TaskQueue host service in manager * feat(plugins): add test-taskqueue plugin for integration testing * feat(plugins): add integration tests for TaskQueue host service * docs: document TaskQueue module for persistent task queues Signed-off-by: Deluan <deluan@navidrome.org> * fix(plugins): harden TaskQueue host service with validation and safety improvements Add input validation (queue name length, payload size limits), extract status string constants to eliminate raw SQL literals, make CreateQueue idempotent via upsert for crash recovery, fix RetentionMs default check for negative values, cap exponential backoff at 1 hour to prevent overflow, and replace manual mutex-based delay enforcement with rate.Limiter from golang.org/x/time/rate for correct concurrent worker serialization. * refactor(plugins): remove capability check for TaskWorker in TaskQueue host service Signed-off-by: Deluan <deluan@navidrome.org> * fix(plugins): use context-aware database execution in TaskQueue host service Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): streamline task queue configuration and error handling Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): increase maxConcurrency for task queue and handle budget exhaustion Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): simplify goroutine management in task queue service Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): update TaskWorker interface to return status messages and refactor task queue service Signed-off-by: Deluan <deluan@navidrome.org> * feat(plugins): add ClearQueue function to remove pending tasks from a specified queue Signed-off-by: Deluan <deluan@navidrome.org> * refactor(plugins): use migrateDB for task queue schema and fix constant name collision Replaced the raw db.Exec call in createTaskQueueSchema with migrateDB, matching the pattern used by createKVStoreSchema. This enables version-tracked schema migrations via SQLite's PRAGMA user_version, allowing future schema changes to be appended incrementally. Also renamed cleanupInterval to taskCleanupInterval to resolve a redeclaration conflict with host_kvstore.go. * regenerate PDKs Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
Navidrome Plugin Development Kit for Go
This directory contains the auto-generated Go PDK (Plugin Development Kit) for building Navidrome plugins. The PDK provides both host function wrappers for interacting with Navidrome and capability interfaces for implementing plugin functionality.
⚠️ Auto-Generated Code
Do not edit files in this directory manually. They are generated by the ndpgen tool.
To regenerate:
make gen
Module Structure
This is a consolidated Go module that includes:
host/- Host function wrappers for calling Navidrome services from pluginslifecycle/- Plugin lifecycle hooks (initialization)metadata/- Metadata agent capability for artist/album infoscheduler/- Scheduler callback capability for scheduled tasksscrobbler/- Scrobbler capability for play trackingwebsocket/- WebSocket callback capability for real-time messages
Usage
Add this module as a dependency in your plugin's go.mod:
require github.com/navidrome/navidrome/plugins/pdk/go v0.0.0
replace github.com/navidrome/navidrome/plugins/pdk/go => ../../pdk/go
Then import the packages you need:
package main
import (
"github.com/navidrome/navidrome/plugins/pdk/go/host"
"github.com/navidrome/navidrome/plugins/pdk/go/lifecycle"
"github.com/navidrome/navidrome/plugins/pdk/go/scheduler"
)
func init() {
lifecycle.Register(&myPlugin{})
scheduler.Register(&myPlugin{})
}
type myPlugin struct{}
func (p *myPlugin) OnInit() error {
// Initialize your plugin
return nil
}
func (p *myPlugin) OnCallback(req scheduler.SchedulerCallbackRequest) error {
// Handle scheduled task
return host.WebSocketBroadcast("task-complete", req.ScheduleID)
}
func main() {}
Host Services
The host package provides wrappers for calling Navidrome's host services:
| Service | Description |
|---|---|
Artwork |
Access album and artist artwork |
Cache |
Temporary key-value storage with TTL |
KVStore |
Persistent key-value storage |
Library |
Access the music library (albums, artists, tracks) |
Scheduler |
Schedule one-time and recurring tasks |
SubsonicAPI |
Make Subsonic API calls |
WebSocket |
Send real-time messages to clients |
Example: Using Host Services
package main
import (
"github.com/navidrome/navidrome/plugins/pdk/go/host"
)
func myPluginFunction() error {
// Use the cache service
_, err := host.CacheSetString("my_key", "my_value", 3600)
if err != nil {
return err
}
// Schedule a recurring task
_, err = host.SchedulerScheduleRecurring("@every 5m", "payload", "task_id")
if err != nil {
return err
}
// Access library data with typed structs
resp, err := host.LibraryGetAllLibraries()
if err != nil {
return err
}
for _, lib := range resp.Result {
// Library: %s with %d songs", lib.Name, lib.TotalSongs
}
return nil
}
Capabilities
Capabilities define what functionality your plugin implements. Register your implementations
in the init() function.
Lifecycle
Provides plugin initialization hooks.
import "github.com/navidrome/navidrome/plugins/pdk/go/lifecycle"
func init() {
lifecycle.Register(&myPlugin{})
}
type myPlugin struct{}
func (p *myPlugin) OnInit() error {
// Called once when plugin is loaded
return nil
}
MetadataAgent
Provides artist and album metadata from external sources.
import "github.com/navidrome/navidrome/plugins/pdk/go/metadata"
func init() {
metadata.Register(&myAgent{})
}
type myAgent struct{}
func (a *myAgent) GetArtistBiography(req metadata.ArtistRequest) (*metadata.ArtistBiographyResponse, error) {
return &metadata.ArtistBiographyResponse{
Biography: "Artist biography text...",
}, nil
}
func (a *myAgent) GetArtistImages(req metadata.ArtistRequest) (*metadata.ArtistImagesResponse, error) {
return &metadata.ArtistImagesResponse{
Images: []metadata.ImageInfo{
{URL: "https://example.com/image.jpg", Size: 1000},
},
}, nil
}
Scheduler
Handles callbacks from scheduled tasks.
import (
"github.com/navidrome/navidrome/plugins/pdk/go/host"
"github.com/navidrome/navidrome/plugins/pdk/go/scheduler"
)
func init() {
scheduler.Register(&myScheduler{})
}
type myScheduler struct{}
func (s *myScheduler) OnCallback(req scheduler.SchedulerCallbackRequest) error {
// Handle the scheduled task
if req.Payload == "update-data" {
// Do work...
return host.WebSocketBroadcast("data-updated", "")
}
return nil
}
Scrobbler
Tracks play activity.
import "github.com/navidrome/navidrome/plugins/pdk/go/scrobbler"
func init() {
scrobbler.Register(&myScrobbler{})
}
type myScrobbler struct{}
func (s *myScrobbler) Scrobble(req scrobbler.ScrobbleRequest) error {
// Track the play
return nil
}
func (s *myScrobbler) NowPlaying(req scrobbler.NowPlayingRequest) error {
// Update now playing status
return nil
}
WebSocket
Handles incoming WebSocket messages.
import "github.com/navidrome/navidrome/plugins/pdk/go/websocket"
func init() {
websocket.Register(&myHandler{})
}
type myHandler struct{}
func (h *myHandler) OnWebSocketMessage(req websocket.WebSocketMessageRequest) error {
// Handle incoming message
return nil
}
Building Plugins
Go plugins must be compiled to WebAssembly using TinyGo:
tinygo build -o plugin.wasm -target=wasip1 -buildmode=c-shared .
Or use the provided Makefile targets in plugin examples:
make plugin.wasm
Testing Plugins
The PDK includes testify/mock implementations for all host services, allowing you to unit test your plugin code on non-WASM platforms (your development machine).
PDK Abstraction Layer
The pdk subpackage provides a testable wrapper around the Extism PDK functions. Instead of importing
github.com/extism/go-pdk directly, import the abstraction layer:
import "github.com/navidrome/navidrome/plugins/pdk/go/pdk"
func myFunction() {
// Use pdk functions - same API as extism/go-pdk
config, ok := pdk.GetConfig("my_setting")
if ok {
pdk.Log(pdk.LogInfo, "Setting: " + config)
}
var input MyInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return
}
output := processInput(input)
pdk.OutputJSON(output)
}
For WASM builds, these functions delegate directly to extism/go-pdk with zero overhead.
For native builds (tests), they use mocks that you can configure:
package myplugin
import (
"testing"
"github.com/navidrome/navidrome/plugins/pdk/go/pdk"
)
func TestMyFunction(t *testing.T) {
// Reset mock state before each test
pdk.ResetMock()
// Set up expectations
pdk.PDKMock.On("GetConfig", "my_setting").Return("test_value", true)
pdk.PDKMock.On("Log", pdk.LogInfo, "Setting: test_value").Return()
pdk.PDKMock.On("InputJSON", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
// Populate the input struct
input := args.Get(0).(*MyInput)
input.Name = "test"
})
pdk.PDKMock.On("OutputJSON", mock.Anything).Return(nil)
// Call your function
myFunction()
// Verify expectations
pdk.PDKMock.AssertExpectations(t)
}
Mock Instances
Each host service has an auto-instantiated mock instance:
| Service | Mock Instance |
|---|---|
Artwork |
host.ArtworkMock |
Cache |
host.CacheMock |
Config |
host.ConfigMock |
KVStore |
host.KVStoreMock |
Library |
host.LibraryMock |
Scheduler |
host.SchedulerMock |
SubsonicAPI |
host.SubsonicAPIMock |
WebSocket |
host.WebSocketMock |
Example Test
package myplugin
import (
"testing"
"github.com/navidrome/navidrome/plugins/pdk/go/host"
)
func TestMyPluginFunction(t *testing.T) {
// Set expectations on the mock
host.CacheMock.On("GetString", "my-key").Return("cached-value", true, nil)
host.CacheMock.On("SetString", "new-key", "new-value", int64(3600)).Return(nil)
// Call your plugin code that uses host.CacheGetString / host.CacheSetString
result, err := myPluginFunction()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Assert the result
if result != "expected" {
t.Errorf("unexpected result: %s", result)
}
// Verify all expected calls were made
host.CacheMock.AssertExpectations(t)
}
Running Tests
Since tests run on your development machine (not WASM), use standard Go testing:
go test ./...
The stub files with mocks are only compiled for non-WASM builds (//go:build !wasip1),
so they won't affect your production WASM binary.
Complete Examples
For more comprehensive examples including HTTP requests, Memory handling, and various testing patterns, see pdk/example_test.go.