feat(plugins): implementation of the Navidrome Plugin Development Kit with generated client wrappers and service interfaces - Phase 5

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-12-30 03:14:50 -05:00
parent 0fb497ed73
commit d81e641cc6
35 changed files with 512 additions and 2020 deletions

View File

@ -705,6 +705,50 @@ tinygo build -o plugin.wasm -target wasip1 -buildmode=c-shared .
zip -j my-plugin.ndp manifest.json plugin.wasm
```
#### Using Go PDK Packages
Navidrome provides type-safe Go packages for each capability in `plugins/pdk/go/`. Instead of manually exporting functions with `//go:wasmexport`, use the `Register()` pattern:
```go
package main
import (
"github.com/navidrome/navidrome/plugins/pdk/go/metadata"
)
type myPlugin struct{}
func (p *myPlugin) GetArtistBiography(input metadata.ArtistBiographyInput) metadata.ArtistBiographyOutput {
return metadata.ArtistBiographyOutput{Biography: "Biography text..."}
}
func init() {
metadata.Register(&myPlugin{})
}
func main() {}
```
Add to your `go.mod`:
```
require github.com/navidrome/navidrome v0.0.0
replace github.com/navidrome/navidrome => ../../..
```
Available capability packages:
| Package | Import Path | Description |
|-------------|----------------------------|--------------------------------------|
| `metadata` | `plugins/pdk/go/metadata` | Artist/album metadata providers |
| `scrobbler` | `plugins/pdk/go/scrobbler` | Scrobbling services |
| `lifecycle` | `plugins/pdk/go/lifecycle` | Plugin initialization |
| `scheduler` | `plugins/pdk/go/scheduler` | Scheduled task callbacks |
| `websocket` | `plugins/pdk/go/websocket` | WebSocket event handlers |
| `host` | `plugins/pdk/go/host` | Host service SDK (HTTP, cache, etc.) |
See the example plugins in [examples/](examples/) for complete usage patterns.
### Rust
```bash
@ -734,7 +778,7 @@ Bootstrap a new plugin from a schema:
# Create a metadata agent plugin
xtp plugin init \
--schema-file plugins/schemas/metadata_agent.yaml \
--schema-file plugins/capabilities/metadata_agent.yaml \
--template go \
--path ./my-agent \
--name my-agent
@ -744,26 +788,26 @@ cd my-agent && xtp plugin build
zip -j my-agent.ndp manifest.json dist/plugin.wasm
```
See [schemas/README.md](schemas/README.md) for available schemas.
See [capabilities/README.md](capabilities/README.md) for available schemas and scaffolding examples.
### Using Host Service SDKs
Generated SDKs for calling host services are in `plugins/host/go/`, `plugins/host/python/` and `plugins/host/rust`.
Generated SDKs for calling host services are in `plugins/pdk/go/host/`, `plugins/pdk/python/` and `plugins/pdk/rust`.
**For Go plugins:** Import the SDK as a Go module:
```go
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
import ndhost "github.com/navidrome/navidrome/plugins/pdk/go/host"
```
Add to your `go.mod`:
```
require github.com/navidrome/navidrome/plugins/host/go v0.0.0
replace github.com/navidrome/navidrome/plugins/host/go => ../../host/go
require github.com/navidrome/navidrome/plugins/pdk/go/host v0.0.0
replace github.com/navidrome/navidrome/plugins/pdk/go/host => ../../pdk/go/host
```
See [plugins/host/go/README.md](host/go/README.md) for detailed documentation.
See [pdk/go/host/README.md](pdk/go/host/README.md) for detailed documentation.
**For Python plugins:** Copy functions from `nd_host_*.py` into your `__init__.py` (see comments in those files for extism-py limitations).
@ -787,6 +831,7 @@ See [examples/](examples/) for complete working plugins:
---
## Security
Plugins run in a secure WebAssembly sandbox provided by [Extism](https://extism.org/) and the [Wazero](https://wazero.io/) runtime:

View File

@ -53,13 +53,13 @@ xtp plugin init --schema-file plugins/capabilities/websocket_callback.yaml \
### Available Capabilities
| Capability | Schema File | Description |
|------------|-------------|-------------|
| Metadata Agent | `metadata_agent.yaml` | Fetch artist biographies, album images, and similar artists |
| Scrobbler | `scrobbler.yaml` | Report listening activity to external services |
| Lifecycle | `lifecycle.yaml` | Plugin initialization callbacks |
| Scheduler Callback | `scheduler_callback.yaml` | Scheduled task execution |
| WebSocket Callback | `websocket_callback.yaml` | Real-time WebSocket message handling |
| Capability | Schema File | Description |
|--------------------|---------------------------|-------------------------------------------------------------|
| Metadata Agent | `metadata_agent.yaml` | Fetch artist biographies, album images, and similar artists |
| Scrobbler | `scrobbler.yaml` | Report listening activity to external services |
| Lifecycle | `lifecycle.yaml` | Plugin initialization callbacks |
| Scheduler Callback | `scheduler_callback.yaml` | Scheduled task execution |
| WebSocket Callback | `websocket_callback.yaml` | Real-time WebSocket message handling |
### Building Your Plugin

View File

@ -2,7 +2,7 @@
Navidrome Plugin Development Kit (PDK) code generator. It reads Go interface definitions with special annotations and generates client wrappers for WASM plugins.
This tool is the unified code generator that replaces `hostgen` and will eventually handle both host function wrappers and capability wrappers.
This tool is the unified code generator that handle both host function wrappers and capability wrappers.
## Usage
@ -149,15 +149,6 @@ type ServiceSearchResponse struct {
}
```
## Migration from hostgen
The `ndpgen` tool replaces `hostgen` for plugin development. Key differences:
1. **Output structure**: Files go directly in the output directory (not a `go/` subdirectory)
2. **Package name**: Generated code uses `ndpdk` instead of `ndhost`
3. **Module path**: Uses `github.com/navidrome/navidrome/plugins/pdk/go/host` instead of `github.com/navidrome/navidrome/plugins/host/go`
4. **Focus**: `ndpgen` generates only client-side code (plugin SDK), not host-side code
## Running Tests
```bash

View File

@ -845,10 +845,11 @@ type TestService interface {
// Check type alias
Expect(codeStr).To(ContainSubstring("type ScrobblerErrorType string"))
// Check consts
Expect(codeStr).To(ContainSubstring("ScrobblerErrorNone"))
// Check consts - all consts should have type annotation
Expect(codeStr).To(ContainSubstring("ScrobblerErrorNone ScrobblerErrorType ="))
Expect(codeStr).To(ContainSubstring(`"none"`))
Expect(codeStr).To(ContainSubstring("ScrobblerErrorRetry"))
Expect(codeStr).To(ContainSubstring("ScrobblerErrorRetry ScrobblerErrorType ="))
Expect(codeStr).To(ContainSubstring(`"retry"`))
})
})

View File

@ -13,7 +13,7 @@ var _ = Describe("Parser", func() {
BeforeEach(func() {
var err error
tmpDir, err = os.MkdirTemp("", "hostgen-test-*")
tmpDir, err = os.MkdirTemp("", "ndpgen-test-*")
Expect(err).NotTo(HaveOccurred())
})

View File

@ -25,11 +25,16 @@ type {{.Name}} {{.Type}}
{{- if .Values}}
const (
{{- range .Values}}
{{- if .Doc}}
{{formatDoc .Doc | indent 1}}
{{- $type := .Type}}
{{- range $i, $v := .Values}}
{{- if $v.Doc}}
{{formatDoc $v.Doc | indent 1}}
{{- end}}
{{- if $type}}
{{$v.Name}} {{$type}} = {{$v.Value}}
{{- else}}
{{$v.Name}} = {{$v.Value}}
{{- end}}
{{.Name}} = {{.Value}}
{{- end}}
)
{{- end}}
@ -38,9 +43,10 @@ const (
{{- /* Generate struct definitions */ -}}
{{- range .Capability.Structs}}
// {{.Name}} represents the {{.Name}} data structure.
{{- if .Doc}}
{{formatDoc .Doc}}
{{- else}}
// {{.Name}} represents the {{.Name}} data structure.
{{- end}}
type {{.Name}} struct {
{{- range .Fields}}

View File

@ -22,11 +22,16 @@ type {{.Name}} {{.Type}}
{{- if .Values}}
const (
{{- range .Values}}
{{- if .Doc}}
{{formatDoc .Doc | indent 1}}
{{- $type := .Type}}
{{- range $i, $v := .Values}}
{{- if $v.Doc}}
{{formatDoc $v.Doc | indent 1}}
{{- end}}
{{- if $type}}
{{$v.Name}} {{$type}} = {{$v.Value}}
{{- else}}
{{$v.Name}} = {{$v.Value}}
{{- end}}
{{.Name}} = {{.Value}}
{{- end}}
)
{{- end}}
@ -35,9 +40,10 @@ const (
{{- /* Generate struct definitions */ -}}
{{- range .Capability.Structs}}
// {{.Name}} represents the {{.Name}} data structure.
{{- if .Doc}}
{{formatDoc .Doc}}
{{- else}}
// {{.Name}} represents the {{.Name}} data structure.
{{- end}}
type {{.Name}} struct {
{{- range .Fields}}

View File

@ -30,12 +30,6 @@ The plugin will append `-USD` to any symbol without a trading pair specified.
## Building
This plugin was scaffolded using XTP CLI:
```bash
xtp plugin init --schema-file ../schemas/websocket_callback.yaml --template go --path ./crypto-ticker --name crypto-ticker
```
To build the plugin and package as `.ndp`:
```bash
@ -75,23 +69,22 @@ Copy the resulting `crypto-ticker.ndp` to your Navidrome plugins folder.
## Files
- `main.go` - Main plugin implementation
- `pdk.gen.go` - Generated WebSocket callback types (from XTP)
- `go.mod` - Go module file (imports `ndhost` SDK)
- `go.mod` - Go module file
## Host SDK
## PDK
This plugin imports the Go host SDK directly:
This plugin imports the Navidrome PDK subpackages directly:
```go
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
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"
"github.com/navidrome/navidrome/plugins/pdk/go/websocket"
)
```
The `go.mod` file uses a `replace` directive to point to the local SDK:
```
require github.com/navidrome/navidrome/plugins/host/go v0.0.0
replace github.com/navidrome/navidrome/plugins/host/go => ../../host/go
```
The `go.mod` file uses `replace` directives to point to the local packages for development.
---

View File

@ -1,10 +1,13 @@
module crypto-ticker
go 1.24
go 1.25
require (
github.com/extism/go-pdk v1.1.3
github.com/navidrome/navidrome/plugins/pdk/go/host v0.0.0
github.com/navidrome/navidrome v0.0.0-00010101000000-000000000000
github.com/navidrome/navidrome/plugins/pdk/go/host v0.0.0-00010101000000-000000000000
)
replace github.com/navidrome/navidrome => ../../..
replace github.com/navidrome/navidrome/plugins/pdk/go/host => ../../pdk/go/host

View File

@ -2,9 +2,6 @@
//
// This plugin connects to Coinbase's WebSocket API to receive real-time
// cryptocurrency price updates and logs them to the Navidrome console.
//
// Note: run `go doc -all` in this package to see all of the types and functions available.
// ./pdk.gen.go contains the domain types from the host where your plugin will run.
package main
import (
@ -13,7 +10,10 @@ import (
"strings"
pdk "github.com/extism/go-pdk"
host "github.com/navidrome/navidrome/plugins/pdk/go/host"
"github.com/navidrome/navidrome/plugins/pdk/go/host"
"github.com/navidrome/navidrome/plugins/pdk/go/lifecycle"
"github.com/navidrome/navidrome/plugins/pdk/go/scheduler"
"github.com/navidrome/navidrome/plugins/pdk/go/websocket"
)
const (
@ -49,26 +49,29 @@ type CoinbaseTicker struct {
Time string `json:"time"`
}
// OnInitInput is the input for nd_on_init (currently empty, reserved for future use)
type OnInitInput struct{}
// cryptoTickerPlugin implements the lifecycle, websocket and scheduler interfaces.
type cryptoTickerPlugin struct{}
// OnInitOutput is the output from nd_on_init
type OnInitOutput struct {
Error *string `json:"error,omitempty"`
// init registers the plugin capabilities
func init() {
lifecycle.Register(&cryptoTickerPlugin{})
websocket.Register(&cryptoTickerPlugin{})
scheduler.Register(&cryptoTickerPlugin{})
}
// nd_on_init is called when the plugin is loaded.
// We use this to establish the initial WebSocket connection.
//
//export nd_on_init
func ndOnInit() int32 {
// Read input (currently empty)
var input OnInitInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return -1
}
// Ensure cryptoTickerPlugin implements the required provider interfaces
var (
_ lifecycle.InitProvider = (*cryptoTickerPlugin)(nil)
_ websocket.TextMessageProvider = (*cryptoTickerPlugin)(nil)
_ websocket.BinaryMessageProvider = (*cryptoTickerPlugin)(nil)
_ websocket.ErrorProvider = (*cryptoTickerPlugin)(nil)
_ websocket.CloseProvider = (*cryptoTickerPlugin)(nil)
_ scheduler.SchedulerCallbackProvider = (*cryptoTickerPlugin)(nil)
)
// OnInit is called when the plugin is loaded.
// We use this to establish the initial WebSocket connection.
func (p *cryptoTickerPlugin) OnInit(_ lifecycle.OnInitInput) (lifecycle.OnInitOutput, error) {
pdk.Log(pdk.LogInfo, "Crypto Ticker Plugin initializing...")
// Get ticker configuration
@ -87,12 +90,7 @@ func ndOnInit() int32 {
// Don't fail init - let reconnect logic handle it
}
// Return success output
if err := pdk.OutputJSON(OnInitOutput{}); err != nil {
pdk.SetError(err)
return -1
}
return 0
return lifecycle.OnInitOutput{}, nil
}
// parseTickerSymbols parses a comma-separated list of ticker symbols
@ -144,11 +142,11 @@ func connectAndSubscribe(tickers []string) error {
return nil
}
// NdWebsocketOnTextMessage is called when a text message is received
func NdWebsocketOnTextMessage(input OnTextMessageInput) (OnTextMessageOutput, error) {
// OnTextMessage is called when a text message is received
func (p *cryptoTickerPlugin) OnTextMessage(input websocket.OnTextMessageInput) (websocket.OnTextMessageOutput, error) {
// Only process messages from our connection
if input.ConnectionId != connectionID {
return OnTextMessageOutput{}, nil
if input.ConnectionID != connectionID {
return websocket.OnTextMessageOutput{}, nil
}
// Try to parse as a ticker message
@ -156,7 +154,7 @@ func NdWebsocketOnTextMessage(input OnTextMessageInput) (OnTextMessageOutput, er
err := json.Unmarshal([]byte(input.Message), &ticker)
if err != nil {
// Not a valid JSON message, ignore
return OnTextMessageOutput{}, nil
return websocket.OnTextMessageOutput{}, nil
}
// Only process ticker messages
@ -165,7 +163,7 @@ func NdWebsocketOnTextMessage(input OnTextMessageInput) (OnTextMessageOutput, er
if ticker.Type != "" {
pdk.Log(pdk.LogDebug, fmt.Sprintf("Received %s message", ticker.Type))
}
return OnTextMessageOutput{}, nil
return websocket.OnTextMessageOutput{}, nil
}
// Calculate 24h change percentage
@ -180,29 +178,29 @@ func NdWebsocketOnTextMessage(input OnTextMessageInput) (OnTextMessageOutput, er
ticker.BestAsk,
))
return OnTextMessageOutput{}, nil
return websocket.OnTextMessageOutput{}, nil
}
// NdWebsocketOnBinaryMessage is called when a binary message is received
func NdWebsocketOnBinaryMessage(input OnBinaryMessageInput) (OnBinaryMessageOutput, error) {
// OnBinaryMessage is called when a binary message is received
func (p *cryptoTickerPlugin) OnBinaryMessage(input websocket.OnBinaryMessageInput) (websocket.OnBinaryMessageOutput, error) {
// Coinbase doesn't send binary messages, but we implement the handler anyway
pdk.Log(pdk.LogWarn, fmt.Sprintf("Received unexpected binary message on connection %s", input.ConnectionId))
return OnBinaryMessageOutput{}, nil
pdk.Log(pdk.LogWarn, fmt.Sprintf("Received unexpected binary message on connection %s", input.ConnectionID))
return websocket.OnBinaryMessageOutput{}, nil
}
// NdWebsocketOnError is called when an error occurs on the WebSocket connection
func NdWebsocketOnError(input OnErrorInput) (OnErrorOutput, error) {
pdk.Log(pdk.LogError, fmt.Sprintf("WebSocket error on connection %s: %s", input.ConnectionId, input.Error))
return OnErrorOutput{}, nil
// OnError is called when an error occurs on the WebSocket connection
func (p *cryptoTickerPlugin) OnError(input websocket.OnErrorInput) (websocket.OnErrorOutput, error) {
pdk.Log(pdk.LogError, fmt.Sprintf("WebSocket error on connection %s: %s", input.ConnectionID, input.Error))
return websocket.OnErrorOutput{}, nil
}
// NdWebsocketOnClose is called when the WebSocket connection is closed
func NdWebsocketOnClose(input OnCloseInput) (OnCloseOutput, error) {
// OnClose is called when the WebSocket connection is closed
func (p *cryptoTickerPlugin) OnClose(input websocket.OnCloseInput) (websocket.OnCloseOutput, error) {
pdk.Log(pdk.LogInfo, fmt.Sprintf("WebSocket connection %s closed (code: %d, reason: %s)",
input.ConnectionId, input.Code, input.Reason))
input.ConnectionID, input.Code, input.Reason))
// Only attempt reconnect for our connection
if input.ConnectionId == connectionID {
if input.ConnectionID == connectionID {
pdk.Log(pdk.LogInfo, "Scheduling reconnection attempt in 5 seconds...")
// Schedule a one-time reconnection attempt
@ -212,33 +210,14 @@ func NdWebsocketOnClose(input OnCloseInput) (OnCloseOutput, error) {
}
}
return OnCloseOutput{}, nil
return websocket.OnCloseOutput{}, nil
}
// Scheduler callback input/output types
type SchedulerCallbackInput struct {
ScheduleId string `json:"scheduleId"`
Payload string `json:"payload"`
IsRecurring bool `json:"isRecurring"`
}
type SchedulerCallbackOutput struct {
Error *string `json:"error,omitempty"`
}
// nd_scheduler_callback is called when a scheduled task fires
//
//export nd_scheduler_callback
func ndSchedulerCallback() int32 {
var input SchedulerCallbackInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return -1
}
// OnSchedulerCallback is called when a scheduled task fires
func (p *cryptoTickerPlugin) OnSchedulerCallback(input scheduler.SchedulerCallbackInput) (scheduler.SchedulerCallbackOutput, error) {
// Only handle our reconnection schedule
if input.ScheduleId != reconnectScheduleID {
return 0
if input.ScheduleID != reconnectScheduleID {
return scheduler.SchedulerCallbackOutput{}, nil
}
pdk.Log(pdk.LogInfo, "Attempting to reconnect to Coinbase WebSocket API...")
@ -265,12 +244,7 @@ func ndSchedulerCallback() int32 {
pdk.Log(pdk.LogInfo, "Successfully reconnected!")
}
output := SchedulerCallbackOutput{}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return -1
}
return 0
return scheduler.SchedulerCallbackOutput{}, nil
}
// calculatePercentChange calculates the percentage change between open and current price

View File

@ -1,185 +0,0 @@
// THIS FILE WAS GENERATED BY `xtp-go-bindgen`. DO NOT EDIT.
package main
import (
pdk "github.com/extism/go-pdk"
)
//export nd_websocket_on_binary_message
func _NdWebsocketOnBinaryMessage() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "NdWebsocketOnBinaryMessage: getting JSON input")
var input OnBinaryMessageInput
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdWebsocketOnBinaryMessage: calling implementation function")
output, err := NdWebsocketOnBinaryMessage(input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdWebsocketOnBinaryMessage: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdWebsocketOnBinaryMessage: returning")
return 0
}
//export nd_websocket_on_close
func _NdWebsocketOnClose() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "NdWebsocketOnClose: getting JSON input")
var input OnCloseInput
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdWebsocketOnClose: calling implementation function")
output, err := NdWebsocketOnClose(input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdWebsocketOnClose: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdWebsocketOnClose: returning")
return 0
}
//export nd_websocket_on_error
func _NdWebsocketOnError() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "NdWebsocketOnError: getting JSON input")
var input OnErrorInput
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdWebsocketOnError: calling implementation function")
output, err := NdWebsocketOnError(input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdWebsocketOnError: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdWebsocketOnError: returning")
return 0
}
//export nd_websocket_on_text_message
func _NdWebsocketOnTextMessage() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "NdWebsocketOnTextMessage: getting JSON input")
var input OnTextMessageInput
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdWebsocketOnTextMessage: calling implementation function")
output, err := NdWebsocketOnTextMessage(input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdWebsocketOnTextMessage: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdWebsocketOnTextMessage: returning")
return 0
}
// Input provided when a binary message is received
type OnBinaryMessageInput struct {
// The unique identifier for the WebSocket connection that received the message.
ConnectionId string `json:"connectionId"`
// The binary data received from the WebSocket, encoded as base64.
Data string `json:"data"`
}
// Output from the binary message handler
type OnBinaryMessageOutput struct {
// Error message if the callback failed. Empty or null indicates success.
Error *string `json:"error,omitempty"`
}
// Input provided when a WebSocket connection is closed
type OnCloseInput struct {
// The WebSocket close status code (e.g., 1000 for normal closure,
// 1001 for going away, 1006 for abnormal closure).
Code int32 `json:"code"`
// The unique identifier for the WebSocket connection that was closed.
ConnectionId string `json:"connectionId"`
// The human-readable reason for the connection closure, if provided.
Reason string `json:"reason"`
}
// Output from the close handler
type OnCloseOutput struct {
// Error message if the callback failed. Empty or null indicates success.
Error *string `json:"error,omitempty"`
}
// Input provided when an error occurs on a WebSocket connection
type OnErrorInput struct {
// The unique identifier for the WebSocket connection where the error occurred.
ConnectionId string `json:"connectionId"`
// The error message describing what went wrong.
Error string `json:"error"`
}
// Output from the error handler
type OnErrorOutput struct {
// Error message if the callback failed. Empty or null indicates success.
Error *string `json:"error,omitempty"`
}
// Input provided when a text message is received
type OnTextMessageInput struct {
// The unique identifier for the WebSocket connection that received the message.
ConnectionId string `json:"connectionId"`
// The text message content received from the WebSocket.
Message string `json:"message"`
}
// Output from the text message handler
type OnTextMessageOutput struct {
// Error message if the callback failed. Empty or null indicates success.
Error *string `json:"error,omitempty"`
}

View File

@ -98,25 +98,24 @@ Folder = "/path/to/plugins"
| File | Description |
|--------------|--------------------------------------------------------|
| `main.go` | Plugin entry point, manifest, scrobbler implementation |
| `main.go` | Plugin entry point, capability registration, and implementations |
| `rpc.go` | Discord gateway communication and RPC logic |
| `pdk.gen.go` | Generated types from XTP schemas (combined) |
| `go.mod` | Go module file (imports `ndhost` SDK) |
| `go.mod` | Go module file |
## Host SDK
## PDK
This plugin imports the Go host SDK directly:
This plugin imports the Navidrome PDK subpackages directly:
```go
import ndhost "github.com/navidrome/navidrome/plugins/host/go"
import (
"github.com/navidrome/navidrome/plugins/pdk/go/host"
"github.com/navidrome/navidrome/plugins/pdk/go/scheduler"
"github.com/navidrome/navidrome/plugins/pdk/go/scrobbler"
"github.com/navidrome/navidrome/plugins/pdk/go/websocket"
)
```
The `go.mod` file uses a `replace` directive to point to the local SDK:
```
require github.com/navidrome/navidrome/plugins/host/go v0.0.0
replace github.com/navidrome/navidrome/plugins/host/go => ../../host/go
```
The `go.mod` file uses `replace` directives to point to the local packages for development.
## Host Services Used

View File

@ -1,10 +1,13 @@
module discord-rich-presence
go 1.24
go 1.25
require (
github.com/extism/go-pdk v1.1.3
github.com/navidrome/navidrome/plugins/pdk/go/host v0.0.0
github.com/navidrome/navidrome v0.0.0-00010101000000-000000000000
github.com/navidrome/navidrome/plugins/pdk/go/host v0.0.0-00010101000000-000000000000
)
replace github.com/navidrome/navidrome => ../../..
replace github.com/navidrome/navidrome/plugins/pdk/go/host => ../../pdk/go/host

View File

@ -16,7 +16,10 @@ import (
"time"
"github.com/extism/go-pdk"
host "github.com/navidrome/navidrome/plugins/pdk/go/host"
"github.com/navidrome/navidrome/plugins/pdk/go/host"
"github.com/navidrome/navidrome/plugins/pdk/go/scheduler"
"github.com/navidrome/navidrome/plugins/pdk/go/scrobbler"
"github.com/navidrome/navidrome/plugins/pdk/go/websocket"
)
// Configuration keys
@ -25,6 +28,26 @@ const (
usersKey = "users"
)
// discordPlugin implements the scrobbler, scheduler, and websocket interfaces.
type discordPlugin struct{}
// init registers the plugin capabilities
func init() {
scrobbler.Register(&discordPlugin{})
scheduler.Register(&discordPlugin{})
websocket.Register(&discordPlugin{})
}
// Ensure discordPlugin implements the required provider interfaces
var (
_ scrobbler.Scrobbler = (*discordPlugin)(nil)
_ scheduler.SchedulerCallbackProvider = (*discordPlugin)(nil)
_ websocket.TextMessageProvider = (*discordPlugin)(nil)
_ websocket.BinaryMessageProvider = (*discordPlugin)(nil)
_ websocket.ErrorProvider = (*discordPlugin)(nil)
_ websocket.CloseProvider = (*discordPlugin)(nil)
)
// getConfig loads the plugin configuration.
func getConfig() (clientID string, users map[string]string, err error) {
clientID, ok := pdk.GetConfig(clientIDKey)
@ -69,39 +92,41 @@ func getImageURL(trackID string) string {
// Scrobbler Implementation
// ============================================================================
// NdScrobblerIsAuthorized checks if a user is authorized for Discord Rich Presence.
func NdScrobblerIsAuthorized(input AuthInput) (AuthOutput, error) {
// IsAuthorized checks if a user is authorized for Discord Rich Presence.
func (p *discordPlugin) IsAuthorized(input scrobbler.AuthInput) (scrobbler.AuthOutput, error) {
_, users, err := getConfig()
if err != nil {
return AuthOutput{}, fmt.Errorf("failed to check user authorization: %w", err)
return scrobbler.AuthOutput{}, fmt.Errorf("failed to check user authorization: %w", err)
}
_, authorized := users[input.Username]
pdk.Log(pdk.LogInfo, fmt.Sprintf("IsAuthorized for user %s: %v", input.Username, authorized))
return AuthOutput{Authorized: authorized}, nil
return scrobbler.AuthOutput{Authorized: authorized}, nil
}
// NdScrobblerNowPlaying sends a now playing notification to Discord.
func NdScrobblerNowPlaying(input NowPlayingInput) (ScrobblerOutput, error) {
// NowPlaying sends a now playing notification to Discord.
func (p *discordPlugin) NowPlaying(input scrobbler.NowPlayingInput) (scrobbler.ScrobblerOutput, error) {
pdk.Log(pdk.LogInfo, fmt.Sprintf("Setting presence for user %s, track: %s", input.Username, input.Track.Title))
// Load configuration
clientID, users, err := getConfig()
if err != nil {
return ScrobblerOutput{}, fmt.Errorf("failed to get config: %w", err)
return scrobbler.ScrobblerOutput{}, fmt.Errorf("failed to get config: %w", err)
}
// Check authorization
userToken, authorized := users[input.Username]
if !authorized {
errMsg := fmt.Sprintf("user '%s' not authorized", input.Username)
return ScrobblerOutput{Error: &errMsg, ErrorType: ScrobblerErrorTypeNotAuthorized}, nil
errType := scrobbler.ScrobblerErrorNotAuthorized
return scrobbler.ScrobblerOutput{Error: &errMsg, ErrorType: &errType}, nil
}
// Connect to Discord
if err := connect(input.Username, userToken); err != nil {
errMsg := fmt.Sprintf("failed to connect to Discord: %v", err)
return ScrobblerOutput{Error: &errMsg, ErrorType: ScrobblerErrorTypeRetryLater}, nil
errType := scrobbler.ScrobblerErrorRetryLater
return scrobbler.ScrobblerOutput{Error: &errMsg, ErrorType: &errType}, nil
}
// Cancel any existing completion schedule
@ -124,12 +149,13 @@ func NdScrobblerNowPlaying(input NowPlayingInput) (ScrobblerOutput, error) {
End: endTime,
},
Assets: activityAssets{
LargeImage: getImageURL(input.Track.Id),
LargeImage: getImageURL(input.Track.ID),
LargeText: input.Track.Album,
},
}); err != nil {
errMsg := fmt.Sprintf("failed to send activity: %v", err)
return ScrobblerOutput{Error: &errMsg, ErrorType: ScrobblerErrorTypeRetryLater}, nil
errType := scrobbler.ScrobblerErrorRetryLater
return scrobbler.ScrobblerOutput{Error: &errMsg, ErrorType: &errType}, nil
}
// Schedule a timer to clear the activity after the track completes
@ -139,76 +165,76 @@ func NdScrobblerNowPlaying(input NowPlayingInput) (ScrobblerOutput, error) {
pdk.Log(pdk.LogWarn, fmt.Sprintf("Failed to schedule completion timer: %v", err))
}
return ScrobblerOutput{}, nil
return scrobbler.ScrobblerOutput{}, nil
}
// NdScrobblerScrobble handles scrobble requests (no-op for Discord).
func NdScrobblerScrobble(_ ScrobbleInput) (ScrobblerOutput, error) {
// Scrobble handles scrobble requests (no-op for Discord).
func (p *discordPlugin) Scrobble(_ scrobbler.ScrobbleInput) (scrobbler.ScrobblerOutput, error) {
// Discord Rich Presence doesn't need scrobble events
return ScrobblerOutput{}, nil
return scrobbler.ScrobblerOutput{}, nil
}
// ============================================================================
// Scheduler Callback Implementation
// ============================================================================
// NdSchedulerCallback handles scheduler callbacks.
func NdSchedulerCallback(input SchedulerCallbackInput) (SchedulerCallbackOutput, error) {
pdk.Log(pdk.LogDebug, fmt.Sprintf("Scheduler callback: id=%s, payload=%s, recurring=%v", input.ScheduleId, input.Payload, input.IsRecurring))
// OnSchedulerCallback handles scheduler callbacks.
func (p *discordPlugin) OnSchedulerCallback(input scheduler.SchedulerCallbackInput) (scheduler.SchedulerCallbackOutput, error) {
pdk.Log(pdk.LogDebug, fmt.Sprintf("Scheduler callback: id=%s, payload=%s, recurring=%v", input.ScheduleID, input.Payload, input.IsRecurring))
// Route based on payload
switch input.Payload {
case payloadHeartbeat:
// Heartbeat callback - scheduleId is the username
if err := handleHeartbeatCallback(input.ScheduleId); err != nil {
if err := handleHeartbeatCallback(input.ScheduleID); err != nil {
errMsg := err.Error()
return SchedulerCallbackOutput{Error: &errMsg}, nil
return scheduler.SchedulerCallbackOutput{Error: &errMsg}, nil
}
case payloadClearActivity:
// Clear activity callback - scheduleId is "username-clear"
username := strings.TrimSuffix(input.ScheduleId, "-clear")
username := strings.TrimSuffix(input.ScheduleID, "-clear")
if err := handleClearActivityCallback(username); err != nil {
errMsg := err.Error()
return SchedulerCallbackOutput{Error: &errMsg}, nil
return scheduler.SchedulerCallbackOutput{Error: &errMsg}, nil
}
default:
pdk.Log(pdk.LogWarn, fmt.Sprintf("Unknown scheduler callback payload: %s", input.Payload))
}
return SchedulerCallbackOutput{}, nil
return scheduler.SchedulerCallbackOutput{}, nil
}
// ============================================================================
// WebSocket Callback Implementation
// ============================================================================
// NdWebsocketOnTextMessage handles incoming WebSocket text messages.
func NdWebsocketOnTextMessage(input OnTextMessageInput) (OnTextMessageOutput, error) {
if err := handleWebSocketMessage(input.ConnectionId, input.Message); err != nil {
// OnTextMessage handles incoming WebSocket text messages.
func (p *discordPlugin) OnTextMessage(input websocket.OnTextMessageInput) (websocket.OnTextMessageOutput, error) {
if err := handleWebSocketMessage(input.ConnectionID, input.Message); err != nil {
errMsg := err.Error()
return OnTextMessageOutput{Error: &errMsg}, nil
return websocket.OnTextMessageOutput{Error: &errMsg}, nil
}
return OnTextMessageOutput{}, nil
return websocket.OnTextMessageOutput{}, nil
}
// NdWebsocketOnBinaryMessage handles incoming WebSocket binary messages.
func NdWebsocketOnBinaryMessage(input OnBinaryMessageInput) (OnBinaryMessageOutput, error) {
pdk.Log(pdk.LogDebug, fmt.Sprintf("Received unexpected binary message for connection '%s'", input.ConnectionId))
return OnBinaryMessageOutput{}, nil
// OnBinaryMessage handles incoming WebSocket binary messages.
func (p *discordPlugin) OnBinaryMessage(input websocket.OnBinaryMessageInput) (websocket.OnBinaryMessageOutput, error) {
pdk.Log(pdk.LogDebug, fmt.Sprintf("Received unexpected binary message for connection '%s'", input.ConnectionID))
return websocket.OnBinaryMessageOutput{}, nil
}
// NdWebsocketOnError handles WebSocket errors.
func NdWebsocketOnError(input OnErrorInput) (OnErrorOutput, error) {
pdk.Log(pdk.LogWarn, fmt.Sprintf("WebSocket error for connection '%s': %s", input.ConnectionId, input.Error))
return OnErrorOutput{}, nil
// OnError handles WebSocket errors.
func (p *discordPlugin) OnError(input websocket.OnErrorInput) (websocket.OnErrorOutput, error) {
pdk.Log(pdk.LogWarn, fmt.Sprintf("WebSocket error for connection '%s': %s", input.ConnectionID, input.Error))
return websocket.OnErrorOutput{}, nil
}
// NdWebsocketOnClose handles WebSocket connection closure.
func NdWebsocketOnClose(input OnCloseInput) (OnCloseOutput, error) {
pdk.Log(pdk.LogInfo, fmt.Sprintf("WebSocket connection '%s' closed with code %d: %s", input.ConnectionId, input.Code, input.Reason))
return OnCloseOutput{}, nil
// OnClose handles WebSocket connection closure.
func (p *discordPlugin) OnClose(input websocket.OnCloseInput) (websocket.OnCloseOutput, error) {
pdk.Log(pdk.LogInfo, fmt.Sprintf("WebSocket connection '%s' closed with code %d: %s", input.ConnectionID, input.Code, input.Reason))
return websocket.OnCloseOutput{}, nil
}
func main() {}

View File

@ -1,399 +0,0 @@
// THIS FILE WAS GENERATED BY `xtp-go-bindgen`. DO NOT EDIT.
// Combined from: scrobbler.yaml, scheduler_callback.yaml, websocket_callback.yaml
package main
import (
"errors"
pdk "github.com/extism/go-pdk"
)
// ============================================================================
// Scrobbler Capability Functions
// ============================================================================
//export nd_scrobbler_is_authorized
func _NdScrobblerIsAuthorized() int32 {
var input AuthInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return -1
}
output, err := NdScrobblerIsAuthorized(input)
if err != nil {
pdk.SetError(err)
return -1
}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return -1
}
return 0
}
//export nd_scrobbler_now_playing
func _NdScrobblerNowPlaying() int32 {
var input NowPlayingInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return -1
}
output, err := NdScrobblerNowPlaying(input)
if err != nil {
pdk.SetError(err)
return -1
}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return -1
}
return 0
}
//export nd_scrobbler_scrobble
func _NdScrobblerScrobble() int32 {
var input ScrobbleInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return -1
}
output, err := NdScrobblerScrobble(input)
if err != nil {
pdk.SetError(err)
return -1
}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return -1
}
return 0
}
// ============================================================================
// Scheduler Callback Capability Functions
// ============================================================================
//export nd_scheduler_callback
func _NdSchedulerCallback() int32 {
var input SchedulerCallbackInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return -1
}
output, err := NdSchedulerCallback(input)
if err != nil {
pdk.SetError(err)
return -1
}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return -1
}
return 0
}
// ============================================================================
// WebSocket Callback Capability Functions
// ============================================================================
//export nd_websocket_on_text_message
func _NdWebsocketOnTextMessage() int32 {
var input OnTextMessageInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return -1
}
output, err := NdWebsocketOnTextMessage(input)
if err != nil {
pdk.SetError(err)
return -1
}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return -1
}
return 0
}
//export nd_websocket_on_binary_message
func _NdWebsocketOnBinaryMessage() int32 {
var input OnBinaryMessageInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return -1
}
output, err := NdWebsocketOnBinaryMessage(input)
if err != nil {
pdk.SetError(err)
return -1
}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return -1
}
return 0
}
//export nd_websocket_on_error
func _NdWebsocketOnError() int32 {
var input OnErrorInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return -1
}
output, err := NdWebsocketOnError(input)
if err != nil {
pdk.SetError(err)
return -1
}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return -1
}
return 0
}
//export nd_websocket_on_close
func _NdWebsocketOnClose() int32 {
var input OnCloseInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return -1
}
output, err := NdWebsocketOnClose(input)
if err != nil {
pdk.SetError(err)
return -1
}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return -1
}
return 0
}
// ============================================================================
// Scrobbler Types
// ============================================================================
// AuthInput is the input for authorization check
type AuthInput struct {
// The internal Navidrome user ID
UserId string `json:"userId"`
// The username of the user
Username string `json:"username"`
}
// AuthOutput is the output for authorization check
type AuthOutput struct {
// Whether the user is authorized to scrobble
Authorized bool `json:"authorized"`
}
// NowPlayingInput is the input for now playing notification
type NowPlayingInput struct {
// Current playback position in seconds
Position int32 `json:"position"`
// The track currently playing
Track TrackInfo `json:"track"`
// The internal Navidrome user ID
UserId string `json:"userId"`
// The username of the user
Username string `json:"username"`
}
// ScrobbleInput is the input for submitting a scrobble
type ScrobbleInput struct {
// Unix timestamp when the track started playing
Timestamp int64 `json:"timestamp"`
// The track that was played
Track TrackInfo `json:"track"`
// The internal Navidrome user ID
UserId string `json:"userId"`
// The username of the user
Username string `json:"username"`
}
// ScrobblerErrorType indicates how Navidrome should handle the error
type ScrobblerErrorType string
const (
ScrobblerErrorTypeNone ScrobblerErrorType = "none"
ScrobblerErrorTypeNotAuthorized ScrobblerErrorType = "not_authorized"
ScrobblerErrorTypeRetryLater ScrobblerErrorType = "retry_later"
ScrobblerErrorTypeUnrecoverable ScrobblerErrorType = "unrecoverable"
)
func (v ScrobblerErrorType) String() string {
switch v {
case ScrobblerErrorTypeNone:
return `none`
case ScrobblerErrorTypeNotAuthorized:
return `not_authorized`
case ScrobblerErrorTypeRetryLater:
return `retry_later`
case ScrobblerErrorTypeUnrecoverable:
return `unrecoverable`
default:
return ""
}
}
func stringToScrobblerErrorType(s string) (ScrobblerErrorType, error) {
switch s {
case `none`:
return ScrobblerErrorTypeNone, nil
case `not_authorized`:
return ScrobblerErrorTypeNotAuthorized, nil
case `retry_later`:
return ScrobblerErrorTypeRetryLater, nil
case `unrecoverable`:
return ScrobblerErrorTypeUnrecoverable, nil
default:
return ScrobblerErrorType(""), errors.New("unable to convert string to ScrobblerErrorType")
}
}
// ScrobblerOutput is the output for scrobbler operations (now_playing and scrobble)
type ScrobblerOutput struct {
// Error message if the operation failed
Error *string `json:"error,omitempty"`
// Type of error for handling
ErrorType ScrobblerErrorType `json:"errorType,omitempty"`
}
// TrackInfo contains track metadata for scrobbling
type TrackInfo struct {
// Album name
Album string `json:"album"`
// Album artist
AlbumArtist string `json:"albumArtist"`
// Track artist
Artist string `json:"artist"`
// Disc number
DiscNumber int32 `json:"discNumber"`
// Track duration in seconds
Duration float32 `json:"duration"`
// The internal Navidrome track ID
Id string `json:"id"`
// MusicBrainz album artist ID
MbzAlbumArtistId *string `json:"mbzAlbumArtistId,omitempty"`
// MusicBrainz album/release ID
MbzAlbumId *string `json:"mbzAlbumId,omitempty"`
// MusicBrainz artist ID
MbzArtistId *string `json:"mbzArtistId,omitempty"`
// MusicBrainz recording ID
MbzRecordingId *string `json:"mbzRecordingId,omitempty"`
// MusicBrainz release group ID
MbzReleaseGroupId *string `json:"mbzReleaseGroupId,omitempty"`
// MusicBrainz release track ID
MbzReleaseTrackId *string `json:"mbzReleaseTrackId,omitempty"`
// Track title
Title string `json:"title"`
// Track number on the album
TrackNumber int32 `json:"trackNumber"`
}
// ============================================================================
// Scheduler Callback Types
// ============================================================================
// SchedulerCallbackInput is provided when a scheduled task fires
type SchedulerCallbackInput struct {
// True if this is a recurring schedule (created via ScheduleRecurring),
// false if it's a one-time schedule (created via ScheduleOneTime).
IsRecurring bool `json:"isRecurring"`
// The payload data that was provided when the task was scheduled.
// Can be used to pass context or parameters to the callback handler.
Payload string `json:"payload"`
// The unique identifier for this scheduled task. This is either the ID
// provided when scheduling, or an auto-generated UUID if none was specified.
ScheduleId string `json:"scheduleId"`
}
// SchedulerCallbackOutput is the output from the scheduler callback
type SchedulerCallbackOutput struct {
// Error message if the callback failed to process the scheduled task.
// Empty or null indicates success. The error is logged but does not
// affect the scheduling system.
Error *string `json:"error,omitempty"`
}
// ============================================================================
// WebSocket Callback Types
// ============================================================================
// OnTextMessageInput is provided when a text message is received
type OnTextMessageInput struct {
// The unique identifier for the WebSocket connection that received the message.
ConnectionId string `json:"connectionId"`
// The text message content received from the WebSocket.
Message string `json:"message"`
}
// OnTextMessageOutput is the output from the text message handler
type OnTextMessageOutput struct {
// Error message if the callback failed. Empty or null indicates success.
Error *string `json:"error,omitempty"`
}
// OnBinaryMessageInput is provided when a binary message is received
type OnBinaryMessageInput struct {
// The unique identifier for the WebSocket connection that received the message.
ConnectionId string `json:"connectionId"`
// The binary data received from the WebSocket, encoded as base64.
Data string `json:"data"`
}
// OnBinaryMessageOutput is the output from the binary message handler
type OnBinaryMessageOutput struct {
// Error message if the callback failed. Empty or null indicates success.
Error *string `json:"error,omitempty"`
}
// OnErrorInput is provided when an error occurs on a WebSocket connection
type OnErrorInput struct {
// The unique identifier for the WebSocket connection where the error occurred.
ConnectionId string `json:"connectionId"`
// The error message describing what went wrong.
Error string `json:"error"`
}
// OnErrorOutput is the output from the error handler
type OnErrorOutput struct {
// Error message if the callback failed. Empty or null indicates success.
Error *string `json:"error,omitempty"`
}
// OnCloseInput is provided when a WebSocket connection is closed
type OnCloseInput struct {
// The WebSocket close status code (e.g., 1000 for normal closure,
// 1001 for going away, 1006 for abnormal closure).
Code int32 `json:"code"`
// The unique identifier for the WebSocket connection that was closed.
ConnectionId string `json:"connectionId"`
// The human-readable reason for the connection closure, if provided.
Reason string `json:"reason"`
}
// OnCloseOutput is the output from the close handler
type OnCloseOutput struct {
// Error message if the callback failed. Empty or null indicates success.
Error *string `json:"error,omitempty"`
}

View File

@ -1,6 +1,6 @@
# Minimal Navidrome Plugin Example
This is a minimal example demonstrating how to create a Navidrome plugin using Go and the Extism PDK.
This is a minimal example demonstrating how to create a Navidrome plugin using Go and the Navidrome PDK.
## Building
@ -8,7 +8,7 @@ This is a minimal example demonstrating how to create a Navidrome plugin using G
2. Build the plugin:
```bash
go mod tidy
tinygo build -o plugin.wasm -target wasip1 -buildmode=c-shared ./main.go
tinygo build -o plugin.wasm -target wasip1 -buildmode=c-shared .
zip -j minimal.ndp manifest.json plugin.wasm
```
@ -37,19 +37,36 @@ Agents = "lastfm,spotify,minimal"
## What This Example Demonstrates
- Plugin package structure (`.ndp` = zip with `manifest.json` + `plugin.wasm`)
- Implementing `nd_get_artist_biography` as a MetadataAgent capability
- Basic JSON input/output handling with the Extism PDK
- Using the Navidrome PDK `metadata` subpackage
- Implementing the `ArtistBiographyProvider` interface
- Registration pattern with `metadata.Register()`
## PDK Usage
```go
import "github.com/navidrome/navidrome/plugins/pdk/go/metadata"
type myPlugin struct{}
func init() {
metadata.Register(&myPlugin{})
}
func (p *myPlugin) GetArtistBiography(input metadata.ArtistInput) (metadata.ArtistBiographyOutput, error) {
return metadata.ArtistBiographyOutput{Biography: "..."}, nil
}
```
## Extending the Example
To add more capabilities, implement additional exported functions:
To add more capabilities, implement additional provider interfaces from the `metadata` package:
- `nd_get_artist_mbid` - Get MusicBrainz ID for an artist
- `nd_get_artist_url` - Get external URL for an artist
- `nd_get_similar_artists` - Get similar artists
- `nd_get_artist_images` - Get artist images
- `nd_get_artist_top_songs` - Get top songs for an artist
- `nd_get_album_info` - Get album information
- `nd_get_album_images` - Get album images
- `ArtistMBIDProvider` - Get MusicBrainz ID for an artist
- `ArtistURLProvider` - Get external URL for an artist
- `SimilarArtistsProvider` - Get similar artists
- `ArtistImagesProvider` - Get artist images
- `ArtistTopSongsProvider` - Get top songs for an artist
- `AlbumInfoProvider` - Get album information
- `AlbumImagesProvider` - Get album images
See the full documentation in `/plugins/README.md` for input/output formats.

View File

@ -1,5 +1,9 @@
module minimal-plugin
go 1.23
go 1.25
require github.com/extism/go-pdk v1.1.3
require github.com/navidrome/navidrome v0.0.0-00010101000000-000000000000
require github.com/extism/go-pdk v1.1.3 // indirect
replace github.com/navidrome/navidrome => ../../..

View File

@ -2,42 +2,31 @@
//
// Build with:
//
// tinygo build -o minimal.wasm -target wasip1 -buildmode=c-shared ./main.go
// tinygo build -o minimal.wasm -target wasip1 -buildmode=c-shared .
//
// Install by copying minimal.ndp to your Navidrome plugins folder.
package main
import (
"github.com/extism/go-pdk"
"github.com/navidrome/navidrome/plugins/pdk/go/metadata"
)
type ArtistInput struct {
ID string `json:"id"`
Name string `json:"name"`
MBID string `json:"mbid,omitempty"`
// minimalPlugin implements the metadata provider interfaces.
type minimalPlugin struct{}
// init registers the plugin implementation
func init() {
metadata.Register(&minimalPlugin{})
}
type BiographyOutput struct {
Biography string `json:"biography"`
}
// Ensure minimalPlugin implements the ArtistBiographyProvider interface
var _ metadata.ArtistBiographyProvider = (*minimalPlugin)(nil)
//go:wasmexport nd_get_artist_biography
func ndGetArtistBiography() int32 {
var input ArtistInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return 1
}
output := BiographyOutput{
// GetArtistBiography returns a placeholder biography for the artist.
func (p *minimalPlugin) GetArtistBiography(input metadata.ArtistInput) (metadata.ArtistBiographyOutput, error) {
return metadata.ArtistBiographyOutput{
Biography: "This is a placeholder biography for " + input.Name + ".",
}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
}, nil
}
func main() {}

View File

@ -10,7 +10,7 @@ These wrappers provide idiomatic Go APIs for interacting with Navidrome from WAS
To regenerate:
```bash
make generate-pdk
make gen
```
## Usage

View File

@ -11,13 +11,11 @@ import (
pdk "github.com/extism/go-pdk"
)
// OnInitInput represents the OnInitInput data structure.
// OnInitInput is the input provided to the init callback.
// Currently empty, reserved for future use.
type OnInitInput struct {
}
// OnInitOutput represents the OnInitOutput data structure.
// OnInitOutput is the output from the init callback.
type OnInitOutput struct {
// Error is the error message if initialization failed.

View File

@ -8,13 +8,11 @@
package lifecycle
// OnInitInput represents the OnInitInput data structure.
// OnInitInput is the input provided to the init callback.
// Currently empty, reserved for future use.
type OnInitInput struct {
}
// OnInitOutput represents the OnInitOutput data structure.
// OnInitOutput is the output from the init callback.
type OnInitOutput struct {
// Error is the error message if initialization failed.

View File

@ -11,7 +11,6 @@ import (
pdk "github.com/extism/go-pdk"
)
// ArtistMBIDInput represents the ArtistMBIDInput data structure.
// ArtistMBIDInput is the input for GetArtistMBID.
type ArtistMBIDInput struct {
// ID is the internal Navidrome artist ID.
@ -20,51 +19,12 @@ type ArtistMBIDInput struct {
Name string `json:"name"`
}
// TopSongsInput represents the TopSongsInput data structure.
// TopSongsInput is the input for GetArtistTopSongs.
type TopSongsInput struct {
// ID is the internal Navidrome artist ID.
ID string `json:"id"`
// Name is the artist name.
Name string `json:"name"`
// MBID is the MusicBrainz ID for the artist (if known).
MBID *string `json:"mbid,omitempty"`
// Count is the maximum number of top songs to return.
Count int32 `json:"count"`
// ArtistURLOutput is the output for GetArtistURL.
type ArtistURLOutput struct {
// URL is the external URL for the artist.
URL string `json:"url"`
}
// SimilarArtistsInput represents the SimilarArtistsInput data structure.
// SimilarArtistsInput is the input for GetSimilarArtists.
type SimilarArtistsInput struct {
// ID is the internal Navidrome artist ID.
ID string `json:"id"`
// Name is the artist name.
Name string `json:"name"`
// MBID is the MusicBrainz ID for the artist (if known).
MBID *string `json:"mbid,omitempty"`
// Limit is the maximum number of similar artists to return.
Limit int32 `json:"limit"`
}
// TopSongsOutput represents the TopSongsOutput data structure.
// TopSongsOutput is the output for GetArtistTopSongs.
type TopSongsOutput struct {
// Songs is the list of top songs.
Songs []SongRef `json:"songs"`
}
// AlbumInput represents the AlbumInput data structure.
// AlbumInput is the common input for album-related functions.
type AlbumInput struct {
// Name is the album name.
Name string `json:"name"`
// Artist is the album artist name.
Artist string `json:"artist"`
// MBID is the MusicBrainz ID for the album (if known).
MBID *string `json:"mbid,omitempty"`
}
// AlbumInfoOutput represents the AlbumInfoOutput data structure.
// AlbumInfoOutput is the output for GetAlbumInfo.
type AlbumInfoOutput struct {
// Name is the album name.
@ -77,51 +37,20 @@ type AlbumInfoOutput struct {
URL string `json:"url"`
}
// AlbumImagesOutput represents the AlbumImagesOutput data structure.
// AlbumImagesOutput is the output for GetAlbumImages.
type AlbumImagesOutput struct {
// Images is the list of album images.
Images []ImageInfo `json:"images"`
}
// ArtistRef represents the ArtistRef data structure.
// ArtistRef is a reference to an artist with name and optional MBID.
type ArtistRef struct {
// Name is the artist name.
// SongRef is a reference to a song with name and optional MBID.
type SongRef struct {
// Name is the song name.
Name string `json:"name"`
// MBID is the MusicBrainz ID for the artist.
// MBID is the MusicBrainz ID for the song.
MBID *string `json:"mbid,omitempty"`
}
// ArtistURLOutput represents the ArtistURLOutput data structure.
// ArtistURLOutput is the output for GetArtistURL.
type ArtistURLOutput struct {
// URL is the external URL for the artist.
URL string `json:"url"`
}
// ArtistBiographyOutput represents the ArtistBiographyOutput data structure.
// ArtistBiographyOutput is the output for GetArtistBiography.
type ArtistBiographyOutput struct {
// Biography is the artist biography text.
Biography string `json:"biography"`
}
// SimilarArtistsOutput represents the SimilarArtistsOutput data structure.
// SimilarArtistsOutput is the output for GetSimilarArtists.
type SimilarArtistsOutput struct {
// Artists is the list of similar artists.
Artists []ArtistRef `json:"artists"`
}
// ArtistImagesOutput represents the ArtistImagesOutput data structure.
// ArtistImagesOutput is the output for GetArtistImages.
type ArtistImagesOutput struct {
// Images is the list of artist images.
Images []ImageInfo `json:"images"`
}
// ImageInfo represents the ImageInfo data structure.
// ImageInfo represents an image with URL and size.
type ImageInfo struct {
// URL is the URL of the image.
@ -130,14 +59,56 @@ type ImageInfo struct {
Size int32 `json:"size"`
}
// ArtistMBIDOutput represents the ArtistMBIDOutput data structure.
// ArtistBiographyOutput is the output for GetArtistBiography.
type ArtistBiographyOutput struct {
// Biography is the artist biography text.
Biography string `json:"biography"`
}
// SimilarArtistsOutput is the output for GetSimilarArtists.
type SimilarArtistsOutput struct {
// Artists is the list of similar artists.
Artists []ArtistRef `json:"artists"`
}
// TopSongsInput is the input for GetArtistTopSongs.
type TopSongsInput struct {
// ID is the internal Navidrome artist ID.
ID string `json:"id"`
// Name is the artist name.
Name string `json:"name"`
// MBID is the MusicBrainz ID for the artist (if known).
MBID *string `json:"mbid,omitempty"`
// Count is the maximum number of top songs to return.
Count int32 `json:"count"`
}
// TopSongsOutput is the output for GetArtistTopSongs.
type TopSongsOutput struct {
// Songs is the list of top songs.
Songs []SongRef `json:"songs"`
}
// ArtistRef is a reference to an artist with name and optional MBID.
type ArtistRef struct {
// Name is the artist name.
Name string `json:"name"`
// MBID is the MusicBrainz ID for the artist.
MBID *string `json:"mbid,omitempty"`
}
// ArtistImagesOutput is the output for GetArtistImages.
type ArtistImagesOutput struct {
// Images is the list of artist images.
Images []ImageInfo `json:"images"`
}
// ArtistMBIDOutput is the output for GetArtistMBID.
type ArtistMBIDOutput struct {
// MBID is the MusicBrainz ID for the artist.
MBID string `json:"mbid"`
}
// ArtistInput represents the ArtistInput data structure.
// ArtistInput is the common input for artist-related functions.
type ArtistInput struct {
// ID is the internal Navidrome artist ID.
@ -148,12 +119,25 @@ type ArtistInput struct {
MBID *string `json:"mbid,omitempty"`
}
// SongRef represents the SongRef data structure.
// SongRef is a reference to a song with name and optional MBID.
type SongRef struct {
// Name is the song name.
// SimilarArtistsInput is the input for GetSimilarArtists.
type SimilarArtistsInput struct {
// ID is the internal Navidrome artist ID.
ID string `json:"id"`
// Name is the artist name.
Name string `json:"name"`
// MBID is the MusicBrainz ID for the song.
// MBID is the MusicBrainz ID for the artist (if known).
MBID *string `json:"mbid,omitempty"`
// Limit is the maximum number of similar artists to return.
Limit int32 `json:"limit"`
}
// AlbumInput is the common input for album-related functions.
type AlbumInput struct {
// Name is the album name.
Name string `json:"name"`
// Artist is the album artist name.
Artist string `json:"artist"`
// MBID is the MusicBrainz ID for the album (if known).
MBID *string `json:"mbid,omitempty"`
}

View File

@ -8,7 +8,6 @@
package metadata
// ArtistMBIDInput represents the ArtistMBIDInput data structure.
// ArtistMBIDInput is the input for GetArtistMBID.
type ArtistMBIDInput struct {
// ID is the internal Navidrome artist ID.
@ -17,51 +16,12 @@ type ArtistMBIDInput struct {
Name string `json:"name"`
}
// TopSongsInput represents the TopSongsInput data structure.
// TopSongsInput is the input for GetArtistTopSongs.
type TopSongsInput struct {
// ID is the internal Navidrome artist ID.
ID string `json:"id"`
// Name is the artist name.
Name string `json:"name"`
// MBID is the MusicBrainz ID for the artist (if known).
MBID *string `json:"mbid,omitempty"`
// Count is the maximum number of top songs to return.
Count int32 `json:"count"`
// ArtistURLOutput is the output for GetArtistURL.
type ArtistURLOutput struct {
// URL is the external URL for the artist.
URL string `json:"url"`
}
// SimilarArtistsInput represents the SimilarArtistsInput data structure.
// SimilarArtistsInput is the input for GetSimilarArtists.
type SimilarArtistsInput struct {
// ID is the internal Navidrome artist ID.
ID string `json:"id"`
// Name is the artist name.
Name string `json:"name"`
// MBID is the MusicBrainz ID for the artist (if known).
MBID *string `json:"mbid,omitempty"`
// Limit is the maximum number of similar artists to return.
Limit int32 `json:"limit"`
}
// TopSongsOutput represents the TopSongsOutput data structure.
// TopSongsOutput is the output for GetArtistTopSongs.
type TopSongsOutput struct {
// Songs is the list of top songs.
Songs []SongRef `json:"songs"`
}
// AlbumInput represents the AlbumInput data structure.
// AlbumInput is the common input for album-related functions.
type AlbumInput struct {
// Name is the album name.
Name string `json:"name"`
// Artist is the album artist name.
Artist string `json:"artist"`
// MBID is the MusicBrainz ID for the album (if known).
MBID *string `json:"mbid,omitempty"`
}
// AlbumInfoOutput represents the AlbumInfoOutput data structure.
// AlbumInfoOutput is the output for GetAlbumInfo.
type AlbumInfoOutput struct {
// Name is the album name.
@ -74,51 +34,20 @@ type AlbumInfoOutput struct {
URL string `json:"url"`
}
// AlbumImagesOutput represents the AlbumImagesOutput data structure.
// AlbumImagesOutput is the output for GetAlbumImages.
type AlbumImagesOutput struct {
// Images is the list of album images.
Images []ImageInfo `json:"images"`
}
// ArtistRef represents the ArtistRef data structure.
// ArtistRef is a reference to an artist with name and optional MBID.
type ArtistRef struct {
// Name is the artist name.
// SongRef is a reference to a song with name and optional MBID.
type SongRef struct {
// Name is the song name.
Name string `json:"name"`
// MBID is the MusicBrainz ID for the artist.
// MBID is the MusicBrainz ID for the song.
MBID *string `json:"mbid,omitempty"`
}
// ArtistURLOutput represents the ArtistURLOutput data structure.
// ArtistURLOutput is the output for GetArtistURL.
type ArtistURLOutput struct {
// URL is the external URL for the artist.
URL string `json:"url"`
}
// ArtistBiographyOutput represents the ArtistBiographyOutput data structure.
// ArtistBiographyOutput is the output for GetArtistBiography.
type ArtistBiographyOutput struct {
// Biography is the artist biography text.
Biography string `json:"biography"`
}
// SimilarArtistsOutput represents the SimilarArtistsOutput data structure.
// SimilarArtistsOutput is the output for GetSimilarArtists.
type SimilarArtistsOutput struct {
// Artists is the list of similar artists.
Artists []ArtistRef `json:"artists"`
}
// ArtistImagesOutput represents the ArtistImagesOutput data structure.
// ArtistImagesOutput is the output for GetArtistImages.
type ArtistImagesOutput struct {
// Images is the list of artist images.
Images []ImageInfo `json:"images"`
}
// ImageInfo represents the ImageInfo data structure.
// ImageInfo represents an image with URL and size.
type ImageInfo struct {
// URL is the URL of the image.
@ -127,14 +56,56 @@ type ImageInfo struct {
Size int32 `json:"size"`
}
// ArtistMBIDOutput represents the ArtistMBIDOutput data structure.
// ArtistBiographyOutput is the output for GetArtistBiography.
type ArtistBiographyOutput struct {
// Biography is the artist biography text.
Biography string `json:"biography"`
}
// SimilarArtistsOutput is the output for GetSimilarArtists.
type SimilarArtistsOutput struct {
// Artists is the list of similar artists.
Artists []ArtistRef `json:"artists"`
}
// TopSongsInput is the input for GetArtistTopSongs.
type TopSongsInput struct {
// ID is the internal Navidrome artist ID.
ID string `json:"id"`
// Name is the artist name.
Name string `json:"name"`
// MBID is the MusicBrainz ID for the artist (if known).
MBID *string `json:"mbid,omitempty"`
// Count is the maximum number of top songs to return.
Count int32 `json:"count"`
}
// TopSongsOutput is the output for GetArtistTopSongs.
type TopSongsOutput struct {
// Songs is the list of top songs.
Songs []SongRef `json:"songs"`
}
// ArtistRef is a reference to an artist with name and optional MBID.
type ArtistRef struct {
// Name is the artist name.
Name string `json:"name"`
// MBID is the MusicBrainz ID for the artist.
MBID *string `json:"mbid,omitempty"`
}
// ArtistImagesOutput is the output for GetArtistImages.
type ArtistImagesOutput struct {
// Images is the list of artist images.
Images []ImageInfo `json:"images"`
}
// ArtistMBIDOutput is the output for GetArtistMBID.
type ArtistMBIDOutput struct {
// MBID is the MusicBrainz ID for the artist.
MBID string `json:"mbid"`
}
// ArtistInput represents the ArtistInput data structure.
// ArtistInput is the common input for artist-related functions.
type ArtistInput struct {
// ID is the internal Navidrome artist ID.
@ -145,12 +116,25 @@ type ArtistInput struct {
MBID *string `json:"mbid,omitempty"`
}
// SongRef represents the SongRef data structure.
// SongRef is a reference to a song with name and optional MBID.
type SongRef struct {
// Name is the song name.
// SimilarArtistsInput is the input for GetSimilarArtists.
type SimilarArtistsInput struct {
// ID is the internal Navidrome artist ID.
ID string `json:"id"`
// Name is the artist name.
Name string `json:"name"`
// MBID is the MusicBrainz ID for the song.
// MBID is the MusicBrainz ID for the artist (if known).
MBID *string `json:"mbid,omitempty"`
// Limit is the maximum number of similar artists to return.
Limit int32 `json:"limit"`
}
// AlbumInput is the common input for album-related functions.
type AlbumInput struct {
// Name is the album name.
Name string `json:"name"`
// Artist is the album artist name.
Artist string `json:"artist"`
// MBID is the MusicBrainz ID for the album (if known).
MBID *string `json:"mbid,omitempty"`
}

View File

@ -11,16 +11,6 @@ import (
pdk "github.com/extism/go-pdk"
)
// SchedulerCallbackOutput represents the SchedulerCallbackOutput data structure.
// SchedulerCallbackOutput is the output from the scheduler callback.
type SchedulerCallbackOutput struct {
// Error is the error message if the callback failed to process the scheduled task.
// Empty or null indicates success. The error is logged but does not
// affect the scheduling system.
Error *string `json:"error,omitempty"`
}
// SchedulerCallbackInput represents the SchedulerCallbackInput data structure.
// SchedulerCallbackInput is the input provided when a scheduled task fires.
type SchedulerCallbackInput struct {
// ScheduleID is the unique identifier for this scheduled task.
@ -34,6 +24,14 @@ type SchedulerCallbackInput struct {
IsRecurring bool `json:"isRecurring"`
}
// SchedulerCallbackOutput is the output from the scheduler callback.
type SchedulerCallbackOutput struct {
// Error is the error message if the callback failed to process the scheduled task.
// Empty or null indicates success. The error is logged but does not
// affect the scheduling system.
Error *string `json:"error,omitempty"`
}
// Scheduler is the marker interface for scheduler plugins.
// Implement one or more of the provider interfaces below.
// SchedulerCallback provides scheduled task handling.

View File

@ -8,16 +8,6 @@
package scheduler
// SchedulerCallbackOutput represents the SchedulerCallbackOutput data structure.
// SchedulerCallbackOutput is the output from the scheduler callback.
type SchedulerCallbackOutput struct {
// Error is the error message if the callback failed to process the scheduled task.
// Empty or null indicates success. The error is logged but does not
// affect the scheduling system.
Error *string `json:"error,omitempty"`
}
// SchedulerCallbackInput represents the SchedulerCallbackInput data structure.
// SchedulerCallbackInput is the input provided when a scheduled task fires.
type SchedulerCallbackInput struct {
// ScheduleID is the unique identifier for this scheduled task.
@ -31,6 +21,14 @@ type SchedulerCallbackInput struct {
IsRecurring bool `json:"isRecurring"`
}
// SchedulerCallbackOutput is the output from the scheduler callback.
type SchedulerCallbackOutput struct {
// Error is the error message if the callback failed to process the scheduled task.
// Empty or null indicates success. The error is logged but does not
// affect the scheduling system.
Error *string `json:"error,omitempty"`
}
// Scheduler is the marker interface for scheduler plugins.
// Implement one or more of the provider interfaces below.
// SchedulerCallback provides scheduled task handling.

View File

@ -16,16 +16,47 @@ type ScrobblerErrorType string
const (
// ScrobblerErrorNone indicates no error occurred.
ScrobblerErrorNone = "none"
ScrobblerErrorNone ScrobblerErrorType = "none"
// ScrobblerErrorNotAuthorized indicates the user is not authorized.
ScrobblerErrorNotAuthorized = "not_authorized"
ScrobblerErrorNotAuthorized ScrobblerErrorType = "not_authorized"
// ScrobblerErrorRetryLater indicates the operation should be retried later.
ScrobblerErrorRetryLater = "retry_later"
ScrobblerErrorRetryLater ScrobblerErrorType = "retry_later"
// ScrobblerErrorUnrecoverable indicates an unrecoverable error.
ScrobblerErrorUnrecoverable = "unrecoverable"
ScrobblerErrorUnrecoverable ScrobblerErrorType = "unrecoverable"
)
// TrackInfo represents the TrackInfo data structure.
// NowPlayingInput is the input for now playing notification.
type NowPlayingInput struct {
// UserID is the internal Navidrome user ID.
UserID string `json:"userId"`
// Username is the username of the user.
Username string `json:"username"`
// Track is the track currently playing.
Track TrackInfo `json:"track"`
// Position is the current playback position in seconds.
Position int32 `json:"position"`
}
// ScrobblerOutput is the output for scrobbler operations.
type ScrobblerOutput struct {
// Error is the error message if the operation failed.
Error *string `json:"error,omitempty"`
// ErrorType indicates how Navidrome should handle the error.
ErrorType *ScrobblerErrorType `json:"errorType,omitempty"`
}
// ScrobbleInput is the input for submitting a scrobble.
type ScrobbleInput struct {
// UserID is the internal Navidrome user ID.
UserID string `json:"userId"`
// Username is the username of the user.
Username string `json:"username"`
// Track is the track that was played.
Track TrackInfo `json:"track"`
// Timestamp is the Unix timestamp when the track started playing.
Timestamp int64 `json:"timestamp"`
}
// TrackInfo contains track metadata for scrobbling.
type TrackInfo struct {
// ID is the internal Navidrome track ID.
@ -58,7 +89,6 @@ type TrackInfo struct {
MBZReleaseTrackID *string `json:"mbzReleaseTrackId,omitempty"`
}
// AuthInput represents the AuthInput data structure.
// AuthInput is the input for authorization check.
type AuthInput struct {
// UserID is the internal Navidrome user ID.
@ -67,48 +97,12 @@ type AuthInput struct {
Username string `json:"username"`
}
// AuthOutput represents the AuthOutput data structure.
// AuthOutput is the output for authorization check.
type AuthOutput struct {
// Authorized indicates whether the user is authorized to scrobble.
Authorized bool `json:"authorized"`
}
// NowPlayingInput represents the NowPlayingInput data structure.
// NowPlayingInput is the input for now playing notification.
type NowPlayingInput struct {
// UserID is the internal Navidrome user ID.
UserID string `json:"userId"`
// Username is the username of the user.
Username string `json:"username"`
// Track is the track currently playing.
Track TrackInfo `json:"track"`
// Position is the current playback position in seconds.
Position int32 `json:"position"`
}
// ScrobblerOutput represents the ScrobblerOutput data structure.
// ScrobblerOutput is the output for scrobbler operations.
type ScrobblerOutput struct {
// Error is the error message if the operation failed.
Error *string `json:"error,omitempty"`
// ErrorType indicates how Navidrome should handle the error.
ErrorType *ScrobblerErrorType `json:"errorType,omitempty"`
}
// ScrobbleInput represents the ScrobbleInput data structure.
// ScrobbleInput is the input for submitting a scrobble.
type ScrobbleInput struct {
// UserID is the internal Navidrome user ID.
UserID string `json:"userId"`
// Username is the username of the user.
Username string `json:"username"`
// Track is the track that was played.
Track TrackInfo `json:"track"`
// Timestamp is the Unix timestamp when the track started playing.
Timestamp int64 `json:"timestamp"`
}
// Scrobbler requires all methods to be implemented.
// Scrobbler provides scrobbling functionality to external services.
// This capability allows plugins to submit listening history to services like Last.fm,

View File

@ -13,16 +13,47 @@ type ScrobblerErrorType string
const (
// ScrobblerErrorNone indicates no error occurred.
ScrobblerErrorNone = "none"
ScrobblerErrorNone ScrobblerErrorType = "none"
// ScrobblerErrorNotAuthorized indicates the user is not authorized.
ScrobblerErrorNotAuthorized = "not_authorized"
ScrobblerErrorNotAuthorized ScrobblerErrorType = "not_authorized"
// ScrobblerErrorRetryLater indicates the operation should be retried later.
ScrobblerErrorRetryLater = "retry_later"
ScrobblerErrorRetryLater ScrobblerErrorType = "retry_later"
// ScrobblerErrorUnrecoverable indicates an unrecoverable error.
ScrobblerErrorUnrecoverable = "unrecoverable"
ScrobblerErrorUnrecoverable ScrobblerErrorType = "unrecoverable"
)
// TrackInfo represents the TrackInfo data structure.
// NowPlayingInput is the input for now playing notification.
type NowPlayingInput struct {
// UserID is the internal Navidrome user ID.
UserID string `json:"userId"`
// Username is the username of the user.
Username string `json:"username"`
// Track is the track currently playing.
Track TrackInfo `json:"track"`
// Position is the current playback position in seconds.
Position int32 `json:"position"`
}
// ScrobblerOutput is the output for scrobbler operations.
type ScrobblerOutput struct {
// Error is the error message if the operation failed.
Error *string `json:"error,omitempty"`
// ErrorType indicates how Navidrome should handle the error.
ErrorType *ScrobblerErrorType `json:"errorType,omitempty"`
}
// ScrobbleInput is the input for submitting a scrobble.
type ScrobbleInput struct {
// UserID is the internal Navidrome user ID.
UserID string `json:"userId"`
// Username is the username of the user.
Username string `json:"username"`
// Track is the track that was played.
Track TrackInfo `json:"track"`
// Timestamp is the Unix timestamp when the track started playing.
Timestamp int64 `json:"timestamp"`
}
// TrackInfo contains track metadata for scrobbling.
type TrackInfo struct {
// ID is the internal Navidrome track ID.
@ -55,7 +86,6 @@ type TrackInfo struct {
MBZReleaseTrackID *string `json:"mbzReleaseTrackId,omitempty"`
}
// AuthInput represents the AuthInput data structure.
// AuthInput is the input for authorization check.
type AuthInput struct {
// UserID is the internal Navidrome user ID.
@ -64,48 +94,12 @@ type AuthInput struct {
Username string `json:"username"`
}
// AuthOutput represents the AuthOutput data structure.
// AuthOutput is the output for authorization check.
type AuthOutput struct {
// Authorized indicates whether the user is authorized to scrobble.
Authorized bool `json:"authorized"`
}
// NowPlayingInput represents the NowPlayingInput data structure.
// NowPlayingInput is the input for now playing notification.
type NowPlayingInput struct {
// UserID is the internal Navidrome user ID.
UserID string `json:"userId"`
// Username is the username of the user.
Username string `json:"username"`
// Track is the track currently playing.
Track TrackInfo `json:"track"`
// Position is the current playback position in seconds.
Position int32 `json:"position"`
}
// ScrobblerOutput represents the ScrobblerOutput data structure.
// ScrobblerOutput is the output for scrobbler operations.
type ScrobblerOutput struct {
// Error is the error message if the operation failed.
Error *string `json:"error,omitempty"`
// ErrorType indicates how Navidrome should handle the error.
ErrorType *ScrobblerErrorType `json:"errorType,omitempty"`
}
// ScrobbleInput represents the ScrobbleInput data structure.
// ScrobbleInput is the input for submitting a scrobble.
type ScrobbleInput struct {
// UserID is the internal Navidrome user ID.
UserID string `json:"userId"`
// Username is the username of the user.
Username string `json:"username"`
// Track is the track that was played.
Track TrackInfo `json:"track"`
// Timestamp is the Unix timestamp when the track started playing.
Timestamp int64 `json:"timestamp"`
}
// Scrobbler requires all methods to be implemented.
// Scrobbler provides scrobbling functionality to external services.
// This capability allows plugins to submit listening history to services like Last.fm,

View File

@ -11,7 +11,6 @@ import (
pdk "github.com/extism/go-pdk"
)
// OnErrorInput represents the OnErrorInput data structure.
// OnErrorInput is the input provided when an error occurs on a WebSocket connection.
type OnErrorInput struct {
// ConnectionID is the unique identifier for the WebSocket connection where the error occurred.
@ -20,7 +19,6 @@ type OnErrorInput struct {
Error string `json:"error"`
}
// OnErrorOutput represents the OnErrorOutput data structure.
// OnErrorOutput is the output from the error handler.
type OnErrorOutput struct {
// Error is the error message if the callback failed.
@ -28,7 +26,6 @@ type OnErrorOutput struct {
Error *string `json:"error,omitempty"`
}
// OnCloseInput represents the OnCloseInput data structure.
// OnCloseInput is the input provided when a WebSocket connection is closed.
type OnCloseInput struct {
// ConnectionID is the unique identifier for the WebSocket connection that was closed.
@ -40,7 +37,6 @@ type OnCloseInput struct {
Reason string `json:"reason"`
}
// OnCloseOutput represents the OnCloseOutput data structure.
// OnCloseOutput is the output from the close handler.
type OnCloseOutput struct {
// Error is the error message if the callback failed.
@ -48,7 +44,6 @@ type OnCloseOutput struct {
Error *string `json:"error,omitempty"`
}
// OnTextMessageInput represents the OnTextMessageInput data structure.
// OnTextMessageInput is the input provided when a text message is received.
type OnTextMessageInput struct {
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
@ -57,7 +52,6 @@ type OnTextMessageInput struct {
Message string `json:"message"`
}
// OnTextMessageOutput represents the OnTextMessageOutput data structure.
// OnTextMessageOutput is the output from the text message handler.
type OnTextMessageOutput struct {
// Error is the error message if the callback failed.
@ -65,7 +59,6 @@ type OnTextMessageOutput struct {
Error *string `json:"error,omitempty"`
}
// OnBinaryMessageInput represents the OnBinaryMessageInput data structure.
// OnBinaryMessageInput is the input provided when a binary message is received.
type OnBinaryMessageInput struct {
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
@ -74,7 +67,6 @@ type OnBinaryMessageInput struct {
Data string `json:"data"`
}
// OnBinaryMessageOutput represents the OnBinaryMessageOutput data structure.
// OnBinaryMessageOutput is the output from the binary message handler.
type OnBinaryMessageOutput struct {
// Error is the error message if the callback failed.

View File

@ -8,7 +8,6 @@
package websocket
// OnErrorInput represents the OnErrorInput data structure.
// OnErrorInput is the input provided when an error occurs on a WebSocket connection.
type OnErrorInput struct {
// ConnectionID is the unique identifier for the WebSocket connection where the error occurred.
@ -17,7 +16,6 @@ type OnErrorInput struct {
Error string `json:"error"`
}
// OnErrorOutput represents the OnErrorOutput data structure.
// OnErrorOutput is the output from the error handler.
type OnErrorOutput struct {
// Error is the error message if the callback failed.
@ -25,7 +23,6 @@ type OnErrorOutput struct {
Error *string `json:"error,omitempty"`
}
// OnCloseInput represents the OnCloseInput data structure.
// OnCloseInput is the input provided when a WebSocket connection is closed.
type OnCloseInput struct {
// ConnectionID is the unique identifier for the WebSocket connection that was closed.
@ -37,7 +34,6 @@ type OnCloseInput struct {
Reason string `json:"reason"`
}
// OnCloseOutput represents the OnCloseOutput data structure.
// OnCloseOutput is the output from the close handler.
type OnCloseOutput struct {
// Error is the error message if the callback failed.
@ -45,7 +41,6 @@ type OnCloseOutput struct {
Error *string `json:"error,omitempty"`
}
// OnTextMessageInput represents the OnTextMessageInput data structure.
// OnTextMessageInput is the input provided when a text message is received.
type OnTextMessageInput struct {
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
@ -54,7 +49,6 @@ type OnTextMessageInput struct {
Message string `json:"message"`
}
// OnTextMessageOutput represents the OnTextMessageOutput data structure.
// OnTextMessageOutput is the output from the text message handler.
type OnTextMessageOutput struct {
// Error is the error message if the callback failed.
@ -62,7 +56,6 @@ type OnTextMessageOutput struct {
Error *string `json:"error,omitempty"`
}
// OnBinaryMessageInput represents the OnBinaryMessageInput data structure.
// OnBinaryMessageInput is the input provided when a binary message is received.
type OnBinaryMessageInput struct {
// ConnectionID is the unique identifier for the WebSocket connection that received the message.
@ -71,7 +64,6 @@ type OnBinaryMessageInput struct {
Data string `json:"data"`
}
// OnBinaryMessageOutput represents the OnBinaryMessageOutput data structure.
// OnBinaryMessageOutput is the output from the binary message handler.
type OnBinaryMessageOutput struct {
// Error is the error message if the callback failed.

View File

@ -1,182 +0,0 @@
# Navidrome Plugin Schemas
This directory contains [XTP schemas](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema) that define plugin capabilities. Use these schemas to bootstrap new plugins with the `xtp` CLI.
## Available Schemas
| Schema | Description |
|----------------------------------------------------|---------------------------------|
| [metadata_agent.yaml](metadata_agent.yaml) | Artist/album metadata retrieval |
| [scrobbler.yaml](scrobbler.yaml) | Scrobbling to external services |
| [lifecycle.yaml](lifecycle.yaml) | Plugin initialization callback |
| [scheduler_callback.yaml](scheduler_callback.yaml) | Scheduled task callbacks |
| [websocket_callback.yaml](websocket_callback.yaml) | WebSocket event callbacks |
## Prerequisites
Install the XTP CLI:
```bash
curl -fsSL https://static.dylibso.com/cli/install.sh | bash
```
Or see [XTP CLI documentation](https://docs.xtp.dylibso.com/docs/cli) for other methods.
## Bootstrapping a Plugin
### Basic Usage
```bash
xtp plugin init \
--schema-file <schema> \
--template <language> \
--path <output-dir> \
--name <plugin-name>
```
### Supported Languages
- `go` Go (recommended, use with TinyGo)
- `rust` Rust
- `typescript` TypeScript
- `python` Python
- `csharp` C#
- `zig` Zig
- `cpp` C++
### Examples
**Go metadata agent:**
```bash
xtp plugin init \
--schema-file plugins/schemas/metadata_agent.yaml \
--template go \
--path ./my-agent \
--name my-agent
```
**Rust scrobbler:**
```bash
xtp plugin init \
--schema-file plugins/schemas/scrobbler.yaml \
--template rust \
--path ./my-scrobbler \
--name my-scrobbler
```
**TypeScript scrobbler:**
```bash
xtp plugin init \
--schema-file plugins/schemas/scrobbler.yaml \
--template typescript \
--path ./ts-scrobbler \
--name ts-scrobbler
```
## Generated Files
After running `xtp plugin init`, you'll get:
```
my-plugin/
├── main.go # Plugin implementation (stubs)
├── pdk.gen.go # Generated types from schema
├── xtp.toml # Plugin configuration
└── go.mod # Go module (for Go plugins)
```
## Implementing Your Plugin
### 1. Add the Manifest
Every plugin **must** implement `nd_manifest`. This is not in the schemas—add it manually:
```go
import (
"encoding/json"
"github.com/extism/go-pdk"
)
type Manifest struct {
Name string `json:"name"`
Author string `json:"author"`
Version string `json:"version"`
Description string `json:"description,omitempty"`
Website string `json:"website,omitempty"`
Permissions *Permissions `json:"permissions,omitempty"`
}
//go:wasmexport nd_manifest
func ndManifest() int32 {
manifest := Manifest{
Name: "My Plugin",
Author: "Your Name",
Version: "1.0.0",
Description: "What this plugin does",
}
out, _ := json.Marshal(manifest)
pdk.Output(out)
return 0
}
```
### 2. Implement Capability Functions
Replace the generated `panic()` stubs with your implementation:
```go
// Generated stub:
func NdGetArtistBiography(input ArtistInput) (BiographyOutput, error) {
panic("not implemented")
}
// Your implementation:
func NdGetArtistBiography(input ArtistInput) (BiographyOutput, error) {
bio := fetchBiography(input.Name)
return BiographyOutput{Biography: bio}, nil
}
```
### 3. Remove Unused Functions
You don't need to implement all functions from a schema. Delete any you don't need—Navidrome only calls functions that exist.
### 4. Build
```bash
xtp plugin build
```
Or manually with TinyGo:
```bash
tinygo build -o my-plugin.wasm -target wasip1 -buildmode=c-shared .
```
## Combining Capabilities
A single plugin can implement multiple capabilities. Generate from one schema, then manually add functions from others:
```bash
# Start with metadata agent
xtp plugin init --schema-file metadata_agent.yaml --template go --path ./my-plugin --name my-plugin
# Manually add scrobbler functions from scrobbler.yaml
# Manually add scheduler callback from scheduler_callback.yaml
```
Or combine schemas manually before generating (advanced).
## Schema Format
These schemas use [XTP Schema v1-draft](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema) format, which extends JSON Schema with plugin-specific extensions for exports and imports.
## Resources
- [Plugin System Documentation](../README.md)
- [XTP Documentation](https://docs.xtp.dylibso.com/)
- [XTP Schema Reference](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema)
- [Extism PDK](https://extism.org/docs/concepts/pdk)

View File

@ -1,41 +0,0 @@
version: v1-draft
exports:
nd_on_init:
description: |
Called after a plugin is fully loaded with all services registered.
Plugins can use this function to perform one-time initialization tasks,
such as establishing connections, starting background processes, or
validating configuration.
This function is called once when the plugin is loaded, and is NOT
called when the plugin is hot-reloaded. Plugins should not assume
this function will be called on every startup.
The input is currently empty (reserved for future use). The output
can contain an error string if initialization failed, which will be
logged but will not prevent the plugin from being loaded.
input:
$ref: "#/components/schemas/OnInitInput"
contentType: application/json
output:
$ref: "#/components/schemas/OnInitOutput"
contentType: application/json
components:
schemas:
OnInitInput:
description: Input provided to the init callback (currently empty, reserved for future use)
type: object
properties: {}
OnInitOutput:
description: Output from the init callback
type: object
properties:
error:
type: string
nullable: true
description: |
Error message if initialization failed. Empty or null indicates success.
The error is logged but does not prevent the plugin from being loaded.

View File

@ -1,299 +0,0 @@
version: v1-draft
exports:
nd_get_artist_mbid:
description: Retrieve the MusicBrainz ID for an artist
input:
$ref: "#/components/schemas/ArtistMBIDInput"
contentType: application/json
output:
$ref: "#/components/schemas/ArtistMBIDOutput"
contentType: application/json
nd_get_artist_url:
description: Retrieve the external URL for an artist
input:
$ref: "#/components/schemas/ArtistInput"
contentType: application/json
output:
$ref: "#/components/schemas/ArtistURLOutput"
contentType: application/json
nd_get_artist_biography:
description: Retrieve the biography for an artist
input:
$ref: "#/components/schemas/ArtistInput"
contentType: application/json
output:
$ref: "#/components/schemas/ArtistBiographyOutput"
contentType: application/json
nd_get_similar_artists:
description: Retrieve similar artists for a given artist
input:
$ref: "#/components/schemas/SimilarArtistsInput"
contentType: application/json
output:
$ref: "#/components/schemas/SimilarArtistsOutput"
contentType: application/json
nd_get_artist_images:
description: Retrieve images for an artist
input:
$ref: "#/components/schemas/ArtistInput"
contentType: application/json
output:
$ref: "#/components/schemas/ArtistImagesOutput"
contentType: application/json
nd_get_artist_top_songs:
description: Retrieve top songs for an artist
input:
$ref: "#/components/schemas/TopSongsInput"
contentType: application/json
output:
$ref: "#/components/schemas/TopSongsOutput"
contentType: application/json
nd_get_album_info:
description: Retrieve album information
input:
$ref: "#/components/schemas/AlbumInput"
contentType: application/json
output:
$ref: "#/components/schemas/AlbumInfoOutput"
contentType: application/json
nd_get_album_images:
description: Retrieve images for an album
input:
$ref: "#/components/schemas/AlbumInput"
contentType: application/json
output:
$ref: "#/components/schemas/AlbumImagesOutput"
contentType: application/json
components:
schemas:
ArtistMBIDInput:
description: Input for GetArtistMBID
properties:
id:
type: string
description: The internal Navidrome artist ID
name:
type: string
description: The artist name
required:
- id
- name
ArtistMBIDOutput:
description: Output for GetArtistMBID
properties:
mbid:
type: string
description: The MusicBrainz ID for the artist
required:
- mbid
ArtistInput:
description: Common input for artist-related functions
properties:
id:
type: string
description: The internal Navidrome artist ID
name:
type: string
description: The artist name
mbid:
type: string
nullable: true
description: The MusicBrainz ID for the artist (if known)
required:
- id
- name
ArtistURLOutput:
description: Output for GetArtistURL
properties:
url:
type: string
description: The external URL for the artist
required:
- url
ArtistBiographyOutput:
description: Output for GetArtistBiography
properties:
biography:
type: string
description: The artist biography text
required:
- biography
SimilarArtistsInput:
description: Input for GetSimilarArtists
properties:
id:
type: string
description: The internal Navidrome artist ID
name:
type: string
description: The artist name
mbid:
type: string
nullable: true
description: The MusicBrainz ID for the artist (if known)
limit:
type: integer
format: int32
description: Maximum number of similar artists to return
required:
- id
- name
- limit
ArtistRef:
description: Reference to an artist with name and optional MBID
properties:
name:
type: string
description: The artist name
mbid:
type: string
nullable: true
description: The MusicBrainz ID for the artist
required:
- name
SimilarArtistsOutput:
description: Output for GetSimilarArtists
properties:
artists:
type: array
items:
$ref: "#/components/schemas/ArtistRef"
description: List of similar artists
required:
- artists
ImageInfo:
description: Image with URL and size
properties:
url:
type: string
description: The URL of the image
size:
type: integer
format: int32
description: The size of the image in pixels (width or height)
required:
- url
- size
ArtistImagesOutput:
description: Output for GetArtistImages
properties:
images:
type: array
items:
$ref: "#/components/schemas/ImageInfo"
description: List of artist images
required:
- images
TopSongsInput:
description: Input for GetArtistTopSongs
properties:
id:
type: string
description: The internal Navidrome artist ID
name:
type: string
description: The artist name
mbid:
type: string
nullable: true
description: The MusicBrainz ID for the artist (if known)
count:
type: integer
format: int32
description: Maximum number of top songs to return
required:
- id
- name
- count
SongRef:
description: Reference to a song with name and optional MBID
properties:
name:
type: string
description: The song name
mbid:
type: string
nullable: true
description: The MusicBrainz ID for the song
required:
- name
TopSongsOutput:
description: Output for GetArtistTopSongs
properties:
songs:
type: array
items:
$ref: "#/components/schemas/SongRef"
description: List of top songs
required:
- songs
AlbumInput:
description: Common input for album-related functions
properties:
name:
type: string
description: The album name
artist:
type: string
description: The album artist name
mbid:
type: string
nullable: true
description: The MusicBrainz ID for the album (if known)
required:
- name
- artist
AlbumInfoOutput:
description: Output for GetAlbumInfo
properties:
name:
type: string
description: The album name
mbid:
type: string
description: The MusicBrainz ID for the album
description:
type: string
description: The album description/notes
url:
type: string
description: The external URL for the album
required:
- name
- mbid
- description
- url
AlbumImagesOutput:
description: Output for GetAlbumImages
properties:
images:
type: array
items:
$ref: "#/components/schemas/ImageInfo"
description: List of album images
required:
- images

View File

@ -1,49 +0,0 @@
version: v1-draft
exports:
nd_scheduler_callback:
description: |
Called when a scheduled task fires. Plugins that use the scheduler host service
must export this function to receive callbacks when their scheduled tasks execute.
input:
$ref: "#/components/schemas/SchedulerCallbackInput"
contentType: application/json
output:
$ref: "#/components/schemas/SchedulerCallbackOutput"
contentType: application/json
components:
schemas:
SchedulerCallbackInput:
description: Input provided to the scheduler callback when a scheduled task fires
properties:
scheduleId:
type: string
description: |
The unique identifier for this scheduled task. This is either the ID
provided when scheduling, or an auto-generated UUID if none was specified.
payload:
type: string
description: |
The payload data that was provided when the task was scheduled.
Can be used to pass context or parameters to the callback handler.
isRecurring:
type: boolean
description: |
True if this is a recurring schedule (created via ScheduleRecurring),
false if it's a one-time schedule (created via ScheduleOneTime).
required:
- scheduleId
- payload
- isRecurring
SchedulerCallbackOutput:
description: Output from the scheduler callback
properties:
error:
type: string
nullable: true
description: |
Error message if the callback failed to process the scheduled task.
Empty or null indicates success. The error is logged but does not
affect the scheduling system.

View File

@ -1,182 +0,0 @@
version: v1-draft
exports:
nd_scrobbler_is_authorized:
description: Check if a user is authorized to scrobble to this service
input:
$ref: "#/components/schemas/AuthInput"
contentType: application/json
output:
$ref: "#/components/schemas/AuthOutput"
contentType: application/json
nd_scrobbler_now_playing:
description: Send a now playing notification to the scrobbling service
input:
$ref: "#/components/schemas/NowPlayingInput"
contentType: application/json
output:
$ref: "#/components/schemas/ScrobblerOutput"
contentType: application/json
nd_scrobbler_scrobble:
description: Submit a completed scrobble to the scrobbling service
input:
$ref: "#/components/schemas/ScrobbleInput"
contentType: application/json
output:
$ref: "#/components/schemas/ScrobblerOutput"
contentType: application/json
components:
schemas:
AuthInput:
description: Input for authorization check
properties:
userId:
type: string
description: The internal Navidrome user ID
username:
type: string
description: The username of the user
required:
- userId
- username
AuthOutput:
description: Output for authorization check
properties:
authorized:
type: boolean
description: Whether the user is authorized to scrobble
required:
- authorized
TrackInfo:
description: Track metadata for scrobbling
properties:
id:
type: string
description: The internal Navidrome track ID
title:
type: string
description: Track title
album:
type: string
description: Album name
artist:
type: string
description: Track artist
albumArtist:
type: string
description: Album artist
duration:
type: number
format: float
description: Track duration in seconds
trackNumber:
type: integer
format: int32
description: Track number on the album
discNumber:
type: integer
format: int32
description: Disc number
mbzRecordingId:
type: string
nullable: true
description: MusicBrainz recording ID
mbzAlbumId:
type: string
nullable: true
description: MusicBrainz album/release ID
mbzArtistId:
type: string
nullable: true
description: MusicBrainz artist ID
mbzReleaseGroupId:
type: string
nullable: true
description: MusicBrainz release group ID
mbzAlbumArtistId:
type: string
nullable: true
description: MusicBrainz album artist ID
mbzReleaseTrackId:
type: string
nullable: true
description: MusicBrainz release track ID
required:
- id
- title
- album
- artist
- albumArtist
- duration
- trackNumber
- discNumber
NowPlayingInput:
description: Input for now playing notification
properties:
userId:
type: string
description: The internal Navidrome user ID
username:
type: string
description: The username of the user
track:
$ref: "#/components/schemas/TrackInfo"
description: The track currently playing
position:
type: integer
format: int32
description: Current playback position in seconds
required:
- userId
- username
- track
- position
ScrobbleInput:
description: Input for submitting a scrobble
properties:
userId:
type: string
description: The internal Navidrome user ID
username:
type: string
description: The username of the user
track:
$ref: "#/components/schemas/TrackInfo"
description: The track that was played
timestamp:
type: integer
format: int64
description: Unix timestamp when the track started playing
required:
- userId
- username
- track
- timestamp
ScrobblerOutput:
description: Output for scrobbler operations (now_playing and scrobble)
properties:
error:
type: string
nullable: true
description: Error message if the operation failed
errorType:
$ref: "#/components/schemas/ScrobblerErrorType"
nullable: true
description: Type of error for handling
ScrobblerErrorType:
type: string
description: Error type indicating how Navidrome should handle the error
enum:
- none
- not_authorized
- retry_later
- unrecoverable

View File

@ -1,160 +0,0 @@
version: v1-draft
exports:
nd_websocket_on_text_message:
description: |
Called when a text message is received on a WebSocket connection.
Plugins that use the WebSocket host service must export this function
to handle incoming text messages.
input:
$ref: "#/components/schemas/OnTextMessageInput"
contentType: application/json
output:
$ref: "#/components/schemas/OnTextMessageOutput"
contentType: application/json
nd_websocket_on_binary_message:
description: |
Called when a binary message is received on a WebSocket connection.
Plugins that use the WebSocket host service must export this function
to handle incoming binary data.
input:
$ref: "#/components/schemas/OnBinaryMessageInput"
contentType: application/json
output:
$ref: "#/components/schemas/OnBinaryMessageOutput"
contentType: application/json
nd_websocket_on_error:
description: |
Called when an error occurs on a WebSocket connection.
Plugins that use the WebSocket host service must export this function
to handle connection errors.
input:
$ref: "#/components/schemas/OnErrorInput"
contentType: application/json
output:
$ref: "#/components/schemas/OnErrorOutput"
contentType: application/json
nd_websocket_on_close:
description: |
Called when a WebSocket connection is closed.
Plugins that use the WebSocket host service must export this function
to handle connection closure events.
input:
$ref: "#/components/schemas/OnCloseInput"
contentType: application/json
output:
$ref: "#/components/schemas/OnCloseOutput"
contentType: application/json
components:
schemas:
OnTextMessageInput:
description: Input provided when a text message is received
properties:
connectionId:
type: string
description: |
The unique identifier for the WebSocket connection that received the message.
message:
type: string
description: |
The text message content received from the WebSocket.
required:
- connectionId
- message
OnTextMessageOutput:
description: Output from the text message handler
type: object
properties:
error:
type: string
nullable: true
description: |
Error message if the callback failed. Empty or null indicates success.
OnBinaryMessageInput:
description: Input provided when a binary message is received
properties:
connectionId:
type: string
description: |
The unique identifier for the WebSocket connection that received the message.
data:
type: string
format: byte
description: |
The binary data received from the WebSocket, encoded as base64.
required:
- connectionId
- data
OnBinaryMessageOutput:
description: Output from the binary message handler
type: object
properties:
error:
type: string
nullable: true
description: |
Error message if the callback failed. Empty or null indicates success.
OnErrorInput:
description: Input provided when an error occurs on a WebSocket connection
properties:
connectionId:
type: string
description: |
The unique identifier for the WebSocket connection where the error occurred.
error:
type: string
description: |
The error message describing what went wrong.
required:
- connectionId
- error
OnErrorOutput:
description: Output from the error handler
type: object
properties:
error:
type: string
nullable: true
description: |
Error message if the callback failed. Empty or null indicates success.
OnCloseInput:
description: Input provided when a WebSocket connection is closed
properties:
connectionId:
type: string
description: |
The unique identifier for the WebSocket connection that was closed.
code:
type: integer
format: int32
description: |
The WebSocket close status code (e.g., 1000 for normal closure,
1001 for going away, 1006 for abnormal closure).
reason:
type: string
description: |
The human-readable reason for the connection closure, if provided.
required:
- connectionId
- code
- reason
OnCloseOutput:
description: Output from the close handler
type: object
properties:
error:
type: string
nullable: true
description: |
Error message if the callback failed. Empty or null indicates success.