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.