Chat commands:
- !skip: per-user 60 s cooldown (was global 5 s). Each chatter gets one
skip per minute; rejection includes a wait hint so users know
it's their own gate.
- !queue: peek next 3 tracks from the MPD queue. Read-only, no cooldown.
- !info: full metadata for now-playing — artist · title · album (year)
· genre, with missing tags skipped to avoid empty parens.
Mattermost going-live notifier (optional, MM_HOOK env):
- Polls https://ophi118.com/api/status.json (the public service in
station/server/) every 60 s and POSTs a single message to the
configured Mattermost webhook on each offline→live transition.
- Transition detection keys on Twitch's started_at, not just the live
flag, so a Helix flap inside one stream doesn't double-fire. Cold
start mid-stream is intentionally silent — records the current
started_at, awaits next genuine transition. Override the polled
endpoint with STATUS_URL for private status services.
.env.example documents the new MM_HOOK / STATUS_URL knobs.
Adds nextTitles (array of up to 3 display strings) plus mirrored single
nextTitle/nextArtist/nextTrackTitle/nextFile for the immediate next
track. Drives the Music Box overlays' 3-up "coming next" lines without
overlays needing to query MPD themselves.
Reads MPD status.nextsong, walks playlistinfo for the next 3 indices,
applies the existing _flatten_tag + display-string cascade. Fields are
None / [] at end of queue. Backwards-compatible: overlays that don't
read the new keys are unaffected.
Self-hosted async Python bot, runs as systemd --user unit
(obs-twitch-bot.service, sibling to obs-mpd-bridge). Connects to Twitch IRC
over TLS as vault118, listens in #ophi118, advances MPD on !skip and posts
the new track back to chat. 5s cooldown swallows skip-spam silently.
Strips Unicode Tags-block codepoints (U+E0000–U+E007F) before matching the
command so Twitch's anti-duplicate suffix doesn't break every-other !skip
when the broadcaster fires the same command twice in a row.
Bot account is separate (vault118) to keep the broadcaster's display name
out of bot replies; .env holds access token + refresh token + client id
(refresh + client id stored for future token rotation, unused today).
Captures the current OBS scene/window state after wiring Music Daemon,
Project Loading, and Game scenes through the UI. Untitled.json now
includes the new scenes and their browser sources.
Landing index.html: reverted to the simpler pre-doomguard version
(static signal bars, plain footer rig, no telemetry rotation, no
watermark animation), keeping the SIGNAL grid centering and the small
#rig opacity transition.
OBS rewrites these files on shutdown — expect future commits to show
similar dock-geometry diff noise.
Streaming overlay for the Game scene. Reads the manifest written by
loading.sh (game name, subtitle, mic state) and pulls live state from
OBS WebSocket on the same connection that delivers music updates.
- Top HUD: TRANSMISSION ID / SIGNAL bars / UTC clock — same vibe as
Landing/Loading, sized smaller so it doesn't dominate the gameplay.
- Camera frame (top-right, 480×270 with cyan corner ticks): decorative
drop-target. The actual OBS camera source layers ON TOP; a centered
placeholder (camcorder SVG + 'PLACE CAMERA HERE' + frame coordinates
'2008, 100 · 480×270') shows during setup and gets covered when the
camera source is positioned. Configurable via URL params:
?camera=Webcam&scene=Game.
- Status bar (bottom): solid gradient + cyan top edge so it reads on
any gameplay background. Two lines:
LIVE · GAME · SUBTITLE
elapsed timer · mic state · now-playing track
- Live camera-frame sync: queries GetSceneItemEnabled on connect and
subscribes to SceneItemEnableStateChanged, so toggling the camera's
eye-icon in OBS hides/shows the frame within ~50ms — overrides the
manifest's static value.
- Music sync: shares the same WS connection; now-playing line updates
from mpd:state broadcasts.
- CRT overlays kept lighter than Landing (gameplay must remain readable).
Music persistence across scene switches by extracting playback into a
separate browser source (music/index.html) — single source instance
shared across scenes via OBS's "Add Existing", so it doesn't restart on
scene change.
Loading and Game overlays subscribe to mpd:state CustomEvents over OBS
WebSocket; the daemon broadcasts at 1 Hz plus on every track change.
File-based command pipeline lets bash control playback without a
WebSocket client of its own:
- vendor/obs-ws-mini.js — minimal WS v5 client with HMAC-SHA256 auth,
hard timeout, close-rejection, and a pure-JS SHA-256 fallback for
CEF file:// origins where crypto.subtle is unavailable.
- setup.sh — generates vendor/obs-config.js from the OBS plugin config;
checks the live socket instead of the (lagging) config file.
- music/index.html — invisible audio worker. Plays through a shuffled
queue with hardened error/stall recovery, polls loading/cmd.js for
bash-issued commands, broadcasts mpd:state via WS. Includes a
diagnostic status block (visible if the source's eye-icon is on).
- loading/index.html — strips local audio playback; subscribes to
mpd:state to drive the now-playing terminal line live. Removes the
redundant trailing prompt; terminal lines anchor at the bottom.
- loading/playlist.sh — rebuilt as subcommand API:
sync (default), skip, prev, pause, resume, status
control commands write loading/cmd.js (id, type, ts), which the
daemon polls every 250ms.
- loading/convert.sh — strips video tracks from .webm music downloads
to audio-only m4a (~95% smaller, ~20× faster decode); fixes the
CPU pegging from CEF decoding 1080p VP9 video for music playback.
- .gitignore — track manifests, ignore generated wrappers + secrets +
the audio cache.