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
+31 -11
View File
@@ -16,7 +16,7 @@ import {
Trash2,
} from "lucide-react";
import { qk } from "../lib/queryKeys";
import { api, type DownloadJob } from "../lib/api";
import { api, type DownloadJob, type DownloadJobKind } from "../lib/api";
import { notifyActionError } from "../lib/actionErrors";
import { invalidateDownloads, quickShareLink } from "../lib/downloads";
import { libraryRowActions, sharedRowActions } from "../lib/downloadActions";
@@ -46,6 +46,29 @@ const CANCEL_UPLOAD = {
body: "downloads.confirm.cancelUploadBody",
action: "downloads.confirm.cancelUploadAction",
};
const CANCEL_EDIT = {
title: "downloads.confirm.cancelEditTitle",
body: "downloads.confirm.cancelEditBody",
action: "downloads.confirm.cancelEditAction",
};
// Cancelling an upload deletes the file it uploaded (there is nothing to fetch again), so its
// confirmation says so — "stop the download" would read as safe to undo. A clip is being cut, not
// downloaded.
const CANCEL_COPY: Record<DownloadJobKind, typeof CANCEL_DOWNLOAD> = {
download: CANCEL_DOWNLOAD,
upload: CANCEL_UPLOAD,
edit: CANCEL_EDIT,
};
// Deleting a download may leave its shared cache file for other holders. An upload and a clip are
// their owner's alone (a clip's asset is keyed by its user), so deleting one removes the file for good.
const DELETE_COPY: Record<DownloadJobKind, { title: string; body: string }> = {
download: { title: "downloads.confirm.deleteTitle", body: "downloads.confirm.deleteBody" },
upload: {
title: "downloads.confirm.deleteUploadTitle",
body: "downloads.confirm.deleteUploadBody",
},
edit: { title: "downloads.confirm.deleteEditTitle", body: "downloads.confirm.deleteEditBody" },
};
export function useDownloadActions({
onPlay,
@@ -159,10 +182,8 @@ export function useDownloadActions({
label: t("downloads.actions.cancel"),
icon: icon(Ban),
danger: true,
// Cancelling an upload deletes the file it uploaded (there is nothing to fetch again), so
// its confirmation says so — "stop the download" would read as safe to undo.
onSelect: () => {
const copy = job.job_kind === "upload" ? CANCEL_UPLOAD : CANCEL_DOWNLOAD;
const copy = CANCEL_COPY[job.job_kind ?? "download"];
void confirmThen(t(copy.title), t(copy.body), t(copy.action), () =>
act.mutate({ id: job.id, op: "cancel" }),
);
@@ -233,13 +254,12 @@ export function useDownloadActions({
label: t("downloads.actions.delete"),
icon: icon(Trash2),
danger: true,
onSelect: () =>
void confirmThen(
t("downloads.confirm.deleteTitle"),
t("downloads.confirm.deleteBody"),
t("downloads.actions.delete"),
() => act.mutate({ id: job.id, op: "delete" }),
),
onSelect: () => {
const copy = DELETE_COPY[job.job_kind ?? "download"];
void confirmThen(t(copy.title), t(copy.body), t("downloads.actions.delete"), () =>
act.mutate({ id: job.id, op: "delete" }),
);
},
},
],
);