import type { ExtractionMode } from "../lib/channelConfig"; // The per-download persistence rule engine. Each individual download consults // the channel's keep-latest rule (plus any per-run overrides) to decide TWO // things: // 1. persist — keep the source video container (vs. audio-only) // 2. extractionMode — who extracts audio: yt-dlp's `-x` or the app's ffmpeg // // This is the heart of Phase 2: persistence is a per-channel rule evaluated per // individual download, NOT a separate runner/queue. Kept as a pure function so // the precedence is unit-testable in isolation from yt-dlp and the filesystem. export type PersistRunOverrides = { // Force-keep (true) or force-discard (false) the source video for this run, // overriding the channel's keep-latest rule. undefined = no override. keepSourceVideoOverride?: boolean; // Force "extract audio now and discard the container" even for a video the // keep-latest rule would otherwise persist (the backfill / save-disk case). extractImmediately?: boolean; }; export type PersistenceDecisionInput = { // This video falls inside the channel's keep-latest window (by upload date). inWindow: boolean; // This video carries a do-not-clean pin (manually protected / deleted-from- // source). Pinned videos are irreplaceable, so we persist their source too. pinned: boolean; // The channel's configured extraction strategy for audio-only downloads. channelExtractionMode?: ExtractionMode; overrides?: PersistRunOverrides; }; // Why a video's source is persisted — recorded on the saved-video pointer so the // retention prune (Phase 3) can tell a rolling keep-latest container (evictable // once it leaves the window) apart from a manually-archived one ("override") or a // pinned/irreplaceable one ("pin"), which must never be auto-pruned. "none" when // the video is not persisted. export type PersistCategory = "keep-latest" | "pin" | "override" | "none"; export type PersistenceDecision = { // Keep the downloaded source video container (Phase 3 moves it to the store; // Phase 2 leaves it in the data dir as source-media.). persist: boolean; extractionMode: ExtractionMode; // Coarse cause of the decision, used by the saved-store retention prune. category: PersistCategory; // Human-readable justification, surfaced in the download log. reason: string; }; // Precedence (highest first): // 1. per-run keepSourceVideoOverride === true -> persist // 2. per-run keepSourceVideoOverride === false -> don't persist // 3. per-run extractImmediately -> don't persist (extract now) // 4. do-not-clean pin -> persist // 5. inside the keep-latest window -> persist // 6. otherwise -> audio-only // Persisting implies app-side extraction (we need the container in hand); an // audio-only outcome uses the channel's configured mode (default "ytdlp"). export function resolvePersistenceDecision( input: PersistenceDecisionInput, ): PersistenceDecision { const o = input.overrides ?? {}; let persist: boolean; let category: PersistCategory; let reason: string; if (o.keepSourceVideoOverride === true) { persist = true; category = "override"; reason = "per-run keep-source-video override"; } else if (o.keepSourceVideoOverride === false) { persist = false; category = "none"; reason = "per-run no-keep override"; } else if (o.extractImmediately) { persist = false; category = "none"; reason = "per-run extract-immediately override"; } else if (input.pinned) { persist = true; category = "pin"; reason = "do-not-clean pin"; } else if (input.inWindow) { persist = true; category = "keep-latest"; reason = "keep-latest window"; } else { persist = false; category = "none"; reason = "outside keep-latest window"; } const extractionMode: ExtractionMode = persist ? "app" : (input.channelExtractionMode ?? "ytdlp"); return { persist, extractionMode, category, reason }; }