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:
@@ -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)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user