On the pinned savonet/liquidsoap:v2.4.5 image, `source.on_track` no longer
exists as a module function — on_track is a method on the source value. The
old `music = source.on_track(music, …)` form fails type-checking, so the whole
radio.liq is rejected and the stream container never starts. Register the
handler in place via the method form instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fallback played the whole /cache directory, which at cold start holds
only the 2-3 tracks being pre-fetched — so it looped them until the
request.dynamic buffer filled. Restrict the fallback to tracks already
aired: the ingest daemon exposes them at GET /fallback.m3u (played_at set,
still on disk), and the stream fetches that into a local /tmp/fallback.m3u
that playlist watches. Cold start is now silent (assumed) instead of a tight
loop, and a mid-stream drain degrades across the whole listening history.
A local file (not a remote playlist URL) is used to avoid Liquidsoap's http
resolver mis-sniffing the response as text/html; mime_type is forced so an
empty header-only m3u still parses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Serve a small web page at http://<host>:8000/ (stream/index.html) from the
Liquidsoap harbor, alongside the /radio.mp3 stream. It shows the track
currently on air and refreshes it from a /nowplaying JSON endpoint, fed by the
broadcast source's live metadata — accurate even though the ingest daemon runs
a track ahead (prefetch).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fallback playlist probed every file in /cache, including .gitkeep and
in-progress .part downloads, logging ffmpeg "Invalid data" warnings at startup.
Filter candidates with check_next to keep only real audio files and skip hidden
ones, removing the noise.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Insert a 3s crossfade after the fallback so it applies to both
daemon-driven transitions and cache fallbacks, before mksafe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Liquidsoap (v2.4.5) container that plays the /cache directory in random
order and broadcasts it over HTTP at :8000/radio.mp3 (MP3 192 kbps).
mksafe guarantees a continuous stream (silence when the cache is empty).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>