Archilyzer · Source

archilyzer

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

commit 90ab386251e27c00283a58eb3aaaadabd028f594
parent 4b8752ba088b72a72bdad8ee303f2be27810baf7
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sun,  4 Oct 2026 16:09:37 -0400

common: persists honour persistFormatPreset and record the format taken

downloadOneManaged's persistFormatPreset picks the selector a persisting
app-mode pass uses; unset keeps bestvideo*+bestaudio/best. The pass prints
requested_downloads[0]'s height, vcodec and format_id (FORMAT_MARKER), which
lands on the saved-video pointer as format {preset, height, vcodec}; a
video_720 persist above 720p is logged as the last resort. Old pointers parse
unchanged. persistKept resolves the quality through resolveSourceVideoQuality.

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

Diffstat:
Mcommon/controller/persistKept.ts | 15+++++++++++++++
Mcommon/lib/savedVideo-server.ts | 4++++
Mcommon/lib/savedVideo.test.ts | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/lib/savedVideo.ts | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/ytdlp/downloadOneManaged.ts | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Acommon/ytdlp/persistFormatPreset.test.ts | 216+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/ytdlp/runOneYtdlp.ts | 29++++++++++++++++++++++-------
Mplans/FACTS.md | 18++++++++++++++++++
8 files changed, 479 insertions(+), 11 deletions(-)

diff --git a/common/controller/persistKept.ts b/common/controller/persistKept.ts @@ -11,6 +11,10 @@ import { downloadOneManaged, sourceFetchFailure, } from "../ytdlp/downloadOneManaged"; +import { + resolveSourceVideoQuality, + type SourceVideoQuality, +} from "../ytdlp/downloadFormat"; // Bulk "persist kept now" pass (Phase 5). Ensures every video currently in the // channel's keep-latest window has its source container saved to the store, for @@ -48,12 +52,16 @@ export async function persistKept({ channelConfig, onLog, signal, + quality, }: { paths: Paths; channelSlug: string; channelConfig: ChannelConfig; onLog?: (line: string) => void; signal?: AbortSignal; + // Per-run override of the source-video quality; absent = the channel's, + // else the global setting (resolveSourceVideoQuality). + quality?: SourceVideoQuality; }): Promise<PersistKeptResult> { const log = (line: string) => onLog?.(line.endsWith("\n") ? line : `${line}\n`); // downloadOneManaged requires a non-optional onLog/signal; supply inert @@ -83,6 +91,12 @@ export async function persistKept({ return result; } const settings = getSettings(); + const persistFormatPreset = resolveSourceVideoQuality({ + override: quality, + channel: channelConfig.sourceVideoQuality, + global: settings.sourceVideoQuality, + }); + log(`Persist kept: source video quality ${persistFormatPreset}.`); const dataDir = path.join(paths.channelsDir, channelSlug, "data"); // Newest-first so the freshest videos are secured even if the pass is cancelled // partway through. computeKeptVideoIds returns an unordered set; sort by id desc @@ -138,6 +152,7 @@ export async function persistKept({ // pass is otherwise skipped for any video with a transcript or // captions, which is every kept one (release 10 slice N). forceMedia: true, + persistFormatPreset, }); // A download that RETURNED is not a persisted source (release 11 slice // O3, review low 2): a failed forced media pass leaves the download `ok` diff --git a/common/lib/savedVideo-server.ts b/common/lib/savedVideo-server.ts @@ -11,6 +11,7 @@ import { SAVED_VIDEO_POINTER_FILENAME, parseSavedVideoPointer, savedVideoPath, + type SavedVideoFormat, type SavedVideoKeepReason, type SavedVideoOrigin, type SavedVideoPointer, @@ -107,6 +108,8 @@ export async function persistSourceVideo(opts: { // SavedVideoOrigin — it records the requester WITHOUT changing keepReason, // which is what keeps the container out of the retention prune. origin?: SavedVideoOrigin; + // What the persist asked for and what yt-dlp took; see SavedVideoPointer.format. + format?: SavedVideoFormat; // WHEN THE STORE IS BEING MOVED, THIS DOES NOT RUN. Passed by the download // path, which has them; a caller that omits them opts out, which is right for // a store that is not the corpus one. See lib/savedVideoStore.ts for the @@ -130,6 +133,7 @@ export async function persistSourceVideo(opts: { bytes: st.size, ...(opts.keepReason ? { keepReason: opts.keepReason } : {}), ...(opts.origin ? { origin: opts.origin } : {}), + ...(opts.format ? { format: opts.format } : {}), }; await writePointer(opts.videoDir, pointer); return pointer; diff --git a/common/lib/savedVideo.test.ts b/common/lib/savedVideo.test.ts @@ -7,6 +7,7 @@ import type { Paths } from "./paths"; import { parseSavedVideoPointer, savedVideoDir, + savedVideoFormatFromLine, savedVideoRoot, } from "./savedVideo"; import { @@ -80,6 +81,68 @@ test("parseSavedVideoPointer rejects malformed and reads keepReason", () => { assert.equal(noReason?.keepReason, undefined); }); +test("format: an old pointer parses as before, a new one carries its format", () => { + const base = { storedAt: "t", dir: "/a", file: "source-media.mp4", bytes: 10 }; + // Every pointer written before the field: no `format` key at all. + const old = parseSavedVideoPointer(base); + assert.deepEqual(old, base); + assert.equal(old && "format" in old, false); + // The new shape round-trips. + assert.deepEqual( + parseSavedVideoPointer({ + ...base, + format: { preset: "video_720", height: 720, vcodec: "avc1.4d401f" }, + })?.format, + { preset: "video_720", height: 720, vcodec: "avc1.4d401f" }, + ); + // Height and vcodec are optional; junk in them is dropped, not carried. + assert.deepEqual( + parseSavedVideoPointer({ + ...base, + format: { preset: "original", height: "720", vcodec: "" }, + })?.format, + { preset: "original" }, + ); + // A format with no valid preset is dropped whole; the pointer still parses. + const bad = parseSavedVideoPointer({ ...base, format: { preset: "4k", height: 2160 } }); + assert.ok(bad); + assert.equal(bad && "format" in bad, false); +}); + +test("savedVideoFormatFromLine reads the printed format line", () => { + assert.deepEqual(savedVideoFormatFromLine("720 avc1.4d401f 136+140", "video_720"), { + preset: "video_720", + height: 720, + vcodec: "avc1.4d401f", + }); + // yt-dlp prints NA for a field it does not have. + assert.deepEqual(savedVideoFormatFromLine("NA NA original", "original"), { + preset: "original", + }); + // No line: the asked-for preset alone. + assert.deepEqual(savedVideoFormatFromLine(null, "video_720"), { preset: "video_720" }); + assert.deepEqual(savedVideoFormatFromLine("", "original"), { preset: "original" }); +}); + +test("persist writes the format it is given onto the pointer", async () => { + await withTmp(async (root) => { + const videoDir = path.join(root, "data", "vid1"); + const storeDir = path.join(root, "store", "chan", "vid1"); + await mkdir(videoDir, { recursive: true }); + await writeFile(path.join(videoDir, "source-media.mp4"), "video-bytes"); + const format = { preset: "video_720" as const, height: 480, vcodec: "avc1.4d401e" }; + const pointer = await persistSourceVideo({ + videoDir, + sourceFilename: "source-media.mp4", + storeDir, + keepReason: "override", + format, + }); + assert.deepEqual(pointer.format, format); + assert.deepEqual((await loadSavedVideo(videoDir))?.format, format); + }); +}); + test("persist moves the container into the store and writes a pointer", async () => { await withTmp(async (root) => { const videoDir = path.join(root, "data", "vid1"); diff --git a/common/lib/savedVideo.ts b/common/lib/savedVideo.ts @@ -1,6 +1,10 @@ import path from "node:path"; import type { Paths } from "./paths"; import type { ChannelConfig } from "./channelConfig"; +import { + isSourceVideoQuality, + type SourceVideoQuality, +} from "../ytdlp/downloadFormat"; // The saved-video store (Phase 3 of the video-persistence feature). // @@ -50,8 +54,51 @@ export type SavedVideoPointer = { // somebody asked for. Absent on every pointer written before this and on // every automatic persist. origin?: SavedVideoOrigin; + // WHAT WAS DOWNLOADED: the quality the persist asked for and the format + // yt-dlp actually took (its `requested_downloads[0]`, printed at download + // time). A "video_720" persist can fall through to its last-resort rung, so + // the asked-for preset alone does not say how tall the file is. Absent on + // every pointer written before this; `height`/`vcodec` absent when yt-dlp + // did not report them. + format?: SavedVideoFormat; }; +export type SavedVideoFormat = { + preset: SourceVideoQuality; + height?: number; + vcodec?: string; +}; + +function parseFormat(raw: unknown): SavedVideoFormat | null { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null; + const r = raw as Record<string, unknown>; + if (!isSourceVideoQuality(r.preset)) return null; + const out: SavedVideoFormat = { preset: r.preset }; + if (typeof r.height === "number" && Number.isInteger(r.height) && r.height > 0) { + out.height = r.height; + } + if (typeof r.vcodec === "string" && r.vcodec !== "") out.vcodec = r.vcodec; + return out; +} + +// The format a persist pass printed (runOneYtdlp's FORMAT_MARKER line: +// "<height> <vcodec> <format_id>", yt-dlp's "NA" for a missing field), as the +// pointer's `format`. With no line, the asked-for preset alone. +export function savedVideoFormatFromLine( + line: string | null | undefined, + preset: SourceVideoQuality, +): SavedVideoFormat { + const out: SavedVideoFormat = { preset }; + if (!line) return out; + const [heightRaw, vcodecRaw] = line.trim().split(/\s+/); + const height = Number(heightRaw); + if (Number.isInteger(height) && height > 0) out.height = height; + if (vcodecRaw && vcodecRaw !== "NA" && vcodecRaw !== "none") { + out.vcodec = vcodecRaw; + } + return out; +} + // The requester of a manually-sourced container: the tool, and the report clip // it was wanted for. Deliberately the same vocabulary as ClipProvenance // (lib/clipWindow.ts) so the video page renders both the same way. @@ -123,5 +170,8 @@ export function parseSavedVideoPointer(raw: unknown): SavedVideoPointer | null { // TOLERANT: a legacy pointer has no `origin` and must parse exactly as it did. const origin = parseOrigin(r.origin); if (origin) pointer.origin = origin; + // Same rule: a pointer without `format` (every one before it) parses as before. + const format = parseFormat(r.format); + if (format) pointer.format = format; return pointer; } diff --git a/common/ytdlp/downloadOneManaged.ts b/common/ytdlp/downloadOneManaged.ts @@ -33,7 +33,12 @@ import { import { isDoNotClean } from "../lib/doNotClean-server"; import { transcodeAudio } from "../controller/transcode"; import { findSourceMedia } from "../lib/videoStatus"; -import { savedVideoDir, type SavedVideoOrigin } from "../lib/savedVideo"; +import { + savedVideoDir, + savedVideoFormatFromLine, + type SavedVideoFormat, + type SavedVideoOrigin, +} from "../lib/savedVideo"; import { persistSourceVideo } from "../lib/savedVideo-server"; import { type AudioCheckAttemptStats, @@ -70,13 +75,20 @@ import { extractVideoId, outputArgsForUrl } from "./runYtdlp"; import { channelExtraArgs, channelPaceSeconds } from "./channelArgs"; import { ARCHIVE_MARKER, + FORMAT_MARKER, runOneYtdlp as runOneYtdlpRaw, type AttemptOutcome, } from "./runOneYtdlp"; import { runAudioCheckedYtdlp } from "./audioCheckedDownload"; import { + DEFAULT_SOURCE_VIDEO_QUALITY, + LAST_RESORT_FORMAT_SELECTOR, + VIDEO_720_MAX_HEIGHT, + VIDEO_720_PRESET, resolveDownloadFormatSelector, + sourceVideoFormatSelector, type DownloadFormatPreset, + type SourceVideoQuality, } from "./downloadFormat"; import { DOWNLOAD_PROGRESS_TEMPLATE } from "../jobs/progressParsers"; @@ -164,6 +176,14 @@ export type ManagedDownloadOpts = { // selector here, where the per-video source platform is known (so "auto" // picks `original` for Odysee). Defaults to "auto" when omitted. downloadFormatPreset?: DownloadFormatPreset; + // The quality of the source container a PERSIST keeps (a pass whose plan + // persists in app mode: "Persist source video", the whole-recording fetch, + // "Persist kept now", a keep-latest download). Resolved by the caller through + // resolveSourceVideoQuality (override > channel > global). Unset or + // "original" = `bestvideo*+bestaudio/best`, as every persist has always been; + // "video_720" = the ≤720p H.264 selector (downloadFormat.ts). Never touches + // the audio-only selector above. + persistFormatPreset?: SourceVideoQuality; }; // When `reuseInfoJson` is true, the real download reuses the metadata the @@ -216,6 +236,7 @@ function audioFormatSelectionArgs( fmt: AudioFormat, config: ChannelConfig, selector: string, + persistSelector: string, ): string[] { if (plan.extractionMode === "ytdlp") { const args = ["-f", selector, "-x", "--audio-format", fmt]; @@ -223,12 +244,48 @@ function audioFormatSelectionArgs( return args; } // Persisting in app mode keeps the full source container (always a complete - // video), so the truncation-prone audio-only selector doesn't apply. + // video), so the truncation-prone audio-only selector doesn't apply: the + // persist quality's selector does (sourceVideoFormatSelector). The format + // yt-dlp actually took is printed for the saved-video pointer. return plan.persist - ? ["-f", "bestvideo*+bestaudio/best"] + ? ["-f", persistSelector, "--print", PERSIST_FORMAT_PRINT] : ["-f", selector]; } +// After the video, the downloaded format: `requested_downloads[0]` is the +// merged (or single) format, carrying the video stream's height and vcodec. +// runOneYtdlp scrapes it as `formatLine`; savedVideoFormatFromLine parses it. +const PERSIST_FORMAT_PRINT = + `after_video:${FORMAT_MARKER} %(requested_downloads.0.height)s ` + + `%(requested_downloads.0.vcodec)s %(requested_downloads.0.format_id)s`; + +// The log line for a persisted container's format, and the LOUD one for a +// "video_720" persist that the last-resort rung answered: a file above 720p +// (or one whose height yt-dlp did not report) is never silent. +export function persistedFormatLog( + format: SavedVideoFormat, + formatLine: string | null | undefined, +): string { + if (!formatLine) { + return `Source format: not reported by yt-dlp (quality ${format.preset}).\n`; + } + const formatId = formatLine?.trim().split(/\s+/)[2]; + const what = + `${format.height ? `${format.height}p` : "unknown height"}` + + `${format.vcodec ? ` ${format.vcodec}` : ""}` + + `${formatId && formatId !== "NA" ? ` (format ${formatId})` : ""}`; + if ( + format.preset === VIDEO_720_PRESET && + (format.height === undefined || format.height > VIDEO_720_MAX_HEIGHT) + ) { + return ( + `Source quality ${VIDEO_720_PRESET}: no format at or under ${VIDEO_720_MAX_HEIGHT}p matched ` + + `the 720p/480p rungs; the last resort (${LAST_RESORT_FORMAT_SELECTOR}) took ${what}.\n` + ); + } + return `Source format: ${what} (quality ${format.preset}).\n`; +} + // The media output + info-json + format args for a transcribe-handling download. // In app mode the main output is source-media.<ext> (distinct from audio.<ext> // so it's never treated as cleanable audio); otherwise the historical audio.<ext>. @@ -240,12 +297,13 @@ function transcribeMediaArgs( fmt: AudioFormat, reuseInfoJson: boolean, selector: string, + persistSelector: string, ): string[] { const mediaName = plan.extractionMode === "app" ? "source-media" : "audio"; return [ ...outputArgsForUrl(url, { mediaName }), ...(reuseInfoJson ? [] : ["--write-info-json"]), - ...audioFormatSelectionArgs(plan, fmt, config, selector), + ...audioFormatSelectionArgs(plan, fmt, config, selector, persistSelector), ]; } @@ -276,6 +334,9 @@ async function finalizeAppExtraction(opts: { // `keepTranscript`) wants the source kept, and an audio.<fmt> beside an // existing transcript is bytes nothing will read. Default true. extractAudio?: boolean; + // What the persist asked for and what yt-dlp took (the pass's FORMAT_MARKER + // line), recorded on the pointer. Absent = not reported. + format?: SavedVideoFormat | null; onLog: (s: string) => void; signal: AbortSignal; }): Promise<void> { @@ -333,6 +394,7 @@ async function finalizeAppExtraction(opts: { storeDir, keepReason: opts.category === "none" ? undefined : opts.category, origin: opts.origin, + ...(opts.format ? { format: opts.format } : {}), // The store-in-transition guard. A move of the saved-video store is a // multi-hour copy, and a container landing in the middle of it is lost // three different ways — see lib/savedVideoStore.ts. The catch below is @@ -709,6 +771,8 @@ async function runManagedDownload( // recorded on the outcome so the UI can explain it without re-probing. let shortAudioInfo: NonNullable<DownloadOutcomeRecord["shortAudio"]> | undefined; let lastArchiveLine: string | null = null; + // The format the last media pass printed (FORMAT_MARKER), for the pointer. + let lastFormatLine: string | null = null; // Full (untruncated) stderr tail of the most recent attempt, so a failed // download can be classified (rate_limit/network) against everything yt-dlp // printed — not just the last 3 lines stored on the attempt record. @@ -1051,6 +1115,10 @@ async function runManagedDownload( opts.downloadFormatPreset ?? "auto", resolvedPlatform, ); + // The container a persisting pass keeps. Platform-independent: a persist + // wants a whole video, which "auto"'s Odysee rule is not about. + const persistQuality = opts.persistFormatPreset ?? DEFAULT_SOURCE_VIDEO_QUALITY; + const persistSelector = sourceVideoFormatSelector(persistQuality); const pinned = canonicalId ? await isDoNotClean(path.join(channelDir, "data", canonicalId)) : false; @@ -1086,6 +1154,7 @@ async function runManagedDownload( fmt, reuseInfoJson, downloadFormatSelector, + persistSelector, ); if (opts.channelConfig.handling === "transcribe") { opts.onLog( @@ -1213,6 +1282,7 @@ async function runManagedDownload( ...(audioCheckStats ? { audioCheck: audioCheckStats } : {}), }); if (primaryRes.archiveLine) lastArchiveLine = primaryRes.archiveLine; + if (primaryRes.formatLine) lastFormatLine = primaryRes.formatLine; // ---------- A subtitle 429 is not a failed download (release 17, RL) ---------- // YouTube's timedtext endpoint refuses per video while the media requests @@ -1295,6 +1365,7 @@ async function runManagedDownload( : trimError(noSubsRes.stderrTail), }); if (noSubsRes.archiveLine) lastArchiveLine = noSubsRes.archiveLine; + if (noSubsRes.formatLine) lastFormatLine = noSubsRes.formatLine; primaryRes = noSubsRes; if (!attemptSucceeded(noSubsRes.exitCode)) subsRateLimited = false; } @@ -1365,6 +1436,7 @@ async function runManagedDownload( : trimError(retryRes.stderrTail), }); if (retryRes.archiveLine) lastArchiveLine = retryRes.archiveLine; + if (retryRes.formatLine) lastFormatLine = retryRes.formatLine; if (attemptSucceeded(retryRes.exitCode)) { lastSucceeded = true; status = "ok-with-cookies"; @@ -1376,6 +1448,16 @@ async function runManagedDownload( path.join(channelDir, "data", canonicalId ?? "unknown"); const videoId = path.basename(videoDir); + // The pointer's `format`, read off the media pass that just ran, logged as it + // is recorded (persistedFormatLog). Only a persisting plan keeps a container. + const persistedFormat = (): SavedVideoFormat | null => { + if (!plan.persist) return null; + const format = savedVideoFormatFromLine(lastFormatLine, persistQuality); + const line = persistedFormatLog(format, lastFormatLine); + if (line) opts.onLog(line); + return format; + }; + // Download-time duration guard: probe the produced audio.<fmt> and compare its // actual length to the metadata duration. A large shortfall means the source // served a truncated stream (e.g. a CDN-truncated HLS rung) even though yt-dlp @@ -1435,6 +1517,7 @@ async function runManagedDownload( persist: plan.persist, category: plan.category, origin: opts.persistOrigin, + format: persistedFormat(), onLog: opts.onLog, signal: opts.signal, }); @@ -1522,6 +1605,7 @@ async function runManagedDownload( fmt, true, downloadFormatSelector, + persistSelector, ), "--print", `after_video:${ARCHIVE_MARKER} %(extractor)s %(id)s`, @@ -1554,6 +1638,7 @@ async function runManagedDownload( : trimError(fallbackRes.stderrTail), }); if (fallbackRes.archiveLine) lastArchiveLine = fallbackRes.archiveLine; + if (fallbackRes.formatLine) lastFormatLine = fallbackRes.formatLine; if (attemptSucceeded(fallbackRes.exitCode) && keepTranscript) { // Persist only. Not a fallback to transcription — the transcript is @@ -1570,6 +1655,7 @@ async function runManagedDownload( persist: plan.persist, category: plan.category, origin: opts.persistOrigin, + format: persistedFormat(), extractAudio: false, onLog: opts.onLog, signal: opts.signal, @@ -1590,6 +1676,7 @@ async function runManagedDownload( persist: plan.persist, category: plan.category, origin: opts.persistOrigin, + format: persistedFormat(), onLog: opts.onLog, signal: opts.signal, }); diff --git a/common/ytdlp/persistFormatPreset.test.ts b/common/ytdlp/persistFormatPreset.test.ts @@ -0,0 +1,216 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +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 { resolveDownloadFormatSelector } from "./downloadFormat"; + +// Run with: +// pnpm --filter yt-dlp-transcript-common exec tsx --test ytdlp/persistFormatPreset.test.ts +// +// THE PERSIST QUALITY. A pass that persists a source container takes +// `persistFormatPreset`'s selector — unset is `bestvideo*+bestaudio/best`, as +// every persist always was — prints the format yt-dlp actually took, and that +// lands on the saved-video pointer. A "video_720" persist the last-resort rung +// answered (a file above 720p) says so in the log. The yt-dlp here is a temp +// node script; no network. + +const ID = "abc123def45"; +const VIDEO = `https://www.youtube.com/watch?v=${ID}`; + +// The prefetch writes meta.json as the info json; the subtitle pass writes +// nothing; the media pass (the `source-media` output) writes the container and +// prints `format-line` (when present) the way `--print after_video:DLOM_FORMAT` +// would. Every spawn appends its argv to `argv.log`. +const FAKE_YTDLP = `#!/usr/bin/env node +const fs = require("node:fs"); +const path = require("node:path"); +const argv = process.argv.slice(2); +const root = process.env.FAKE_ROOT; +fs.appendFileSync(path.join(root, "argv.log"), JSON.stringify(argv) + "\\n"); +const has = (f) => argv.includes(f); +const arg = (f) => { const i = argv.indexOf(f); return i < 0 ? undefined : argv[i + 1]; }; +const info = arg("--load-info-json"); +const id = info ? path.basename(path.dirname(info)) : "${ID}"; +const dir = path.join("data", id); +fs.mkdirSync(dir, { recursive: true }); +if (has("--skip-download") && has("--write-info-json") && has("--no-write-subs")) { + fs.copyFileSync(path.join(root, "meta.json"), path.join(dir, "metadata.info.json")); + process.exit(0); +} +if (has("--skip-download") && has("--write-auto-subs")) { + console.log("DLOM_ARCHIVE youtube " + id); + process.exit(0); +} +if ((arg("-o") || "").includes("source-media")) { + fs.writeFileSync(path.join(dir, "source-media.mp4"), "container bytes"); + const fl = path.join(root, "format-line"); + if (fs.existsSync(fl)) console.log("DLOM_FORMAT " + fs.readFileSync(fl, "utf8").trim()); + console.log("DLOM_ARCHIVE youtube " + id); + process.exit(0); +} +console.error("fake: unknown invocation " + argv.join(" ")); +process.exit(2); +`; + +const FAKE_FFMPEG = `#!/bin/sh +for last; do :; done +echo "extracted audio" > "$last" +`; + +const META = { + id: ID, + title: "A video", + duration: 60, + webpage_url: VIDEO, + subtitles: {}, + automatic_captions: { en: [{ url: "https://example/cap" }] }, +}; + +async function runWith(opts: { + formatLine?: string; + config?: Partial<ChannelConfig>; + managed?: Partial<ManagedDownloadOpts>; +}) { + const root = await mkdtemp(path.join(tmpdir(), "persist-format-")); + const ytdlp = path.join(root, "fake-ytdlp.cjs"); + const ffmpeg = path.join(root, "fake-ffmpeg.sh"); + await writeFile(ytdlp, FAKE_YTDLP); + await writeFile(ffmpeg, FAKE_FFMPEG); + await chmod(ytdlp, 0o755); + await chmod(ffmpeg, 0o755); + await writeFile(path.join(root, "meta.json"), JSON.stringify(META)); + if (opts.formatLine) await writeFile(path.join(root, "format-line"), opts.formatLine); + process.env.FAKE_ROOT = root; + const paths = { + transcriptsDir: root, + channelsDir: path.join(root, "channels"), + savedVideosDir: path.join(root, "saved-videos"), + ytdlpBin: ytdlp, + ffmpegBin: ffmpeg, + ffprobeBin: path.join(root, "no-ffprobe"), + } as Paths; + const videoDir = path.join(paths.channelsDir, "demo-channel", "data", ID); + await mkdir(videoDir, { recursive: true }); + // A transcript on disk: the forced pass persists and does nothing else. + await writeFile(path.join(videoDir, "transcript.json"), '{"segments":[]}'); + let log = ""; + const record = await downloadOneManaged({ + channelSlug: "demo-channel", + channelConfig: { + handling: "youtube", + url: "https://www.youtube.com/@demo/videos", + ...opts.config, + } as ChannelConfig, + paths, + videoUrl: VIDEO, + onLog: (s) => { + log += s; + }, + signal: new AbortController().signal, + keepSourceVideoOverride: true, + forceMedia: true, + ...opts.managed, + }); + const argvs = (await readFile(path.join(root, "argv.log"), "utf8")) + .split("\n") + .filter(Boolean) + .map((l) => JSON.parse(l) as string[]); + const media = argvs.find((a) => (a[a.indexOf("-o") + 1] ?? "").includes("source-media")); + const pointer = JSON.parse( + await readFile(path.join(videoDir, "saved-video.json"), "utf8"), + ) as Record<string, unknown>; + return { + record, + log, + media: media ?? [], + pointer, + cleanup: () => rm(root, { recursive: true, force: true }), + }; +} + +const selectorOf = (argv: string[]) => argv[argv.indexOf("-f") + 1]; + +test("unset: the persist selector is bestvideo*+bestaudio/best, as it always was", async () => { + const r = await runWith({ formatLine: "1080 avc1.640028 137+140" }); + try { + assert.equal(r.record.status, "ok"); + assert.equal(selectorOf(r.media), "bestvideo*+bestaudio/best"); + // The format yt-dlp took is asked for, and recorded under "original". + assert.ok(r.media.some((a) => a.startsWith("after_video:DLOM_FORMAT ")), r.media.join(" ")); + assert.deepEqual(r.pointer.format, { + preset: "original", + height: 1080, + vcodec: "avc1.640028", + }); + assert.match(r.log, /Source format: 1080p avc1\.640028 \(format 137\+140\) \(quality original\)\.\n/); + } finally { + await r.cleanup(); + } +}); + +test("video_720: the persist takes the 720 selector and records what came back", async () => { + const r = await runWith({ + formatLine: "720 avc1.4d401f 136+140", + managed: { persistFormatPreset: "video_720" }, + }); + try { + assert.equal(selectorOf(r.media), resolveDownloadFormatSelector("video_720", null)); + assert.deepEqual(r.pointer.format, { + preset: "video_720", + height: 720, + vcodec: "avc1.4d401f", + }); + assert.doesNotMatch(r.log, /last resort/); + } finally { + await r.cleanup(); + } +}); + +test("video_720 answered by the last resort: a file above 720p is logged, not silent", async () => { + const r = await runWith({ + formatLine: "1080 vp9 248+251", + managed: { persistFormatPreset: "video_720" }, + }); + try { + assert.match( + r.log, + /Source quality video_720: no format at or under 720p matched the 720p\/480p rungs; the last resort \(bv\*\+ba\/b\) took 1080p vp9 \(format 248\+251\)\.\n/, + ); + assert.deepEqual(r.pointer.format, { preset: "video_720", height: 1080, vcodec: "vp9" }); + } finally { + await r.cleanup(); + } +}); + +test("original asked explicitly is the unset selector; no printed format → preset only", async () => { + const r = await runWith({ managed: { persistFormatPreset: "original" } }); + try { + assert.equal(selectorOf(r.media), "bestvideo*+bestaudio/best"); + assert.deepEqual(r.pointer.format, { preset: "original" }); + assert.match(r.log, /Source format: not reported by yt-dlp \(quality original\)\.\n/); + } finally { + await r.cleanup(); + } +}); + +test("transcribe handling: a persisting primary takes the preset too; the audio selector is untouched", async () => { + const r = await runWith({ + formatLine: "480 avc1.4d401e 135+140", + config: { handling: "transcribe", extractionMode: "app" }, + managed: { persistFormatPreset: "video_720", forceMedia: false }, + }); + try { + assert.equal(selectorOf(r.media), resolveDownloadFormatSelector("video_720", null)); + assert.deepEqual(r.pointer.format, { + preset: "video_720", + height: 480, + vcodec: "avc1.4d401e", + }); + } finally { + await r.cleanup(); + } +}); diff --git a/common/ytdlp/runOneYtdlp.ts b/common/ytdlp/runOneYtdlp.ts @@ -17,6 +17,14 @@ export const STDERR_TAIL_BYTES = 64 * 1024; // learn the archive line without re-reading the archive file. export const ARCHIVE_MARKER = "DLOM_ARCHIVE"; +// A persist pass also `--print`s this marker after the video, with the format +// yt-dlp actually downloaded (height, vcodec, format_id of +// `requested_downloads[0]`). The info json on disk cannot say it: the prefetch +// wrote it with --skip-download, and the download reuses it via +// --load-info-json, so its top-level format is the prefetch's, and +// `requested_downloads` is only set after yt-dlp has written the file. +export const FORMAT_MARKER = "DLOM_FORMAT"; + export type YtdlpRunOpts = { ytdlpBin: string; onLog: (s: string) => void; @@ -30,6 +38,9 @@ export type AttemptOutcome = { // (lib/availability.ts classifyDownloadFailure). stderrTail: string; archiveLine: string | null; + // The text after FORMAT_MARKER, when the invocation printed one. Optional so + // an outcome assembled elsewhere (the audio-checked primary) need not name it. + formatLine?: string | null; }; export async function runOneYtdlp( @@ -55,6 +66,14 @@ export async function runOneYtdlp( let stdoutBuf = ""; let archiveLine: string | null = null; + let formatLine: string | null = null; + const scrape = (line: string) => { + if (line.startsWith(`${ARCHIVE_MARKER} `)) { + archiveLine = line.slice(ARCHIVE_MARKER.length + 1).trim(); + } else if (line.startsWith(`${FORMAT_MARKER} `)) { + formatLine = line.slice(FORMAT_MARKER.length + 1).trim(); + } + }; child.stdout?.on("data", (c: Buffer) => { const chunk = c.toString("utf8"); opts.onLog(chunk); @@ -64,21 +83,17 @@ export async function runOneYtdlp( while ((nl = stdoutBuf.indexOf("\n")) !== -1) { const line = stdoutBuf.slice(0, nl).trim(); stdoutBuf = stdoutBuf.slice(nl + 1); - if (line.startsWith(`${ARCHIVE_MARKER} `)) { - archiveLine = line.slice(ARCHIVE_MARKER.length + 1).trim(); - } + scrape(line); } }); const result = await child; // Flush any final partial line. - const trailing = stdoutBuf.trim(); - if (trailing.startsWith(`${ARCHIVE_MARKER} `)) { - archiveLine = trailing.slice(ARCHIVE_MARKER.length + 1).trim(); - } + scrape(stdoutBuf.trim()); return { exitCode: result.exitCode ?? null, stderrTail, archiveLine, + formatLine, }; } diff --git a/plans/FACTS.md b/plans/FACTS.md @@ -8588,3 +8588,21 @@ phase deletes from the destination. - The e2e app server has no media root (`UMTOOL_MEDIA_DIR=` in `playwright.config.ts`); specs give their CLI one (`<fixture>-media`), so `deliverables.spec.ts` shows an app cutting through a link it did not make and refusing a new batch under `"media"`. + +## A persisted container's format is not in `metadata.info.json` (2026-10-04) + +- **The info json on disk describes the PREFETCH's format, not the download's.** The metadata + prefetch writes it with `--skip-download` (yt-dlp's default selector), and every later pass reads + it back with `--load-info-json` and does not rewrite it. Its top-level `format_id`/`height`/`vcodec` + can name a 1080p format for a file downloaded at 720p. +- **`requested_downloads` is never in it.** yt-dlp sets that key in `process_video_result` after + `process_info` has written the file (`YoutubeDL.py`, `info_dict['requested_downloads'] = …` just + before `run_all_pps('after_video', …)`), so only an `after_video` print can see it. +- So a persist pass prints `DLOM_FORMAT %(requested_downloads.0.height)s …vcodec…format_id` (the + `FORMAT_MARKER` in `common/ytdlp/runOneYtdlp.ts`), and the saved-video pointer's `format` is read + from that line (`savedVideoFormatFromLine`). A field yt-dlp lacks prints as `NA`. +- **Source-video quality resolves through `resolveSourceVideoQuality`** (`common/ytdlp/downloadFormat.ts`): + per-persist override > `ChannelConfig.sourceVideoQuality` > settings `sourceVideoQuality` > + `"original"`. `downloadOneManaged`'s `persistFormatPreset` takes the result; unset is the historical + `bestvideo*+bestaudio/best`. The download lane's keep-latest persists pass nothing, so they stay + `"original"`.