feat(scheduler): add scheduler callback schema and implementation for plugins

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-12-23 23:48:23 -05:00
parent e4b0e31c85
commit 4631d05082
5 changed files with 227 additions and 47 deletions

View File

@ -200,6 +200,106 @@ func main() {}
Scrobbler plugins are automatically discovered and used by Navidrome's PlayTracker alongside built-in scrobblers (Last.fm, ListenBrainz).
### Scheduler
Allows plugins to schedule one-time or recurring tasks. Plugins that use the scheduler host service must export a callback function to receive scheduled events.
| Function | Input | Output | Description |
|-------------------------|----------------------------------------------|-------------------|------------------------------------||
| `nd_scheduler_callback` | `{schedule_id, payload, is_recurring}` | `{error?}` | Called when a scheduled task fires |
#### Scheduler Callback Input
```json
{
"schedule_id": "string",
"payload": "string",
"is_recurring": true
}
```
- `schedule_id`: The unique identifier for the scheduled task
- `payload`: Data passed when the task was scheduled
- `is_recurring`: `true` for recurring schedules, `false` for one-time
#### Scheduler Callback Output
The output is optional on success. On error, return:
```json
{
"error": "error message"
}
```
#### Using the Scheduler Host Service
To schedule tasks, plugins call these host functions (provided by Navidrome):
| Host Function | Parameters | Description |
|------------------------------|-----------------------------------------------|------------------------------------------|
| `scheduler_scheduleonetime` | `delay_seconds, payload, schedule_id` | Schedule a one-time callback |
| `scheduler_schedulerecurring`| `cron_expression, payload, schedule_id` | Schedule a recurring callback |
| `scheduler_cancelschedule` | `schedule_id` | Cancel a scheduled task |
#### Manifest Permissions
Plugins using the scheduler must declare the permission in their manifest:
```json
{
"permissions": {
"scheduler": {
"reason": "Schedule periodic metadata refresh"
}
}
}
```
#### Example Scheduler Plugin
```go
package main
import (
"github.com/extism/go-pdk"
)
type SchedulerCallbackInput struct {
ScheduleId string `json:"schedule_id"`
Payload string `json:"payload"`
IsRecurring bool `json:"is_recurring"`
}
type SchedulerCallbackOutput struct {
Error *string `json:"error,omitempty"`
}
//go:wasmexport nd_scheduler_callback
func ndSchedulerCallback() int32 {
var input SchedulerCallbackInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return 1
}
// Handle the scheduled task based on payload
pdk.Log(pdk.LogInfo, "Task fired: " + input.ScheduleId)
// Return success (empty output)
output := SchedulerCallbackOutput{}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
}
func main() {}
```
To schedule a task from your plugin, use the generated SDK functions (see `plugins/host/go/nd_host_scheduler.go`).
## Developing Plugins
Plugins can be written in any language that compiles to WebAssembly. We recommend using the [Extism PDK](https://extism.org/docs/category/write-a-plug-in) for your language.

View File

@ -6,10 +6,11 @@ new plugins using the `xtp` CLI tool.
## Available Schemas
| Schema | Description |
|--------------------------------------------|-----------------------------------------------------------------|
| [metadata_agent.yaml](metadata_agent.yaml) | Metadata agent for retrieving artist/album information |
| [scrobbler.yaml](scrobbler.yaml) | Scrobbler capability for sending play data to external services |
| Schema | Description |
|------------------------------------------------------|-----------------------------------------------------------------|
| [metadata_agent.yaml](metadata_agent.yaml) | Metadata agent for retrieving artist/album information |
| [scheduler_callback.yaml](scheduler_callback.yaml) | Scheduler callback for plugins using the scheduler host service |
| [scrobbler.yaml](scrobbler.yaml) | Scrobbler capability for sending play data to external services |
## Prerequisites

View File

@ -0,0 +1,49 @@
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:
schedule_id:
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.
is_recurring:
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:
- schedule_id
- payload
- is_recurring
SchedulerCallbackOutput:
description: Output from the scheduler callback
properties:
error:
type: string
nullable: true
description: |
Error message if the callback failed to process the scheduled task.
Empty or null indicates success. The error is logged but does not
affect the scheduling system.

View File

@ -1,12 +1,15 @@
// Fake scheduler plugin for Navidrome plugin system integration tests.
// This plugin was created based on the scheduler_callback.yaml XTP schema.
// Build with: tinygo build -o ../fake-scheduler.wasm -target wasip1 -buildmode=c-shared .
//
// Note: pdk.gen.go contains the domain types from the XTP schema where your plugin will run.
package main
import (
"encoding/json"
"strconv"
"github.com/extism/go-pdk"
pdk "github.com/extism/go-pdk"
)
// Manifest types
@ -26,19 +29,7 @@ type SchedulerPermission struct {
Reason string `json:"reason,omitempty"`
}
// Scheduler callback input
type SchedulerCallbackInput struct {
ScheduleID string `json:"schedule_id"`
Payload string `json:"payload"`
IsRecurring bool `json:"is_recurring"`
}
// Scheduler callback output
type SchedulerCallbackOutput struct {
Error string `json:"error,omitempty"`
}
// CallRecord stores information about a callback that was received
// CallRecord stores information about a callback that was received (for testing)
type CallRecord struct {
ScheduleID string `json:"schedule_id"`
Payload string `json:"payload"`
@ -73,51 +64,33 @@ func ndManifest() int32 {
return 0
}
//go:wasmexport nd_scheduler_callback
func ndSchedulerCallback() int32 {
var input SchedulerCallbackInput
if err := pdk.InputJSON(&input); err != nil {
pdk.SetError(err)
return 1
}
// Payload is now a plain string, no decoding needed
payload := input.Payload
// NdSchedulerCallback implements the scheduler callback logic.
// Called when a scheduled task fires.
// This function is called by the generated wrapper in pdk.gen.go.
func NdSchedulerCallback(input SchedulerCallbackInput) (SchedulerCallbackOutput, error) {
// Check for configured error response
errCfg, hasErr := pdk.GetConfig("callback_error")
if hasErr && errCfg != "" {
output := SchedulerCallbackOutput{Error: errCfg}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
return SchedulerCallbackOutput{Error: &errCfg}, nil
}
// Track the callback
totalCallCount++
if record, exists := callRecords[input.ScheduleID]; exists {
if record, exists := callRecords[input.ScheduleId]; exists {
record.CallCount++
} else {
callRecords[input.ScheduleID] = &CallRecord{
ScheduleID: input.ScheduleID,
Payload: payload,
callRecords[input.ScheduleId] = &CallRecord{
ScheduleID: input.ScheduleId,
Payload: input.Payload,
IsRecurring: input.IsRecurring,
CallCount: 1,
}
}
// Log the callback for debugging
pdk.Log(pdk.LogInfo, "Scheduler callback received: "+input.ScheduleID+" payload="+payload)
pdk.Log(pdk.LogInfo, "Scheduler callback received: "+input.ScheduleId+" payload="+input.Payload)
// Success
output := SchedulerCallbackOutput{}
if err := pdk.OutputJSON(output); err != nil {
pdk.SetError(err)
return 1
}
return 0
return SchedulerCallbackOutput{}, nil
}
// Helper function to get call records (for testing)

View File

@ -0,0 +1,57 @@
// THIS FILE WAS GENERATED BY `xtp-go-bindgen`. DO NOT EDIT.
package main
import (
pdk "github.com/extism/go-pdk"
)
//go:wasmexport nd_scheduler_callback
func _NdSchedulerCallback() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "NdSchedulerCallback: getting JSON input")
var input SchedulerCallbackInput
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdSchedulerCallback: calling implementation function")
output, err := NdSchedulerCallback(input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdSchedulerCallback: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "NdSchedulerCallback: returning")
return 0
}
// Input provided to the scheduler callback 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:"is_recurring"`
// 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:"schedule_id"`
}
// 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"`
}