Architecture
This page describes how the pieces fit together end-to-end, from a fresh YouTube download to a listener's browser, including the live takeover path.
System diagram
LOCAL NODE (Windows, C:\Users\omar\radionew)
┌───────────────────────────────────────────────────────────────────────────────┐
│ │
│ ┌──────────────┐ ┌───────────────────────────┐ ┌──────────────────┐ │
│ │ yt-dlp │ │ content-pipeline.js │ │ smart-scheduler │ │
│ │ discovery │────▶│ sanitize / loudness / │─────▶│ .js │ │
│ │ (YouTube…) │ │ registry / purge │ │ Dirichlet + RL │ │
│ └──────────────┘ └───────────────────────────┘ └────────┬─────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ downloads/ ──▶ media/music/… (E:/radionew) ──▶ playlists/*.m3u │
│ │
│ ┌────────── social-platform.js ──▶ scheduler overlay (ranked posts) │
│ │ live-manager.js ─────▶ restream winner ➜ harbor :8006 │
│ │ imaging-engine.js ───▶ sweepers / IDs / stingers (R128) │
│ │ fun-jingles.js ──────▶ karaoke jingles + sound FX │
│ └─────── station-control.js ─────▶ /api/station/* (MCP front door) │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ radio-engine.js (sync) HTTP-pull ➜ curl │ │
│ │ /api/sync/media?token= (one ssh control) │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ server.js (Socket.io + HTTP API) ──▶ web frontends (public/, radio-web/) │
└───────────────────────────────────────────────────────────────────────────────┘
│ HTTP pull (curl, media + playlists)
▼
PRODUCTION NODE (Ubuntu, 10.10.8.230 /home/sms/radio)
┌────────────────────────────────────────────────────────┐
│ Liquidsoap (radio_worker.liq) │
│ │ ducking: live mic :9001 (music → 5%) │
│ │ switch : channel_live harbor :8006 (WHIP/restream)│
│ └──▶ Icecast :8005 (OGG) ──▶ ffmpeg bridge │
│ └──▶ MediaMTX :19351 HLS │
│ whip_server.py :8089 (go-live.html + /whip-info) │
│ FIFO whep_live.wav → ffmpeg → harbor :8006 │
│ chat-server.js :8094 (long-poll chat + presence) │
│ mmtx_listeners_proxy.py :19353 (listener count) │
│ radio-web :4477 (Next.js player) │
│ video_feed/ (LOKLOK TV: ffmpeg renderer + ticker) │
└────────────────────────────────────────────────────────┘
│
files.mediahubnetwork.net/main (OGG)
api.mediahubnetwork.net/loklok/index.m3u8 (HLS, same-origin)
files.mediahubnetwork.net:19351/loklokvideo/index.m3u8 (video)
files.mediahubnetwork.net:4477 (player)
api.mediahubnetwork.net/docs (docs)
│
▼
LISTENERS / PLAYERS
Component responsibilities
1. Content discovery & download
yt-dlp fetches tracks into downloads/ using per-category manifests. A download
archive (yt_download_archive.txt) prevents re-downloading the same video. The
extractor is pinned to YouTube's android/web clients to dodge throttling/403s.
2. Sanitization & registry
The pipeline re-derives clean {artist, title} pairs from the embedded ID3 TIT2
tag (yt-dlp stores the raw YouTube video title), stripping suffixes like
| ..., /, and [Official ...]. Categories are validated against curated
singer catalogs and a junk gate (BAD_TITLE/REJECT_TITLE) that bans
live/concert/show/news/paparazzi material, lyric/cover/karaoke uploads,
animation/stock channels, TV promos — and the entire mahraganat category
(genre terms + a mahraganat-artist blocklist, because most mahragan titles carry
no genre word). Since 2026-08-11 a dedicated _BAD_TERMS blocklist bans
AI-generated music at every gate (yt-dlp --reject-title pre-download,
post-download BAD_TITLE, scrubCategory, deriveMeta re-check):
ai-generated|ai song|suno|udio|melovox|ميوزك ليو|MUSIC L3O|حمو القماش|Qomash|vibeluxe|….
Results are cached in meta_registry.json.
3. Loudness normalization
Every asset passes through ffmpeg loudnorm targeting EBU R128: −14 LUFS,
−1.5 dBTP, 11 LRA. Playlists therefore never have jarring level jumps between a
whispered jingle and a loud song.
4. Scheduling
smart-scheduler.js consumes the registry and category manifests to build M3U
playlists. It uses:
- Dirichlet sampling for category mix (never a pure random shuffle).
- Energy sequencing — tags each track with an energy 1–5 and avoids jarring transitions.
- 90-minute freshness window per track for the lookahead queue, plus a
stable 120-track on-air rotation rebuilt only at 85% consumption (see
Anti-repeat & M3U rotation) — nothing repeats on
the hour. Anti-repeat state (cooldowns, song-keys, artist spacing, recent
on-air) is persisted to
scheduler_antirepeat.jsonso a server restart never forgets what just played. - Adjacency constraints — different songs by the same artist stay apart.
- Thompson-sampling RL arms — CHILL / BALANCED / PARTY moods that re-weight the category distribution based on accumulated reward.
- Imaging elements (jingles/sweepers/ads/sfx/shorts) recycle every 10 minutes
and are hard-cut by the transition logic. Since 2026-08-10 the sweepers,
station IDs and stingers are synthesized by
imaging-engine.js; since 2026-08-11 karaoke jingles + sound FX come fromfun-jingles.js. - Rotation variety —
popFrompicks the least-recently-aired eligible candidate (never-aired tracks win; tie-break = oldest in library), so rebuilds don't re-recycle the newest few tracks once their fresh window expires. The music fresh window isMUSIC_FRESH_MS= 3 h (matchesCOOLDOWN_PERIOD).
5. Social overlay
social-platform.js takes listener-submitted audio (song/mashup/ads/jingles/
sweepers/sfx), validates + R128-masters + dedupes it, and injects the top-ranked
posts as a scheduler overlay ahead of the rolling library. sync pushes the
updated rotation to the remote.
6. Sync to production
radio-engine.js pushes playlists and media to the Ubuntu broadcast node over
HTTP pull — one ssh per file runs a remote curl against the token-gated
/api/sync/* endpoints; sizes are verified and stale fingerprints re-upload
(see Media sync).
7. Broadcast
On the Ubuntu node, Liquidsoap reads the generated M3Us, applies a 2-second
music crossfade (hard cuts for imaging/shorts), and streams to Icecast. A
WHIP server (whip_server.py:8089) accepts mobile browser mic streams, feeds
them into a FIFO, and ffmpeg restreams the winner into the channel_live harbor
(:8006) where Liquidsoap's switch puts it on air over the mix. MediaMTX
transmuxes the master OGG into HLS for the player app. A second MediaMTX
path, loklokvideo, carries the branded 1080p video program: an ffmpeg
renderer composites the audio's spectrum + logo + lower-third + news ticker and
pushes it to MediaMTX (see LOKLOK TV).
8. Delivery & frontends
- Classic frontends (
public/) connect over Socket.io for live metadata, chat, studio injection, voting, and now-playing updates. - The Next.js player plays the HLS stream (
…/loklok/index.m3u8) with OGG fallback. - The PWA (
html/index.html, served by the Go server atapi.mediahubnetwork.net) is the installable radio player: HLS + OGG fallback, artist-backdrop hero, push notifications, offline shell, live chat + listener presence (via the long-poll chat server:8094). - The Flutter app (
~/loklok_radio_app, built remotely with Flutter 3.44) plays the HLS stream throughmedia_kit(mpv) and adds a premium client: live now-playing + listener counts, a full on-air queue, chat, and vibe voting — see Flutter app.
Control & agent API
- station-control.js mounts
/api/station/*— the agent-facing API over the scheduler (moods, genres, dayparts, singers, rotation, live). - mcp-radio.js is an MCP (Model Context Protocol) stdio server that wraps those endpoints for any AI agent.
- live-manager.js ranks live streams and restreams the winner into the harbor.
station-config.jsonpersists config overrides across restarts.
Paths & configuration
All filesystem roots are centralized in paths.js and are environment-overridable:
| Key | Default | Purpose |
|---|---|---|
RADIO_ROOT | C:/Users/omar/radionew | Project root |
MEDIA_ROOT | E:/radionew/media | Library (music, sweepers, ads…) |
LOG_ROOT | E:/radionew/logs | Runtime logs |
MODEL_ROOT | E:/radionew/models | ML models |
PLAYLIST_ROOT | E:/radionew/playlists | Generated M3Us |
ARCHIVE_ROOT | E:/radionew/pro_archive | Backup archives |
DOWNLOAD_ROOT | E:/radionew/downloads | Fresh downloads |
Environment & host inventory
| Host | Role | Address / Credentials |
|---|---|---|
| Local dev (Windows) | Source, pipeline, scheduler, API | 10.40.3.174, SSH omar@10.40.3.174 (Windows OpenSSH) |
| Prod broadcast (Ubuntu) | Liquidsoap / Icecast / MediaMTX / WHIP / chat | 10.10.8.230, SSH sms@10.10.8.230 |
| Public stream (master) | Icecast mount | files.mediahubnetwork.net/main (OGG) |
| Public stream (HLS) | MediaMTX via ffmpeg bridge | api.mediahubnetwork.net/loklok/index.m3u8 (same-origin cookie flow; :19351 is the raw MediaMTX port) |
| Listener count | MediaMTX listener proxy (read-only) | 10.10.8.230:19353/v3/paths/list |
| Live ingest | WHIP/WHEP server + go-live page | 10.10.8.230:8089 (/whip-info, /go-live) |
| Web / API | Socket.io + frontends | api.mediahubnetwork.net |
| Web player / PWA | Go server static | api.mediahubnetwork.net (PWA) / files.mediahubnetwork.net:4477 (Next.js) |
| Chat | long-poll chat + presence | files.mediahubnetwork.net:8094 |