Compare commits

...

2 Commits

Author SHA1 Message Date
Jakub Zych
c69eb19af3 deploy: default MPD music_directory to ~/Music
Midgolem was the last fresh rig to set up and the music library lives
in ~/Music/Kolekcja, so ~/HDD/Music — the path inherited from Ignia's
secondary-disk layout — no longer reflects the common case. Three
references flipped: the ASSET_DIRS ensure list, the mpd.conf heredoc,
and the post-install empty-library check + warn message.
2026-05-22 21:07:30 +02:00
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
13 changed files with 43 additions and 217 deletions

View File

@@ -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:**

View File

@@ -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

View File

@@ -220,7 +220,7 @@ fi
ASSET_DIRS=( ASSET_DIRS=(
"$HOME/Videos" "$HOME/Videos"
"$HOME/HDD/Music" "$HOME/Music"
"$HOME/HDD/Images/Gifs" "$HOME/HDD/Images/Gifs"
"$HOME/cloud.jakubzych.com/_img/logos" "$HOME/cloud.jakubzych.com/_img/logos"
) )
@@ -372,7 +372,7 @@ write_mpd_conf() {
# ~/.config/mpd/mpd.conf — generated by scripts/deploy-rig.sh. # ~/.config/mpd/mpd.conf — generated by scripts/deploy-rig.sh.
# Re-run the deploy script to regenerate, or hand-edit for local tweaks. # Re-run the deploy script to regenerate, or hand-edit for local tweaks.
music_directory "~/HDD/Music" music_directory "~/Music"
playlist_directory "~/.config/mpd/playlists" playlist_directory "~/.config/mpd/playlists"
db_file "~/.config/mpd/database" db_file "~/.config/mpd/database"
state_file "~/.config/mpd/state" state_file "~/.config/mpd/state"
@@ -421,13 +421,13 @@ else
fi fi
fi fi
if [[ -d "$HOME/HDD/Music" ]] && [[ -n "$(find "$HOME/HDD/Music" -maxdepth 3 -type f -print -quit 2>/dev/null)" ]]; then if [[ -d "$HOME/Music" ]] && [[ -n "$(find "$HOME/Music" -maxdepth 3 -type f -print -quit 2>/dev/null)" ]]; then
if would "mpc update"; then if would "mpc update"; then
mpc update >/dev/null 2>&1 || warn "mpc update failed (mpd not yet running?)" mpc update >/dev/null 2>&1 || warn "mpc update failed (mpd not yet running?)"
ok "MPD library scan kicked off" ok "MPD library scan kicked off"
fi fi
else else
warn "~/HDD/Music is empty — MPD will have no tracks until you populate it" warn "~/Music is empty — MPD will have no tracks until you populate it"
fi fi
# Avahi for zeroconf advertisement (system service). # Avahi for zeroconf advertisement (system service).

View File

@@ -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
``` ```

View File

@@ -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)));
}
}

View File

@@ -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

View File

@@ -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');

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 —';

View File

@@ -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');

View File

@@ -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=&lt;base64&gt;</a> <span class="note"> audio stream</span></li> <li><a class="route" href="/track">/track?p=&lt;base64&gt;</a> <span class="note"> audio stream</span></li>
</ul> </ul>

View File

@@ -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']);