POST /api/downloads/clip takes a source (a YouTube id or a Plex rating key), a range of at most ten minutes and, for Plex, the audio track being heard. The clip becomes a per-user asset in the library like an editor clip, shareable as soon as it is ready. A YouTube clip fetches only its range (yt-dlp download_ranges: 3-7 s whatever the video's length); a Plex clip reads the library file in place. Both then go through one re-encode: frame-exact at the marks, H.264 at most 1080p with HDR tonemapped to SDR, stereo AAC, no chapter track, .mp4. Job kinds are a JobKind StrEnum now, and Re-download refuses every kind but a download.
41 lines
2.2 KiB
TypeScript
41 lines
2.2 KiB
TypeScript
import type { DownloadJob, DownloadJobKind } from "./api";
|
|
|
|
// The i18n keys that explain a failed download, most specific first — the list form `t()` takes, so
|
|
// the first key a locale has wins. A failure-catalog entry (`error_detail`, e.g. `facebook.login_wall`)
|
|
// names what went wrong on that site and what to do; the category (`error_code`) is the generic
|
|
// sentence, and the fallback when an older client meets a detail it has no copy for; a category it has
|
|
// no copy for falls back to the unknown one, so the row never prints a key. A job that is not a
|
|
// download (an upload's conversion, a clip) may word a category its own way, ahead of the shared
|
|
// sentence — "this download was stopped" is not what happened to it — and its own unknown sentence
|
|
// comes before the download's. One helper for the queue row's headline and the failure window, so
|
|
// the two can never explain the same job differently.
|
|
export function failCopyKeys(
|
|
job: Pick<DownloadJob, "error_code" | "error_detail" | "job_kind">,
|
|
part: "short" | "body",
|
|
): string[] {
|
|
// A clip from YouTube or Plex is worded as the editor's clip: both are "making the clip".
|
|
const kind = job.job_kind === "clip" ? "edit" : (job.job_kind ?? "download");
|
|
const category = (code: string) => [
|
|
...(kind === "download" ? [] : [`downloads.errors.fail.${kind}.${code}.${part}`]),
|
|
`downloads.errors.fail.cat.${code}.${part}`,
|
|
];
|
|
const code = job.error_code || "unknown";
|
|
return [
|
|
...(job.error_detail ? [`downloads.errors.fail.catalog.${job.error_detail}.${part}`] : []),
|
|
...category(code),
|
|
...(code === "unknown" ? [] : category("unknown")),
|
|
];
|
|
}
|
|
|
|
const TITLE_KEYS: Record<DownloadJobKind, string> = {
|
|
download: "downloads.errors.fail.title",
|
|
upload: "downloads.errors.fail.titleUpload", // its failure is a conversion, not a download
|
|
edit: "downloads.errors.fail.titleEdit", // a clip being cut
|
|
clip: "downloads.errors.fail.titleEdit", // a clip being cut, from YouTube or Plex
|
|
};
|
|
|
|
/** The title over a failed job's explanation, worded for what the job was making. */
|
|
export function failTitleKey(job: Pick<DownloadJob, "job_kind">): string {
|
|
return TITLE_KEYS[job.job_kind ?? "download"];
|
|
}
|