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:
@@ -76,6 +76,7 @@ export interface DownloadJob {
|
||||
phase: string | null;
|
||||
error: string | null; // raw yt-dlp/ffmpeg text (technical detail)
|
||||
error_code: string | null; // failure category, see backend downloads.errors.classify
|
||||
error_detail: string | null; // host-specific failure-catalog entry, see downloads.errors.diagnose
|
||||
queue_pos: number;
|
||||
created_at: string | null;
|
||||
source_kind: string;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// A failed download's explanation: the failure-catalog entry first, its category as the fallback
|
||||
// (E11.5 S1). The second case runs the REAL i18next with the real locales, because the fallback is
|
||||
// i18next's list-key lookup, not code of ours — an older client meeting a detail it has no copy for
|
||||
// must still say the category, not print a key.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import i18n from "../i18n";
|
||||
import { failCopyKeys } from "./downloadFailure";
|
||||
|
||||
describe("failCopyKeys", () => {
|
||||
it("puts a catalog entry before its category, and is the category alone without one", () => {
|
||||
expect(
|
||||
failCopyKeys({ error_code: "login_required", error_detail: "facebook.login_wall" }, "body"),
|
||||
).toEqual([
|
||||
"downloads.errors.fail.catalog.facebook.login_wall.body",
|
||||
"downloads.errors.fail.cat.login_required.body",
|
||||
]);
|
||||
expect(failCopyKeys({ error_code: "network", error_detail: null }, "short")).toEqual([
|
||||
"downloads.errors.fail.cat.network.short",
|
||||
]);
|
||||
expect(failCopyKeys({ error_code: null, error_detail: null }, "short")).toEqual([
|
||||
"downloads.errors.fail.cat.unknown.short",
|
||||
]);
|
||||
});
|
||||
|
||||
it("reads the entry's copy, and the category's for an entry this client does not know", async () => {
|
||||
await i18n.changeLanguage("en");
|
||||
expect(
|
||||
i18n.t(
|
||||
failCopyKeys(
|
||||
{ error_code: "login_required", error_detail: "facebook.login_wall" },
|
||||
"short",
|
||||
),
|
||||
),
|
||||
).toBe("Facebook sign-in required");
|
||||
expect(
|
||||
i18n.t(
|
||||
failCopyKeys({ error_code: "login_required", error_detail: "facebook.newer" }, "short"),
|
||||
),
|
||||
).toBe("Sign-in required");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { DownloadJob } 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. 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">,
|
||||
part: "short" | "body",
|
||||
): string[] {
|
||||
const category = `downloads.errors.fail.cat.${job.error_code || "unknown"}.${part}`;
|
||||
return job.error_detail
|
||||
? [`downloads.errors.fail.catalog.${job.error_detail}.${part}`, category]
|
||||
: [category];
|
||||
}
|
||||
Reference in New Issue
Block a user