fix: mpv loadfile index arg, cache polling, and sidebar CSS

- Pass integer index (-1) to mpv loadfile command for newer mpv versions
- Poll /api/cache/status instead of streaming endpoints to avoid
  downloading video bodies during readiness checks
- Cancel previous polling when selecting a new file
- Fix sidebar flex-shrink and file name text overflow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 08:17:23 +02:00
parent 2b6c56cd15
commit a67e189aa0
4 changed files with 43 additions and 7 deletions
+7 -2
View File
@@ -117,9 +117,14 @@ impl Mpv {
} }
pub fn load_file(&mut self, video_url: &str, audio_url: &str) -> Result<(), String> { 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); 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> { pub fn seek(&mut self, time: f64) -> Result<(), String> {
+4 -2
View File
@@ -162,10 +162,12 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
font-size: 12px; font-size: 12px;
white-space: nowrap;
} }
.file-list li:hover { background: #333; } .file-list li:hover { background: #333; }
.file-list li.selected { background: #0066cc; } .file-list li.selected { background: #0066cc; }
.file-list li.folder { color: #88aaff; } .file-list li.folder { color: #88aaff; }
.size { color: #888; font-size: 11px; } .name { flex: 1; overflow: hidden; text-overflow: ellipsis; }
.badge { color: #666; font-size: 10px; } .size { flex-shrink: 0; margin-left: 8px; color: #888; font-size: 11px; }
.badge { flex-shrink: 0; margin-left: 8px; color: #666; font-size: 10px; }
</style> </style>
+19
View File
@@ -63,6 +63,25 @@ export function audioUrl(path: string, root: string): string {
return `${serverUrl}/api/audio/${encodePath(path)}?root=${encodeURIComponent(root)}`; 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<void> {
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<Record<string, string>> { export function cacheStatus(path: string, root: string): Promise<Record<string, string>> {
return get(`/api/cache/status/${encodePath(path)}?root=${encodeURIComponent(root)}`); return get(`/api/cache/status/${encodePath(path)}?root=${encodeURIComponent(root)}`);
} }
+13 -3
View File
@@ -5,7 +5,7 @@
import ExportPanel from "../components/ExportPanel.svelte"; import ExportPanel from "../components/ExportPanel.svelte";
import ProfileBar from "../components/ProfileBar.svelte"; import ProfileBar from "../components/ProfileBar.svelte";
import { mpvStart, mpvLoad, mpvSeek, mpvPause, mpvResume, mpvSetLoop, mpvClearLoop, mpvTimePos, mpvDuration } from "$lib/mpv"; 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 { connectExportWs, disconnectExportWs } from "$lib/ws";
import { loadSettings, saveSettings } from "$lib/settings"; import { loadSettings, saveSettings } from "$lib/settings";
import { import {
@@ -48,16 +48,24 @@
}); });
// Load file into mpv when currentFile OR quality changes // Load file into mpv when currentFile OR quality changes
let loadAbort: AbortController | null = null;
$effect(() => { $effect(() => {
const file = $currentFile; const file = $currentFile;
const q = $quality; const q = $quality;
if (file) { if (file) {
// Cancel any previous polling
loadAbort?.abort();
const ac = new AbortController();
loadAbort = ac;
const vUrl = streamUrl(file.path, file.root, q); const vUrl = streamUrl(file.path, file.root, q);
const aUrl = audioUrl(file.path, file.root); 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)); await new Promise(r => setTimeout(r, 500));
try { $duration = await mpvDuration(); } catch {} try { $duration = await mpvDuration(); } catch {}
}); }).catch(() => {}); // aborted or error
} }
}); });
@@ -198,6 +206,8 @@
} }
.sidebar { .sidebar {
width: 220px; width: 220px;
min-width: 220px;
flex-shrink: 0;
border-right: 1px solid #333; border-right: 1px solid #333;
overflow: hidden; overflow: hidden;
} }