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>
7.9 KiB
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:
- 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.
- 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, plugin capability list.
How it works
- You star a track in Navidrome (web UI, phone, anything Subsonic).
- The bridge polls
getStarred2and notices the new star. - It translates the file's path from Navidrome's mount point to the radio's.
- It hands that track to betoniarka, which appends it to the request playlist; Liquidsoap is already watching that file and picks it up.
- 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:
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:
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)
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
cd navidrome_bridge
pip install -r requirements.txt
set -a; . ./.env; set +a
python3 bridge.py
As a systemd unit:
[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. 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_QUEUEand 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.gitignorealready covers.env).