docs(plugins): sync plugins README with the implementation (#5912)

* docs(plugins): document the Matcher host service

The Matcher host service (matcher_matchsongs, permission 'matcher') was
missing from the README entirely. Add its section with permission example,
the matcher-requires-library rule, result ordering/scoping semantics, and
a Go usage example.

* docs(plugins): add missing nd_scrobbler_playback_report to Scrobbler

The Scrobbler capability has four required methods, but the README said
three and omitted nd_scrobbler_playback_report from the function table.
Add the table row and document the PlaybackReport input payload and its
possible playback states.

* docs(plugins): remove stale coverartarchive-as example row

The AssemblyScript coverartarchive-as example no longer exists in
plugins/examples/ (and no AssemblyScript code remains in the repo), so
drop its row from the examples table.

* docs(plugins): add Command Line Interface section

The 'navidrome plugin' CLI (list, info, validate, enable, disable, edit,
rescan) was undocumented; the README's only mention was a single inline
'plugin edit --write-access' reference, and it claimed plugins could only
be enabled/disabled via the UI. Add a CLI section with all subcommands
and the 'plugin edit' flags, and link it from Runtime Management.

* docs(plugins): add pdk and types packages to Go PDK table

The Go PDK package table was missing two of the packages that ship under
plugins/pdk/go/: 'types' (shared DTOs used across capabilities and host
services) and 'pdk' (the extism/go-pdk wrapper the README's own examples
already import).

* docs(plugins): correct capabilities and host services in examples table

Several example rows understated what the plugins implement: the
scheduler/websocket callback capabilities were omitted for nowplaying-py,
library-inspector-rs, crypto-ticker, and discord-rich-presence-rs, and
the Discord example also uses the Config host service.

* docs(plugins): state that scheduler/taskqueue permissions require callbacks

Manifest validation rejects a plugin that declares the scheduler or
taskqueue permission without exporting nd_scheduler_callback or
nd_task_execute, but the README worded both callbacks as soft
suggestions (TaskWorker was even labeled optional). Make the load-time
failure explicit in both capability sections.

* docs(plugins): document HTTP, WebSocket, and Task limits and defaults

Add the runtime limits plugins actually hit: HTTP default 10s timeout,
5-redirect cap with per-hop host re-validation, and 10MB response cap;
the 30s timeout on WebSocket callbacks; and the full task_createqueue
parameter list (delayMs, retentionMs) with queue defaults, retention
bounds, the 1MB payload cap, and task persistence across restarts.

* docs(plugins): include scrobbleRetriever in user-scoped authorization note

The ScrobbleRetriever host service added in #5795 is user-scoped like
subsonicapi and scrobbler, but the Security section's user-scoped
authorization item didn't list it.

* docs(plugins): clarify matcher user scoping requires the users permission

The Matcher usage example set opts.Username while the section's manifest
example declares only matcher+library; without the users permission no
users can be assigned to the plugin, so userAccess.resolve rejects the
call at runtime. Make the example unscoped and state that user scoping
requires the users permission with users assigned. Raised by Codex
review on PR #5912.
This commit is contained in:
Deluan Quintão 2026-08-08 22:25:48 -04:00 committed by GitHub
parent b0c6d2e444
commit 5c5b849a4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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 <id\|file.ndp> [-f text\|json]` | Show details for an installed plugin or a `.ndp` package |
| `navidrome plugin validate <id\|file.ndp>` | Validate an installed plugin or a `.ndp` package manifest |
| `navidrome plugin enable <id>` | Enable a plugin |
| `navidrome plugin disable <id>` | Disable a plugin |
| `navidrome plugin edit <id>` | Update a plugin's config and/or permissions |
| `navidrome plugin rescan` | Re-discover plugins in the plugins folder |
**`plugin edit` flags:**
- `--config <json>` / `--config-file <path>` Set the plugin configuration (`-` reads from stdin)
- `--users <list>` / `--all-users` Usernames the plugin may access (comma-separated or JSON array), or all users
- `--libraries <list>` / `--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