* feat(plugins): add JSONForms schema for plugin configuration Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance error handling by formatting validation errors with field names Signed-off-by: Deluan <deluan@navidrome.org> * feat: enforce required fields in config validation and improve error handling Signed-off-by: Deluan <deluan@navidrome.org> * format JS code Signed-off-by: Deluan <deluan@navidrome.org> * feat: add config schema validation and enhance manifest structure Signed-off-by: Deluan <deluan@navidrome.org> * feat: refactor plugin config parsing and add unit tests Signed-off-by: Deluan <deluan@navidrome.org> * feat: add config validation error message in Portuguese * feat: enhance AlwaysExpandedArrayLayout with description support and improve array control testing Signed-off-by: Deluan <deluan@navidrome.org> * feat: update Discord Rust plugin configuration to use JSONForm for user tokens and enhance schema validation Signed-off-by: Deluan <deluan@navidrome.org> * fix: resolve React Hooks linting issues in plugin UI components * Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * format code Signed-off-by: Deluan <deluan@navidrome.org> * feat: migrate schema validation to use santhosh-tekuri/jsonschema and improve error formatting Signed-off-by: Deluan <deluan@navidrome.org> * address PR comments Signed-off-by: Deluan <deluan@navidrome.org> * fix flaky test Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance array layout and configuration handling with AJV defaults Signed-off-by: Deluan <deluan@navidrome.org> * feat: implement custom tester to exclude enum arrays from AlwaysExpandedArrayLayout Signed-off-by: Deluan <deluan@navidrome.org> * feat: add error boundary for schema rendering and improve error messages Signed-off-by: Deluan <deluan@navidrome.org> * feat: refine non-enum array control logic by utilizing JSONForms schema resolution Signed-off-by: Deluan <deluan@navidrome.org> * feat: add error styling to ToggleEnabledSwitch for disabled state Signed-off-by: Deluan <deluan@navidrome.org> * feat: adjust label positioning and styling in SchemaConfigEditor for improved layout Signed-off-by: Deluan <deluan@navidrome.org> * feat: implement outlined input controls renderers to replace custom fragile CSS Signed-off-by: Deluan <deluan@navidrome.org> * feat: remove margin from last form control inside array items for better spacing Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance AJV error handling to transform required errors for field-level validation Signed-off-by: Deluan <deluan@navidrome.org> * feat: set default value for User Tokens in manifest.json to improve user experience Signed-off-by: Deluan <deluan@navidrome.org> * format Signed-off-by: Deluan <deluan@navidrome.org> * feat: add margin to outlined input controls for improved spacing Signed-off-by: Deluan <deluan@navidrome.org> * feat: remove redundant margin rule for last form control in array items Signed-off-by: Deluan <deluan@navidrome.org> * feat: adjust font size of label elements in SchemaConfigEditor for improved readability Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Discord Rich Presence Plugin
This example plugin integrates Navidrome with Discord Rich Presence. It shows how a plugin can keep a real-time connection to an external service while remaining completely stateless. This plugin is based on the Navicord project, which provides similar functionality.
⚠️ WARNING: This plugin is for demonstration purposes only. It relies on the user's Discord token being stored in the Navidrome configuration file, which is not secure and may be against Discord's terms of service. Use it at your own risk.
Overview
The plugin exposes three capabilities:
- Scrobbler – receives
NowPlayingnotifications from Navidrome - WebSocketCallback – handles Discord gateway messages
- SchedulerCallback – used to clear presence and send periodic heartbeats
It relies on several host services declared in the manifest:
http– queries Discord API endpointswebsocket– maintains gateway connectionsscheduler– schedules heartbeats and presence cleanupcache– stores sequence numbers for heartbeatsartwork– resolves track artwork URLs
Architecture
The plugin registers capabilities using the PDK Register pattern:
import (
"github.com/navidrome/navidrome/plugins/pdk/go/scrobbler"
"github.com/navidrome/navidrome/plugins/pdk/go/scheduler"
"github.com/navidrome/navidrome/plugins/pdk/go/websocket"
)
type discordPlugin struct{}
func init() {
scrobbler.Register(&discordPlugin{})
scheduler.Register(&discordPlugin{})
websocket.Register(&discordPlugin{})
}
The PDK generates the appropriate export wrappers automatically.
When NowPlaying is invoked the plugin:
- Loads
clientidand user tokens from the configuration (because plugins are stateless). - Connects to Discord using
WebSocketServiceif no connection exists. - Sends the activity payload with track details and artwork.
- Schedules a one-time callback to clear the presence after the track finishes.
Heartbeat messages are sent by a recurring scheduler job. Sequence numbers received from Discord are stored in CacheService to remain available across plugin instances.
The scheduler callback uses the payload field to route to the appropriate handler:
"heartbeat"– sends a heartbeat to Discord (recurring)"clear-activity"– clears the presence and disconnects (one-time)
Stateless Operation
Navidrome plugins are completely stateless – each method call instantiates a new plugin instance and discards it afterwards.
To work within this model the plugin stores no in-memory state. Connections are keyed by username inside the host services and any transient data (like Discord sequence numbers) is kept in the cache. Configuration is reloaded on every method call.
Configuration
Configure in the Navidrome UI (Settings → Plugins → discord-rich-presence):
| Key | Description | Example |
|---|---|---|
clientid |
Your Discord application ID | 123456789012345678 |
user.<name> |
Discord token for the specified user | user.alice = token123 |
Each user is configured as a separate key with the user. prefix.
Building
From the plugins/examples/ directory:
make discord-rich-presence.ndp
Or manually:
cd discord-rich-presence
tinygo build -target wasip1 -buildmode=c-shared -o plugin.wasm .
zip -j discord-rich-presence.ndp manifest.json plugin.wasm
Installation
Place the resulting discord-rich-presence.ndp in your Navidrome plugins folder and enable plugins in your configuration:
[Plugins]
Enabled = true
Folder = "/path/to/plugins"
Files
| File | Description |
|---|---|
main.go |
Plugin entry point, capability registration, and implementations |
rpc.go |
Discord gateway communication and RPC logic |
go.mod |
Go module file |
PDK
This plugin imports the Navidrome PDK subpackages directly:
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 replace directives to point to the local packages for development.
Host Services Used
| Service | Purpose |
|---|---|
| Cache | Store Discord sequence numbers and processed image URLs |
| Scheduler | Schedule heartbeats (recurring) and activity clearing (one-time) |
| WebSocket | Maintain persistent connection to Discord gateway |
| Artwork | Get track artwork URLs for rich presence display |
Implementation Details
See main.go and rpc.go for the complete implementation.