Files
obs-config/webapp/resources/views/overlays/music/box.blade.php
Jakub Zych fd1c78b333 webapp: drop rig:playlist pipeline, source track count from mpc stats
The four overlays that read /data/playlist.js (loading, goodbye,
music-box, music/nc) only ever consumed `tracks.length` for cosmetic
"LIBRARY :: N TRACKS" / "indexed N tracks" flavor text — all live
playback signal flows through bridges/mpd-state.py over OBS WS. The
142 KB JSON, ffprobe walk, and ETag-cached route were all producing
one integer that MPD itself already knows.

RigData::playlistCount() now shells `mpc stats` and parses "Songs: N",
returning 0 on any failure (treated by callers as empty library).
Removes RigPlaylistCommand, DataController::playlist(), the
/data/playlist.js route, the per-overlay <script src> + window.__PLAYLIST
shims, and the stale storage/data/playlist.json artifact.

AudioController still consumes config('rig.music_dirs') for the HTTP
audio stream — that's orthogonal and stays.
2026-05-22 21:04:24 +02:00

194 lines
6.4 KiB
PHP

@extends('layouts.overlay')
@section('title', 'MUSIC BOX')
@section('styles')
<style>
body.overlay-body {
display: grid;
grid-template-rows: auto 1fr auto auto auto;
gap: 22px;
}
.stage {
display: flex;
align-items: center;
justify-content: center;
}
.terminal {
width: 64vw;
max-width: 1480px;
height: 60vh;
max-height: 760px;
min-height: 420px;
font-size: 24px;
}
</style>
@endsection
@push('data-scripts')
<script>window.__TRACK_COUNT = @json($trackCount);</script>
@endpush
@section('content')
@include('partials.hud-strip', [
'variant' => 'onair',
'label' => 'ON AIR',
'idText' => 'OPHI-118 // MUSIC',
'signalGlyph' => '▮▮▮▮▯',
'signalProfile' => 'strong',
])
<main class="stage">
<div class="terminal">
<div id="termOutput"><div id="pinnedBlock"><div class="pin-spacer"></div><div class="term-line next-line"><span class="next-arrow"></span><span class="next-label">next:</span><span class="next-title empty" data-slot="0"> end of queue </span></div><div class="term-line next-line"><span class="next-arrow"></span><span class="next-label">then:</span><span class="next-title empty" data-slot="1"></span></div><div class="term-line next-line"><span class="next-arrow"></span><span class="next-label">then:</span><span class="next-title empty" data-slot="2"></span></div><div class="pin-spacer"></div><div class="term-line np-line"><span class="np-arrow"></span><span class="np-title">connecting to daemon…</span><span class="np-time">[--:-- / --:--]</span></div></div></div>
</div>
</main>
<section class="status-strip">
<span class="label">CHAT</span>
<span class="cmd-group">
<span><span class="cmd">!skip</span><span class="desc">next track</span></span>
<span><span class="cmd">!queue</span><span class="desc">peek next 3</span></span>
<span><span class="cmd">!info</span><span class="desc">full metadata</span></span>
</span>
</section>
<section class="banner banner--green">
<div class="label"> TRANSMISSION OPEN </div>
<div class="title">MUSIC&nbsp;BOX</div>
<div class="sub"> STAY TUNED </div>
</section>
<footer class="foot">
<span>CH 118.0 MHz</span>
<span id="rig"> LOADING PLAYLIST </span>
<span class="onair"> ON AIR</span>
</footer>
@endsection
@section('scripts')
<script>
'use strict';
const pad = HUD.pad;
const trackCount = window.__TRACK_COUNT || 0;
document.getElementById('rig').textContent =
trackCount > 0 ? `LIBRARY :: ${trackCount} TRACKS` : '— EMPTY LIBRARY —';
const sleep = ms => new Promise(r => setTimeout(r, ms));
const term = document.getElementById('termOutput');
const pinnedBlock = document.getElementById('pinnedBlock');
const npTitle = pinnedBlock.querySelector('.np-title');
const npTime = pinnedBlock.querySelector('.np-time');
const nextSlots = [0, 1, 2].map(i => pinnedBlock.querySelector(`[data-slot="${i}"]`));
const PROMPT = 'OPHI-118://> ';
const MAX_TERM_LINES = 200;
function pruneTerminal() {
while (term.children.length > MAX_TERM_LINES) {
const first = term.firstElementChild;
if (!first || first === pinnedBlock) break;
term.removeChild(first);
}
while (term.children.length > 1) {
let total = 0;
for (const c of term.children) total += c.getBoundingClientRect().height;
if (total <= term.clientHeight) break;
const first = term.firstElementChild;
if (!first || first === pinnedBlock) break;
term.removeChild(first);
}
}
function logBeforePins(text, cls = 'term-out', prompt = '> ') {
const div = document.createElement('div');
div.className = 'term-line';
if (prompt) {
const p = document.createElement('span');
p.className = 'term-prompt';
p.textContent = prompt;
div.appendChild(p);
}
const t = document.createElement('span');
if (cls) t.className = cls;
t.textContent = text;
div.appendChild(t);
term.insertBefore(div, pinnedBlock);
pruneTerminal();
}
async function typeCommand(text) {
const div = document.createElement('div');
div.className = 'term-line';
const p = document.createElement('span');
p.className = 'term-prompt';
p.textContent = PROMPT;
div.appendChild(p);
const body = document.createElement('span');
body.className = 'term-out';
div.appendChild(body);
term.insertBefore(div, pinnedBlock);
for (const c of text) {
body.textContent += c;
await sleep(38);
}
await sleep(280);
}
function fmtClock(s) {
if (!isFinite(s) || s < 0) return '--:--';
return `${pad(Math.floor(s / 60))}:${pad(Math.floor(s % 60))}`;
}
let lastTrackFile = null;
function applyState(s) {
if (lastTrackFile !== null && s.file && s.file !== lastTrackFile && s.title) {
logBeforePins(`[audio] now: ${s.title}`);
}
if (s.file) lastTrackFile = s.file;
npTitle.textContent = s.title || '—';
npTime.textContent = `[${fmtClock(s.currentTime)} / ${fmtClock(s.duration)}]`;
const titles = Array.isArray(s.nextTitles) ? s.nextTitles : [];
nextSlots.forEach((slot, i) => {
const v = titles[i];
if (v) {
slot.textContent = v;
slot.classList.remove('empty');
} else {
slot.textContent = i === 0 ? '— end of queue —' : '—';
slot.classList.add('empty');
}
});
pruneTerminal();
}
function setOffline(msg) {
npTitle.textContent = msg;
npTime.textContent = '[--:-- / --:--]';
}
function startMusic() {
OBSWSBootstrap.onState(applyState, {
onClose: () => {
logBeforePins('[audio] daemon connection lost — retrying', 'term-dim');
setOffline('daemon disconnected');
},
});
}
(async () => {
await sleep(450);
await typeCommand('music --boot --shuffle');
logBeforePins('[audio] subscribing to mpd:state events', 'term-out');
await sleep(220);
if (trackCount > 0) {
logBeforePins(`[audio] library: ${trackCount} tracks`, 'term-out');
} else {
logBeforePins('[audio] library manifest empty', 'term-dim');
}
await sleep(180);
logBeforePins('[audio] queue: shuffle on', 'term-out');
await sleep(180);
logBeforePins('[audio] standby for transmission ✓', 'term-out');
await sleep(280);
startMusic();
})();
</script>
@endsection