# 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. ## Features - **Always-on stream**: MP3 at 192 kbps over HTTP, several simultaneous listeners. - **Automatic programming from mixable sources**, drawn at weighted random: - a playlist from any [OpenSubsonic](https://opensubsonic.netlify.app/)-compatible server ([Navidrome](https://www.navidrome.org/), Gonic, Airsonic…); - a hand-maintained list of [yt-dlp](https://github.com/yt-dlp/yt-dlp) URLs (Bandcamp, SoundCloud, YouTube…); playlist/album/label/artist URLs are expanded and one track is picked at random each round; - a [ListenBrainz](https://listenbrainz.org/) recommendations feed, whose suggestions are resolved to a real file (Subsonic first, then yt-dlp). - **Cross-source de-duplication**: each track is canonicalized to a MusicBrainz recording MBID (no API key), so the same song from two sources collapses to one; a recent-plays window prevents repeats. - **Push-with-cache**: tracks are downloaded *ahead* of playback into a local cache with LRU retention; if the pipeline ever runs dry the stream falls back to the tracks already aired, and stays silent at a cold start rather than looping. - **Jingles**: station jingles inserted every two songs, plus time-of-day jingles (noon, snack time, ...) played once when their time comes. - **Smooth playback**: a 3 s crossfade between tracks. - **Built-in web player** at `http://localhost:8000/`: now playing (linked to its source page — a Bandcamp/YouTube page for yt-dlp tracks, or an on-demand Subsonic share for library tracks), track history and the upcoming queue, skip/restart controls, per-track download, volume memory, live auto-reconnect, prefetch progress, and a synthwave look. - **OS media controls**: the player exposes current track metadata and play/pause/next through the Media Session API, so it wires into system media controls (MPRIS on Linux, macOS Control Center, Windows, mobile lock screen) and keyboard media keys. - **Robust in a container**: Docker healthcheck, graceful shutdown, retries on transient HTTP errors. ## Getting started Requirements: Docker with Compose v2. ### 1. Jingles (optional) Drop `.mp3` files into `jingles/`: they rotate in every two songs. For time-of-day jingles, add files to these subfolders (each played once per day just after its slot): | Folder | Plays around | | ----------------- | ------------ | | `jingles/midi/` | 11:00 | | `jingles/moment/` | 15:00, 21:00 | | `jingles/gouter/` | 16:30 | An empty folder simply means no jingle, the music plays through. ### 2. Configure the sources (`.env`) ```sh cp .env.example .env ``` Fill in `.env`: - **OpenSubsonic server**: `RADIEO_SUBSONIC_URL` / `USER` / `PASSWORD` and the playlist(s) to broadcast in `RADIEO_SUBSONIC_PLAYLIST` — a comma-separated list of names or ids, each optionally weighted with a `=` suffix (e.g. `Chill=3, Focus, Party=2`; default weight 1, `0` disables). Works with any OpenSubsonic-compatible server (Navidrome, Gonic, Airsonic…). Leave empty to disable this source. To make the player's "source" link work for library tracks, enable sharing on the server (Navidrome: `ND_ENABLESHARING=true`); the link then mints a public share on demand, only when clicked. - **Mix**: `RADIEO_WEIGHT_SUBSONIC` / `RADIEO_WEIGHT_YTDLP` / `RADIEO_WEIGHT_LISTENBRAINZ` set the relative draw weight of each source (`0` disables one). - Optional: `RADIEO_RETENTION_KEEP` (cached tracks kept on disk), `RADIEO_CANONICAL_ENABLED`, `RADIEO_USER_AGENT`. ### 3. yt-dlp URL list (`config/urls.txt`) ```sh cp config/urls.txt.example config/urls.txt ``` Add one URL per line: a single track, or a playlist/album/label/artist page to pick from. The file is mounted read-only, so you can edit it without rebuilding. A missing file just disables the yt-dlp source. To favour fresh music, set `RADIEO_YTDLP_RECENT_BOOST` (in `.env`) above `1.0`: when picking from a Bandcamp label/artist/discography page — which lists releases newest-first — the newest `RADIEO_YTDLP_RECENT_COUNT` releases (default `5`) get that multiplier on their odds (e.g. `2.0` makes them twice as likely). The default `1.0` disables it. The boost only applies to such discography listings; single `/album/` and `/track/` pages and non-Bandcamp sources keep a uniform pick. ### 4. ListenBrainz suggestions Point `RADIEO_LISTENBRAINZ_URL` (in `.env`) at your recommendations syndication feed, e.g.: ``` RADIEO_LISTENBRAINZ_URL=https://listenbrainz.org/syndication-feed/user//recommendations/weekly-exploration ``` ListenBrainz only *names* tracks; each suggestion is resolved to a concrete file (an OpenSubsonic `search3`, then a yt-dlp `ytsearch1:` fallback) and keyed by the MusicBrainz MBID the feed already carries. Leave the variable empty to disable. A local file path under `config/` also works for testing. ### 5. Run it ```sh docker compose up -d ``` Open the player at **`http://localhost:8000/`**; the raw stream is at `http://localhost:8000/radio.mp3` (open it in VLC or any audio player). Stop with `docker compose down`. The station name shown in the player is the `STATION_NAME` constant near the top of `stream/index.html`. ## Architecture radieo is two Docker containers sharing a cache volume. `ingest` is the brain; `stream` is a deliberately dumb broadcaster. ``` Providers Scheduler Fetchers Broadcast ───────── ───────── ──────── ───────── Subsonic ──┐ ┌ Subsonic ┐ yt-dlp ────┼─▶ weighted pick ─▶ Canonicalizer┤ ├▶ cache ─▶ queue ─▶ Liquidsoap ─▶ HTTP ListenBrz ─┘ + anti-repeat (MBID) └ yt-dlp ──┘ (LRU) /next (request.dynamic (SQLite) + jingles + fallback) ``` **`ingest`** (Python) chooses what to play, resolves and downloads it ahead of time, and serves it over an internal HTTP API: - *Providers* produce a resolved reference (which backend + a locator); *fetchers* turn that into a local file (`Subsonic` stream / `yt-dlp` download). - The *scheduler* draws a source by weight, applies anti-repeat, and runs the *canonicalizer* (MusicBrainz MBID, cached, rate-limited, best-effort) for a source-agnostic identity. - A *prefetch queue* keeps a few ready tracks; a SQLite database under `state/` holds play history, the MBID cache, and LRU cache-file retention. - API: `GET /next` (annotated Liquidsoap URI), `/fallback.m3u` (already-aired tracks), `/status` (prefetch progress), `/queue` (upcoming tracks), `/healthz`, and `POST /share?id=` (mint a Subsonic share on demand). **`stream`** (Liquidsoap) pulls `/next` via `request.dynamic`, inserts jingles (a `switch` that also handles the time-of-day slots), applies the crossfade and `mksafe`, and outputs the MP3. On the same harbor port 8000 it also serves the web player and its API: `/nowplaying`, `/history`, `/skip`, `/restart-track`, `/download`, and — proxied from `ingest` — `/queue`, `/ingest/status`, and `/share` (which forwards to `ingest`, then redirects to the created share). Only port **8000** is published to the host. The browser never talks to `ingest` directly — the Liquidsoap harbor acts as a small reverse proxy for the data the player needs (e.g. `/ingest/status`, `/queue`, `/share`), keeping everything on a single origin.