diff --git a/plugins/README.md b/plugins/README.md index adc177324..b04e12bd9 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -34,12 +34,14 @@ The plugin system is built on **[Extism](https://extism.org/)**, a cross-languag - [Task](#task) - [WebSocket](#websocket) - [Library](#library) + - [Matcher](#matcher) - [Artwork](#artwork) - [SubsonicAPI](#subsonicapi) - [Config](#config) - [Users](#users) - [ScrobbleRetriever](#scrobbleretriever) - [Configuration](#configuration) +- [Command Line Interface](#command-line-interface) - [Building Plugins](#building-plugins) - [Examples](#examples) - [Security](#security) @@ -226,13 +228,14 @@ func ndGetArtistBiography() int32 { ### Scrobbler -Integrates with external scrobbling services. All three methods are **required**. +Integrates with external scrobbling services. All four methods are **required**. -| Function | Input | Output | Description | -|------------------------------|-----------------------|--------|-----------------------------| -| `nd_scrobbler_is_authorized` | `{username}` | `bool` | Check if user is authorized | -| `nd_scrobbler_now_playing` | See below | (none) | Send now playing | -| `nd_scrobbler_scrobble` | See below | (none) | Submit a scrobble | +| Function | Input | Output | Description | +|---------------------------------|--------------|--------|------------------------------| +| `nd_scrobbler_is_authorized` | `{username}` | `bool` | Check if user is authorized | +| `nd_scrobbler_now_playing` | See below | (none) | Send now playing | +| `nd_scrobbler_scrobble` | See below | (none) | Submit a scrobble | +| `nd_scrobbler_playback_report` | See below | (none) | Send playback state report | > **Important:** Scrobbler plugins require the `users` permission in their manifest. Scrobble events are only sent for users assigned to the plugin through Navidrome's configuration. @@ -270,6 +273,25 @@ Integrates with external scrobbling services. All three methods are **required** } ``` +**PlaybackReport Input:** + +Same `username` and `track` fields, plus playback state details: + +```json +{ + "username": "john", + "track": { ... }, + "state": "playing", + "positionMs": 45000, + "playbackRate": 1.0, + "playerId": "player-id", + "playerName": "My Client", + "timestamp": 1703270400 +} +``` + +`state` is one of `starting`, `playing`, `paused`, `stopped`, or `expired`. + **Error Handling:** On success, return `0`. On failure, use `pdk.SetError()` with one of these error types: @@ -309,7 +331,7 @@ Each match contains a `song` reference and a `similarity` score (float64, 0.0– ### TaskWorker -Processes tasks from a queue. The method is **optional** — export it if your plugin uses the [Task](#task) host service for background work. +Processes tasks from a queue. **Required** if your plugin uses the [Task](#task) host service: declaring the `taskqueue` permission without exporting this function fails the plugin load. | Function | Input | Output | Description | |---------------------|---------------------------------------------|---------|----------------------| @@ -329,7 +351,7 @@ Useful for initializing connections, scheduling recurring tasks, etc. Errors are ### SchedulerCallback -Receives scheduled task events. **Required** if your plugin uses the [Scheduler](#scheduler) host service. +Receives scheduled task events. **Required** if your plugin uses the [Scheduler](#scheduler) host service: declaring the `scheduler` permission without exporting this function fails the plugin load. | Function | Input | Output | Description | |---------------------------|----------------------------------------------|--------|-----------------------------| @@ -346,6 +368,8 @@ Receives WebSocket events. Export any subset of these to handle events from the | `nd_websocket_on_error` | `{connectionId, error}` | Connection error | | `nd_websocket_on_close` | `{connectionId, code, reason}` | Connection closed | +Each callback invocation is subject to a 30-second timeout. + --- ## Host Services @@ -390,6 +414,8 @@ Make HTTP requests to external services. This is a dedicated host service (separ |-------------|----------------------------------------------------------|----------------------------------| | `http_send` | `method, url, headers, body, timeoutMs, noFollowRedirects` | `statusCode, headers, body` | +**Limits:** Requests time out after 10 seconds by default (override per request with `timeoutMs`). Redirects are followed up to 5 times, re-checking the allowed hosts on every hop. Response bodies are capped at 10MB. + **Usage:** ```go @@ -615,13 +641,21 @@ Background task queue with retry support. Plugins enqueue tasks and process them **Host functions:** -| Function | Parameters | Description | -|---------------------|---------------------------------------------------|----------------------------| -| `task_createqueue` | `name, concurrency, maxRetries, backoffMs, ...` | Create a named task queue | -| `task_enqueue` | `queueName, payload` | Add a task to the queue | -| `task_get` | `taskID` | Get task status and result | -| `task_cancel` | `taskID` | Cancel a pending task | -| `task_clearqueue` | `queueName` | Remove all tasks from queue| +| Function | Parameters | Description | +|---------------------|---------------------------------------------------------------|----------------------------| +| `task_createqueue` | `name, concurrency, maxRetries, backoffMs, delayMs, retentionMs` | Create a named task queue | +| `task_enqueue` | `queueName, payload` | Add a task to the queue | +| `task_get` | `taskID` | Get task status and result | +| `task_cancel` | `taskID` | Cancel a pending task | +| `task_clearqueue` | `queueName` | Remove all tasks from queue| + +Tasks are persisted to SQLite, so pending tasks survive server restarts. Queue behavior: + +- `concurrency` – Parallel workers (default 1), capped by the manifest's `maxConcurrency` +- `maxRetries` – Retries for a failed task (default 0); `backoffMs` (default 1000) doubles on each retry +- `delayMs` – Minimum delay between consecutive task starts, useful for rate limiting (default 0) +- `retentionMs` – How long finished tasks are kept (default 1 hour, min 1 minute, max 1 week) +- Payloads are capped at 1MB **Usage:** @@ -747,6 +781,45 @@ for _, lib := range libraries { } ``` +### Matcher + +Match externally-obtained songs (e.g. results from a recommendation or similarity API) to tracks in the local library, reusing Navidrome's matching algorithm (ID > MBID > ISRC > fuzzy title). + +**Manifest permission:** + +```json +{ + "permissions": { + "matcher": { + "reason": "Resolve external recommendations to library tracks" + }, + "library": { + "reason": "Required by the matcher permission" + } + } +} +``` + +> **Important:** The `matcher` permission requires the `library` permission. + +**Host functions:** + +| Function | Parameters | Returns | +|----------------------|---------------|-------------------------| +| `matcher_matchsongs` | `songs, opts` | Array of matched tracks | + +The result has one entry per input song, in the same order; the entry for a song with no match is empty. Results are limited to the libraries the plugin (and the scoped user, if any) can access. Set `opts.username` to run the match as a specific user: their favorites and ratings inform tiebreaking, and the returned tracks carry their annotations. User scoping additionally requires the [`users`](#users) permission, with users assigned to the plugin. + +**Usage:** + +```go +import "github.com/navidrome/navidrome/plugins/pdk/go/types" + +matches, err := host.MatcherMatchSongs([]types.SongRef{ + {Name: "Song Title", Artists: []types.ArtistRef{{Name: "Artist Name"}}}, +}, host.MatchOptions{}) +``` + ### Artwork Generate public URLs for Navidrome artwork (albums, artists, tracks, playlists). @@ -1008,6 +1081,29 @@ For more advanced access (listing keys, integer values), use the [Config](#confi --- +## Command Line Interface + +Manage plugins from the command line with `navidrome plugin`: + +| Command | Description | +|--------------------------------------------------------|------------------------------------------------------------| +| `navidrome plugin list [-f table\|csv\|json]` | List installed plugins | +| `navidrome plugin info [-f text\|json]` | Show details for an installed plugin or a `.ndp` package | +| `navidrome plugin validate ` | Validate an installed plugin or a `.ndp` package manifest | +| `navidrome plugin enable ` | Enable a plugin | +| `navidrome plugin disable ` | Disable a plugin | +| `navidrome plugin edit ` | Update a plugin's config and/or permissions | +| `navidrome plugin rescan` | Re-discover plugins in the plugins folder | + +**`plugin edit` flags:** + +- `--config ` / `--config-file ` – Set the plugin configuration (`-` reads from stdin) +- `--users ` / `--all-users` – Usernames the plugin may access (comma-separated or JSON array), or all users +- `--libraries ` / `--all-libraries` – Library IDs the plugin may access (comma-separated or JSON array), or all libraries +- `--write-access` / `--no-write-access` – Allow or deny the plugin write access to libraries + +--- + ## Building Plugins ### Supported Languages @@ -1070,6 +1166,8 @@ replace github.com/navidrome/navidrome => ../../.. | `scheduler` | `plugins/pdk/go/scheduler` | Scheduled task callbacks | | `websocket` | `plugins/pdk/go/websocket` | WebSocket event handlers | | `host` | `plugins/pdk/go/host` | Host service SDK (all services) | +| `types` | `plugins/pdk/go/types` | Shared data types (tracks, artists, song refs) | +| `pdk` | `plugins/pdk/go/pdk` | Low-level helpers (wraps extism/go-pdk: config, logging, memory) | See the example plugins in [examples/](examples/) for complete usage patterns. @@ -1168,12 +1266,11 @@ See [examples/](examples/) for complete working plugins: | [minimal](examples/minimal/) | Go | MetadataAgent | – | Basic structure example | | [wikimedia](examples/wikimedia/) | Go | MetadataAgent | HTTP | Wikidata/Wikipedia integration | | [coverartarchive-py](examples/coverartarchive-py/) | Python | MetadataAgent | HTTP | Cover Art Archive | -| [coverartarchive-as](examples/coverartarchive-as/) | AssemblyScript | MetadataAgent | HTTP | Cover Art Archive | -| [webhook-rs](examples/webhook-rs/) | Rust | Scrobbler | HTTP | HTTP webhooks | -| [nowplaying-py](examples/nowplaying-py/) | Python | Lifecycle | Scheduler, SubsonicAPI | Periodic now-playing logger | -| [library-inspector-rs](examples/library-inspector-rs/) | Rust | Lifecycle | Library, Scheduler | Periodic library stats logging | -| [crypto-ticker](examples/crypto-ticker/) | Go | Lifecycle | WebSocket, Scheduler | Real-time crypto prices demo | -| [discord-rich-presence-rs](examples/discord-rich-presence-rs/) | Rust | Scrobbler | HTTP, WebSocket, Cache, Scheduler, Artwork | Discord integration | +| [webhook-rs](examples/webhook-rs/) | Rust | Scrobbler | HTTP | HTTP webhooks | +| [nowplaying-py](examples/nowplaying-py/) | Python | Lifecycle, SchedulerCallback | Scheduler, SubsonicAPI | Periodic now-playing logger | +| [library-inspector-rs](examples/library-inspector-rs/) | Rust | Lifecycle, SchedulerCallback | Library, Scheduler | Periodic library stats logging | +| [crypto-ticker](examples/crypto-ticker/) | Go | Lifecycle, SchedulerCallback, WebSocketCallback | WebSocket, Scheduler | Real-time crypto prices demo | +| [discord-rich-presence-rs](examples/discord-rich-presence-rs/) | Rust | Scrobbler, SchedulerCallback, WebSocketCallback | HTTP, WebSocket, Cache, Scheduler, Artwork, Config | Discord integration | --- @@ -1186,7 +1283,7 @@ Plugins run in a secure WebAssembly sandbox provided by [Extism](https://extism. 3. **No Network Listeners** – Plugins cannot bind ports 4. **Config Isolation** – Plugins only receive their own config section 5. **Memory Limits** – Controlled by the WebAssembly runtime -6. **User-Scoped Authorization** – Plugins with `subsonicapi` or `scrobbler` capabilities can only access/receive events for users assigned to them through Navidrome's configuration +6. **User-Scoped Authorization** – Plugins with `subsonicapi`, `scrobbleRetriever`, or `scrobbler` capabilities can only access/receive events for users assigned to them through Navidrome's configuration 7. **Users Permission** – Plugins requesting user access must be explicitly configured with allowed users; sensitive data (passwords, emails) is never exposed --- @@ -1201,7 +1298,7 @@ If `AutoReload` is disabled, Navidrome needs to be restarted to pick up plugin c ### Enabling/Disabling Plugins -Plugins can be enabled/disabled via the Navidrome UI. The plugin state is persisted in the database. +Plugins can be enabled/disabled via the Navidrome UI or the [`navidrome plugin` CLI](#command-line-interface). The plugin state is persisted in the database. ### Important Notes