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 * even if a caller forgets the ?v= cache-bust. 404 returns a 1×1 * transparent PNG so the music-box cover placeholder doesn't get a * broken-image icon while the bridge is starting up. */ public function coverImage(RigData $rig): SymfonyResponse { $path = $rig->coverPath(); if (is_file($path) && is_readable($path)) { return response()->file($path, [ 'Content-Type' => 'image/jpeg', 'Cache-Control' => 'no-store, max-age=0', ]); } // 1x1 transparent PNG (67 bytes). $png = base64_decode( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=' ); return response($png, 200, [ 'Content-Type' => 'image/png', 'Cache-Control' => 'no-store, max-age=0', ]); } }