diff --git a/client/src-tauri/src/mpv.rs b/client/src-tauri/src/mpv.rs index 9dc0009..45394d6 100644 --- a/client/src-tauri/src/mpv.rs +++ b/client/src-tauri/src/mpv.rs @@ -117,9 +117,14 @@ impl Mpv { } pub fn load_file(&mut self, video_url: &str, audio_url: &str) -> Result<(), String> { - // Pass audio-file option during load so both streams sync from the start let options = format!("audio-file={}", audio_url); - self.command(&["loadfile", video_url, "replace", &options]) + let resp = self.send_and_recv(json!({ + "command": ["loadfile", video_url, "replace", -1, options] + }))?; + if resp.get("error").and_then(|e| e.as_str()) != Some("success") { + return Err(format!("mpv error: {}", resp.get("error").unwrap_or(&Value::Null))); + } + Ok(()) } pub fn seek(&mut self, time: f64) -> Result<(), String> { diff --git a/client/src/components/FileBrowser.svelte b/client/src/components/FileBrowser.svelte index ae22bf8..05a3154 100644 --- a/client/src/components/FileBrowser.svelte +++ b/client/src/components/FileBrowser.svelte @@ -162,10 +162,12 @@ display: flex; justify-content: space-between; font-size: 12px; + white-space: nowrap; } .file-list li:hover { background: #333; } .file-list li.selected { background: #0066cc; } .file-list li.folder { color: #88aaff; } - .size { color: #888; font-size: 11px; } - .badge { color: #666; font-size: 10px; } + .name { flex: 1; overflow: hidden; text-overflow: ellipsis; } + .size { flex-shrink: 0; margin-left: 8px; color: #888; font-size: 11px; } + .badge { flex-shrink: 0; margin-left: 8px; color: #666; font-size: 10px; } diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index e40bebf..4cd7f26 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -63,6 +63,25 @@ export function audioUrl(path: string, root: string): string { return `${serverUrl}/api/audio/${encodePath(path)}?root=${encodeURIComponent(root)}`; } +/** Poll cache status until both video and audio are ready. */ +export async function waitForCache( + path: string, root: string, quality: string, + signal: AbortSignal, interval = 2000 +): Promise { + const url = `${serverUrl}/api/cache/status/${encodePath(path)}?root=${encodeURIComponent(root)}`; + // Trigger transcode/audio extraction by hitting stream+audio once + await fetch(streamUrl(path, root, quality), { signal }).catch(() => {}); + await fetch(audioUrl(path, root), { signal }).catch(() => {}); + + while (!signal.aborted) { + const res = await fetch(url, { signal }); + const status = await res.json(); + if (status[quality] === "ready" && status.audio === "ready") return; + await new Promise(r => setTimeout(r, interval)); + } + throw new Error("Aborted"); +} + export function cacheStatus(path: string, root: string): Promise> { return get(`/api/cache/status/${encodePath(path)}?root=${encodeURIComponent(root)}`); } diff --git a/client/src/routes/+page.svelte b/client/src/routes/+page.svelte index 53ab6c5..91e6625 100644 --- a/client/src/routes/+page.svelte +++ b/client/src/routes/+page.svelte @@ -5,7 +5,7 @@ import ExportPanel from "../components/ExportPanel.svelte"; import ProfileBar from "../components/ProfileBar.svelte"; import { mpvStart, mpvLoad, mpvSeek, mpvPause, mpvResume, mpvSetLoop, mpvClearLoop, mpvTimePos, mpvDuration } from "$lib/mpv"; - import { streamUrl, audioUrl, deleteExport, getMarkers } from "$lib/api"; + import { streamUrl, audioUrl, waitForCache, deleteExport, getMarkers } from "$lib/api"; import { connectExportWs, disconnectExportWs } from "$lib/ws"; import { loadSettings, saveSettings } from "$lib/settings"; import { @@ -48,16 +48,24 @@ }); // Load file into mpv when currentFile OR quality changes + let loadAbort: AbortController | null = null; $effect(() => { const file = $currentFile; const q = $quality; if (file) { + // Cancel any previous polling + loadAbort?.abort(); + const ac = new AbortController(); + loadAbort = ac; + const vUrl = streamUrl(file.path, file.root, q); const aUrl = audioUrl(file.path, file.root); - mpvLoad(vUrl, aUrl).then(async () => { + waitForCache(file.path, file.root, q, ac.signal).then(() => + mpvLoad(vUrl, aUrl) + ).then(async () => { await new Promise(r => setTimeout(r, 500)); try { $duration = await mpvDuration(); } catch {} - }); + }).catch(() => {}); // aborted or error } }); @@ -198,6 +206,8 @@ } .sidebar { width: 220px; + min-width: 220px; + flex-shrink: 0; border-right: 1px solid #333; overflow: hidden; }