import type { Platform } from "../lib/platform"; // The user-selectable yt-dlp `-f` download format, as a small preset enum (kept // an enum rather than a free-form `-f` string so it validates like audioFormat). // "auto" is platform-aware: Odysee/LBRY only serves the full-length audio in its // `original` format (every HLS rung is CDN-truncated to a few minutes), so auto // prefers `original` there, archive.org gets the uploader's original file // (ARCHIVE_ORG_AUTO_FORMAT_SELECTOR), and the historical `bestaudio/worst` // everywhere else — BitChute included, whose one format (an mp4 with no codec // fields) `bestaudio` never matches and `worst` takes. export type DownloadFormatPreset = | "auto" | "original" | "bestaudio" | "bestvideo_audio" | "video_720"; // The clip/editing preset's id, named once for every caller that asks for it // by name (the persist control, the source-video quality setting, the batch // persist and the clip fetch). export const VIDEO_720_PRESET = "video_720" as const; export const DOWNLOAD_FORMAT_PRESETS: ReadonlyArray = [ "auto", "original", "bestaudio", "bestvideo_audio", VIDEO_720_PRESET, ]; export function isDownloadFormatPreset( value: unknown, ): value is DownloadFormatPreset { return ( typeof value === "string" && (DOWNLOAD_FORMAT_PRESETS as ReadonlyArray).includes(value) ); } // Human labels for the UI selects (global / channel / per-video). export const DOWNLOAD_FORMAT_LABELS: Record = { auto: "Auto (per-source)", original: "Original (full file)", bestaudio: "Best audio only", bestvideo_audio: "Best video + audio", video_720: "Video 720p (H.264, for clips/editing)", }; // THE FORMAT SELECTOR A CLIP IS CUT FROM, and why it is not "bestvideo_audio". // // "bestvideo_audio" answers "what should we ARCHIVE" — best available, // container-agnostic. This answers "what can ffmpeg cut and re-encode cheaply, // right now". Those differ on one specific trap: left alone yt-dlp picks // VP9+Opus at these heights, and since --force-keyframes-at-cuts re-encodes, // that means libvpx-vp9 — 27 s to cut a 5 s clip, measured — and it writes // .webm, which it then appends to the -o name. So H.264/AAC in mp4 is pinned, // with three progressively looser fallbacks. Byte-identical to the selector // umtool's build-video.mjs uses, on purpose: a window this fetches and a window // that fetched must be the same file, or the two caches diverge. // // Lives here (not in fetchWindowManaged.ts, which re-exports it) because the // "video_720" preset below is built from its rungs, and a persist must not // import the clip-window fetch to learn a selector. export function clipFormatSelector(maxHeight: number): string { return [...clipSplitRungs(maxHeight), ...clipSingleFileRungs(maxHeight)].join( "/", ); } // The clip selector's two halves. SPLIT: separate H.264 video + AAC audio // streams, merged. SINGLE-FILE: one file carrying both (mp4 first). Split // first, because a single-file format at these heights is often a low rung // (YouTube's 360p mp4). function clipSplitRungs(maxHeight: number): string[] { return [ `bv*[vcodec^=avc1][height<=${maxHeight}]+ba[acodec^=mp4a]`, `bv*[ext=mp4][height<=${maxHeight}]+ba[ext=m4a]`, ]; } function clipSingleFileRungs(maxHeight: number): string[] { return [`b[ext=mp4][height<=${maxHeight}]`, `b[height<=${maxHeight}]`]; } // The height "video_720" asks for, and the rung it falls back to. export const VIDEO_720_MAX_HEIGHT = 720; export const VIDEO_720_FALLBACK_HEIGHT = 480; // The codec-agnostic split rung of "video_720", after the H.264 split ones: a // source whose only video at or under 720p is VP9 or AV1 still lands at or // under 720p, at its best such height — not on a 360p single-file mp4, and not // on the best (possibly 4K) file. export const VIDEO_720_ANY_CODEC_RUNG = `bv*[height<=${VIDEO_720_MAX_HEIGHT}]+ba`; // The last rung of "video_720": anything at all. Reached only when the source // has nothing at or under 720p, so a file above 720p is possible — the // download logs it (downloadOneManaged, persistedFormatLog) rather than // letting it pass silently. export const LAST_RESORT_FORMAT_SELECTOR = "bv*+ba/b"; // The source container a persist has always taken: best video + best audio. export const ORIGINAL_SOURCE_FORMAT_SELECTOR = "bestvideo*+bestaudio/best"; // Resolve a preset into the concrete yt-dlp `-f` selector string. The platform // only matters for "auto"; explicit presets apply literally regardless of // source (so an explicit choice always wins over the per-source default). export function resolveDownloadFormatSelector( preset: DownloadFormatPreset, platform: Platform | null | undefined, ): string { switch (preset) { case "original": return "original/bestaudio/worst"; case "bestaudio": return "bestaudio/worst"; case "bestvideo_audio": return ORIGINAL_SOURCE_FORMAT_SELECTOR; case "video_720": // NOT clipFormatSelector(720) whole: its single-file rungs would take a // 360p mp4 before a 720p VP9 split pair is ever tried. Split H.264 at // 720, then at 480, then any codec split at or under 720, then the // single-file rungs, then anything (logged). return [ ...clipSplitRungs(VIDEO_720_MAX_HEIGHT), ...clipSplitRungs(VIDEO_720_FALLBACK_HEIGHT), VIDEO_720_ANY_CODEC_RUNG, ...clipSingleFileRungs(VIDEO_720_MAX_HEIGHT), LAST_RESORT_FORMAT_SELECTOR, ].join("/"); case "auto": default: if (platform === "archiveorg") return ARCHIVE_ORG_AUTO_FORMAT_SELECTOR; return platform === "odysee" ? "original/bestaudio/worst" : "bestaudio/worst"; } } // archive.org's "auto": the ORIGINAL file the uploader put up, in a common // container, and for an audio item its MP3 (or Ogg) — not "bestaudio/worst". // yt-dlp knows no codecs for an archive.org format (only its extension and // `format_note`, "original" or "derivative"), so `bestaudio` never matches a // video file and `worst` would take whichever transcode sorts last. A video // original in another container (.avi, .mpeg) is the next rung; a FLAC/WAV // original loses to its MP3 derivative, a fraction of the bytes for the same // transcript. Anything at all is the last rung. export const ARCHIVE_ORG_AUTO_FORMAT_SELECTOR = [ "b[format_note=original][ext=mp4]", "b[format_note=original][ext=mkv]", "b[format_note=original][ext=webm]", "b[format_note=original][ext=mp3]", "mp3", "ogg", "b[format_note=original]", "b", ].join("/"); // The override chain mirrors audioFormat: per-run override beats the per-channel // default beats the global default; "auto" is the baseline when nothing is set. export function resolveDownloadFormatPreset(opts: { override?: DownloadFormatPreset | null; channel?: DownloadFormatPreset | null; global?: DownloadFormatPreset | null; }): DownloadFormatPreset { return opts.override ?? opts.channel ?? opts.global ?? "auto"; } // ── SOURCE-VIDEO QUALITY ─────────────────────────────────────────────────── // // What a FULL-SOURCE persist downloads: "original" (best video + best audio, // the behaviour every persist had before this setting) or "video_720" (≤720p // H.264, for clip and editing work). A separate axis from DownloadFormatPreset: // that one picks what a download fetches to get AUDIO from; this one picks the // container a persist keeps. Set globally (settings `sourceVideoQuality`), per // channel (ChannelConfig.sourceVideoQuality) and per persist (the editor's // "Persist source video" control). export type SourceVideoQuality = "original" | typeof VIDEO_720_PRESET; export const SOURCE_VIDEO_QUALITIES: ReadonlyArray = [ "original", VIDEO_720_PRESET, ]; export const DEFAULT_SOURCE_VIDEO_QUALITY: SourceVideoQuality = "original"; export const SOURCE_VIDEO_QUALITY_LABELS: Record = { original: "Original (best video + audio)", video_720: DOWNLOAD_FORMAT_LABELS.video_720, }; export function isSourceVideoQuality( value: unknown, ): value is SourceVideoQuality { return ( typeof value === "string" && (SOURCE_VIDEO_QUALITIES as ReadonlyArray).includes(value) ); } // The `-f` selector a persist at this quality passes. "original" is the // selector a persist has always used, byte for byte. export function sourceVideoFormatSelector(quality: SourceVideoQuality): string { return quality === VIDEO_720_PRESET ? resolveDownloadFormatSelector(VIDEO_720_PRESET, null) : ORIGINAL_SOURCE_FORMAT_SELECTOR; } // THE ONE EFFECTIVE-QUALITY RESOLVER. Every surface that persists a full // source — the persist control, the whole-recording fetch, "Persist kept now", // the batch persist — asks this, so a channel's choice cannot win on one and // lose on another. Per-persist override beats the channel beats the global; // "original" when nothing is set. Each input is checked, so a stale or // hand-typed value falls through to the next rather than reaching yt-dlp. export function resolveSourceVideoQuality(opts: { override?: unknown; channel?: unknown; global?: unknown; }): SourceVideoQuality { for (const v of [opts.override, opts.channel, opts.global]) { if (isSourceVideoQuality(v)) return v; } return DEFAULT_SOURCE_VIDEO_QUALITY; } // The source-video quality a whole-recording fetch that names a height cap // asks for. At or under 720 that is "video_720" (≤720p H.264); above it, // "original" — the caller said in so many words that 720 is not enough, so the // channel's own "video_720" must not quietly overrule it. No cap at all is not // handled here: that fetch inherits the channel's, else the global, quality. export function sourceVideoQualityForMaxHeight( maxHeight: number, ): SourceVideoQuality { return maxHeight <= VIDEO_720_MAX_HEIGHT ? VIDEO_720_PRESET : "original"; }