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:
- Loads all tracks from beets DB + genre cache + Essentia audio features
- For each hour, looks up target energy from an energy curve (ambient at night, high energy in the evening)
- Picks a mode (weighted random): shuffle 40%, deep cuts 20%, new to library 15%, old favourites 15%, full album 10%
- Selects a track using contrast maximisation — prefers energy distance from recent picks
- Every 3 tracks, generates a TTS announcement via edge-tts
- Outputs
playlist.json+playlist.m3ufor Liquidsoap
Liquidsoap picks up the new playlist automatically via file watching.
Fallback Chain
If the daily playlist is empty or missing:
radio_dj.py— same smart DJ logic, picks one track at a timeall.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 | 00–07 | 1 | Ambient, drone |
| Morning Calm | 07–09 | 2–3 | Downtempo, chill |
| Late Morning | 09–12 | 4–5 | Indie, jazz, trip-hop |
| Lunch Break | 12–14 | 4 | Soul, dub |
| Afternoon Session | 14–17 | 5–6 | Electronic, funk |
| Drive Time | 17–19 | 5–6 | Mixed |
| Evening Heat | 19–22 | 7–8 | Techno, house, DnB |
| Wind Down | 22–00 | 2–4 | 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 1–10 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 ~50–70MB RAM. Playlist generation ~2.5 minutes (mostly TTS). Analysis ~1s/track.