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.
This commit is contained in:
@@ -51,7 +51,6 @@ Or via the app menu ("OBS Studio"). The native `obs` binary still exists from `p
|
|||||||
- `php artisan rig:setup` — generate `webapp/public/js/obs-config.js` + `OBS_WS_*` env from `plugin_config/obs-websocket/config.json`.
|
- `php artisan rig:setup` — generate `webapp/public/js/obs-config.js` + `OBS_WS_*` env from `plugin_config/obs-websocket/config.json`.
|
||||||
- `php artisan rig:telemetry [--collect]` — refresh hardware/OBS-profile snapshot at `webapp/storage/data/telemetry.json`.
|
- `php artisan rig:telemetry [--collect]` — refresh hardware/OBS-profile snapshot at `webapp/storage/data/telemetry.json`.
|
||||||
- `php artisan rig:loading` — interactive manifest builder; updates Twitch channel via Helix (no Python shell-out).
|
- `php artisan rig:loading` — interactive manifest builder; updates Twitch channel via Helix (no Python shell-out).
|
||||||
- `php artisan rig:playlist` — index audio dirs with ffprobe → `webapp/storage/data/playlist.json`.
|
|
||||||
- `php artisan rig:cmd <skip|prev|pause|resume>` — broadcast `mpd:cmd` over OBS WS (CLI mirror of `POST /cmd/{type}`).
|
- `php artisan rig:cmd <skip|prev|pause|resume>` — broadcast `mpd:cmd` over OBS WS (CLI mirror of `POST /cmd/{type}`).
|
||||||
|
|
||||||
**Audio + automation surfaces:**
|
**Audio + automation surfaces:**
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -50,8 +50,8 @@ playback from chat.
|
|||||||
through the per-app private dir.
|
through the per-app private dir.
|
||||||
- **Laravel 13** webapp under `webapp/`, served by `php artisan serve` on
|
- **Laravel 13** webapp under `webapp/`, served by `php artisan serve` on
|
||||||
`127.0.0.1:1118` and managed by **supervisord**. Routes return Blade-rendered
|
`127.0.0.1:1118` and managed by **supervisord**. Routes return Blade-rendered
|
||||||
overlay pages and a small data API (`/data/playlist.js`, `/cover.jpg`,
|
overlay pages and a small data API (`/cover.jpg`, `/track?p=…`,
|
||||||
`/track?p=…`, `POST /cmd/{skip|prev|pause|resume}`).
|
`POST /cmd/{skip|prev|pause|resume}`).
|
||||||
- **MPD** (system service) is the audio player. A small Python bridge
|
- **MPD** (system service) is the audio player. A small Python bridge
|
||||||
(`bridges/mpd-state.py`, `obs-mpd-bridge.service`) listens to MPD and
|
(`bridges/mpd-state.py`, `obs-mpd-bridge.service`) listens to MPD and
|
||||||
re-broadcasts state as `mpd:state` CustomEvents on the OBS WS bus.
|
re-broadcasts state as `mpd:state` CustomEvents on the OBS WS bus.
|
||||||
@@ -71,7 +71,7 @@ playback from chat.
|
|||||||
├── webapp/ Laravel app — every overlay served from here
|
├── webapp/ Laravel app — every overlay served from here
|
||||||
│ ├── app/
|
│ ├── app/
|
||||||
│ │ ├── Console/Commands/ rig:setup, rig:telemetry, rig:loading,
|
│ │ ├── Console/Commands/ rig:setup, rig:telemetry, rig:loading,
|
||||||
│ │ │ rig:playlist, rig:cmd
|
│ │ │ rig:cmd
|
||||||
│ │ ├── Http/Controllers/ Overlays/{Landing,Loading,Game,…}, Data,
|
│ │ ├── Http/Controllers/ Overlays/{Landing,Loading,Game,…}, Data,
|
||||||
│ │ │ MusicCommand, Audio
|
│ │ │ MusicCommand, Audio
|
||||||
│ │ ├── Services/ ObsWsClient, TwitchHelix, HardwareSnapshot
|
│ │ ├── Services/ ObsWsClient, TwitchHelix, HardwareSnapshot
|
||||||
@@ -107,8 +107,8 @@ sudo supervisorctl restart obs-webapp
|
|||||||
# refresh a per-session manifest before going live (game / mode / countdown)
|
# refresh a per-session manifest before going live (game / mode / countdown)
|
||||||
cd webapp && php artisan rig:loading
|
cd webapp && php artisan rig:loading
|
||||||
|
|
||||||
# reindex the music library (after dropping new tracks into ~/HDD/Music)
|
# rescan the music library (after dropping new tracks into ~/Music/Kolekcja)
|
||||||
cd webapp && php artisan rig:playlist
|
mpc update --wait
|
||||||
|
|
||||||
# re-snapshot hardware/OBS profile telemetry (kernel/GPU/encoder changes)
|
# re-snapshot hardware/OBS profile telemetry (kernel/GPU/encoder changes)
|
||||||
cd webapp && php artisan rig:telemetry --collect
|
cd webapp && php artisan rig:telemetry --collect
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ restart needed.
|
|||||||
|
|
||||||
| Route | Notes |
|
| Route | Notes |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `GET /data/playlist.js` | Returns `window.__PLAYLIST = {...};` with ETag |
|
|
||||||
| `GET /cover.jpg` | Streams `bridges/cover.jpg` (kept fresh by `mpd-state.py`) |
|
| `GET /cover.jpg` | Streams `bridges/cover.jpg` (kept fresh by `mpd-state.py`) |
|
||||||
| `GET /track?p=<base64>` | Audio stream — path-traversal-guarded |
|
| `GET /track?p=<base64>` | Audio stream — path-traversal-guarded |
|
||||||
| `POST /cmd/{skip\|prev\|pause\|resume}` | Broadcasts `mpd:cmd` over OBS WebSocket |
|
| `POST /cmd/{skip\|prev\|pause\|resume}` | Broadcasts `mpd:cmd` over OBS WebSocket |
|
||||||
@@ -82,7 +81,6 @@ php artisan rig:setup # gen public/js/obs-config.js + OBS_WS_* in .en
|
|||||||
php artisan rig:telemetry # refresh storage/data/telemetry.json
|
php artisan rig:telemetry # refresh storage/data/telemetry.json
|
||||||
php artisan rig:telemetry --collect # re-prompt every field interactively
|
php artisan rig:telemetry --collect # re-prompt every field interactively
|
||||||
php artisan rig:loading # interactive Project Loading manifest (CLI mirror of /onboard)
|
php artisan rig:loading # interactive Project Loading manifest (CLI mirror of /onboard)
|
||||||
php artisan rig:playlist # reindex audio dirs → storage/data/playlist.json
|
|
||||||
php artisan rig:cmd skip # CLI mirror of POST /cmd/skip
|
php artisan rig:cmd skip # CLI mirror of POST /cmd/skip
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -118,7 +116,7 @@ php artisan serve --host=127.0.0.1 --port=1118
|
|||||||
webapp/
|
webapp/
|
||||||
├── app/
|
├── app/
|
||||||
│ ├── Console/Commands/ rig:setup, rig:telemetry, rig:loading,
|
│ ├── Console/Commands/ rig:setup, rig:telemetry, rig:loading,
|
||||||
│ │ rig:playlist, rig:cmd
|
│ │ rig:cmd
|
||||||
│ ├── Http/Controllers/ Overlays/{Landing,Loading,Game,Desktop,
|
│ ├── Http/Controllers/ Overlays/{Landing,Loading,Game,Desktop,
|
||||||
│ │ Goodbye,MusicBox,MusicDaemon}, Data,
|
│ │ Goodbye,MusicBox,MusicDaemon}, Data,
|
||||||
│ │ MusicCommand, Audio, Onboard
|
│ │ MusicCommand, Audio, Onboard
|
||||||
@@ -143,7 +141,6 @@ webapp/
|
|||||||
├── config/rig.php storage paths, OBS WS creds, Twitch keys
|
├── config/rig.php storage paths, OBS WS creds, Twitch keys
|
||||||
└── storage/data/ rig:* commands write JSON here
|
└── storage/data/ rig:* commands write JSON here
|
||||||
├── loading.json
|
├── loading.json
|
||||||
├── playlist.json
|
|
||||||
└── telemetry.json
|
└── telemetry.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Symfony\Component\Finder\Finder;
|
|
||||||
use Symfony\Component\Process\Process;
|
|
||||||
use Throwable;
|
|
||||||
|
|
||||||
class RigPlaylistCommand extends Command
|
|
||||||
{
|
|
||||||
protected $signature = 'rig:playlist';
|
|
||||||
protected $description = 'Index audio under the configured music roots → storage/data/playlist.json';
|
|
||||||
|
|
||||||
private const EXTS = ['m4a', 'mp3', 'opus', 'ogg', 'flac', 'webm', 'aac', 'wav'];
|
|
||||||
|
|
||||||
public function handle(): int
|
|
||||||
{
|
|
||||||
$roots = array_values(array_filter((array) config('rig.music_dirs')));
|
|
||||||
if (empty($roots)) {
|
|
||||||
$this->warn(' ! no music dirs configured (set MUSIC_DIR_1 in .env)');
|
|
||||||
}
|
|
||||||
|
|
||||||
$ffprobe = (string) config('rig.ffprobe', '/usr/bin/ffprobe');
|
|
||||||
if (!is_executable($ffprobe)) {
|
|
||||||
$this->error(" ✗ ffprobe not found at {$ffprobe} (install ffmpeg or set FFPROBE_BIN)");
|
|
||||||
return self::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->line('── Indexing audio roots ──');
|
|
||||||
foreach ($roots as $r) {
|
|
||||||
$this->line(is_dir($r) ? " + {$r}" : " ! {$r} (missing — skipped)");
|
|
||||||
}
|
|
||||||
|
|
||||||
$tracks = [];
|
|
||||||
foreach ($roots as $root) {
|
|
||||||
if (!is_dir($root)) continue;
|
|
||||||
$finder = (new Finder())
|
|
||||||
->files()
|
|
||||||
->in($root)
|
|
||||||
->name('/\.(' . implode('|', self::EXTS) . ')$/i')
|
|
||||||
->ignoreUnreadableDirs()
|
|
||||||
->sortByName();
|
|
||||||
|
|
||||||
foreach ($finder as $file) {
|
|
||||||
$name = $file->getFilename();
|
|
||||||
$base = $file->getFilenameWithoutExtension();
|
|
||||||
// Skip yt-dlp intermediates (e.g. foo.f140.m4a, foo.temp.opus).
|
|
||||||
if (preg_match('/\.(?:f\d+|temp|part)$/i', $base)) continue;
|
|
||||||
|
|
||||||
$path = $file->getRealPath();
|
|
||||||
$meta = $this->probe($ffprobe, $path);
|
|
||||||
$tracks[] = [
|
|
||||||
'title' => $this->cleanTitle($base),
|
|
||||||
'file' => 'file://' . $this->urlEncodePath($path),
|
|
||||||
'durationSec' => isset($meta['durationSec']) ? (int) $meta['durationSec'] : null,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$doc = [
|
|
||||||
'syncedAt' => gmdate('Y-m-d\TH:i:s\Z'),
|
|
||||||
'sourceDirs' => $roots,
|
|
||||||
'trackCount' => count($tracks),
|
|
||||||
'tracks' => $tracks,
|
|
||||||
];
|
|
||||||
$path = rtrim(config('rig.storage_data'), '/') . '/playlist.json';
|
|
||||||
@mkdir(dirname($path), 0775, true);
|
|
||||||
file_put_contents(
|
|
||||||
$path,
|
|
||||||
json_encode($doc, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->line('');
|
|
||||||
$this->info(" ✓ wrote {$path} ({$doc['trackCount']} tracks)");
|
|
||||||
$this->line(' → refresh the Music Daemon browser source in OBS to reload the queue.');
|
|
||||||
return self::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function probe(string $ffprobe, string $path): array
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$proc = new Process([
|
|
||||||
$ffprobe, '-v', 'error',
|
|
||||||
'-show_entries', 'format=duration:format_tags=title,artist',
|
|
||||||
'-of', 'json',
|
|
||||||
$path,
|
|
||||||
]);
|
|
||||||
$proc->setTimeout(10);
|
|
||||||
$proc->run();
|
|
||||||
if (!$proc->isSuccessful()) return [];
|
|
||||||
$json = json_decode($proc->getOutput(), true) ?: [];
|
|
||||||
$fmt = $json['format'] ?? [];
|
|
||||||
return [
|
|
||||||
'durationSec' => isset($fmt['duration']) ? (float) $fmt['duration'] : null,
|
|
||||||
];
|
|
||||||
} catch (Throwable) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mirrors scripts/playlist.sh's clean_title():
|
|
||||||
* - drop "| ..." (or fullwidth |) tails (genre/label noise)
|
|
||||||
* - strip trailing YouTube IDs in brackets like "[-XxZTgMWKV0]"
|
|
||||||
* - strip "[NCS Release]" / "(NCS10 Release)" suffixes
|
|
||||||
*/
|
|
||||||
private function cleanTitle(string $t): string
|
|
||||||
{
|
|
||||||
$t = preg_replace('/\s*[||].*$/u', '', $t) ?? $t;
|
|
||||||
$t = preg_replace('/\s*\[[A-Za-z0-9_-]{11}\]\s*$/', '', $t) ?? $t;
|
|
||||||
$t = preg_replace(
|
|
||||||
'/\s*[\[(](?:NCS\d*|No Copyright Sounds)(?:\s+Release)?[\])]\s*$/i',
|
|
||||||
'',
|
|
||||||
$t
|
|
||||||
) ?? $t;
|
|
||||||
return trim($t);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function urlEncodePath(string $path): string
|
|
||||||
{
|
|
||||||
// Same as Python's urllib.parse.quote(..., safe='/') — keep separators, encode the rest.
|
|
||||||
return implode('/', array_map('rawurlencode', explode('/', $path)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,53 +3,10 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Support\RigData;
|
use App\Support\RigData;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\Response;
|
|
||||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
||||||
|
|
||||||
class DataController extends Controller
|
class DataController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Serve the playlist as a script-tag-friendly JS global. Cached via ETag on file mtime
|
|
||||||
* so CEF can revalidate cheaply (the playlist file changes rarely but the body is ~142 KB).
|
|
||||||
*/
|
|
||||||
public function playlist(Request $request, RigData $rig)
|
|
||||||
{
|
|
||||||
$path = $rig->playlistPath();
|
|
||||||
$tracks = $rig->playlist();
|
|
||||||
$mtime = is_file($path) ? filemtime($path) : 0;
|
|
||||||
$etag = '"' . md5('playlist:' . $mtime) . '"';
|
|
||||||
|
|
||||||
if ($request->headers->get('If-None-Match') === $etag) {
|
|
||||||
return response('', 304)->header('ETag', $etag);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rewrite `file://` URIs into `/audio?p=...` HTTP URLs so the
|
|
||||||
// music-daemon page (served over HTTP) can load tracks. Chromium
|
|
||||||
// blocks HTTP-origin pages from loading file:// media.
|
|
||||||
if (isset($tracks['tracks']) && is_array($tracks['tracks'])) {
|
|
||||||
foreach ($tracks['tracks'] as $i => $track) {
|
|
||||||
if (!isset($track['file'])) continue;
|
|
||||||
if (str_starts_with($track['file'], 'file://')) {
|
|
||||||
$absolute = rawurldecode(substr($track['file'], 7));
|
|
||||||
$tracks['tracks'][$i]['file'] = \App\Http\Controllers\AudioController::urlFor($absolute);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$body = 'window.__PLAYLIST = ' . json_encode(
|
|
||||||
$tracks,
|
|
||||||
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
|
|
||||||
) . ';';
|
|
||||||
|
|
||||||
return response($body, 200, [
|
|
||||||
'Content-Type' => 'application/javascript; charset=utf-8',
|
|
||||||
'Cache-Control' => 'public, must-revalidate',
|
|
||||||
'ETag' => $etag,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serve bridges/cover.jpg. `no-store` so CEF never holds a stale image
|
* Serve bridges/cover.jpg. `no-store` so CEF never holds a stale image
|
||||||
* even if a caller forgets the ?v=<hash> cache-bust. 404 returns a 1×1
|
* even if a caller forgets the ?v=<hash> cache-bust. 404 returns a 1×1
|
||||||
|
|||||||
@@ -2,9 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Support;
|
namespace App\Support;
|
||||||
|
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
class RigData
|
class RigData
|
||||||
{
|
{
|
||||||
private array $cache = [];
|
private array $cache = [];
|
||||||
|
private ?int $mpdSongCount = null;
|
||||||
|
|
||||||
public function loading(): array
|
public function loading(): array
|
||||||
{
|
{
|
||||||
@@ -23,17 +27,26 @@ class RigData
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function playlist(): array
|
/**
|
||||||
{
|
* Track count from MPD (the live library, queried via `mpc stats`).
|
||||||
return $this->read('playlist.json', [
|
* Returns 0 if mpc is missing or MPD is unreachable — callers treat
|
||||||
'syncedAt' => null, 'sourceDirs' => [], 'trackCount' => 0, 'tracks' => [],
|
* that as "empty library" and degrade their flavor text accordingly.
|
||||||
]);
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
public function playlistCount(): int
|
public function playlistCount(): int
|
||||||
{
|
{
|
||||||
$p = $this->playlist();
|
if ($this->mpdSongCount !== null) return $this->mpdSongCount;
|
||||||
return (int) ($p['trackCount'] ?? count($p['tracks'] ?? []));
|
|
||||||
|
try {
|
||||||
|
$proc = new Process(['mpc', 'stats']);
|
||||||
|
$proc->setTimeout(2);
|
||||||
|
$proc->run();
|
||||||
|
if ($proc->isSuccessful() && preg_match('/^Songs:\s*(\d+)/m', $proc->getOutput(), $m)) {
|
||||||
|
return $this->mpdSongCount = (int) $m[1];
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
return $this->mpdSongCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function cmd(): array
|
public function cmd(): array
|
||||||
@@ -49,11 +62,6 @@ class RigData
|
|||||||
return $cmd;
|
return $cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function playlistPath(): string
|
|
||||||
{
|
|
||||||
return rtrim(config('rig.storage_data'), '/') . '/playlist.json';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function coverPath(): string
|
public function coverPath(): string
|
||||||
{
|
{
|
||||||
return (string) config('rig.cover_jpg');
|
return (string) config('rig.cover_jpg');
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
|
|
||||||
@push('data-scripts')
|
@push('data-scripts')
|
||||||
<script>window.__TRACK_COUNT = @json($trackCount);</script>
|
<script>window.__TRACK_COUNT = @json($trackCount);</script>
|
||||||
<script src="{{ asset('data/playlist.js') }}"></script>
|
|
||||||
@endpush
|
@endpush
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
@@ -140,8 +139,7 @@
|
|||||||
return parts.join(' ');
|
return parts.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
const playlistData = window.__PLAYLIST || { tracks: [] };
|
const trackCount = window.__TRACK_COUNT || 0;
|
||||||
const allTracks = playlistData.tracks || [];
|
|
||||||
let bootStarted = false;
|
let bootStarted = false;
|
||||||
let bootFinished = false;
|
let bootFinished = false;
|
||||||
let lastPlaybackModes = null;
|
let lastPlaybackModes = null;
|
||||||
@@ -206,12 +204,12 @@
|
|||||||
bootStarted = true;
|
bootStarted = true;
|
||||||
lastPlaybackModes = normalizePlayback(s.playback);
|
lastPlaybackModes = normalizePlayback(s.playback);
|
||||||
await typeCommand(formatMusicCommand(s.playback));
|
await typeCommand(formatMusicCommand(s.playback));
|
||||||
if (allTracks.length === 0) {
|
if (trackCount === 0) {
|
||||||
append('> ', '[audio] no tracks queued', 'term-dim');
|
append('> ', '[audio] no tracks queued', 'term-dim');
|
||||||
bootFinished = true;
|
bootFinished = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
append('> ', `[audio] queue retained: ${allTracks.length} tracks`, 'term-out');
|
append('> ', `[audio] queue retained: ${trackCount} tracks`, 'term-out');
|
||||||
await sleep(180);
|
await sleep(180);
|
||||||
append('> ', '[audio] subscribing to mpd:state events', 'term-out');
|
append('> ', '[audio] subscribing to mpd:state events', 'term-out');
|
||||||
await sleep(180);
|
await sleep(180);
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
window.__TEL = @json($telemetry);
|
window.__TEL = @json($telemetry);
|
||||||
window.__TRACK_COUNT = @json($trackCount);
|
window.__TRACK_COUNT = @json($trackCount);
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ asset('data/playlist.js') }}"></script>
|
|
||||||
@endpush
|
@endpush
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
@@ -142,8 +141,7 @@
|
|||||||
return parts.join(' ');
|
return parts.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
const playlistData = window.__PLAYLIST || { tracks: [] };
|
const trackCount = window.__TRACK_COUNT || 0;
|
||||||
const allTracks = playlistData.tracks || [];
|
|
||||||
let bootStarted = false;
|
let bootStarted = false;
|
||||||
let bootFinished = false;
|
let bootFinished = false;
|
||||||
let lastPlaybackModes = null;
|
let lastPlaybackModes = null;
|
||||||
@@ -214,13 +212,13 @@
|
|||||||
lastPlaybackModes = normalizePlayback(s.playback);
|
lastPlaybackModes = normalizePlayback(s.playback);
|
||||||
|
|
||||||
await typeCommand(formatMusicCommand(s.playback));
|
await typeCommand(formatMusicCommand(s.playback));
|
||||||
if (allTracks.length === 0) {
|
if (trackCount === 0) {
|
||||||
append('> ', '[audio] no tracks queued — run `php artisan rig:playlist`', 'term-dim');
|
append('> ', '[audio] mpd library empty — check music_directory + run `mpc update`', 'term-dim');
|
||||||
bootFinished = true;
|
bootFinished = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
append('> ', `[audio] indexed ${allTracks.length} tracks`, 'term-out');
|
append('> ', `[audio] indexed ${trackCount} tracks`, 'term-out');
|
||||||
await sleep(180);
|
await sleep(180);
|
||||||
append('> ', `[audio] uplink to daemon @ ophi-118://${rigName.toLowerCase()}.audio.bus`, 'term-out');
|
append('> ', `[audio] uplink to daemon @ ophi-118://${rigName.toLowerCase()}.audio.bus`, 'term-out');
|
||||||
await sleep(180);
|
await sleep(180);
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
|
|
||||||
@push('data-scripts')
|
@push('data-scripts')
|
||||||
<script>window.__TRACK_COUNT = @json($trackCount);</script>
|
<script>window.__TRACK_COUNT = @json($trackCount);</script>
|
||||||
<script src="{{ asset('data/playlist.js') }}"></script>
|
|
||||||
@endpush
|
@endpush
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
@@ -72,8 +71,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
const pad = HUD.pad;
|
const pad = HUD.pad;
|
||||||
|
|
||||||
const playlistData = window.__PLAYLIST || { tracks: [] };
|
const trackCount = window.__TRACK_COUNT || 0;
|
||||||
const trackCount = (playlistData.tracks || []).length || (window.__TRACK_COUNT || 0);
|
|
||||||
document.getElementById('rig').textContent =
|
document.getElementById('rig').textContent =
|
||||||
trackCount > 0 ? `LIBRARY :: ${trackCount} TRACKS` : '— EMPTY LIBRARY —';
|
trackCount > 0 ? `LIBRARY :: ${trackCount} TRACKS` : '— EMPTY LIBRARY —';
|
||||||
|
|
||||||
|
|||||||
@@ -165,7 +165,6 @@
|
|||||||
|
|
||||||
@push('data-scripts')
|
@push('data-scripts')
|
||||||
<script>window.__TRACK_COUNT = @json($trackCount);</script>
|
<script>window.__TRACK_COUNT = @json($trackCount);</script>
|
||||||
<script src="{{ asset('data/playlist.js') }}"></script>
|
|
||||||
@endpush
|
@endpush
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
@@ -251,8 +250,7 @@
|
|||||||
const fmtClock = s => (!isFinite(s) || s < 0)
|
const fmtClock = s => (!isFinite(s) || s < 0)
|
||||||
? '--:--' : `${pad(Math.floor(s / 60))}:${pad(Math.floor(s % 60))}`;
|
? '--:--' : `${pad(Math.floor(s / 60))}:${pad(Math.floor(s % 60))}`;
|
||||||
|
|
||||||
const playlistData = window.__PLAYLIST || { tracks: [] };
|
const trackCount = window.__TRACK_COUNT || 0;
|
||||||
const trackCount = (playlistData.tracks || []).length || (window.__TRACK_COUNT || 0);
|
|
||||||
|
|
||||||
const term = document.getElementById('termOutput');
|
const term = document.getElementById('termOutput');
|
||||||
const pinnedBlock = document.getElementById('pinnedBlock');
|
const pinnedBlock = document.getElementById('pinnedBlock');
|
||||||
|
|||||||
@@ -85,7 +85,6 @@
|
|||||||
|
|
||||||
<div class="group-title">DATA ENDPOINTS</div>
|
<div class="group-title">DATA ENDPOINTS</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a class="route" href="/data/playlist.js">/data/playlist.js</a> <span class="note">— window.__PLAYLIST</span></li>
|
|
||||||
<li><a class="route" href="/cover.jpg">/cover.jpg</a> <span class="note">— current album art</span></li>
|
<li><a class="route" href="/cover.jpg">/cover.jpg</a> <span class="note">— current album art</span></li>
|
||||||
<li><a class="route" href="/track">/track?p=<base64></a> <span class="note">— audio stream</span></li>
|
<li><a class="route" href="/track">/track?p=<base64></a> <span class="note">— audio stream</span></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ Route::prefix('music-box')->controller(MusicBoxController::class)->group(functio
|
|||||||
Route::get('nc', 'nc');
|
Route::get('nc', 'nc');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::get('/data/playlist.js', [DataController::class, 'playlist']);
|
|
||||||
Route::get('/cover.jpg', [DataController::class, 'coverImage']);
|
Route::get('/cover.jpg', [DataController::class, 'coverImage']);
|
||||||
Route::get('/track', [AudioController::class, 'stream']);
|
Route::get('/track', [AudioController::class, 'stream']);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user