A lightweight, Docker-ready, multi-station Internet Radio server written in Python.
182
Lightweight, Docker-ready multi-station Internet Radio server written in Python. Recursively reads MP3, FLAC, WAV and other audio files and streams each station continuously as gapless AAC or MP3. Includes a browser admin panel, DJ console with live broadcast/auto-ducking/skip/jingles, and a mobile-friendly listener player.
Requires Docker.
Put music in ./music/<station-folder>, one folder per station, then:
docker build -t stormradio .
docker run -d \
--name stormradio \
-p 8080:8080 \
-v $(pwd)/music:/mnt/music \
-v stormradio_db:/opt/db \
-e ADMIN_LOGIN=admin \
-e ADMIN_PASSWORD=change-me \
-e DB_ENGINE=sqlite \
stormradio
Open http://localhost:8080/admin, sign in, and create the first station. Station and DJ configuration, domain and contacts are managed from the admin panel.
/stats can expose machine-readable metrics when enabled.Admins and permitted DJs upload through the web UI. Files are staged in /mnt/tmp, validated by the audio decoder and checked for free disk space before acceptance. Admin uploads require publishing; DJ uploads normally require approval. Rejected uploads show the reason to the DJ.
When station-format storage is enabled, conversions run on a dedicated low-priority background thread, one file at a time, using PyAV (no external ffmpeg process). Temporary .tmp files are atomically renamed after successful conversion, so the playlist scanner never sees partial audio. The admin UI shows the conversion queue.
Behind nginx/Traefik/Caddy/CDN, increase the proxy body-size and read-timeout limits for large uploads. Example nginx:
client_max_body_size 1024m;
proxy_read_timeout 600s;
At /dj, DJs can:
/ lists running stations. /listen/<STREAM_NAME> opens the browser player; /stream/<STREAM_NAME> is the raw stream for external players.
AAC streams use audio/aac (raw ADTS); MP3 streams use audio/mpeg. The player shows codec/bitrate, reconnects automatically after interruptions or station setting changes, and displays configured contacts. Stream responses include ICY metadata for compatible players such as VLC.
Fixed container paths:
/mnt/music - station music, one subdirectory per station./mnt/sounds - DJ jingles./mnt/advertisement - ad clips./mnt/tmp - pending uploads; mount a volume if they must survive container recreation./opt/db/stormradio.db - SQLite database; mount a volume to persist configuration.Port is fixed to 8080.
| Variable | Default | Description |
|---|---|---|
ADMIN_LOGIN | admin | Admin login. |
ADMIN_PASSWORD | password | Admin password; change in production. |
DB_ENGINE | sqlite | sqlite or mysql. |
DB_MYSQL_HOST | - | MySQL host. |
DB_MYSQL_PORT | 3306 | MySQL port. |
DB_MYSQL_DBNAME | - | Existing MySQL database name. StormRadio creates tables, not the database. |
DB_MYSQL_USER | - | MySQL user. |
DB_MYSQL_PASSWORD | - | MySQL password. |
DB_MYSQL_CONNECT_RETRIES | 50 | Initial MySQL connection retries. |
DB_MYSQL_CONNECT_RETRY_DELAY | 3 | Seconds between MySQL retries. |
ADMIN_RESET_2FA | false | Reset admin 2FA on startup; remove after recovery. |
LOG_VERBOSITY | 0 | 0 minimal; 1+ listener connect/disconnect; 2+ proxy IP substitutions. |
LOG_IP_ADDRESSES | true | Set false to hide client IPs in logs. |
LOG_MASK_IP | true | Mask trailing IPv4/IPv6 parts in logs. |
DJ_WS_MAX_CONNECTIONS_PER_DJ | 2 | Max simultaneous live WebSockets per DJ. |
TRUSTED_PROXY | - | Space-separated trusted proxy IPs/CIDRs for X-Forwarded-For. Only use when the app is not directly exposed and the proxy overwrites the header. |
TRACK_IO_TIMEOUT_SECONDS | 8 | Timeout for blocking filesystem/decoder operations. |
STATISTICS_EXPORT_TOKEN | - | ≥32-byte Bearer token enabling GET /stats; unset/short disables it. |
The external domain is configured in Settings → Domain, not by an environment variable.
Unauthenticated read-only endpoints:
GET /api/stations - running stations with name, stream, listeners, current track, bitrate and codec.GET /api/stations/<STREAM_NAME> - same plus enabled.GET /api/config - configured domain and site-wide contacts.DJ/admin details are available only through authenticated /dj/api/* and /admin/api/*.
Ads are registered in Advertisement, assigned to stations and scheduled with ads_per_hour. Ads play only between regular tracks, never over jingles or live DJs. While an ad plays, DJ live/skip/sound controls are disabled. Stations with no assigned ad clips continue normal playback.
StormRadio can integrate with the separate StormRadio-vDJ product over HTTP. A vDJ has a display name, endpoint, Bearer token, announcement language, announcements/hour, timeout and music-ducking level; it has no login or upload permissions.
A station can have at most one vDJ. At track start, StormRadio checks GET /health, then calls POST /get_back_forward with track metadata. If the response arrives in time, it goes live exactly 5 seconds after the track starts and is mixed like a human DJ. vDJ never interrupts a human DJ, ad or jingle. A human DJ takes priority and immediately stops an active vDJ announce. Each station's vDJ processing is isolated, and health/request/on-air activity is logged.
On startup StormRadio validates all required tables/columns and the admin_settings row. It records the schema-writing version:
For MySQL, the target database must already exist and the configured user needs CREATE TABLE privileges.
Audio decoding/encoding uses PyAV with bundled FFmpeg libraries inside the Python process; no external ffmpeg CLI is spawned. Each station has its own mixer/encoder/playlist thread; NumPy handles PCM mixing and DJ/vDJ ducking. Artist/Title tags are read from files, falling back to cleaned filenames.
TRACK_IO_TIMEOUT_SECONDS, protecting stations from stalled NFS/SMB mounts.scrypt; plaintext passwords are never stored.nosniff, DENY, restrictive CSP and same-origin referrer policy.GET /stats returns JSON containing rate-limit counters, login lockouts, session counts and per-station play/skip/ad/jingle/listener counters. It is disabled by default. Set STATISTICS_EXPORT_TOKEN to a random value of at least 32 bytes and send Authorization: Bearer <token>. This endpoint is not rate-limited.
Content type
Image
Digest
sha256:9c81e4aa0…
Size
193.8 MB
Last updated
6 days ago
docker pull stormotron/stormradio