Commit graph

22 commits

Author SHA1 Message Date
dcdfda2fdb stream: listen on IPv6 as well as IPv4
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:42:51 +08:00
534ade0ba5 stream: add a synthwave favicon
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:42:51 +08:00
a65cc61ccd stream: play a scheduled jingle right after key times of day
Adds special jingle folders (midi, gouter, bisous) that take priority
over the default jingle rotation once per day, briefly after 11h00,
15h00, 16h30 and 21h00.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:42:51 +08:00
1648030eba stream: surface ingest prefetch progress in the player
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:22:53 +08:00
c12e522fee stream: link the now-playing title to its source page
For yt-dlp tracks the locator is the original web page, so pass it as a
url annotation, carry it through the stream metadata and history, and
turn the track title into a link back to that page (both live and in the
history). Subsonic ids are opaque and stay plain text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:14:50 +08:00
fa1be6df77 stream: make the station name a single configurable variable
Default it to "Nemu FM" and derive the tab title, logo, and dynamic
document title from one STATION_NAME constant for easy renaming.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:12:48 +08:00
6b52795ae1 stream: auto-reconnect to the live stream with exponential backoff
If the stream drops (server restart, network loss), the player now retries
goLive() on error/ended/stalled with a delay that doubles each failure, capped
at 30s, and resets once audio flows again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:09:39 +08:00
155a13d50e stream: remember the listener's volume via localStorage
Defaults to 40% on first visit, then restores whatever the user last set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:00:55 +08:00
aad3c9d0f7 stream: let listeners download any track still in the cache
All checks were successful
continuous-integration/drone/push Build is passing
/download now accepts ?file=<name> to fetch any cached file, not just the
current track. History entries carry that filename token (via /history), so
the web UI renders each aired track as a download link. A shared
serve_attachment helper validates the request (basename-only, real audio file,
no hidden/.part files) before streaming it; LRU-evicted tracks return 404.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:15:25 +08:00
453ecd1353 stream: fix jingle counter for liquidsoap 2.4.5 (on_track is a method)
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>
2026-07-03 10:15:25 +08:00
3469b2680c stream: interleave a random jingle every two songs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:15:25 +08:00
1f6937f22c stream: let the listener download the current track (/download)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:15:25 +08:00
7fc372f18d stream: show a history of aired tracks (/history)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:15:25 +08:00
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