navidrome_bridge: add dropbox mode writing straight to the request spool
Reading radio_conjurer.liq shows request.playlist is not a Liquidsoap playlist at all - it is a drop box. queue_processing() runs every 60s, reads every line, pushes each into request.queue() as a URI, then deletes the file and recreates it empty. That changes what writing to it means, so the mode is now named and documented for what it is. BRIDGE_MODE=dropbox (with "playlist" kept as an alias) appends the exact translated path to that file. Because the lines are pushed as URIs it is an exact hand-off - no keyword search - and it involves neither betoniarka nor the bot, just the file and Liquidsoap. The liq also puts requests_queue first in the fallback and does not apply the check_next replay guard to it, so a request interrupts the rotation and plays even if the track ran recently; both are now documented rather than left to be discovered. Fixes found while wiring this up: * the existence check was unconditional while the library mount was documented as optional, so a drop-box-only setup could never queue anything. It is now BRIDGE_VERIFY_FILE_EXISTS (auto|true|false), defaulting to checking only when the library is actually visible; * a missing parent dir was silently created, which would swallow requests into the container's own filesystem when the radio's data dir was not mounted. It is now a hard error naming the likely cause. Verified: alias resolves; append/drain/append cycle against a simulation of the liq's read-remove-recreate; both misconfigurations raise instead of silently succeeding; drop-box-only path works with the library absent; api mode unchanged, still prepending the sentinel that wyszukaj() discards. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+72
-19
@@ -14,14 +14,17 @@ request queue. One click, from whatever you already listen with.
|
||||
> * 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).
|
||||
> * it reads **Navidrome's Subsonic API**, and hands the track to the radio by
|
||||
> one of two routes — through **betoniarka**, or straight into the file
|
||||
> Liquidsoap drains.
|
||||
>
|
||||
> ```
|
||||
> Navidrome ──(Subsonic API: read stars, optional unstar)──► bridge
|
||||
> bridge ──(/request_radio_file, or the request playlist)──► betoniarka ──► Liquidsoap
|
||||
> Navidrome ──(Subsonic API: read stars, optional unstar)──► bridge
|
||||
>
|
||||
> Kondziu (Discord bot) ....................... not involved
|
||||
> bridge ──(api mode: /request_radio_file)──► betoniarka ──┐
|
||||
> bridge ──(dropbox mode: request.playlist)────────────────┴─► Liquidsoap
|
||||
>
|
||||
> Kondziu (Discord bot) ....................... not involved in either
|
||||
> ```
|
||||
|
||||
---
|
||||
@@ -58,8 +61,10 @@ Sources: [Navidrome plugin docs](https://www.navidrome.org/docs/usage/features/p
|
||||
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.
|
||||
4. It hands that track over — either to betoniarka (`api` mode) or by writing
|
||||
the path straight into the request spool (`dropbox` mode). Either way the
|
||||
path ends up as a line in `request.playlist`, which Liquidsoap drains into
|
||||
its request queue once a minute.
|
||||
5. Optionally it un-stars the track, so the heart behaves like a "send" button
|
||||
you can press again next time.
|
||||
|
||||
@@ -94,12 +99,13 @@ BRIDGE_DRY_RUN=true docker compose up # logs every translation, sends nothi
|
||||
|
||||
## Two delivery modes
|
||||
|
||||
| | `api` (default) | `playlist` |
|
||||
| | `api` (default) | `dropbox` |
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
| How | `POST /request_radio_file` | writes the exact path into the file Liquidsoap drains |
|
||||
| Goes through | betoniarka | nothing — straight to Liquidsoap |
|
||||
| Needs | network access to betoniarka | that one file reachable here |
|
||||
| Accuracy | fuzzy — betoniarka re-finds the track from keywords | exact hand-off |
|
||||
| Auth | `CONJURER_API_KEY` must match | none needed |
|
||||
|
||||
`api` is the default because it keeps the bridge genuinely external — no shared
|
||||
filesystem, no assumptions about where the radio stores things.
|
||||
@@ -109,8 +115,51 @@ 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.
|
||||
same title, another album of the same name. It is still a search.
|
||||
|
||||
### `dropbox` mode — writing straight to the request spool
|
||||
|
||||
`request.playlist` is **not a Liquidsoap playlist**. In `radio_conjurer.liq` it
|
||||
is a drop box that gets drained every 60 seconds:
|
||||
|
||||
```liquidsoap
|
||||
def queue_processing()
|
||||
text = file.lines("/srv/betoniarka/data/request.playlist")
|
||||
if text != [] then
|
||||
list.iter(fun(item) -> requests_queue.push.uri(item), text)
|
||||
file.remove("/srv/betoniarka/data/request.playlist") # then recreated empty
|
||||
end
|
||||
end
|
||||
thread.run(every=60., queue_processing)
|
||||
```
|
||||
|
||||
Set `BRIDGE_MODE=dropbox` and point `BETONIARKA_REQUEST_PLAYLIST` at that file;
|
||||
the bridge appends one absolute path per line. What that buys you:
|
||||
|
||||
- **Exact hand-off.** Lines are pushed as URIs, so the radio plays *that* file —
|
||||
no search, no chance of a different take of the same song.
|
||||
- **Nothing in the middle.** No betoniarka, no bot, no API key — just a file and
|
||||
Liquidsoap. This is the most direct route there is.
|
||||
- **Priority.** `requests_queue` is first in the `fallback`, so a request cuts
|
||||
into the rotation at the next track boundary.
|
||||
- **No replay guard.** The 10-hour "don't repeat" check (`check_next=check`)
|
||||
is attached to the regular playlists, *not* to the request queue — so a
|
||||
request plays even if that track ran recently.
|
||||
|
||||
Costs and caveats:
|
||||
|
||||
- Up to 60 s before Liquidsoap notices, on top of the bridge's own poll.
|
||||
- The file is emptied under you by design; never treat it as durable state. The
|
||||
bridge appends in a single `O_APPEND` write, so a drain can never catch half a
|
||||
path — but a line appended in the narrow window between the liq's `file.lines`
|
||||
and `file.remove` is dropped. It is rare, and re-starring re-sends.
|
||||
- The name is singular: `request.playlist`.
|
||||
|
||||
Enable it by also mounting the radio's data dir — see the commented volume in
|
||||
`compose.yaml`. Mounting the music library too is optional: with
|
||||
`BRIDGE_VERIFY_FILE_EXISTS=auto` the bridge verifies paths only when the library
|
||||
is actually visible, so a drop-box-only container works without it. A missing
|
||||
data dir is always a hard error rather than a silently-swallowed request.
|
||||
|
||||
## Install
|
||||
|
||||
@@ -132,8 +181,8 @@ 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`.
|
||||
For `dropbox` mode, also set `BRIDGE_MODE=dropbox` and uncomment the
|
||||
betoniarka data volume in `compose.yaml`.
|
||||
|
||||
### Without Docker
|
||||
|
||||
@@ -174,7 +223,9 @@ Every setting, its default, and what it does is documented inline in
|
||||
| `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_MODE` | `api` | `api` or `dropbox` |
|
||||
| `BETONIARKA_REQUEST_PLAYLIST` | `/srv/betoniarka/data/request.playlist` | dropbox mode: the file the liq drains |
|
||||
| `BRIDGE_VERIFY_FILE_EXISTS` | `auto` | check the path exists before queueing |
|
||||
| `BRIDGE_UNSTAR_AFTER_QUEUE` | `false` | makes the heart a repeatable "send" button |
|
||||
| `BRIDGE_DRY_RUN` | `false` | translate and log, send nothing |
|
||||
|
||||
@@ -186,8 +237,10 @@ Every setting, its default, and what it does is documented inline in
|
||||
| `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 |
|
||||
| Track queued but the radio plays something else | `api` mode picked a different match — switch to `dropbox` |
|
||||
| `… not visible here` | the library roots don't line up, or the library isn't mounted |
|
||||
| `… does not exist here - is the radio's data dir mounted?` | dropbox mode without the data dir volume |
|
||||
| dropbox: queued, file stays empty, nothing plays | Liquidsoap drained it — that is the file working as intended; check the radio log |
|
||||
| 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
|
||||
|
||||
Reference in New Issue
Block a user