Add Navidrome Scrobbled scrobbler plugin
A Python Extism plugin for Navidrome that scrobbles playback to a self-hosted Scrobbled (Last.fm-compatible) server, plus Gitea Actions CI that lints, tests, builds/validates, and publishes the .ndp artifact to pkgs.daubney.dev. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018n5ffHCn5QUjpDtuWTk3fh
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
# Scrobbled Navidrome Plugin
|
||||
|
||||
A [Navidrome](https://www.navidrome.org/) scrobbler plugin that sends now-playing
|
||||
updates and scrobbles to a self-hosted [Scrobbled](https://scrobbled.uk) server
|
||||
(a Last.fm Audioscrobbler 2.0-compatible service).
|
||||
|
||||
It's a WebAssembly plugin built with [Extism](https://extism.org/), written in
|
||||
Python. Navidrome has no first-class Python SDK for plugins ("no Python PDK"),
|
||||
so this plugin calls Navidrome's host services directly, following the pattern
|
||||
used by Navidrome's own [`nowplaying-py`](https://github.com/navidrome/navidrome/tree/master/plugins/examples/nowplaying-py)
|
||||
and [`coverartarchive-py`](https://github.com/navidrome/navidrome/tree/master/plugins/examples/coverartarchive-py)
|
||||
example plugins.
|
||||
|
||||
## How it works
|
||||
|
||||
Navidrome calls this plugin's four `nd_scrobbler_*` exports as users play
|
||||
music. The plugin translates each call into a signed, session-authenticated
|
||||
request against Scrobbled's `/2.0/` Last.fm-compatible API:
|
||||
|
||||
- `nd_scrobbler_is_authorized` — a fast local check (no network call) that the
|
||||
plugin is configured and, if `navidrome_username` is set, that the request
|
||||
is for that user.
|
||||
- `nd_scrobbler_now_playing` → `track.updateNowPlaying`
|
||||
- `nd_scrobbler_scrobble` → `track.scrobble`
|
||||
- `nd_scrobbler_playback_report` — required by Navidrome's Scrobbler
|
||||
capability, but implemented as a no-op since Scrobbled has no playback-state
|
||||
concept beyond now-playing/scrobble.
|
||||
|
||||
The Scrobbled session key (`sk`), obtained once via the `auth.getMobileSession`
|
||||
handshake, is cached in Navidrome's per-plugin KVStore so it isn't re-derived
|
||||
from the configured password on every call. If Scrobbled reports the cached
|
||||
session key as invalid, the plugin re-authenticates once and retries.
|
||||
|
||||
This is a single-account plugin: one Navidrome user's plays scrobble to one
|
||||
Scrobbled account, configured directly in the plugin's config.
|
||||
|
||||
## Configuration
|
||||
|
||||
Set these via the Navidrome admin UI (Plugins page) or
|
||||
`navidrome plugin edit <id> --config '...'`:
|
||||
|
||||
| Key | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `server_url` | yes | Base URL of your Scrobbled instance, e.g. `https://scrobbled.uk` |
|
||||
| `api_key` | yes | API key from a Scrobbled "app" you create in its web UI |
|
||||
| `shared_secret` | yes | Shared secret paired with the API key |
|
||||
| `scrobbled_username` | yes | Your Scrobbled account username |
|
||||
| `scrobbled_password` | yes | Your Scrobbled account password (used only for the session handshake, never sent on scrobble calls) |
|
||||
| `navidrome_username` | no | If set, only this Navidrome username is treated as authorized |
|
||||
|
||||
You also need to assign the plugin to the relevant Navidrome user(s), e.g.:
|
||||
|
||||
```sh
|
||||
navidrome plugin edit navidrome-scrobbled --users <your-navidrome-username>
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Requires [`extism-py`](https://github.com/extism/python-pdk) and
|
||||
[Binaryen](https://github.com/WebAssembly/binaryen) (`wasm-opt`/`wasm-merge`)
|
||||
on `PATH`. The official installer fetches both:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://raw.githubusercontent.com/extism/python-pdk/main/install.sh | bash
|
||||
|
||||
make build # compiles plugin/__init__.py -> dist/plugin.wasm
|
||||
make package # packages manifest.json + dist/plugin.wasm -> dist/navidrome-scrobbled.ndp
|
||||
make validate # navidrome plugin validate dist/navidrome-scrobbled.ndp (needs a navidrome binary on PATH)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```sh
|
||||
make test # or: python3 -m unittest discover -s tests -v
|
||||
```
|
||||
|
||||
`tests/test_logic.py` exercises the plugin's pure business logic (request
|
||||
signing, form-encoding, track-field mapping) under plain CPython, using a
|
||||
minimal fake `extism` module (`tests/stubs/extism.py`) — no wasm toolchain
|
||||
required. If a checkout of the `ScrobblingService` server source is available
|
||||
on the same machine, it also cross-checks the signing algorithm against that
|
||||
project's own implementation.
|
||||
|
||||
## Installing
|
||||
|
||||
Copy the built `.ndp` file into your Navidrome plugins folder (or install it
|
||||
via whatever mechanism your Navidrome deployment provides for plugin files),
|
||||
then configure and assign users as described above.
|
||||
|
||||
## CI/CD
|
||||
|
||||
Gitea Actions (`.gitea/workflows/ci.yml`) lints and tests every push and pull
|
||||
request, builds and packages the `.ndp` on every push, and — on pushes to
|
||||
`main` or `v*` tags — publishes the built artifact to the same
|
||||
[pkgs.daubney.dev](https://pkgs.daubney.dev) ProGet asset feed used by
|
||||
`ScrobblingService`, under the `navidrome-scrobbled` content path.
|
||||
Reference in New Issue
Block a user