diff --git a/README.md b/README.md index aaadaf4..b9cc519 100644 --- a/README.md +++ b/README.md @@ -7,145 +7,140 @@ 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 +## Features -radieo is built as two layers, each running in its own Docker container and -sharing a cache volume: +- **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), track history, skip button, per-track download, volume + memory, live auto-reconnect, prefetch progress, and a synthwave look. +- **Robust in a container**: Docker healthcheck, graceful shutdown, retries on + transient HTTP errors. -- **`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 at `GET /next`. *(currently it only serves the cache - directory; the download providers come in later milestones — see roadmap)* -- **`stream`** (Liquidsoap) — deliberately dumb. It pulls the next track from - the `ingest` daemon, broadcasts the audio over HTTP, and falls back to the - already-aired tracks (via `/fallback.m3u`) if the daemon has nothing ready. - -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 +## Getting started Requirements: Docker with Compose v2. -```sh -# Drop some .mp3 files into the cache directory -cp /path/to/music/*.mp3 cache/ +### 1. Jingles (optional) -# Build and start the stream -docker compose up -d +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): -# Listen (VLC, a browser, any audio player) -# http://localhost:8000/radio.mp3 -``` +| Folder | Plays around | +| ----------------- | ------------ | +| `jingles/midi/` | 11:00 | +| `jingles/moment/` | 15:00, 21:00 | +| `jingles/gouter/` | 16:30 | -Stop it with `docker compose down`. +An empty folder simply means no jingle, the music plays through. -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). - -## Configuration - -Copy `.env.example` to `.env` and fill in your Navidrome details: +### 2. Configure the sources (`.env`) ```sh cp .env.example .env -# edit .env: RADIEO_NAVIDROME_URL / USER / PASSWORD / PLAYLIST ``` -If the Navidrome variables are left empty, the source is simply disabled and -the stream plays whatever is already in `cache/` (the milestone-1/2 behaviour). +Fill in `.env`: -For the yt-dlp source, list the URLs to draw from in `config/urls.txt` (copy -`config/urls.txt.example`). Each line is either a direct track URL or a -container URL (playlist, album, label, artist page) from which one track is -picked at random. +- **OpenSubsonic server**: `RADIEO_SUBSONIC_URL` / `USER` / `PASSWORD` and the + playlist to broadcast in `RADIEO_SUBSONIC_PLAYLIST` (name or id). Works with + any OpenSubsonic-compatible server (Navidrome, Gonic, Airsonic…). Leave empty + to disable this source. +- **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`. -For the ListenBrainz source, set `RADIEO_LISTENBRAINZ_URL` to your -recommendations feed (the Atom syndication URL, e.g. -`https://listenbrainz.org/syndication-feed/user//recommendations/weekly-exploration`, -or a local file path under `config/` for testing). ListenBrainz only *names* -tracks, so each suggestion is resolved to a real file: Navidrome first -(a `search3` lookup), then yt-dlp (`ytsearch1:`) as a fallback. The -MusicBrainz recording MBID that the feed already carries is used as the -track's canonical identity (no extra lookup needed). +### 3. yt-dlp URL list (`config/urls.txt`) -The relative mix between sources is set by `RADIEO_WEIGHT_NAVIDROME` / -`RADIEO_WEIGHT_YTDLP` / `RADIEO_WEIGHT_LISTENBRAINZ` (a weight of 0 disables a -source); an empty URL / missing file also disables the corresponding source. +```sh +cp config/urls.txt.example config/urls.txt +``` -Open the player at `http://localhost:8000/` — a small web page with an -`