Milestone 2: ingestion daemon driving the stream

Add the Python `ingest` container exposing `GET /next`, which returns the
next track as an annotated Liquidsoap URI (or an empty body when nothing is
ready). Liquidsoap switches from a static playlist to a `request.dynamic`
source pulling from the daemon, with the local cache as fallback and mksafe
for guaranteed continuous output.

For now the daemon just cycles through the files already in the cache; the
download providers (Navidrome, yt-dlp, ListenBrainz) come in later milestones.

Also commit the implementation plan (PLAN.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nemunaire 2026-07-02 16:52:49 +08:00
commit f8eb0655eb
9 changed files with 247 additions and 19 deletions

View file

@ -14,9 +14,11 @@ sharing a cache volume:
- **`ingest`** (Python) — the brain. It decides what to play next, resolves and
downloads tracks into a local cache, keeps a pre-filled queue, and exposes the
next track over HTTP. *(planned — see roadmap below)*
- **`stream`** (Liquidsoap) — deliberately dumb. It broadcasts the audio over
HTTP and never goes silent thanks to a local fallback.
next track over HTTP at `GET /next`. *(currently it only serves the cache
directory; the download providers come in later milestones — see roadmap)*
- **`stream`** (Liquidsoap) — deliberately dumb. It pulls the next track from
the `ingest` daemon, broadcasts the audio over HTTP, and never goes silent
thanks to a local cache fallback.
Playback sources (planned): a [Navidrome](https://www.navidrome.org/) library
via the OpenSubsonic API, arbitrary tracks fetched with
@ -46,21 +48,25 @@ reloaded when the directory changes).
## Current status
**Milestone 1 — broadcasting skeleton: done.**
**Milestone 2 — ingestion daemon: done.**
- Liquidsoap (v2.4.5) container plays the `cache/` directory in random order.
- `ingest` (Python) container exposes `GET /next`, returning the next track as
an annotated Liquidsoap URI (or an empty body when nothing is ready).
- `stream` (Liquidsoap v2.4.5) pulls from `ingest` via a `request.dynamic`
source, and falls back to the local `cache/` directory when the daemon has
nothing to offer.
- HTTP stream served at `http://localhost:8000/radio.mp3` (MP3, 192 kbps).
- Continuous output guaranteed: silence rather than a crash when the cache is
- Continuous output guaranteed: silence rather than a crash when everything is
empty (`mksafe`).
- Multiple simultaneous listeners supported.
At this stage the playlist is filled manually; the automatic ingestion layer is
not implemented yet.
At this stage the daemon just cycles through the files already in `cache/`; the
download providers (Navidrome, yt-dlp, ListenBrainz) come next.
## Roadmap
1. ✅ **Broadcasting skeleton** — Liquidsoap serving the cache directory.
2. **Ingestion daemon** — Python daemon exposing `GET /next`; Liquidsoap
2. **Ingestion daemon** — Python daemon exposing `GET /next`; Liquidsoap
switches to a `request.dynamic` source with the cache as fallback.
3. **Navidrome provider** — play from an OpenSubsonic playlist, with caching,
LRU retention and play history.