navidrome_bridge: one-click radio requests from Navidrome
Star a track in Navidrome and it lands in betoniarka's request queue. EXTERNAL component: imports nothing from Conjurer and bypasses the Discord bot and its API entirely. It speaks only to Navidrome's Subsonic API and to the radio operator, so the bot can be down and this keeps working. Not shipped as a .ndp plugin, deliberately. Navidrome's plugin capabilities are MetadataAgent, Scrobbler, Lyrics, SonicSimilarity, TaskWorker, Lifecycle, SchedulerCallback and WebSocketCallback - there is no UI extension capability (a plugin cannot add a button) and no star/love event to react to. The only plugin-shaped alternative, Scrobbler, sees every track played, which is a firehose rather than a one-click request. So the trigger is Navidrome's own star control, which also works from any Subsonic client including phones. The README documents this with sources. Both sides index the same library under different mount points, so every path is translated between the two roots; paths reported relative to Navidrome's library are handled as well as absolute ones. Two delivery modes: "api" (default) posts to /request_radio_file and needs no shared filesystem, at the cost of the radio re-finding the track by keywords; "playlist" appends the exact translated path and is exact but needs the radio's data dir mounted. The api path prepends a sentinel token because betoniarka's wyszukaj() drops lista_slow[0], where the Discord command word normally sits. Verified: path translation for both absolute and relative forms; keyword extraction (no regex metacharacters, extension dropped, Polish characters intact); and an end-to-end check feeding the generated keywords through betoniarka's real wyszukaj() against a library seeded with near-miss traps (same artist, live version of the same title, same album name under another artist) - it resolved to the exact intended file. Docker/compose/systemd install paths documented. Not run against a live Navidrome or radio - no instance reachable from here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
# Navidrome → betoniarka bridge
|
||||
|
||||
Star the track you are listening to in Navidrome and it lands in the radio's
|
||||
request queue. One click, from whatever you already listen with.
|
||||
|
||||
---
|
||||
|
||||
> ### ⚠️ This is an EXTERNAL component
|
||||
>
|
||||
> It lives in this repo for convenience, but it is **not part of Conjurer**:
|
||||
>
|
||||
> * it imports **nothing** from the bot's codebase — it is a standalone script
|
||||
> with a single dependency (`requests`);
|
||||
> * it **bypasses Kondziu and his API entirely**. It never talks to the Discord
|
||||
> bot, never touches `/prepped_tracks`, and needs no cog enabled. The bot can
|
||||
> be dead and this keeps working;
|
||||
> * it talks to exactly two things: **Navidrome's Subsonic API** and
|
||||
> **betoniarka** (the radio operator).
|
||||
>
|
||||
> ```
|
||||
> Navidrome ──(Subsonic API: read stars, optional unstar)──► bridge
|
||||
> bridge ──(/request_radio_file, or the request playlist)──► betoniarka ──► Liquidsoap
|
||||
>
|
||||
> Kondziu (Discord bot) ....................... not involved
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
## Why this is not a `.ndp` Navidrome plugin
|
||||
|
||||
Navidrome does have a plugin system (WASM/Extism, `.ndp` packages), so this is
|
||||
worth spelling out. Its plugin capabilities are:
|
||||
|
||||
`MetadataAgent`, `Scrobbler`, `Lyrics`, `SonicSimilarity`, `TaskWorker`,
|
||||
`Lifecycle`, `SchedulerCallback`, `WebSocketCallback`.
|
||||
|
||||
Two things follow, and both kill the "button in the Navidrome UI" idea:
|
||||
|
||||
1. **There is no UI extension capability.** A plugin cannot add a button,
|
||||
menu entry or context action to the web interface. Plugins are backend
|
||||
extensions running in a WASM sandbox.
|
||||
2. **There is no star/love event.** Nothing fires when a user favourites a
|
||||
track, so even a headless plugin could not react to the click.
|
||||
|
||||
The remaining plugin-shaped option would be the `Scrobbler` capability, which
|
||||
receives *every* track you play — that is a firehose, not a one-click request.
|
||||
|
||||
So instead of inventing a button, this uses the button Navidrome already has:
|
||||
**the star/love control**. That choice has a real advantage over a hypothetical
|
||||
custom button — it works in *every* Subsonic client, so you can also queue a
|
||||
track from your phone.
|
||||
|
||||
Sources: [Navidrome plugin docs](https://www.navidrome.org/docs/usage/features/plugins/),
|
||||
[plugin capability list](https://github.com/navidrome/navidrome/blob/master/plugins/README.md).
|
||||
|
||||
## How it works
|
||||
|
||||
1. You star a track in Navidrome (web UI, phone, anything Subsonic).
|
||||
2. The bridge polls `getStarred2` and notices the new star.
|
||||
3. It translates the file's path from Navidrome's mount point to the radio's.
|
||||
4. It hands that track to betoniarka, which appends it to the request playlist;
|
||||
Liquidsoap is already watching that file and picks it up.
|
||||
5. Optionally it un-stars the track, so the heart behaves like a "send" button
|
||||
you can press again next time.
|
||||
|
||||
## Path translation
|
||||
|
||||
Both sides index **the same mp3 files under different mount points**, which is
|
||||
the one thing you must get right:
|
||||
|
||||
```
|
||||
Navidrome sees: /music/Rock/Behemoth/Demigod/01 - Ora Pro Nobis.mp3
|
||||
└─────┘ NAVIDROME_LIBRARY_ROOT
|
||||
radio sees: /srv/betoniarka/music/Rock/Behemoth/Demigod/01 - Ora Pro Nobis.mp3
|
||||
└──────────────────┘ BETONIARKA_LIBRARY_ROOT
|
||||
```
|
||||
|
||||
The bridge strips the first root and re-roots the remainder at the second. Paths
|
||||
that Navidrome reports *relative* to its library are handled too, so both forms
|
||||
give the same result.
|
||||
|
||||
Find your two values with:
|
||||
|
||||
```bash
|
||||
docker exec navidrome printenv ND_MUSICFOLDER # Navidrome side
|
||||
docker exec conjurer-radio printenv BETONIARKA_MUSIC # radio side
|
||||
```
|
||||
|
||||
Then confirm they line up **before** going live:
|
||||
|
||||
```bash
|
||||
BRIDGE_DRY_RUN=true docker compose up # logs every translation, sends nothing
|
||||
```
|
||||
|
||||
## Two delivery modes
|
||||
|
||||
| | `api` (default) | `playlist` |
|
||||
|---|---|---|
|
||||
| How | `POST /request_radio_file` | appends to `request.playlist` |
|
||||
| Needs | network access to betoniarka | the radio's data dir mounted here |
|
||||
| Accuracy | fuzzy — the radio re-finds the track from keywords | exact path |
|
||||
| Runs anywhere | yes | only where the radio's volume is reachable |
|
||||
|
||||
`api` is the default because it keeps the bridge genuinely external — no shared
|
||||
filesystem, no assumptions about where the radio stores things.
|
||||
|
||||
**About the fuzziness:** betoniarka's request endpoint takes *keywords*, not a
|
||||
path, and re-searches its own library. The bridge feeds it every distinctive
|
||||
token of the translated path (`Rock`, `Behemoth`, `Demigod`, `01`, `Ora`,
|
||||
`Pro`, `Nobis`), which in testing resolved to the exact intended file even
|
||||
against deliberately confusing libraries — same artist, a live version of the
|
||||
same title, another album of the same name. It is still a search: if you need a
|
||||
guarantee, use `playlist` mode.
|
||||
|
||||
## Install
|
||||
|
||||
### Docker (recommended)
|
||||
|
||||
```bash
|
||||
cd navidrome_bridge
|
||||
cp env.example .env
|
||||
# edit .env: Navidrome URL + credentials, the two library roots, betoniarka URL
|
||||
docker compose up -d --build
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
Expected on a healthy start:
|
||||
|
||||
```
|
||||
Navidrome http://navidrome:4533 -> betoniarka http://radio-vm:5005 (mode=api)
|
||||
Path translation: /music -> /srv/betoniarka/music
|
||||
First run: ignoring 128 already-starred tracks
|
||||
```
|
||||
|
||||
For `playlist` mode, also set `BRIDGE_MODE=playlist` and uncomment the
|
||||
betoniarka volume in `compose.yaml`.
|
||||
|
||||
### Without Docker
|
||||
|
||||
```bash
|
||||
cd navidrome_bridge
|
||||
pip install -r requirements.txt
|
||||
set -a; . ./.env; set +a
|
||||
python3 bridge.py
|
||||
```
|
||||
|
||||
As a systemd unit:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Navidrome to betoniarka request bridge
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/opt/conjurer/navidrome_bridge/.env
|
||||
ExecStart=/usr/bin/python3 /opt/conjurer/navidrome_bridge/bridge.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Every setting, its default, and what it does is documented inline in
|
||||
[`env.example`](env.example). The ones that matter most:
|
||||
|
||||
| Variable | Default | Notes |
|
||||
|---|---|---|
|
||||
| `NAVIDROME_URL` | `http://localhost:4533` | |
|
||||
| `NAVIDROME_USER` / `NAVIDROME_PASSWORD` | — | required; sent as a salted token, never in clear |
|
||||
| `NAVIDROME_LIBRARY_ROOT` | `/music` | Navidrome's mount point |
|
||||
| `BETONIARKA_LIBRARY_ROOT` | `/srv/betoniarka/music` | the radio's mount point |
|
||||
| `BETONIARKA_URL` | `http://localhost:5005` | |
|
||||
| `CONJURER_API_KEY` | empty | must match the radio's, if it has one |
|
||||
| `BRIDGE_MODE` | `api` | `api` or `playlist` |
|
||||
| `BRIDGE_UNSTAR_AFTER_QUEUE` | `false` | makes the heart a repeatable "send" button |
|
||||
| `BRIDGE_DRY_RUN` | `false` | translate and log, send nothing |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause |
|
||||
|---|---|
|
||||
| `FATAL: missing required config` | `NAVIDROME_USER` / `NAVIDROME_PASSWORD` unset |
|
||||
| `Subsonic getStarred2 failed: 40 Wrong username or password` | bad credentials |
|
||||
| Starring does nothing, no log line | already-starred tracks are skipped on first run by design — un-star and re-star |
|
||||
| `Failed to queue …: 401` | `CONJURER_API_KEY` does not match the radio's |
|
||||
| Track queued but the radio plays something else | `api` mode picked a different match — switch to `playlist` |
|
||||
| `… not visible here` (playlist mode) | the library roots don't line up, or the volume isn't mounted |
|
||||
| Nothing plays although the playlist grew | check Liquidsoap is watching `request.playlist` |
|
||||
|
||||
Retries are automatic: a track that fails to queue is not marked as handled, so
|
||||
the next poll tries it again.
|
||||
|
||||
## Notes and limitations
|
||||
|
||||
- **Polling, not push.** Up to `BRIDGE_POLL_SECONDS` (default 15s) between the
|
||||
click and the queue. Navidrome exposes no star webhook.
|
||||
- **Stars are the trigger.** If you also use stars as favourites, either accept
|
||||
that favouriting queues a track, or turn on `BRIDGE_UNSTAR_AFTER_QUEUE` and
|
||||
treat the heart purely as a send button.
|
||||
- **One Navidrome user.** It watches the stars of the account it logs in as.
|
||||
- **No de-duplication against the radio's queue** — star the same track twice
|
||||
(with un-star on) and it is requested twice.
|
||||
- Credentials live in `.env`; keep it out of git (the repo's `.gitignore`
|
||||
already covers `.env`).
|
||||
Reference in New Issue
Block a user