fix(r5): address the /code-review high round — 10 findings

- HLS sweep only deletes directories matching the session-name shape; the root
  is admin-configurable and may be a shared path
- playlist unwrap picks the first entry that actually downloaded, not entries[0]
- concat quoting keeps backslashes literal (ffmpeg treats them so inside '')
- ArrowUp/Down defer while the focused element can still scroll that way
- the staging fallback skips yt-dlp working files (.part-Frag/.ytdl/.temp/.fNNN)
- the boot HLS sweep runs off the event loop
- volume persistence is debounced (was a setItem per wheel notch) and a spin no
  longer loses notches to the iframe's lagging getVolume
- _Breaker.run owns the whole open/call/record protocol; four call sites shrink
- should_prune extracted and unit-tested, plus RSS error-vs-empty tests
- the worker exits non-zero when the schema never lands
This commit is contained in:
2026-07-23 23:25:06 +02:00
parent 5130584d0a
commit 2a7f49c981
9 changed files with 307 additions and 79 deletions
+58 -11
View File
@@ -55,6 +55,21 @@ type LoopMode = "off" | "one" | "all";
const AUTO_MODES: AutoMode[] = ["off", "next", "prev", "random"];
const LOOP_MODES: LoopMode[] = ["off", "one", "all"];
/** Can `el` (or an ancestor inside the modal) still scroll vertically in `dir` (-1 up, 1 down)?
* Used to leave ArrowUp/ArrowDown alone while there is content to scroll — the volume shortcut
* must not eat the only keyboard way to read a long description. Stops at `<body>`: the page
* behind the modal is `overflow:hidden` anyway. */
function canScrollBy(el: HTMLElement | null, dir: -1 | 1): boolean {
for (let node = el; node && node !== document.body; node = node.parentElement) {
const style = getComputedStyle(node);
if (!/(auto|scroll|overlay)/.test(style.overflowY)) continue;
const room =
dir < 0 ? node.scrollTop > 0 : node.scrollTop + node.clientHeight < node.scrollHeight - 1; // -1: sub-pixel heights
if (room) return true;
}
return false;
}
// --- IFrame Player API loader (singleton) ---
let apiPromise: Promise<any> | null = null;
function loadYouTubeApi(): Promise<any> {
@@ -168,9 +183,15 @@ export default function PlayerModal({
const [playerPrefs, patchPlayerPrefs] = useAccountPersistedObject(LS.ytPlayerPrefs, {
volume: 100,
});
// The window/wheel listeners below are bound once, so they must read the LIVE prefs.
const prefsRef = useRef(playerPrefs);
prefsRef.current = playerPrefs;
// The live volume: updated on every notch, while the localStorage write behind it is debounced
// (a wheel spin fires ~16 events a second, and each patch is a synchronous setItem on the frame
// the player is decoding on). Everything that READS the level reads this ref, so it never sees
// the stale persisted value mid-spin. The window/wheel listeners are bound once, so a ref — not
// state — is what they can safely close over.
const volumeRef = useRef(playerPrefs.volume);
const volSaveTimerRef = useRef<number | undefined>(undefined);
// When the last volume gesture happened — see nudgeVolume (a spin must not re-read the player).
const lastNudgeAtRef = useRef(0);
const focusModal = () => cardRef.current?.focus();
const togglePlay = () => {
@@ -193,6 +214,11 @@ export default function PlayerModal({
};
/** Push a level (0100) to the player and remember it; 0 also mutes (a player left at zero
* volume but unmuted shows YouTube's speaker icon as ON, which reads as broken). */
const persistVolume = () => {
window.clearTimeout(volSaveTimerRef.current);
volSaveTimerRef.current = undefined;
patchPlayerPrefs({ volume: volumeRef.current });
};
const applyVolume = (level: number, flash = true) => {
const next = Math.max(0, Math.min(100, Math.round(level)));
const p = playerRef.current;
@@ -201,15 +227,29 @@ export default function PlayerModal({
p.setVolume(next);
if (next === 0) p.mute?.();
}
patchPlayerPrefs({ volume: next });
volumeRef.current = next;
// Debounced: one write when the spin settles, not one per notch. The unmount cleanup flushes
// a pending write, so the last change is never lost.
window.clearTimeout(volSaveTimerRef.current);
volSaveTimerRef.current = window.setTimeout(persistVolume, 400);
if (flash) flashVolume(next);
};
const nudgeVolume = (delta: number) => {
const p = playerRef.current;
if (!p || typeof p.getVolume !== "function") return;
// Read the live player (the user may have used YouTube's own slider), falling back to the
// stored level when the player can't answer yet.
const cur = p.isMuted?.() ? 0 : (p.getVolume?.() ?? prefsRef.current.volume);
// Whose level do we step from? Our own ref, EXCEPT when the gesture starts fresh — then read
// the player, because the user may have moved YouTube's own slider in the meantime.
// `getVolume()` is answered across the iframe boundary and lags our `setVolume` by a beat, so
// re-reading it mid-spin resurrects a stale level and silently drops notches (measured: five
// wheel notches moved 100 → 90 instead of 75).
const now = Date.now();
const continuing = now - lastNudgeAtRef.current < 1000;
lastNudgeAtRef.current = now;
const cur = continuing
? volumeRef.current
: p.isMuted?.()
? 0
: (p.getVolume?.() ?? volumeRef.current);
applyVolume(cur + delta);
};
@@ -425,10 +465,14 @@ export default function PlayerModal({
togglePlay();
} else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
// Volume by keyboard (YouTube/Plex-style ±5). The only volume control that survives
// fullscreen, where the wheel goes to YouTube's own iframe.
if (typing) return;
// fullscreen, where the wheel goes to YouTube's own iframe. But the modal card scrolls
// (a long description) and Up/Down is how you scroll it from the keyboard — so defer
// whenever the focused element can still scroll that way, exactly like Space defers to a
// focused button.
const up = e.key === "ArrowUp";
if (typing || canScrollBy(el, up ? -1 : 1)) return;
e.preventDefault();
nudgeVolume(e.key === "ArrowUp" ? 5 : -5);
nudgeVolume(up ? 5 : -5);
} else if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
// Plain arrows seek within the video (YouTube-style ±5s); Shift+arrow steps to the
// previous/next video in the queue (the feed's order or a playlist).
@@ -453,6 +497,9 @@ export default function PlayerModal({
window.clearTimeout(closeTimer.current);
window.clearTimeout(openTimer.current);
window.clearTimeout(volTimerRef.current);
// Flush a debounced volume write — closing the player right after a wheel spin (the normal
// way to leave) must not drop the level the user just set.
if (volSaveTimerRef.current != null) persistVolume();
};
}, [onClose]);
@@ -582,7 +629,7 @@ export default function PlayerModal({
// restore the remembered volume — a fresh player always starts at 100.
onReady: () => {
focusModal();
applyVolume(prefsRef.current.volume, false);
applyVolume(volumeRef.current, false);
},
onStateChange: (e: any) => {
// 1 === playing → sync the navigated-to video's title/author for display.