feat(downloads): a per-site failure catalog — Facebook's walls say why and what to do
A failed Facebook download read "Download failed": yt-dlp words the login wall (18+ reel, closed group) as "Cannot parse data", which no category matched. Measured on dev with the owner's cookie before writing a rule — the same text is the login wall without a cookie and a refused cookie with one. - errors.diagnose(msg, url, cookie_attached) tries a host-scoped catalog first (all-of markers, cookie state, link fragment) and falls back to the category. First entries: facebook.login_wall, facebook.cookie_rejected, facebook.group_post_video. The module doc is the recipe for the next entry. - download_jobs.error_detail (0084) + backfill of the stored Facebook parse failures that were unknown. - The pipeline's failure branch passes the link and whether the cookie went with it; resume and redownload clear code and detail (redownload never cleared error_code). - A job meeting an already-failed asset got the text but no code, so it read "Download failed" where the first job said why. It is diagnosed again, with the cookie state from credentials.is_authed_asset (now shared with the cookie gate); the redacted network text classifies back to network. - FE: failCopyKeys gives the row and the failure window one lookup — catalog copy, else the category. Copy in EN and HU; a contract test pins every backend entry to both locales.
This commit is contained in:
@@ -24,6 +24,7 @@ const baseJob: DownloadJob = {
|
||||
phase: null,
|
||||
error: null,
|
||||
error_code: null,
|
||||
error_detail: null,
|
||||
queue_pos: 0,
|
||||
created_at: null,
|
||||
source_kind: "youtube",
|
||||
|
||||
@@ -8,6 +8,7 @@ import { api, type DownloadJob } from "../lib/api";
|
||||
import { inputCls } from "./ui/form";
|
||||
import { Button } from "./ui/Button";
|
||||
import { IconBtn } from "./downloadRow";
|
||||
import { failCopyKeys } from "../lib/downloadFailure";
|
||||
|
||||
// Per-item user modals split out of DownloadCenter (self-contained, prop-driven): rename/metadata
|
||||
// editing and the failure-detail window.
|
||||
@@ -142,8 +143,8 @@ export function ErrorModal({
|
||||
return (
|
||||
<Modal title={t("downloads.errors.fail.title")} onClose={onClose}>
|
||||
<div className="space-y-3">
|
||||
<div className="font-medium">{t(`downloads.errors.fail.cat.${code}.short`)}</div>
|
||||
<p className="text-sm text-muted">{t(`downloads.errors.fail.cat.${code}.body`)}</p>
|
||||
<div className="font-medium">{t(failCopyKeys(job, "short"))}</div>
|
||||
<p className="text-sm text-muted">{t(failCopyKeys(job, "body"))}</p>
|
||||
{job.error && (
|
||||
<div>
|
||||
<button
|
||||
|
||||
@@ -24,6 +24,7 @@ const baseJob: DownloadJob = {
|
||||
phase: null,
|
||||
error: null,
|
||||
error_code: null,
|
||||
error_detail: null,
|
||||
queue_pos: 0,
|
||||
created_at: null,
|
||||
source_kind: "youtube",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { notify } from "../lib/notifications";
|
||||
import { copyText } from "../lib/clipboard";
|
||||
import { formatBytes, formatDuration, formatEta, formatSpeed, relativeTime } from "../lib/format";
|
||||
import { navItemAttrs } from "../lib/listNav";
|
||||
import { failCopyKeys } from "../lib/downloadFailure";
|
||||
|
||||
// Download-row presentation + the small shared primitives (status pill, icon button, usage bar),
|
||||
// split out of DownloadCenter (all purely prop-driven, no shared state). Rendered by the queue,
|
||||
@@ -260,7 +261,7 @@ export function DownloadRow({
|
||||
className="text-danger hover:opacity-80 inline-flex items-center gap-1 truncate"
|
||||
title={t("downloads.errors.fail.title")}
|
||||
>
|
||||
· {t(`downloads.errors.fail.cat.${job.error_code || "unknown"}.short`)}
|
||||
· {t(failCopyKeys(job, "short"))}
|
||||
<span className="text-muted underline">{t("downloads.errors.fail.detailsLink")}</span>
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user