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