Commit graph

9 commits

Author SHA1 Message Date
a468d78153 stream: add a skip-to-next button and /skip route
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:15:25 +08:00
04ea54c03e stream: show a copyable stream URL for external players
Add a read-only field with the bare /radio.mp3 URL and a copy button
(clipboard API, with a select+execCommand fallback), so the stream can
easily be opened in VLC or another external player.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:15:25 +08:00
c6d642a945 stream: reconnect the player to live instead of stale buffer
Firefox kept resuming the <audio> element from a stale buffer after a
pause, drifting behind the live point. Load the stream with an anti-cache
query parameter, and on resume-from-pause reconnect to live rather than
replaying the buffered audio.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:15:25 +08:00
80f27d2795 stream: fallback only replays already-aired tracks
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>
2026-07-03 10:15:25 +08:00
f4eaf8e7d1 stream: web player with now-playing
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>
2026-07-02 22:46:37 +08:00
3bd7edbb16 stream: quieter fallback logs — skip non-audio files
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>
2026-07-02 22:46:08 +08:00
8774f5c2a1 Add smooth crossfade transition between tracks
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>
2026-07-02 18:28:57 +08:00
f8eb0655eb 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>
2026-07-02 17:57:38 +08:00
29ab0be7cb Milestone 1: Liquidsoap broadcasting skeleton
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>
2026-07-02 16:14:14 +08:00