1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
# 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 | 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
```bash
# 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.
|