Active Express routes in server.js and its mounted modules. The service is
served at api.mediahubnetwork.net (dev: port 5000).
Pages
| Method | Path | Serves |
|---|
| GET | / | Home (index.html) |
| GET | /chat | Chat page (public/chat.html) |
| GET | /lok | Studio page (public/lok.html) |
| GET | /social.html | Studio UI for the social platform |
Streaming
| Method | Path | Purpose |
|---|
| GET | /stream | Stream the main channel (proxy passthrough) |
| GET | /stream/:channelId | Stream any channel by id |
Channels
| Method | Path | Purpose |
|---|
| GET | /api/channels | List available channels |
| POST | /api/channels/create | Create a channel |
| POST | /api/channels/:id/queue | Push a track onto a channel's queue |
Content & library
| Method | Path | Purpose |
|---|
| GET | /api/library | Inventory of the media library (from the registry/rundown) |
| GET | /api/content/status | Content pipeline status (counts per category, last run) |
| POST | /api/content/refresh | Trigger a content refresh run |
| POST | /api/content/restore | Emergency restore from the archive (returns restored counts + resyncs the scheduler) |
Imaging — /api/imaging/*
| Method | Path | Purpose |
|---|
| POST | /api/imaging/generate | Run all four imaging generators (sweepers/IDs/voice drops/stingers) |
| POST | /api/imaging/sweepers | Regenerate sweepers ({songDrops,ids,voiceDrops,count,archiveOld}) |
| POST | /api/imaging/stingers | Regenerate the stinger library ({count,archiveOld}) |
Each imaging endpoint resyncs the decks and pushes the rotation. See
Imaging engine.
Bulletin — /api/bulletin/*
| Method | Path | Purpose |
|---|
| POST | /api/bulletin/generate | Generate a spoken NEWS + time + weather bulletin, resync the rotation, push to remote |
| GET | /api/bulletin/status | Latest bulletin file, current news.txt headlines, live Cairo time-check sentence |
Hourly auto-gen: boot +30s then every BULLETIN_HOURS (default 1h, 0
disables). See Bulletin engine.
Reactions — /api/reactions
| Method | Path | Purpose |
|---|
| GET | /api/reactions | Station-wide emoji counts {"🔥":2,"❤️":1,…} |
| POST | /api/reactions | {emoji, delta} — +1 per click, persisted to models/reactions.json |
The chat-server proxy (:8094) forwards both so the public web page and the
Flutter app can read/write reactions from the public origin.
POST /api/content/refresh accepts an options body, e.g.
{"songs":20,"ads":16,"restore":false}. Defaults: restore is false
(emergency-only — the old
default of true resurrected purged OLD tracks every cycle), songs 20, ads
16. The automatic 6-hour refresh (CONTENT_REFRESH_HOURS, 0 to disable) is a
chained setTimeout so a long run never overlaps the next tick, and
liveProtectedPaths() + resyncSchedulerAfterRefresh() keep the on-air rotation
safe across the swap.
Sync — /api/sync/* (HTTP-pull upload)
The production node pulls media over plain HTTP; these are the token-gated
endpoints it curls (see Media Sync):
| Method | Path | Guards | Serves |
|---|
| GET | /api/sync/media?rel=<rel>&token=<t> | SYNC_TOKEN (default loklok-sync-pull), rel path-guarded under MEDIA_ROOT | Any media file |
| GET | /api/sync/m3u?token=<t> | SYNC_TOKEN | PLAYLIST_ROOT/main.m3u |
Station control — /api/station/* (agent-facing)
Auth: if STATION_KEY is set, require x-station-key (or Authorization)
header.
| Method | Path | Purpose |
|---|
| GET | /api/station/state | Full snapshot: rotation, queue, decks, config, live |
| GET | /api/station/config | Current scheduler config |
| PUT | /api/station/config | Deep-merge a config patch |
| PUT | /api/station/genres | Update GENRE_SPLIT |
| PUT | /api/station/weights | Update MASHUP_CHANCE / STOCHASTIC_SFX_CHANCE |
| PUT | /api/station/rotation | Update rotation buffer/fresh-window/cooldowns |
| PUT | /api/station/dayparts | Set daypart windows (weights / hard genre) |
| POST | /api/station/dayparts/clear | Clear all dayparts |
| PUT | /api/station/mood | Apply a mood preset (`chill |
| GET | /api/station/singers | Singer catalogs per category |
| POST | /api/station/singers | Add/remove/replace singers for a category |
| PUT | /api/station/queries | Replace the trend queries for a category |
| POST | /api/station/content/fetch | Fetch content for a category ({category,target,kind}) |
| POST | /api/station/content/scrub | Archive junk from a category deck |
| POST | /api/station/content/refresh | Run the full content refresh |
| POST | /api/station/rotation/rebuild | Rebuild the on-air rotation |
| POST | /api/station/skip | Skip the rotation head (rebuild) |
| POST | /api/station/play | Force a track from a category to the head |
| POST | /api/station/config/save | Persist current config |
| POST | /api/station/config/reset | Delete station-config.json |
| GET | /api/station/log | Tail content or server log (?file=&lines=) |
| GET | /api/station/live | Live manager status |
| POST | /api/station/live/register | Register a live stream {url,title,creator,listeners,ingest} |
| POST | /api/station/live/remove | Remove a stream by id |
| POST | /api/station/live/vote | Vote on a stream {id,delta} |
| POST | /api/station/live/start | Go live: {id} optional, else top-ranked |
| POST | /api/station/live/stop | Back to the normal mix |
See Station control and Live manager.
| Method | Path | Purpose |
|---|
| GET | /api/social/posts | List posts (?status=&type=&sort=) |
| GET | /api/social/posts/:id | Single post |
| POST | /api/social/posts | Submit {type,title,artist,description,tags,sourceUrl|base64,createdBy} |
| POST | /api/social/upload/:type | Raw audio body upload (?title=&artist=) |
| POST | /api/social/posts/:id/vote | {delta:±1} |
| POST | /api/social/posts/:id/react | {kind} |
| POST | /api/social/posts/:id/approve · reject · process | Admin (gate via SOCIAL_ADMIN_KEY) |
| POST | /api/social/sync | Attach overlay + force rotation push |
| GET | /api/social/status | Platform status |
| GET | /api/social/next | Next ranked post (?type=&category=) |
Telemetry
| Method | Path | Purpose |
|---|
| GET | /api/status | Live system status (now playing, listeners, bandwidth) |
| GET | /api/schedule | The full 120-track on-air rotation resolved through the registry, with onAir/aired flags |
| GET | /api/upnext | The upcoming queue (with titles/artists) |
| POST | /api/upnext/votes | Vote on the up-next queue |
| GET | /api/upnext/votes | Current up-next vote counts |
| GET | /next-tracks.json | The upcoming queue as JSON |
| GET | /network-test-payload | Synthetic payload for network testing |
| GET | /ping | Liveness probe (Date.now() as text) |
The listener count shown in /api/status and stats_update comes from MediaMTX
HLS (polled every 5s from the read-only proxy at 10.10.8.230:19353/v3/paths/list),
with Icecast as fallback (source = mediamtx | icecast).
Push notifications
| Method | Path | Purpose |
|---|
| POST | /api/subscribe | Register a web-push subscription |
The subscription payload is stored and used to send push notifications (see
Notifications).
| Method | Path | Purpose |
|---|
| POST | /metadata | Publish a now-playing metadata update |
/metadata is also the authoritative "a track finished on-air" signal.
Liquidsoap posts metadata after each track; the handler calls
scheduler.markOnAirTrack() to advance the rotation cursor and cooldown the
exact track that just played, so the next rotation never schedules it again. See
Anti-repeat & M3U rotation.
Response conventions
- JSON for all
/api/* routes.
- HTML
text/html for page routes.
/ping returns a plain-text timestamp for load-balancer health checks.
Typical client flow
GET /api/status for a snapshot.
socket.emit('join_channel', 'main') for live status pushes.
GET /api/library + /next-tracks.json for queue visualization on the
monitor page.
GET /api/station/state (or via MCP) to inspect/control the rotation.