diff --git a/plugins/README.md b/plugins/README.md index 760dc8032..78685d0d8 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -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: diff --git a/plugins/capabilities/README.md b/plugins/capabilities/README.md index 40743a17f..b3df8ff99 100644 --- a/plugins/capabilities/README.md +++ b/plugins/capabilities/README.md @@ -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 diff --git a/plugins/cmd/ndpgen/README.md b/plugins/cmd/ndpgen/README.md index 2279e05a3..341c8aece 100644 --- a/plugins/cmd/ndpgen/README.md +++ b/plugins/cmd/ndpgen/README.md @@ -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 diff --git a/plugins/cmd/ndpgen/internal/generator_test.go b/plugins/cmd/ndpgen/internal/generator_test.go index 73b40e30b..842936cb2 100644 --- a/plugins/cmd/ndpgen/internal/generator_test.go +++ b/plugins/cmd/ndpgen/internal/generator_test.go @@ -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"`)) }) }) diff --git a/plugins/cmd/ndpgen/internal/parser_test.go b/plugins/cmd/ndpgen/internal/parser_test.go index 90edec036..c8abbf945 100644 --- a/plugins/cmd/ndpgen/internal/parser_test.go +++ b/plugins/cmd/ndpgen/internal/parser_test.go @@ -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()) }) diff --git a/plugins/cmd/ndpgen/internal/templates/capability.go.tmpl b/plugins/cmd/ndpgen/internal/templates/capability.go.tmpl index db87f4769..42495737d 100644 --- a/plugins/cmd/ndpgen/internal/templates/capability.go.tmpl +++ b/plugins/cmd/ndpgen/internal/templates/capability.go.tmpl @@ -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}} diff --git a/plugins/cmd/ndpgen/internal/templates/capability_stub.go.tmpl b/plugins/cmd/ndpgen/internal/templates/capability_stub.go.tmpl index e2e8ce8ad..ea3a210fe 100644 --- a/plugins/cmd/ndpgen/internal/templates/capability_stub.go.tmpl +++ b/plugins/cmd/ndpgen/internal/templates/capability_stub.go.tmpl @@ -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}} diff --git a/plugins/examples/crypto-ticker/README.md b/plugins/examples/crypto-ticker/README.md index 30c9327a8..7f23433f5 100644 --- a/plugins/examples/crypto-ticker/README.md +++ b/plugins/examples/crypto-ticker/README.md @@ -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. --- diff --git a/plugins/examples/crypto-ticker/go.mod b/plugins/examples/crypto-ticker/go.mod index c126c2d0a..83f494861 100755 --- a/plugins/examples/crypto-ticker/go.mod +++ b/plugins/examples/crypto-ticker/go.mod @@ -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 diff --git a/plugins/examples/crypto-ticker/main.go b/plugins/examples/crypto-ticker/main.go index 364a9a451..2b29350c2 100755 --- a/plugins/examples/crypto-ticker/main.go +++ b/plugins/examples/crypto-ticker/main.go @@ -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 diff --git a/plugins/examples/crypto-ticker/pdk.gen.go b/plugins/examples/crypto-ticker/pdk.gen.go deleted file mode 100755 index 6aa7dfaf4..000000000 --- a/plugins/examples/crypto-ticker/pdk.gen.go +++ /dev/null @@ -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"` -} diff --git a/plugins/examples/discord-rich-presence/README.md b/plugins/examples/discord-rich-presence/README.md index cc4f90cc5..48488f198 100644 --- a/plugins/examples/discord-rich-presence/README.md +++ b/plugins/examples/discord-rich-presence/README.md @@ -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 diff --git a/plugins/examples/discord-rich-presence/go.mod b/plugins/examples/discord-rich-presence/go.mod index 2e18b9784..6c0858a12 100644 --- a/plugins/examples/discord-rich-presence/go.mod +++ b/plugins/examples/discord-rich-presence/go.mod @@ -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 diff --git a/plugins/examples/discord-rich-presence/main.go b/plugins/examples/discord-rich-presence/main.go index d2817b1ec..39690c84b 100644 --- a/plugins/examples/discord-rich-presence/main.go +++ b/plugins/examples/discord-rich-presence/main.go @@ -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() {} diff --git a/plugins/examples/discord-rich-presence/pdk.gen.go b/plugins/examples/discord-rich-presence/pdk.gen.go deleted file mode 100644 index 73da3ea92..000000000 --- a/plugins/examples/discord-rich-presence/pdk.gen.go +++ /dev/null @@ -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"` -} diff --git a/plugins/examples/minimal/README.md b/plugins/examples/minimal/README.md index 53f6597d6..d0bd7a30a 100644 --- a/plugins/examples/minimal/README.md +++ b/plugins/examples/minimal/README.md @@ -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. diff --git a/plugins/examples/minimal/go.mod b/plugins/examples/minimal/go.mod index a22fe9055..69313a879 100644 --- a/plugins/examples/minimal/go.mod +++ b/plugins/examples/minimal/go.mod @@ -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 => ../../.. diff --git a/plugins/examples/minimal/main.go b/plugins/examples/minimal/main.go index 9f9460a7d..91872afd8 100644 --- a/plugins/examples/minimal/main.go +++ b/plugins/examples/minimal/main.go @@ -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() {} diff --git a/plugins/pdk/go/host/README.md b/plugins/pdk/go/host/README.md index f7efebdd0..cb0c9f240 100644 --- a/plugins/pdk/go/host/README.md +++ b/plugins/pdk/go/host/README.md @@ -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 diff --git a/plugins/pdk/go/lifecycle/lifecycle.go b/plugins/pdk/go/lifecycle/lifecycle.go index b8b395ad2..519cf35b9 100644 --- a/plugins/pdk/go/lifecycle/lifecycle.go +++ b/plugins/pdk/go/lifecycle/lifecycle.go @@ -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. diff --git a/plugins/pdk/go/lifecycle/lifecycle_stub.go b/plugins/pdk/go/lifecycle/lifecycle_stub.go index e0c6a2eda..97c111964 100644 --- a/plugins/pdk/go/lifecycle/lifecycle_stub.go +++ b/plugins/pdk/go/lifecycle/lifecycle_stub.go @@ -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. diff --git a/plugins/pdk/go/metadata/metadata.go b/plugins/pdk/go/metadata/metadata.go index 30fde3d98..69c6522ce 100644 --- a/plugins/pdk/go/metadata/metadata.go +++ b/plugins/pdk/go/metadata/metadata.go @@ -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"` } diff --git a/plugins/pdk/go/metadata/metadata_stub.go b/plugins/pdk/go/metadata/metadata_stub.go index eb969e0a7..4d6a2ea4f 100644 --- a/plugins/pdk/go/metadata/metadata_stub.go +++ b/plugins/pdk/go/metadata/metadata_stub.go @@ -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"` } diff --git a/plugins/pdk/go/scheduler/scheduler.go b/plugins/pdk/go/scheduler/scheduler.go index 537ae21cb..b1e8bf9c0 100644 --- a/plugins/pdk/go/scheduler/scheduler.go +++ b/plugins/pdk/go/scheduler/scheduler.go @@ -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. diff --git a/plugins/pdk/go/scheduler/scheduler_stub.go b/plugins/pdk/go/scheduler/scheduler_stub.go index e9eccece5..e87d9862a 100644 --- a/plugins/pdk/go/scheduler/scheduler_stub.go +++ b/plugins/pdk/go/scheduler/scheduler_stub.go @@ -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. diff --git a/plugins/pdk/go/scrobbler/scrobbler.go b/plugins/pdk/go/scrobbler/scrobbler.go index cd5629698..96d97b797 100644 --- a/plugins/pdk/go/scrobbler/scrobbler.go +++ b/plugins/pdk/go/scrobbler/scrobbler.go @@ -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, diff --git a/plugins/pdk/go/scrobbler/scrobbler_stub.go b/plugins/pdk/go/scrobbler/scrobbler_stub.go index 88d6c3537..02d8ea16b 100644 --- a/plugins/pdk/go/scrobbler/scrobbler_stub.go +++ b/plugins/pdk/go/scrobbler/scrobbler_stub.go @@ -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, diff --git a/plugins/pdk/go/websocket/websocket.go b/plugins/pdk/go/websocket/websocket.go index 2653ae09b..0c7050644 100644 --- a/plugins/pdk/go/websocket/websocket.go +++ b/plugins/pdk/go/websocket/websocket.go @@ -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. diff --git a/plugins/pdk/go/websocket/websocket_stub.go b/plugins/pdk/go/websocket/websocket_stub.go index c9b4d02a7..c0bc5a943 100644 --- a/plugins/pdk/go/websocket/websocket_stub.go +++ b/plugins/pdk/go/websocket/websocket_stub.go @@ -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. diff --git a/plugins/schemas/README.md b/plugins/schemas/README.md deleted file mode 100644 index 65d32b160..000000000 --- a/plugins/schemas/README.md +++ /dev/null @@ -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 \ - --template \ - --path \ - --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) diff --git a/plugins/schemas/lifecycle.yaml b/plugins/schemas/lifecycle.yaml deleted file mode 100644 index dec4d49ac..000000000 --- a/plugins/schemas/lifecycle.yaml +++ /dev/null @@ -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. diff --git a/plugins/schemas/metadata_agent.yaml b/plugins/schemas/metadata_agent.yaml deleted file mode 100644 index f25f1f4c2..000000000 --- a/plugins/schemas/metadata_agent.yaml +++ /dev/null @@ -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 diff --git a/plugins/schemas/scheduler_callback.yaml b/plugins/schemas/scheduler_callback.yaml deleted file mode 100644 index b4c8d8f40..000000000 --- a/plugins/schemas/scheduler_callback.yaml +++ /dev/null @@ -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. diff --git a/plugins/schemas/scrobbler.yaml b/plugins/schemas/scrobbler.yaml deleted file mode 100644 index 160531c54..000000000 --- a/plugins/schemas/scrobbler.yaml +++ /dev/null @@ -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 diff --git a/plugins/schemas/websocket_callback.yaml b/plugins/schemas/websocket_callback.yaml deleted file mode 100644 index 83ee94a67..000000000 --- a/plugins/schemas/websocket_callback.yaml +++ /dev/null @@ -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.