MPD and Scene fixes + twitch-bot cleanup
This commit is contained in:
@@ -189,11 +189,11 @@ body {
|
||||
color: var(--term-fg);
|
||||
text-shadow: 0 0 6px var(--term-glow);
|
||||
display: grid;
|
||||
/* art | meta | LIBRARY | <slack 1fr> | spectrum | progress
|
||||
/* art | meta | FILE | LIBRARY | <slack 1fr> | spectrum | progress
|
||||
Slack sits between the upper info block and the bottom strip so the
|
||||
spectrum analyzer reserve docks just above the progress bar — pairs
|
||||
them visually as a single "playback" zone at the foot of the pane. */
|
||||
grid-template-rows: auto auto auto 1fr auto auto;
|
||||
grid-template-rows: auto auto auto auto 1fr auto auto;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
@@ -283,6 +283,14 @@ body {
|
||||
}
|
||||
.np-stats .stats-grid .val.empty { color: var(--term-fg-dim); }
|
||||
|
||||
/* Lossless badge — bright green for FORMAT when the codec is FLAC/ALAC/WAV
|
||||
so the "high quality" cue reads at a glance. Lossy formats (MP3/Opus/AAC)
|
||||
stay in the regular term-fg color. */
|
||||
.np-stats .stats-grid .val.lossless {
|
||||
color: var(--term-fg-bright);
|
||||
text-shadow: 0 0 8px var(--term-glow);
|
||||
}
|
||||
|
||||
/* Spectrum slot — reserves the row that the waveform plugin source sits
|
||||
on top of. Empty + transparent: the plugin draws into this region, the
|
||||
widget just holds the layout space so progress stays in its expected
|
||||
@@ -438,6 +446,14 @@ body {
|
||||
<span class="key">YEAR</span> <span class="val empty" id="metaYear">—</span>
|
||||
<span class="key">GENRE</span> <span class="val empty" id="metaGenre">—</span>
|
||||
</div>
|
||||
<div class="np-stats">
|
||||
<div class="stats-label">─ FILE ─</div>
|
||||
<div class="stats-grid">
|
||||
<span class="key">FORMAT</span> <span class="val empty" id="fileFormat">—</span>
|
||||
<span class="key">QUALITY</span><span class="val empty" id="fileQuality">—</span>
|
||||
<span class="key">BITRATE</span><span class="val empty" id="fileBitrate">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="np-stats">
|
||||
<div class="stats-label">─ LIBRARY ─</div>
|
||||
<div class="stats-grid">
|
||||
@@ -530,6 +546,7 @@ body {
|
||||
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 nextRows = nextSlots.map(slot => slot.closest('.next-line'));
|
||||
const PROMPT = 'OPHI-118://> ';
|
||||
const MAX_TERM_LINES = 200;
|
||||
const MODE_KEYS = ['repeat', 'random', 'single'];
|
||||
@@ -610,6 +627,20 @@ body {
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
function nextPreviewConfig(playback) {
|
||||
const p = normalizePlayback(playback);
|
||||
if (p.single && p.repeat) {
|
||||
return { count: 1, empty: '— repeat single armed —' };
|
||||
}
|
||||
if (p.single) {
|
||||
return { count: 0, empty: '— single mode —' };
|
||||
}
|
||||
if (p.random) {
|
||||
return { count: 1, empty: '— shuffle active —' };
|
||||
}
|
||||
return { count: 3, empty: '— end of queue —' };
|
||||
}
|
||||
|
||||
async function logModeChange(mode, enabled) {
|
||||
await typeCommand(`music --${mode}=${enabled ? 'true' : 'false'}`);
|
||||
logBeforePins(`[audio] ${mode} mode ${enabled ? 'enabled' : 'disabled'}`, 'term-out', null);
|
||||
@@ -640,14 +671,23 @@ body {
|
||||
npTitle.textContent = s.title || '—';
|
||||
npTime.textContent = `[${fmtClock(s.currentTime)} / ${fmtClock(s.duration)}]`;
|
||||
const titles = Array.isArray(s.nextTitles) ? s.nextTitles : [];
|
||||
const preview = nextPreviewConfig(s.playback);
|
||||
nextSlots.forEach((slot, i) => {
|
||||
const v = titles[i];
|
||||
const v = i < preview.count ? titles[i] : null;
|
||||
if (v) {
|
||||
slot.textContent = v;
|
||||
slot.classList.remove('empty');
|
||||
nextRows[i].style.display = '';
|
||||
} else {
|
||||
slot.textContent = i === 0 ? '— end of queue —' : '—';
|
||||
slot.classList.add('empty');
|
||||
if (i === 0) {
|
||||
slot.textContent = preview.empty;
|
||||
slot.classList.add('empty');
|
||||
nextRows[i].style.display = '';
|
||||
} else {
|
||||
slot.textContent = '';
|
||||
slot.classList.add('empty');
|
||||
nextRows[i].style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -669,6 +709,15 @@ body {
|
||||
progElapsed.textContent = fmtClock(cur);
|
||||
progTotal.textContent = fmtClock(dur);
|
||||
|
||||
// File / audio info — codec from extension, sample rate + bit depth +
|
||||
// bitrate from MPD `status.audio` and `status.bitrate`. FLAC and friends
|
||||
// get the bright "lossless" treatment on FORMAT to advertise quality.
|
||||
const audio = s.audio || {};
|
||||
setMeta(fileFormat, audio.format);
|
||||
fileFormat.classList.toggle('lossless', LOSSLESS_FORMATS.has(audio.format));
|
||||
setMeta(fileQuality, fmtQuality(audio));
|
||||
setMeta(fileBitrate, audio.bitrate ? `${fmtCount(audio.bitrate)} kbps` : null);
|
||||
|
||||
// Library stats — bridge updates `stats` ~every 30s. Queue is built
|
||||
// from the per-tick index/total fields; render 1-indexed (mpc style).
|
||||
const stats = s.stats;
|
||||
@@ -721,6 +770,33 @@ body {
|
||||
const statSongs = document.getElementById('statSongs');
|
||||
const statPlaytime = document.getElementById('statPlaytime');
|
||||
const statQueue = document.getElementById('statQueue');
|
||||
const fileFormat = document.getElementById('fileFormat');
|
||||
const fileQuality = document.getElementById('fileQuality');
|
||||
const fileBitrate = document.getElementById('fileBitrate');
|
||||
|
||||
// Codecs whose bitstream is mathematically lossless — drives the bright
|
||||
// FORMAT highlight in the FILE block.
|
||||
const LOSSLESS_FORMATS = new Set(['FLAC', 'ALAC', 'WAV', 'WAVPACK', 'APE']);
|
||||
|
||||
// "44100" → "44.1 kHz", "96000" → "96 kHz". Trim the decimal when the rate
|
||||
// is a clean multiple of 1000 so common rates render as "44.1/48/96 kHz".
|
||||
function fmtSampleRate(hz) {
|
||||
if (!hz || !isFinite(hz) || hz <= 0) return null;
|
||||
const khz = hz / 1000;
|
||||
const s = (khz % 1 === 0) ? khz.toFixed(0) : khz.toFixed(1);
|
||||
return `${s} kHz`;
|
||||
}
|
||||
|
||||
// Quality cell merges sample rate + bit depth: "96 kHz · 24-bit". Either
|
||||
// half can be missing (DSD streams report bits='dsd64' which we drop, MP3s
|
||||
// sometimes lack a clean rate at start of decode).
|
||||
function fmtQuality(audio) {
|
||||
const sr = fmtSampleRate(audio.samplerate);
|
||||
const bits = (typeof audio.bits === 'number' && audio.bits > 0)
|
||||
? `${audio.bits}-bit` : null;
|
||||
if (sr && bits) return `${sr} · ${bits}`;
|
||||
return sr || bits || null;
|
||||
}
|
||||
|
||||
const fmtCount = n => (typeof n === 'number' && isFinite(n))
|
||||
? n.toLocaleString('en-US') : null;
|
||||
|
||||
Reference in New Issue
Block a user