Files
mattandClaude Sonnet 5 7da2405d50
CI / test (push) Successful in 13s
CI / lint (push) Successful in 13s
CI / build (push) Successful in 25s
CI / publish (push) Failing after 9s
CI / notify (push) Successful in 5s
License as BSD-3-Clause, publish artifacts to Gitea packages too
Adds LICENSE + pyproject.toml license metadata, and extends CI to
publish the built .ndp to this repo's own Gitea generic package
registry (linked to the repo) alongside the existing pkgs.daubney.dev
upload.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018n5ffHCn5QUjpDtuWTk3fh
2026-09-27 23:29:39 +01:00

4.4 KiB

Scrobbled Navidrome Plugin

A Navidrome scrobbler plugin that sends now-playing updates and scrobbles to a self-hosted Scrobbled server (a Last.fm Audioscrobbler 2.0-compatible service).

It's a WebAssembly plugin built with Extism, 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 and 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.:

navidrome plugin edit navidrome-scrobbled --users <your-navidrome-username>

Building

Requires extism-py and Binaryen (wasm-opt/wasm-merge) on PATH. The official installer fetches both:

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

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, packages and validates the .ndp on every push, and — on pushes to main or v* tags — publishes the built artifact both to this repo's own Gitea package registry and to the same pkgs.daubney.dev ProGet asset feed used by ScrobblingService, under the navidrome-scrobbled content path.

License

BSD 3-Clause. See LICENSE.