# 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: ```bash make gen ``` ## Module Structure This is a consolidated Go module that includes: - `host/` - Host function wrappers for calling Navidrome services from plugins - `lifecycle/` - Plugin lifecycle hooks (initialization) - `metadata/` - Metadata agent capability for artist/album info - `scheduler/` - Scheduler callback capability for scheduled tasks - `scrobbler/` - Scrobbler capability for play tracking - `websocket/` - WebSocket callback capability for real-time messages ## Usage Add this module as a dependency in your plugin's `go.mod`: ```go 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: ```go 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 ```go 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. ```go 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. ```go 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. ```go 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. ```go 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. ```go 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: ```bash tinygo build -o plugin.wasm -target=wasip1 -buildmode=c-shared . ``` Or use the provided Makefile targets in plugin examples: ```bash make plugin.wasm ``` ## Testing Plugins The PDK includes [testify/mock](https://github.com/stretchr/testify) 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: ```go 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: ```go 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 ```go 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: ```bash 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](pdk/example_test.go).