navidrome/plugins/examples/discord-rich-presence
Deluan Quintão 45c408a674
feat(plugins): allow Plugins to call the Subsonic API (#4260)
* chore: .gitignore any navidrome binary

Signed-off-by: Deluan <deluan@navidrome.org>

* feat: implement internal authentication handling in middleware

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(manager): add SubsonicRouter to Manager for API routing

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(plugins): add SubsonicAPI Host service for plugins and an example plugin

Signed-off-by: Deluan <deluan@navidrome.org>

* fix lint

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(plugins): refactor path handling in SubsonicAPI to extract endpoint correctly

Signed-off-by: Deluan <deluan@navidrome.org>

* docs(plugins): add SubsonicAPI service documentation to README

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(plugins): implement permission checks for SubsonicAPI service

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(plugins): enhance SubsonicAPI service initialization with atomic router handling

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor(plugins): better encapsulated dependency injection

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor(plugins): rename parameter in WithInternalAuth for clarity

Signed-off-by: Deluan <deluan@navidrome.org>

* docs(plugins): update SubsonicAPI permissions section in README for clarity and detail

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(plugins): enhance SubsonicAPI permissions output with allowed usernames and admin flag

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(plugins): add schema reference to example plugins

Signed-off-by: Deluan <deluan@navidrome.org>

* remove import alias

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2025-06-25 14:18:32 -04:00
..

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 a similar functionality.

NOTE: 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 NowPlaying notifications from Navidrome
  • WebSocketCallback handles Discord gateway messages
  • SchedulerCallback used to clear presence and send periodic heartbeats

It relies on several host services declared in manifest.json:

  • http queries Discord API endpoints
  • websocket maintains gateway connections
  • scheduler schedules heartbeats and presence cleanup
  • cache stores sequence numbers for heartbeats
  • config retrieves the plugin configuration on each call
  • artwork resolves track artwork URLs

Architecture

Each call from Navidrome creates a new plugin instance. The init function registers the capabilities and obtains the scheduler service:

api.RegisterScrobbler(plugin)
api.RegisterWebSocketCallback(plugin.rpc)
plugin.sched = api.RegisterNamedSchedulerCallback("close-activity", plugin)
plugin.rpc.sched = api.RegisterNamedSchedulerCallback("heartbeat", plugin.rpc)

When NowPlaying is invoked the plugin:

  1. Loads clientid and user tokens from the configuration (because plugins are stateless).
  2. Connects to Discord using WebSocketService if no connection exists.
  3. Sends the activity payload with track details and artwork.
  4. Schedules a onetime 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 OnSchedulerCallback method clears the presence and closes the connection when the scheduled time is reached.

// The plugin is stateless, we need to load the configuration every time
clientID, users, err := d.getConfig(ctx)

Configuration

Add the following to navidrome.toml and adjust for your tokens:

[PluginSettings.discord-rich-presence]
ClientID = "123456789012345678"
Users = "alice:token123,bob:token456"
  • clientid is your Discord application ID
  • users is a commaseparated list of username:token pairs used for authorization

Building

GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o plugin.wasm ./discord-rich-presence/...

Place the resulting plugin.wasm and manifest.json in a discord-rich-presence folder under your Navidrome plugins directory.

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 user name inside the host services and any transient data (like Discord sequence numbers) is kept in the cache. Configuration is reloaded on every method call.

For more implementation details see plugin.go and rpc.go.