Files

4.9 KiB
Raw Permalink Blame History

Radio Susan 📻

Personal internet radio station. 24/7 automated playback from a music library with smart DJ logic, energy-based scheduling, TTS announcements, and jingles.

Stream: https://radio.jihakuz.xyz/stream (MP3 192kbps)

Stack

Component Role
Icecast2 Streaming server
Liquidsoap Radio automation — reads playlist, inserts jingles, outputs to Icecast
generate_daily_playlist.py Builds a full 24h playlist with baked-in TTS announcements
generate_site.py Generates a static HTML "now playing" site from the playlist
radio_dj.py Fallback per-track DJ when no daily playlist exists
dj_core.py Shared logic: track loading, energy filtering, cooldowns, modes
analyse_tracks.py Essentia audio feature extraction (energy, BPM)
build_genre_cache.py Genre cache from beets DB
generate_jingles.py Generates jingle MP3s from TTS + SFX
edge-tts Microsoft Edge TTS engine for announcements

How It Works

Daily Pipeline

Every midnight, generate_daily_playlist.py builds a complete 24-hour playlist:

  1. Loads all tracks from beets DB + genre cache + Essentia audio features
  2. For each hour, looks up target energy from an energy curve (ambient at night, high energy in the evening)
  3. Picks a mode (weighted random): shuffle 40%, deep cuts 20%, new to library 15%, old favourites 15%, full album 10%
  4. Selects a track using contrast maximisation — prefers energy distance from recent picks
  5. Every 3 tracks, generates a TTS announcement via edge-tts
  6. Outputs playlist.json + playlist.m3u for Liquidsoap

Liquidsoap picks up the new playlist automatically via file watching.

Fallback Chain

If the daily playlist is empty or missing:

  1. radio_dj.py — same smart DJ logic, picks one track at a time
  2. all.m3u — pure random shuffle of the entire library (last resort)

Energy Schedule

The DJ schedules by energy level, not genre. Whatever genre fits the energy gets played.

Show Hours Energy Vibe
Night Drift 0007 1 Ambient, drone
Morning Calm 0709 23 Downtempo, chill
Late Morning 0912 45 Indie, jazz, trip-hop
Lunch Break 1214 4 Soul, dub
Afternoon Session 1417 56 Electronic, funk
Drive Time 1719 56 Mixed
Evening Heat 1922 78 Techno, house, DnB
Wind Down 2200 24 Comedown, ambient

Smart Modes

Mode Weight Strategy
shuffle 40% Random from energy-matched pool
deep_cuts 20% Tracks with 0 plays
new_to_library 15% Added in last 14 days
old_favourites 15% Highest play count
full_album 10% Plays entire album in order

Cooldowns

  • Track: 7 days
  • Artist: 3 hours
  • Album: 24 hours
  • Genre: no back-to-back same genre

Announcements vs Jingles

  • Announcements are baked into the playlist at generation time — pre-rendered TTS clips inserted into the M3U every 3 tracks.
  • Jingles are dynamic in Liquidsoap — randomly rotated from /jingles/, roughly 1 per 4 tracks.

Audio Analysis

analyse_tracks.py uses Essentia to extract per-track features:

  • Energy — normalised via percentile breakpoints to a 110 scale
  • BPM — via RhythmExtractor2013

Results in audio_features.db (SQLite). Incremental — only analyses new files.

Known issue: BPM detection is unreliable for fast genres (hardcore, breakcore, DnB) — often reads half or a third of the real tempo.

Static Website

generate_site.py builds a single-page dark-themed site:

  • Now Playing banner (polls track state)
  • Up Next preview
  • Stats bar
  • Collapsible show cards with full tracklists
  • Inline listen button

Setup

# Copy secrets template
cp secrets.liq.example secrets.liq
# Edit with your Icecast password
nano secrets.liq

# Install Python deps
python3 -m venv venv
source venv/bin/activate
pip install edge-tts essentia

# Crons (add to crontab)
0  0 * * *  sg mediaserver "python3 generate_daily_playlist.py"
5  0 * * *  sg mediaserver "python3 generate_site.py"
0  7 * * *  python3 build_genre_cache.py
30 7 * * *  venv/bin/python3 analyse_tracks.py

Data Sources

Source Used for
Beets DB Track metadata, genres, added date
Navidrome DB Play counts, starred tracks
audio_features.db Essentia energy + BPM
genre_cache.json Genre-to-energy mapping

SFX Samples

The sfx/ directory contains sound effects used by generate_jingles.py:

airhorn · cashregister · countdown · crash · demon · meow · pirate_arr · pirate_plank · pirate_yaargh · reverb_hit · scream · static · sweep · synth_horn · text_tone

Resource Usage

Minimal. Icecast ~10MB RAM, Liquidsoap ~5070MB RAM. Playlist generation ~2.5 minutes (mostly TTS). Analysis ~1s/track.