radieo/README.md
Pierre-Olivier Mercier 8c27498632 Milestone 3: Navidrome (OpenSubsonic) playback provider
Replace the directory-scan queue with a real ingestion pipeline:
provider -> fetcher -> cache -> ready queue, driven by a background
prefetch thread.

- subsonic.py: minimal OpenSubsonic client (salted-token auth,
  getPlaylists/getPlaylist, raw streaming download).
- providers/navidrome.py: pick tracks from a playlist (by name or id),
  with anti-repeat and periodic playlist reload.
- fetchers/subsonic.py: atomic download into the shared cache.
- db.py: SQLite state — append-only play history (anti-repeat + stats)
  and cache_files LRU retention (keep the N most recently played).
- queue.py: prefetch buffer + retention on play; graceful degradation
  to the stream's local-cache fallback when no source is configured.
- api.py: GET /next now carries real title/artist metadata.
- Config via .env (Navidrome credentials), persistent state/ volume,
  httpx dependency.

Verified end-to-end against a live Navidrome: playlist resolved,
tracks downloaded and broadcast, retention and history correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 17:57:38 +08:00

3.7 KiB

radieo

A personal music radio: an always-on HTTP audio stream, automatically fed from several sources and broadcast with Liquidsoap.

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 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 never goes silent thanks to a local cache fallback.

Playback sources (planned): a Navidrome library via the OpenSubsonic API, arbitrary tracks fetched with yt-dlp (Bandcamp, SoundCloud, YouTube…), and listening suggestions from a ListenBrainz RSS feed.

Usage

Requirements: Docker with Compose v2.

# 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).

Configuration

Copy .env.example to .env and fill in your Navidrome details:

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).

Current status

Milestone 3 — Navidrome provider: done.

  • ingest pulls tracks from an OpenSubsonic playlist (Navidrome), downloading them into the shared cache ahead of playback (prefetch buffer).
  • Play history and LRU retention are tracked in a SQLite database under state/: only the N most recently played files are kept on disk (RADIEO_RETENTION_KEEP, default 20); anti-repeat avoids replaying a track seen among the last plays.
  • GET /next returns the next track as an annotated Liquidsoap URI with real title/artist metadata (or an empty body when nothing is ready).
  • stream (Liquidsoap v2.4.5) pulls via request.dynamic and falls back to the local cache/ directory; mksafe guarantees silence rather than a crash.
  • HTTP stream served at http://localhost:8000/radio.mp3 (MP3, 192 kbps), multiple simultaneous listeners supported.

The yt-dlp and ListenBrainz sources come next.

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.