commit 4b8752ba088b72a72bdad8ee303f2be27810baf7
parent 08fef051b78888d31baeb4f784d9c91a1553df4c
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sun, 4 Oct 2026 16:09:37 -0400
common: sourceVideoQuality setting and per-channel override
Global settings.sourceVideoQuality (default original) and an optional
ChannelConfig.sourceVideoQuality; an invalid channel value is omitted, an
invalid global falls back to original. SETTINGS.md, CHANNEL.md and
settings.json.example regenerated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
8 files changed, 51 insertions(+), 3 deletions(-)
diff --git a/CHANNEL.md b/CHANNEL.md
@@ -23,6 +23,7 @@ Regenerate this file with `pnpm --filter yt-dlp-transcript-common exec tsx bin/f
| `url` | config | The channel / playlist / account URL syncs enumerate. Absent = the channel is never auto-synced. |
| `audioFormat` | config | `"m4a"`, `"mp3"` or `"opus"`: the audio a transcribe-handling download keeps. |
| `downloadFormat` | config | Per-channel override for the yt-dlp `-f` download format preset. Absent = inherit the global `downloadFormat`, which itself falls back to the per-source "auto" selector. Lets a channel whose source serves full-length audio only in its `original` format (e.g. Odysee) force it. |
+| `sourceVideoQuality` | config | Per-channel override for the quality of the source container a full persist keeps ("Persist source video", the whole-recording fetch, "Persist kept now"). `"original"` (best video + audio) or `"video_720"` (≤720p H.264, for clips/editing). Absent = inherit the global `sourceVideoQuality`. |
| `keepSourceVideo` | config | Keep the downloaded source video beside the audio. |
| `keepLatest` | config | Keep-latest window: the newest N videos (by upload date) are protected from the Clean-audio sweep AND have their source video persisted to the saved-video store. 0 or absent = disabled; positives clamp to [1, 100000]. A kept video later found deleted at the source is pinned permanently via the do-not-clean marker. |
| `extractionMode` | config | `"ytdlp"` (default — yt-dlp's own `-x --audio-format` postprocessor, no source container kept) or `"app"` (yt-dlp downloads the source container and the app runs ffmpeg). The keep-latest persistence rule forces `"app"` for the videos it persists. |
diff --git a/SETTINGS.md b/SETTINGS.md
@@ -23,6 +23,7 @@ A copied example PINS every default it spells — including each lane's `autoQue
| [`sleepBetweenDownloadsSeconds`](#sleepbetweendownloadsseconds) | `10` |
| [`pacing`](#pacing) | object — see below |
| [`downloadFormat`](#downloadformat) | `"auto"` |
+| [`sourceVideoQuality`](#sourcevideoquality) | `"original"` |
| [`minFreeDiskGB`](#minfreediskgb) | `5` |
| [`resumeMarginGB`](#resumemargingb) | `2` |
| [`parallelTranscriptions`](#paralleltranscriptions) | `2` |
@@ -220,6 +221,12 @@ Default yt-dlp `-f` download format for every channel that doesn't set its own (
Default: `"auto"`
+## `sourceVideoQuality`
+
+Quality of the source container a full persist keeps — "Persist source video", the whole-recording fetch (`full: true`) and "Persist kept now" — for every channel that doesn't set its own (ChannelConfig.sourceVideoQuality). "original" (default) = `bestvideo*+bestaudio/best`; "video_720" = ≤720p H.264/AAC mp4 for clip and editing work, falling back to 480p and then to anything (logged). A single persist can override it. See common/ytdlp/downloadFormat.ts.
+
+Default: `"original"`
+
## `minFreeDiskGB`
Minimum free disk space (GB) required on the transcripts data directory for downloads to run. When free space is below this floor, a download job is prevented from starting and a running batch stops launching new videos (the in-flight one finishes). 0 disables the gate. See common/lib/diskSpace.ts.
diff --git a/common/lib/channelConfig.ts b/common/lib/channelConfig.ts
@@ -1,7 +1,9 @@
import { PLATFORM_VALUES, type Platform } from "./platform";
import {
isDownloadFormatPreset,
+ isSourceVideoQuality,
type DownloadFormatPreset,
+ type SourceVideoQuality,
} from "../ytdlp/downloadFormat";
import { isCookieMode, type CookieMode } from "./cookiePolicy";
import type { FieldDocs } from "./fieldDocs";
@@ -121,6 +123,7 @@ export type ChannelConfig = {
url?: string;
audioFormat?: AudioFormat;
downloadFormat?: DownloadFormatPreset;
+ sourceVideoQuality?: SourceVideoQuality;
keepSourceVideo?: boolean;
keepLatest?: number;
extractionMode?: ExtractionMode;
@@ -160,6 +163,8 @@ export const CHANNEL_CONFIG_FIELD_DOCS: FieldDocs<ChannelConfig> = {
audioFormat: '`"m4a"`, `"mp3"` or `"opus"`: the audio a transcribe-handling download keeps.',
downloadFormat:
"Per-channel override for the yt-dlp `-f` download format preset. Absent = inherit the global `downloadFormat`, which itself falls back to the per-source \"auto\" selector. Lets a channel whose source serves full-length audio only in its `original` format (e.g. Odysee) force it.",
+ sourceVideoQuality:
+ 'Per-channel override for the quality of the source container a full persist keeps ("Persist source video", the whole-recording fetch, "Persist kept now"). `"original"` (best video + audio) or `"video_720"` (≤720p H.264, for clips/editing). Absent = inherit the global `sourceVideoQuality`.',
keepSourceVideo: "Keep the downloaded source video beside the audio.",
keepLatest:
"Keep-latest window: the newest N videos (by upload date) are protected from the Clean-audio sweep AND have their source video persisted to the saved-video store. 0 or absent = disabled; positives clamp to [1, 100000]. A kept video later found deleted at the source is pinned permanently via the do-not-clean marker.",
@@ -366,6 +371,7 @@ export const CHANNEL_CONFIG_COERCIONS: {
url: str,
audioFormat: (v) => (v === "m4a" || v === "mp3" || v === "opus" ? v : undefined),
downloadFormat: (v) => (isDownloadFormatPreset(v) ? v : undefined),
+ sourceVideoQuality: (v) => (isSourceVideoQuality(v) ? v : undefined),
keepSourceVideo: bool,
keepLatest: zeroOrClamped(1, KEEP_LATEST_MAX),
extractionMode: (v) => (v === "ytdlp" || v === "app" ? v : undefined),
diff --git a/common/lib/channelConfigSchema.test.ts b/common/lib/channelConfigSchema.test.ts
@@ -39,7 +39,7 @@ test("one key list: docs = coercions = schema shape, sync-state keys inside it",
assert.deepEqual(Object.keys(CHANNEL_CONFIG_COERCIONS), [...CHANNEL_CONFIG_KEYS]);
assert.deepEqual(Object.keys(channelConfigObjectSchema.shape), [...CHANNEL_CONFIG_KEYS]);
assert.deepEqual(Object.keys(CHANNEL_CONFIG_FIELD_DOCS), [...CHANNEL_CONFIG_KEYS]);
- assert.equal(CHANNEL_CONFIG_KEYS.length, 30);
+ assert.equal(CHANNEL_CONFIG_KEYS.length, 31);
assert.equal(sameKeys, true);
assert.equal(fits, true);
for (const k of CHANNEL_SYNC_STATE_KEYS) assert.ok(CHANNEL_CONFIG_KEYS.includes(k), k);
@@ -69,6 +69,17 @@ test("an invalid optional key is OMITTED, not defaulted", () => {
}
});
+test("sourceVideoQuality: the two qualities parse, anything else is omitted", () => {
+ for (const q of ["original", "video_720"] as const) {
+ assert.equal(parse({ handling: "youtube", sourceVideoQuality: q })!.sourceVideoQuality, q);
+ assert.equal(parseChannelConfig({ handling: "youtube", sourceVideoQuality: q })!.sourceVideoQuality, q);
+ }
+ for (const junk of ["720", "bestvideo_audio", "", 720, null]) {
+ const cfg = parse({ handling: "youtube", sourceVideoQuality: junk })!;
+ assert.equal("sourceVideoQuality" in cfg, false, JSON.stringify(junk));
+ }
+});
+
test("clamps: 0 is preserved as the disabled sentinel, positives clamp, fractions floor", () => {
const cfg = parse({
handling: "transcribe",
@@ -142,6 +153,7 @@ test("the zod schema and the client-safe parser agree, key order included", () =
handling: ["youtube", "transcribe"],
platform: ["youtube", "rumble", "nope"],
cookieMode: ["always", "never", "nope"],
+ sourceVideoQuality: ["original", "video_720", "nope"],
};
let seed = 7;
const rand = () => ((seed = (seed * 1103515245 + 12345) % 2 ** 31) / 2 ** 31);
diff --git a/common/lib/channelConfigSchema.ts b/common/lib/channelConfigSchema.ts
@@ -52,6 +52,7 @@ export const channelConfigObjectSchema = z.object({
url: field("url"),
audioFormat: field("audioFormat"),
downloadFormat: field("downloadFormat"),
+ sourceVideoQuality: field("sourceVideoQuality"),
keepSourceVideo: field("keepSourceVideo"),
keepLatest: field("keepLatest"),
extractionMode: field("extractionMode"),
diff --git a/common/lib/settingsSchema.test.ts b/common/lib/settingsSchema.test.ts
@@ -24,7 +24,10 @@ import type { Worker } from "./workers";
import type { AutoQueueSettings } from "./autoQueueTypes";
import type { ChannelPriority } from "./channelPriority";
import type { CookieMode } from "./cookiePolicy";
-import type { DownloadFormatPreset } from "../ytdlp/downloadFormat";
+import type {
+ DownloadFormatPreset,
+ SourceVideoQuality,
+} from "../ytdlp/downloadFormat";
import type { StorageSettings } from "./storageLocations";
import type {
AttributionSettings,
@@ -70,6 +73,8 @@ type PreSchemaSiteSettings = {
// Release 17 slice RL — the adaptive pace and the hold.
pacing: PacingSettingsBlock;
downloadFormat: DownloadFormatPreset;
+ // The source-video quality a full persist keeps.
+ sourceVideoQuality: SourceVideoQuality;
minFreeDiskGB: number;
resumeMarginGB: number;
parallelTranscriptions: number;
@@ -98,7 +103,7 @@ type PreSchemaSiteSettings = {
type Same<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
const shapeUnchanged: Same<SiteSettings, PreSchemaSiteSettings> = true;
-test("SiteSettings keeps its 33 fields, in file order", () => {
+test("SiteSettings keeps its 34 fields, in file order", () => {
assert.equal(shapeUnchanged, true);
assert.deepEqual(Object.keys(siteSettingsSchema.shape), [
"adminTitle",
@@ -112,6 +117,7 @@ test("SiteSettings keeps its 33 fields, in file order", () => {
"sleepBetweenDownloadsSeconds",
"pacing",
"downloadFormat",
+ "sourceVideoQuality",
"minFreeDiskGB",
"resumeMarginGB",
"parallelTranscriptions",
@@ -338,12 +344,19 @@ test("enum fields fall back to their default on junk", () => {
const s = siteSettingsSchema.parse({
cookieMode: "sometimes",
downloadFormat: "best",
+ sourceVideoQuality: "1080p",
reportDebouncePreset: "instant",
transcriptionApp: "no-such-app",
});
const d = defaults();
assert.equal(s.cookieMode, d.cookieMode);
assert.equal(s.downloadFormat, "auto");
+ assert.equal(s.sourceVideoQuality, "original");
+ assert.equal(d.sourceVideoQuality, "original");
+ assert.equal(
+ siteSettingsSchema.parse({ sourceVideoQuality: "video_720" }).sourceVideoQuality,
+ "video_720",
+ );
assert.equal(s.reportDebouncePreset, d.reportDebouncePreset);
assert.equal(s.transcriptionApp, d.transcriptionApp);
});
diff --git a/common/lib/settingsSchema.ts b/common/lib/settingsSchema.ts
@@ -44,8 +44,11 @@ import {
} from "./settingsFieldSchemas";
import { SYNC_INTERVAL_MAX_MINUTES } from "./channelConfig";
import {
+ DEFAULT_SOURCE_VIDEO_QUALITY,
isDownloadFormatPreset,
+ isSourceVideoQuality,
type DownloadFormatPreset,
+ type SourceVideoQuality,
} from "../ytdlp/downloadFormat";
import {
type AppInstanceConfig,
@@ -1566,6 +1569,10 @@ export const siteSettingsSchema = z.object({
isDownloadFormatPreset(v) ? v : "auto").describe(
"Default yt-dlp `-f` download format for every channel that doesn't set its own (ChannelConfig.downloadFormat). \"auto\" picks per-source: `original` for Odysee (whose HLS rungs are CDN-truncated), `bestaudio/worst` elsewhere. See common/ytdlp/downloadFormat.ts.",
),
+ sourceVideoQuality: settingsField((v): SourceVideoQuality =>
+ isSourceVideoQuality(v) ? v : DEFAULT_SOURCE_VIDEO_QUALITY).describe(
+ "Quality of the source container a full persist keeps — \"Persist source video\", the whole-recording fetch (`full: true`) and \"Persist kept now\" — for every channel that doesn't set its own (ChannelConfig.sourceVideoQuality). \"original\" (default) = `bestvideo*+bestaudio/best`; \"video_720\" = ≤720p H.264/AAC mp4 for clip and editing work, falling back to 480p and then to anything (logged). A single persist can override it. See common/ytdlp/downloadFormat.ts.",
+ ),
minFreeDiskGB: settingsField((v): number => clampMinFreeDiskGB(v)).describe(
"Minimum free disk space (GB) required on the transcripts data directory for downloads to run. When free space is below this floor, a download job is prevented from starting and a running batch stops launching new videos (the in-flight one finishes). 0 disables the gate. See common/lib/diskSpace.ts.",
),
diff --git a/settings.json.example b/settings.json.example
@@ -16,6 +16,7 @@
"holdProbeMinutes": 60
},
"downloadFormat": "auto",
+ "sourceVideoQuality": "original",
"minFreeDiskGB": 5,
"resumeMarginGB": 2,
"parallelTranscriptions": 2,