Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 5df886be3bbe8b7d9eb4724752e8b8e7dd3a1c2b
parent 781493ed9b18aef9cd766ef040a9a8b5d834c871
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 28 Sep 2026 01:42:57 -0400

common: a forced download whose media pass fails keeps the subtitle pass's status

A forceMedia pass over a transcript ("Persist source video", fetch_clip
full: true, Persist kept now) that failed set the whole download `failed`,
so the video page said "Download failed" over a transcript that is fine
(release 10 slice N, review L2). The status now stays the subtitle pass's;
only the failed media attempt (n: 3) is recorded, with its error, and one
log line says so and names yt-dlp's partial (left for a retry to resume).

sourceFetchFailure(record) says whether a download asked for the source got
it: the status for an outright failure, the last attempt for a failed forced
pass. archiveSourceVideo throws on it, so the Persist / whole-recording job
ends `failed` with yt-dlp's line in the log tail the poll returns, instead
of `done` with no file ("finished but named no file" in the MCP).

forceMedia.test.ts +2; the fake yt-dlp's `.fake-ytdlp-media-fail` knob;
fetch-window.spec.ts +1 (the full fetch fails with the line, outcome ok),
persist-youtube-handling.spec.ts +1 (the run fails, no failed badge).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mcommon/ytdlp/downloadOneManaged.ts | 55++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcommon/ytdlp/forceMedia.test.ts | 103++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Meditor/app/channels/[slug]/videos/[id]/videoActions.ts | 17+++++++++++++++--
Meditor/e2e/fetch-window.spec.ts | 56+++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Meditor/e2e/fixtures/bin/fake-ytdlp.mjs | 13+++++++++++++
Meditor/e2e/persist-youtube-handling.spec.ts | 45+++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 284 insertions(+), 5 deletions(-)

diff --git a/common/ytdlp/downloadOneManaged.ts b/common/ytdlp/downloadOneManaged.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import { appendFile, mkdir, readdir, readFile, rm } from "node:fs/promises"; +import { appendFile, mkdir, readdir, readFile, rm, stat } from "node:fs/promises"; import { createWriteStream, type Dirent, type WriteStream } from "node:fs"; import { execa } from "execa"; import { @@ -38,6 +38,7 @@ import { type DownloadOutcomeStatus, } from "../lib/downloadOutcome"; import { writeDownloadOutcome } from "../lib/downloadOutcome-server"; +import { formatBytes } from "../lib/format"; import { recordAvailability } from "../lib/availability-server"; import { withMetadataHistory } from "../lib/metadataHistory-server"; import { @@ -517,6 +518,38 @@ function attemptSucceeded(exitCode: number | null): boolean { return exitCode === 0 || exitCode === 101; } +// DID A DOWNLOAD THAT WAS ASKED FOR THE SOURCE GET IT? (release 11 slice O3.) +// For "Persist source video" and the whole-recording fetch: null when nothing +// failed, else yt-dlp's reason. Two ways to fail: the download failed outright +// (the status says so), or the forced media pass over a transcript failed — +// which leaves the status as the subtitle pass made it, so only the last +// attempt (the n: 3 media pass) says so. Reading the status alone ended those +// jobs `done` with no file. +export function sourceFetchFailure(record: DownloadOutcomeRecord): string | null { + const last = record.attempts.at(-1); + const reason = (fallback: string) => last?.error?.trim() || fallback; + if (record.status === "failed" || record.status === "failed-corrupt-source") { + return reason(`the download ended ${record.status}`); + } + if (last?.kind === "no-subs-fallback" && !attemptSucceeded(last.ytdlpExitCode)) { + return reason(`yt-dlp exited ${last.ytdlpExitCode ?? "without a code"}`); + } + return null; +} + +// yt-dlp's partial source container(s) in a video dir — `source-media.<fmt>.part` +// and its fragments — with their sizes, for the failed-media log line. +async function sourceMediaPartials(videoDir: string): Promise<string[]> { + const entries = await readdir(videoDir).catch(() => [] as string[]); + const out: string[] = []; + for (const name of entries.sort()) { + if (!name.startsWith("source-media.") || !/\.part(?:-Frag\d+)?$/.test(name)) continue; + const st = await stat(path.join(videoDir, name)).catch(() => null); + out.push(st ? `${name} (${formatBytes(st.size)})` : name); + } + return out; +} + async function hasAnyTranscriptOnDisk(videoDir: string): Promise<boolean> { const entries = await readdir(videoDir).catch(() => [] as string[]); return entries.some((e) => { @@ -1467,6 +1500,26 @@ async function runManagedDownload( // fellBackToTranscribe flag distinguishes from a normal "ok" // so the UI and downstream jobs can tell what happened. } + } else if (keepTranscript) { + // THE MEDIA FAILED, THE DOWNLOAD DID NOT (release 11 slice O3). A + // persist-only pass over a transcript is an extra the caller asked for, + // not the download: the subtitle pass succeeded and the transcript is + // on disk, so the status stays the subtitle pass's and only the failed + // media attempt is recorded (n: 3 above, with its error). Marking the + // whole download failed put "Download failed" on a video whose + // transcript is fine. A caller that asked for the source reads the + // attempt through sourceFetchFailure. yt-dlp's partial container is + // left where it is — a retry resumes it — and named here, because + // nothing else surfaces a source-media partial. + const partials = await sourceMediaPartials(videoDir); + opts.onLog( + `forceMedia: the source download failed (yt-dlp exit ${fallbackRes.exitCode}); ` + + `the transcript on disk is untouched and the download stays ${status}.` + + (partials.length + ? ` Left for a retry to resume: ${partials.join(", ")}.` + : "") + + "\n", + ); } else { status = "failed"; lastSucceeded = false; diff --git a/common/ytdlp/forceMedia.test.ts b/common/ytdlp/forceMedia.test.ts @@ -13,7 +13,12 @@ import { tmpdir } from "node:os"; import path from "node:path"; import type { Paths } from "../lib/paths"; import type { ChannelConfig } from "../lib/channelConfig"; -import { downloadOneManaged, type ManagedDownloadOpts } from "./downloadOneManaged"; +import { + downloadOneManaged, + sourceFetchFailure, + type ManagedDownloadOpts, +} from "./downloadOneManaged"; +import type { DownloadOutcomeRecord } from "../lib/downloadOutcome"; // Run with: // pnpm --filter yt-dlp-transcript-common exec tsx --test ytdlp/forceMedia.test.ts @@ -56,6 +61,12 @@ if (has("--skip-download") && has("--write-auto-subs")) { process.exit(0); } if ((arg("-o") || "").includes("source-media")) { + if (fs.existsSync(path.join(root, "media-fail"))) { + // A media pass that dies mid-download: the partial yt-dlp leaves, then a 403. + fs.writeFileSync(path.join(dir, "source-media.f137.mp4.part"), "half a container"); + console.error("ERROR: [download] Got error: HTTP Error 403: Forbidden"); + process.exit(1); + } fs.writeFileSync(path.join(dir, "source-media.mp4"), "container bytes for " + id); console.log("DLOM_ARCHIVE youtube " + id); process.exit(0); @@ -77,6 +88,8 @@ type Run = { videoDir: string; storeDir: string; files: string[]; + // The channel's download archive after the run. + archive: string; }; async function runWith(opts: { @@ -88,6 +101,9 @@ async function runWith(opts: { priorMeta?: Record<string, unknown>; config?: Partial<ChannelConfig>; managed?: Partial<ManagedDownloadOpts>; + // The media pass (the source-media output) fails with a 403 after writing a + // partial. + mediaFail?: boolean; }): Promise<Run & { cleanup: () => Promise<void> }> { const root = await mkdtemp(path.join(tmpdir(), "force-media-")); const ytdlp = path.join(root, "fake-ytdlp.cjs"); @@ -97,6 +113,7 @@ async function runWith(opts: { await chmod(ytdlp, 0o755); await chmod(ffmpeg, 0o755); await writeFile(path.join(root, "meta.json"), JSON.stringify(opts.meta)); + if (opts.mediaFail) await writeFile(path.join(root, "media-fail"), ""); process.env.FAKE_ROOT = root; const paths = { transcriptsDir: root, @@ -144,6 +161,7 @@ async function runWith(opts: { videoDir, storeDir: path.join(paths.savedVideosDir, "c", ID), files: (await readdir(videoDir)).sort(), + archive: await readFile(path.join(paths.channelsDir, "c", "archive"), "utf8").catch(() => ""), cleanup: () => rm(root, { recursive: true, force: true }), }; } @@ -300,3 +318,86 @@ test("the prefetch's rewrite of metadata.info.json lands in metadata.history.jso await r.cleanup(); } }); + +// A FAILED MEDIA PASS OVER A TRANSCRIPT IS NOT A FAILED DOWNLOAD (release 11 +// slice O3; release 10 slice N's review L2). The subtitle pass succeeded and the +// transcript is on disk; the extra the caller asked for — the source — failed. +// The status stays the subtitle pass's (the video page said "Download failed" +// on a video whose transcript is fine), the failed attempt is recorded, and the +// caller that asked for the source learns it through sourceFetchFailure. +test("forceMedia over a transcript whose media pass fails: the download stays ok, only the media attempt failed", async () => { + const r = await runWith({ meta: META, transcript: TRANSCRIPT, managed: FORCED, mediaFail: true }); + try { + assert.equal(r.argvs.length, 3); + assert.equal(r.record.status, "ok"); + assert.equal(r.record.failureClass, undefined); + assert.deepEqual( + r.record.attempts.map((a) => [a.kind, a.ytdlpExitCode]), + [ + ["metadata-prefetch", 0], + ["primary", 0], + ["no-subs-fallback", 1], + ], + ); + assert.match(r.record.attempts[2].error ?? "", /HTTP Error 403: Forbidden/); + assert.match(sourceFetchFailure(r.record) ?? "", /HTTP Error 403: Forbidden/); + assert.match( + r.log, + /forceMedia: the source download failed \(yt-dlp exit 1\); the transcript on disk is untouched and the download stays ok\. Left for a retry to resume: source-media\.f137\.mp4\.part \(16 B\)\.\n/, + ); + // The transcript is untouched and still counts: the video stays in the + // archive as the subtitle pass left it. + assert.equal(await readFile(path.join(r.videoDir, TRANSCRIPT.name), "utf8"), TRANSCRIPT.body); + assert.match(r.archive, new RegExp(`youtube ${ID}`)); + // Nothing persisted; the partial is left for a retry to resume. + assert.ok(!r.files.includes("saved-video.json")); + assert.ok(r.files.includes("source-media.f137.mp4.part"), r.files.join(",")); + // The sidecar on disk says the same as the returned record. + const onDisk = JSON.parse( + await readFile(path.join(r.videoDir, "download-outcome.json"), "utf8"), + ) as DownloadOutcomeRecord; + assert.equal(onDisk.status, "ok"); + } finally { + await r.cleanup(); + } +}); + +test("sourceFetchFailure: a failed download, a failed forced pass, and the clean cases", () => { + const base = { videoId: ID, startedAt: "t0", finishedAt: "t1" }; + const attempt = (kind: DownloadOutcomeRecord["attempts"][number]["kind"], code: number | null, error?: string) => ({ + n: 1, + kind, + handling: "youtube" as const, + usedCookies: false, + ytdlpExitCode: code, + ...(error ? { error } : {}), + }); + assert.equal( + sourceFetchFailure({ ...base, status: "failed", attempts: [attempt("primary", 1, "ERROR: gone")] }), + "ERROR: gone", + ); + assert.equal( + sourceFetchFailure({ ...base, status: "failed-corrupt-source", attempts: [] }), + "the download ended failed-corrupt-source", + ); + assert.equal( + sourceFetchFailure({ + ...base, + status: "ok", + attempts: [attempt("primary", 0), attempt("no-subs-fallback", 1)], + }), + "yt-dlp exited 1", + ); + assert.equal( + sourceFetchFailure({ + ...base, + status: "ok", + attempts: [attempt("primary", 0), attempt("no-subs-fallback", 0)], + }), + null, + ); + assert.equal( + sourceFetchFailure({ ...base, status: "ok-with-cookies", attempts: [attempt("auth-retry", 0)] }), + null, + ); +}); diff --git a/editor/app/channels/[slug]/videos/[id]/videoActions.ts b/editor/app/channels/[slug]/videos/[id]/videoActions.ts @@ -70,7 +70,10 @@ import { } from "yt-dlp-transcript-common/jobs/downloadBackoff"; import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { resolveCookiePolicy } from "yt-dlp-transcript-common/lib/cookiePolicy"; -import { downloadOneManaged } from "yt-dlp-transcript-common/ytdlp/downloadOneManaged"; +import { + downloadOneManaged, + sourceFetchFailure, +} from "yt-dlp-transcript-common/ytdlp/downloadOneManaged"; import { runYtdlp } from "yt-dlp-transcript-common/ytdlp/runYtdlp"; import { runManagedFunction, @@ -291,7 +294,7 @@ async function archiveSourceVideo( onLog(`${requesterLine(persistOrigin)}\n`); } onLog(`Re-downloading ${videoId} to archive its source video…\n`); - await downloadOneManaged({ + const record = await downloadOneManaged({ channelSlug: slug, channelConfig: r.config, paths, @@ -314,6 +317,16 @@ async function archiveSourceVideo( `/channels/${slug}/videos/${videoId}`, `/channels/${slug}`, ]); + // THE JOB IS THE SOURCE, SO ITS STATUS IS THE SOURCE'S (release 11 + // slice O3). A forced media pass that fails over a transcript leaves + // the download `ok` — the transcript is fine — so without this the job + // ended `done` with no file, and the whole-recording fetch's caller was + // told only that it "finished but named no file". Failing the job puts + // yt-dlp's line in the log tail the poll returns. + const failure = sourceFetchFailure(record); + if (failure) { + throw new Error(`The source video was not downloaded: ${failure}`); + } } finally { task.end(); } diff --git a/editor/e2e/fetch-window.spec.ts b/editor/e2e/fetch-window.spec.ts @@ -9,7 +9,7 @@ // Same token as /api/worker/* (the test server runs with // WORKER_TOKEN=test-worker-token; see package.json dev:test). -import { readdir, readFile, stat } from "node:fs/promises"; +import { readdir, readFile, stat, writeFile } from "node:fs/promises"; import { test, expect, type APIRequestContext } from "@playwright/test"; import { generateReport, @@ -455,3 +455,57 @@ test("a full-source fetch on a youtube-handling video that already has a transcr to: `Synthetic ${HAS_TRANSCRIPT}`, }); }); + +// …AND WHEN THAT DOWNLOAD FAILS, THE JOB SAYS SO (release 11 slice O3). The +// forced media pass failing over a transcript is not a failed download — the +// subtitle pass succeeded, the transcript is fine, so the outcome stays `ok` — +// but the job exists for the source, so the job fails, with yt-dlp's line in +// the log tail the poll returns. Before, it ended `done` with no file, and the +// MCP could only say the job "finished but named no file". +test("a full-source fetch whose media download fails ends failed with yt-dlp's line; the download stays ok", async ({ + request, +}) => { + test.setTimeout(90_000); + const HAS_TRANSCRIPT = "fake00000001"; + await writeFile(resolvePath(rel(".fake-ytdlp-media-fail")), ""); + const post = await request.post(`${baseUrl}/api/media/fetch-window`, { + headers: AUTH, + data: { + channelSlug: SLUG, + videoId: HAS_TRANSCRIPT, + full: true, + requestedBy: "mcp", + }, + }); + expect(post.status(), await post.text()).toBe(202); + const { jobId } = (await post.json()) as { jobId: string }; + + const finished = await pollJob(request, jobId); + expect(finished.status, JSON.stringify(finished)).toBe("failed"); + expect(finished.file).toBeUndefined(); + expect(String(finished.error)).toContain( + "The source video was not downloaded: ERROR: [download] Got error: HTTP Error 403: Forbidden", + ); + expect(String(finished.error)).toContain( + "forceMedia: the source download failed (yt-dlp exit 1); the transcript on disk is untouched and the download stays ok.", + ); + + const outcome = await readJson<{ + status: string; + attempts: Array<{ kind: string; ytdlpExitCode: number | null }>; + }>(rel(`data/${HAS_TRANSCRIPT}/download-outcome.json`)); + expect(outcome.status).toBe("ok"); + expect(outcome.attempts.map((a) => [a.kind, a.ytdlpExitCode])).toEqual([ + ["metadata-prefetch", 0], + ["primary", 0], + ["no-subs-fallback", 1], + ]); + const files = await readdir(resolvePath(rel(`data/${HAS_TRANSCRIPT}`))); + expect(files).toContain("transcript.en.vtt"); + expect(files).not.toContain("saved-video.json"); + // The partial is left for a retry to resume, and the log named it. + expect(files).toContain("source-media.f137.mp4.part"); + expect(String(finished.error)).toContain( + "Left for a retry to resume: source-media.f137.mp4.part (", + ); +}); diff --git a/editor/e2e/fixtures/bin/fake-ytdlp.mjs b/editor/e2e/fixtures/bin/fake-ytdlp.mjs @@ -939,6 +939,19 @@ async function main() { await writeMetadata(videoDir, id, urlSentinels(url)); } process.stdout.write(`[download] Fetching ${id}\n`); + // `.fake-ytdlp-media-fail` in the channel root (the fake's cwd): the media + // download dies part-way — the partial yt-dlp leaves behind, then a 403 — + // for the forced-media failure specs (release 11 slice O3). + if (existsSync(".fake-ytdlp-media-fail")) { + await writeFile( + path.join(videoDir, "source-media.f137.mp4.part"), + `fake-ytdlp partial source container for ${id}\n`, + ); + process.stderr.write( + `ERROR: [download] Got error: HTTP Error 403: Forbidden\n`, + ); + process.exit(1); + } await writeFile( path.join(videoDir, "source-media.mp4"), `fake-ytdlp synthesised source container for ${id}\n`, diff --git a/editor/e2e/persist-youtube-handling.spec.ts b/editor/e2e/persist-youtube-handling.spec.ts @@ -173,3 +173,48 @@ test("a download whose metadata changed upstream appends to metadata.history.jso await expect(entries).toContainText("view_count"); await expect(entries).toContainText("100 → 150"); }); + +// A FAILED MEDIA PASS IS NOT A FAILED DOWNLOAD (release 11 slice O3). With the +// fake's media download dying part-way, the run fails — the operator asked for +// the source and did not get it — but the download outcome stays the subtitle +// pass's `ok`, so the video page no longer says "Download failed" over a +// transcript that is fine. +test("Persist source video whose media download fails: the run fails with yt-dlp's line, the video is not marked Download failed", async ({ + page, +}) => { + test.setTimeout(120_000); + await resetData("youtube-with-playlist"); + await writeFile(resolvePath(rel(".fake-ytdlp-media-fail")), ""); + + await page.goto(`/channels/${SLUG}/videos/${HAS_TRANSCRIPT}`); + await page.getByLabel("Source video stage summary").click(); + await page.getByRole("button", { name: "Persist source video", exact: true }).click(); + const log = page.getByLabel(`Persist source video for ${HAS_TRANSCRIPT} output`); + await expect(log).toContainText( + "forceMedia: the source download failed (yt-dlp exit 1); the transcript on disk is untouched and the download stays ok.", + { timeout: 30_000 }, + ); + await expect(log).toContainText( + "[error] The source video was not downloaded: ERROR: [download] Got error: HTTP Error 403: Forbidden", + { timeout: 30_000 }, + ); + + const outcome = await outcomeOf(HAS_TRANSCRIPT); + expect(outcome?.status).toBe("ok"); + expect(outcome?.attempts.map((a) => [a.kind, a.ytdlpExitCode])).toEqual([ + ["metadata-prefetch", 0], + ["primary", 0], + ["no-subs-fallback", 1], + ]); + // The transcript is there (the subtitle pass re-fetched it, as on any + // re-download); the failed media pass wrote none. + const files = await readdir(resolvePath(rel(`data/${HAS_TRANSCRIPT}`))); + expect(files).toContain("transcript.en.vtt"); + expect(files).not.toContain("saved-video.json"); + + // The page, fresh: no failed-download badge, and nothing persisted. + await page.reload(); + await expect(page.getByLabel("download outcome")).toHaveCount(0); + await page.getByLabel("Source video stage summary").click(); + await expect(page.getByLabel(`unpersist source video ${HAS_TRANSCRIPT}`)).toHaveCount(0); +});