Back to Projects

Automated Internet Radio Station

Liquidsoap Icecast Piper TTS PostgreSQL Python ffmpeg Flask Ansible Jenkins
project-internet-radio-2026-09

Tags and play history in PostgreSQL drive the show playlists, while Liquidsoap mixes music with text-to-speech DJ segments and Icecast serves the stream on the LAN.

The Challenge

A large personal music library sat on network storage with no good way to experience it as a continuous, programmed station. Plain shuffle ignored genre and time of day and repeated artists back to back.

Earlier iterations also kept play history and tag data in local files on a single host, so sharing data meant copying files between machines, and a nightly metadata scan took hours. The goal was a hands-off station that behaves like real radio, with scheduled shows, announcements between songs and a browser player, and that could be rebuilt entirely from code.

The Solution

The station pairs Liquidsoap and Icecast with a scheduling and playlist layer backed by PostgreSQL. A metadata scanner indexes the library's tags into the database, and a smart playlist builder uses those tags together with recent play history to generate each show's playlist.

A scheduler rotates genre shows through the day, each with its own synthetic DJ persona that introduces the show and recaps recent tracks using locally run Piper text-to-speech. The whole stack is defined in Ansible and deployed through Jenkins.

Implementation Details

The station is built from small, scheduled components around a single Liquidsoap pipeline.

  • Incremental indexing: a Python scanner upserts tags into PostgreSQL and removes rows for deleted files. A full scan takes under two minutes and an unchanged library takes a few seconds, replacing an hours-long nightly scan.
  • Smart playlists: every four hours a builder selects tracks by genre for each show, applies a per-artist cap and title de-duplication, weights against the last seven days of plays, and sizes playlists to stay within Liquidsoap's stable limits.
  • Show scheduling: a five-minute scheduler maps the time of day to one of 13 genre shows, swaps the active playlist, reloads Liquidsoap over its control interface and generates a spoken show introduction.
  • Synthetic DJs: a Liquidsoap end-of-track hook triggers a recap after every fifth song. A script renders templated announcement text with one of 13 per-show Piper voices, encodes it with ffmpeg and queues it, and a track-sensitive fallback plays it between songs.
  • Play logging: every play is recorded in PostgreSQL, replacing a host-local database in a cutover that preserved existing history. The playlist builder and a Flask statistics dashboard read the same data.
  • Browser player: shows now playing and recently played tracks, pulls artist summaries with client-side caching, supports lock-screen metadata via the MediaSession API, and installs as a progressive web app.
  • Infrastructure as code: two Ansible playbooks, run from Jenkins, deploy Icecast, Liquidsoap, the DJ tooling, the scanner, the scheduler and their timers, with database credentials fetched from a secrets manager at deploy time.