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:
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"`.