fix(downloads): a conversion's commit, a stopped upload, the fast join and a clip's copy hold up

- The fast join gives each ffconcat entry its own media-only format whitelist, so a library file
  can no longer be opened as a nested concat list (the list's whitelist was handed down to it).
- A file the guarded probe cannot read is a download-only file, not a guess from yt-dlp's info.
- A conversion's or an ingest's commit that raises is re-read in a fresh session before any file
  is removed: a commit that landed keeps the file its row now names.
- An original that cannot be removed after conversion is logged; the upload stays ready.
- An upload whose conversion is cancelled or deleted midway is freed once nothing holds it; a
  paused one is kept.
- A shared link says "preparing" only while its video's job is queued or running.
- The admin storage view counts every file on disk ("Files on disk"), not only ready ones.
- A clip's failure title, unknown failure and cancel dialog, and a clip's and an upload's delete
  dialog speak of what the job made; an upload's quota refusal explains that the original is kept.
- /watch announces "preparing" as an alert and moves focus to the new Retry after an action.
This commit is contained in:
2026-10-06 21:02:07 +02:00
parent d44d8d6dda
commit ed6facb703
19 changed files with 661 additions and 88 deletions
+24 -4
View File
@@ -114,6 +114,10 @@ export default function WatchPage() {
// Bumped by Retry, which asks again.
const [attempt, setAttempt] = useState(0);
// Set once the viewer acted (Retry, the password): the state that answers replaces the button they
// were on, so its own Retry takes the focus rather than leaving it on <body>. Not on the first load,
// where nothing had the focus to lose.
const [acted, setActed] = useState(false);
useEffect(() => {
if (!token) return; // the seed above already says "gone"
send(`/api/public/watch/${encodeURIComponent(token)}`)
@@ -136,6 +140,7 @@ export default function WatchPage() {
.catch(() => setStatus("error"));
}, [token, attempt]);
const retry = () => {
setActed(true);
setStatus("loading");
setAttempt((n) => n + 1);
};
@@ -167,6 +172,7 @@ export default function WatchPage() {
};
const unlock = async () => {
setActed(true);
setUnlocking(true);
setError(null);
try {
@@ -203,11 +209,19 @@ export default function WatchPage() {
{status === "preparing" && (
<div className="flex-1 grid place-items-center text-center">
{/* Announced like the failure below: it replaces the loading line. */}
<div role="status">
{/* An alert, like the failure below: it is inserted already filled, which a polite status
region is not reliably announced for — and a Retry that answers "still preparing"
would otherwise say nothing at all. */}
<div role="alert">
<div className="text-lg font-semibold">{T.preparing}</div>
<div className="text-sm text-muted mt-1">{T.preparingHint}</div>
<Button variant="primary" size="md" onClick={retry} className="mt-4">
<Button
variant="primary"
size="md"
onClick={retry}
autoFocus={acted}
className="mt-4"
>
{T.retry}
</Button>
</div>
@@ -220,7 +234,13 @@ export default function WatchPage() {
<div role="alert">
<div className="text-lg font-semibold">{T.failed}</div>
<div className="text-sm text-muted mt-1">{T.failedHint}</div>
<Button variant="primary" size="md" onClick={retry} className="mt-4">
<Button
variant="primary"
size="md"
onClick={retry}
autoFocus={acted}
className="mt-4"
>
{T.retry}
</Button>
</div>