From 29ab0be7cbd99d888fd1bd1a570294633e599733 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 2 Jul 2026 16:10:34 +0800 Subject: [PATCH] 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 --- .gitignore | 13 +++++++++ README.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++ cache/.gitkeep | 0 docker-compose.yml | 9 ++++++ stream/Dockerfile | 5 ++++ stream/radio.liq | 29 ++++++++++++++++++ 6 files changed, 129 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cache/.gitkeep create mode 100644 docker-compose.yml create mode 100644 stream/Dockerfile create mode 100644 stream/radio.liq diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..71b799e --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Fichiers audio et état du cache (ne pas versionner) +/cache/* +!/cache/.gitkeep + +# État de l'ingestion (jalons suivants) +*.db +*.db-journal +*.db-wal + +# Python (jalons suivants) +__pycache__/ +*.pyc +.venv/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f3033d --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# radieo + +A personal music radio: an always-on HTTP audio stream, automatically fed from +several sources and broadcast with [Liquidsoap](https://www.liquidsoap.info/). + +The goal is a hassle-free stream that always has something playing, where the +next track is picked automatically. It is meant for personal use (a couple of +simultaneous listeners), not for public broadcasting. + +## How it works + +radieo is built as two layers, each running in its own Docker container and +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. + +Playback sources (planned): a [Navidrome](https://www.navidrome.org/) library +via the OpenSubsonic API, arbitrary tracks fetched with +[yt-dlp](https://github.com/yt-dlp/yt-dlp) (Bandcamp, SoundCloud, YouTube…), and +listening suggestions from a ListenBrainz RSS feed. + +## Usage + +Requirements: Docker with Compose v2. + +```sh +# Drop some .mp3 files into the cache directory +cp /path/to/music/*.mp3 cache/ + +# Build and start the stream +docker compose up -d + +# Listen (VLC, a browser, any audio player) +# http://localhost:8000/radio.mp3 +``` + +Stop it with `docker compose down`. + +The stream is MP3 at 192 kbps. Multiple clients can listen at the same time. +New files dropped into `cache/` are picked up automatically (the playlist is +reloaded when the directory changes). + +## Current status + +**Milestone 1 — broadcasting skeleton: done.** + +- Liquidsoap (v2.4.5) container plays the `cache/` directory in random order. +- HTTP stream served at `http://localhost:8000/radio.mp3` (MP3, 192 kbps). +- Continuous output guaranteed: silence rather than a crash when the cache is + empty (`mksafe`). +- Multiple simultaneous listeners supported. + +At this stage the playlist is filled manually; the automatic ingestion layer is +not implemented yet. + +## Roadmap + +1. ✅ **Broadcasting skeleton** — Liquidsoap serving the cache directory. +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. +4. **yt-dlp provider** — fetch tracks from a maintained URL/artist list; weighted + mixing between sources. +5. **Canonicalizer** — ListenBrainz MBID lookup for source-agnostic + de-duplication. +6. **ListenBrainz provider** — parse the RSS suggestions feed and resolve each + one to Navidrome or yt-dlp. +7. **Polish** — crossfade, robustness, optional web player, config file. diff --git a/cache/.gitkeep b/cache/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..70ff6c2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +services: + stream: + build: ./stream + image: radieo-stream + ports: + - "8000:8000" # flux HTTP : http://localhost:8000/radio.mp3 + volumes: + - ./cache:/cache:ro # jalon 1 : lecture seule, rempli à la main + restart: unless-stopped diff --git a/stream/Dockerfile b/stream/Dockerfile new file mode 100644 index 0000000..5315fc4 --- /dev/null +++ b/stream/Dockerfile @@ -0,0 +1,5 @@ +FROM savonet/liquidsoap:v2.4.5 + +COPY radio.liq /etc/liquidsoap/radio.liq + +CMD ["/etc/liquidsoap/radio.liq"] diff --git a/stream/radio.liq b/stream/radio.liq new file mode 100644 index 0000000..8388d60 --- /dev/null +++ b/stream/radio.liq @@ -0,0 +1,29 @@ +#!/usr/bin/liquidsoap + +# radieo — couche diffusion (jalon 1) +# Joue le dossier /cache en boucle aléatoire et le diffuse en HTTP. +# Les jalons suivants remplaceront la source par un request.dynamic piloté +# par le daemon d'ingestion, en gardant ce dossier comme secours. + +# --- Journalisation : tout sur la sortie standard (pratique en conteneur) --- +settings.log.stdout := true +settings.log.file := false +settings.log.level := 3 + +# --- Harbor : écoute sur toutes les interfaces du conteneur --- +settings.harbor.bind_addrs := ["0.0.0.0"] + +# --- Source : le dossier de cache, rechargé quand son contenu change --- +radio = playlist(mode="randomize", reload_mode="watch", "/cache") + +# mksafe garantit un flux continu : si la source échoue ou est vide, +# Liquidsoap émet du silence plutôt que de planter. +radio = mksafe(radio) + +# --- Sortie : flux MP3 sur http://:8000/radio.mp3 --- +output.harbor( + %mp3(bitrate=192), + port=8000, + mount="radio.mp3", + radio +)