// THE APP'S AUDIO EXTRACTION, after a download that left a source container. // Split out of ytdlp/downloadOneManaged.ts so the archive.org fetch // (controller/archiveOrgDownload.ts), which downloads no-yt-dlp files into the // same data//, finalises them through the same code: audio. by // ffmpeg, the container kept in the saved-video store or removed. import path from "node:path"; import { readdir } from "node:fs/promises"; import { removeMediaFile } from "../lib/mediaTier-server"; import type { AudioFormat, ChannelConfig } from "../lib/channelConfig"; import type { Paths } from "../lib/paths"; import type { PersistenceDecision } from "./persistencePlan"; import { transcodeAudio } from "../controller/transcode"; import { findSourceMedia } from "../lib/videoStatus"; import { savedVideoDir, type SavedVideoFormat, type SavedVideoOrigin, } from "../lib/savedVideo"; import { persistSourceVideo } from "../lib/savedVideo-server"; // After an app-mode transcribe download, produce audio. from the downloaded // source-media. container via ffmpeg. When the video is persisted the // container is MOVED into the saved-video store (Phase 3) and a pointer is left // in the data dir; otherwise the container is removed (the extract-now / // save-disk path). Best-effort throughout: a failed extraction leaves the // container in place (so a later pass or the transcribe-from-container fallback // can still recover), and a failed store-move leaves the container in the data // dir as source-media. (still persisted, just not relocated). Neither fails // the whole download. export async function finalizeAppExtraction(opts: { paths: Paths; channelSlug: string; channelConfig: ChannelConfig; videoDir: string; videoId: string; fmt: AudioFormat; persist: boolean; // The persistence cause, recorded on the saved-video pointer (governs the // retention prune). Only meaningful when persist is true. category: PersistenceDecision["category"]; // Threaded straight onto the pointer; see ManagedDownloadOpts.persistOrigin. origin?: SavedVideoOrigin; // PERSIST ONLY when false: move the container, extract nothing. A forced // media download of a video that already has a transcript (attempt 3, // `keepTranscript`) wants the source kept, and an audio. 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; // The container's name in videoDir, when the caller knows it (the // archive.org fetch). Absent: the sorted first source-media. there. sourceFilename?: string; onLog: (s: string) => void; signal: AbortSignal; }): Promise { const source = opts.sourceFilename ?? findSourceMedia(await readdir(opts.videoDir).catch(() => [] as string[])); if (!source) { opts.onLog( `App extraction: no source-media container in ${opts.videoDir}; skipping.\n`, ); return; } if (opts.extractAudio === false) { opts.onLog( `Persist only: a transcript is on disk, so no audio.${opts.fmt} is extracted from ${source}.\n`, ); if (!opts.persist) { // Unreachable from attempt 3 today (it does not download when the plan // persists nothing), kept so the option means one thing on its own. opts.onLog(`Not persisting ${source}; it stays in the data dir.\n`); return; } } else { try { await transcodeAudio({ paths: opts.paths, videoDir: opts.videoDir, sourceFilename: source, targetFormat: opts.fmt, onLog: opts.onLog, signal: opts.signal, }); } catch (err) { opts.onLog( `App extraction failed (${source} -> audio.${opts.fmt}): ${(err as Error).message}. Keeping the source container.\n`, ); return; } if (!opts.persist) { await removeMediaFile(opts.videoDir, source); opts.onLog(`Discarded source container ${source} (audio-only).\n`); return; } } // Persist: move the container into the saved-video store + write a pointer. const storeDir = savedVideoDir( opts.paths, opts.channelConfig, opts.channelSlug, opts.videoId, ); try { const pointer = await persistSourceVideo({ videoDir: opts.videoDir, sourceFilename: source, 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 // already the right handling: the container stays in the data dir with a // line in the log, and the next persist (after the move) picks it up. paths: opts.paths, }); opts.onLog( `Persisted source video to ${path.join(pointer.dir, pointer.file)} (${pointer.bytes} bytes).\n`, ); } catch (err) { opts.onLog( `Failed to move source container into the saved-video store (${(err as Error).message}); leaving ${source} in the data dir.\n`, ); } }