# 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`).