import path from "node:path"; import type { Paths } from "../lib/paths"; import { isPermanentlyGone } from "../lib/availability"; import { resolveEffectiveAvailability } from "../lib/availability-server"; import { loadDoNotClean, setDoNotClean } from "../lib/doNotClean-server"; import { readChannelConfig } from "./channels"; import { computeKeptVideoIds } from "./keptVideos"; import { runAvailabilityCheck } from "./checkAvailability"; // Periodic "are my kept videos still there?" pass. For a channel's keep-latest // window, re-probe source availability and pin (do-not-clean) any video found // permanently gone (deleted / private / members_only). A pinned video survives // even after it rolls out of the rolling window — it's now irreplaceable, so we // keep its media forever. Reuses runAvailabilityCheck (onlyIds = the kept set) // for the actual yt-dlp probing. export type CheckKeptDeletedOptions = { channelSlug: string; paths: Paths; onLog?: (msg: string) => void; signal?: AbortSignal; concurrency?: number; }; export type CheckKeptDeletedResult = { // Size of the keep-latest window inspected. kept: number; // Videos that probed as permanently gone (EXCLUDED_FROM_DOWNLOAD). deleted: number; // Videos newly pinned with the do-not-clean marker this run. pinned: number; }; export async function checkKeptDeleted({ channelSlug, paths, onLog, signal, concurrency, }: CheckKeptDeletedOptions): Promise { const log = onLog ?? ((m: string) => console.log(m)); const config = await readChannelConfig(paths, channelSlug); const keepLatest = config?.keepLatest ?? 0; if (keepLatest <= 0) { log(`${channelSlug}: keep-latest disabled — nothing to check.`); return { kept: 0, deleted: 0, pinned: 0 }; } const keptIds = await computeKeptVideoIds({ paths, channelSlug, keepLatest }); if (keptIds.size === 0) { log(`${channelSlug}: no videos in the keep-latest window.`); return { kept: 0, deleted: 0, pinned: 0 }; } const onlyIds = [...keptIds]; log(`${channelSlug}: checking ${onlyIds.length} kept video(s) for deletion…`); // Probe only the kept set, re-checking everything not already known-gone. await runAvailabilityCheck({ channelSlug, paths, mode: "recheck-non-deleted", onlyIds, concurrency, onLog: log, signal, }); const dataDir = path.join(paths.channelsDir, channelSlug, "data"); let deleted = 0; let pinned = 0; for (const id of onlyIds) { if (signal?.aborted) break; const videoDir = path.join(dataDir, id); const availability = await resolveEffectiveAvailability(videoDir); if (!isPermanentlyGone(availability)) continue; deleted++; // Only count/log a NEW pin — leave an existing marker (and its note) intact. const already = await loadDoNotClean(videoDir); if (already) continue; const note = `deleted from source (keep-latest): ${availability}`; await setDoNotClean(videoDir, true, note); pinned++; log(`Pinned ${id} as do-not-clean (${availability}).`); } log( `${channelSlug}: ${deleted} of ${onlyIds.length} kept video(s) gone from source; pinned ${pinned}.`, ); return { kept: onlyIds.length, deleted, pinned }; }