Sign inSign up

stormotron/stormradio

By stormotron

•Updated 6 days ago

A lightweight, Docker-ready, multi-station Internet Radio server written in Python.

Image
0

182

stormotron/stormradio repository overview

⁠StormRadio v0.1.4

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.

⁠Quick Start

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.

⁠Features

⁠Admin panel
  • Create/edit/start/stop/delete stations: stream name, music folder, codec, bitrate, fades, ads/hour and DJ uploads.
  • Optional Prefer storage in the station's format: approved/auto-approved uploads are transcoded in the background to the station codec at 2× its bitrate, capped at 320k.
  • Create/block/delete/reset DJ accounts, set upload quotas and assign stations.
  • Optional Allow uploads without admin confirmation for trusted DJs.
  • See online DJs and kick them; reset DJ 2FA.
  • Manage Sounds/jingles and Advertisement clips and assign them to stations.
  • Upload, review, preview, approve/reject files; browse/search/delete station music, sounds and ads with Artist/Title tags.
  • Create/delete music subfolders.
  • Optional admin 2FA.
  • Configure external Domain, site-wide Telegram/E-Mail contacts, API/jingle rate limits and statistics.
  • /stats can expose machine-readable metrics when enabled.
⁠Uploads

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;
⁠DJ console

At /dj, DJs can:

  • See assigned stations, current/next track and listeners.
  • Skip the current track.
  • Go live with microphone audio, automatic music ducking and self-monitoring (disabled during ads or when no music exists).
  • Trigger jingles immediately or after the current track, optionally holding the station afterward.
  • Build an in-memory temporary playlist, reorder/exclude tracks and play it from the next track without interrupting the current one.
  • Upload songs/jingles when permitted and set up 2FA.
⁠Listener

/ 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.

⁠Storage

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.

⁠Environment Variables

VariableDefaultDescription
ADMIN_LOGINadminAdmin login.
ADMIN_PASSWORDpasswordAdmin password; change in production.
DB_ENGINEsqlitesqlite or mysql.
DB_MYSQL_HOST-MySQL host.
DB_MYSQL_PORT3306MySQL 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_RETRIES50Initial MySQL connection retries.
DB_MYSQL_CONNECT_RETRY_DELAY3Seconds between MySQL retries.
ADMIN_RESET_2FAfalseReset admin 2FA on startup; remove after recovery.
LOG_VERBOSITY00 minimal; 1+ listener connect/disconnect; 2+ proxy IP substitutions.
LOG_IP_ADDRESSEStrueSet false to hide client IPs in logs.
LOG_MASK_IPtrueMask trailing IPv4/IPv6 parts in logs.
DJ_WS_MAX_CONNECTIONS_PER_DJ2Max 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_SECONDS8Timeout 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.

⁠Public Station API

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/*.

⁠Advertisement Scheduling

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.

⁠Virtual DJs (vDJ)

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.

⁠Database Validation & Migration

On startup StormRadio validates all required tables/columns and the admin_settings row. It records the schema-writing version:

  • Fresh/current database: starts normally.
  • Exactly one version behind: migrates automatically.
  • More than one version behind or newer than the image: refuses to start; upgrade one version at a time.
  • Missing schema elements not covered by migration produce a clear error and immediate exit.

For MySQL, the target database must already exist and the configured user needs CREATE TABLE privileges.

⁠Audio Pipeline & Reliability

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.

  • A station without tracks cannot start or accept DJ live audio.
  • Changing a running station's settings restarts it cleanly; listeners reconnect automatically.
  • Blocking filesystem/decoder calls are bounded by TRACK_IO_TIMEOUT_SECONDS, protecting stations from stalled NFS/SMB mounts.
  • Tag reads do not block playback; filename-derived titles appear immediately.
  • Tracks can be automatically skipped after configurable near-silence (default 10 seconds).
  • A queued end-of-track jingle preempts the crossfade and starts immediately after the current track.

⁠Security

  • Passwords use scrypt; plaintext passwords are never stored.
  • TOTP 2FA is available for admins and DJs.
  • Failed logins are throttled and do not reveal which credential/2FA field was wrong.
  • Only one active session per account is allowed.
  • DJ WebSockets limit message size, decode buffering and concurrent connections.
  • Public, DJ and admin APIs are rate-limited; jingle triggering has additional DJ/station throttling.
  • Uploaded files are decoder-validated before acceptance and again before publication; bad tracks are blacklisted from station rotation.
  • Playing/queued sounds and ads cannot be deleted.
  • Upload filenames and music-folder names are sanitized.
  • Security headers include nosniff, DENY, restrictive CSP and same-origin referrer policy.
  • IP logging can be disabled or masked; rate limiting and login throttling still use the real IP internally.

⁠Statistics Export

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.

Tag summary

Content type

Image

Digest

sha256:9c81e4aa0…

Size

193.8 MB

Last updated

6 days ago

docker pull stormotron/stormradio