import path from "node:path"; import { lstat, readdir, stat } from "node:fs/promises"; import type { Dirent } from "node:fs"; import { isTierable } from "yt-dlp-transcript-common/lib/mediaTier"; import { onDrive } from "yt-dlp-transcript-common/lib/storageHealth"; import type { VideoFile } from "../videos/[id]/components/cards/videoFiles"; // ONE VIDEO DIRECTORY'S FILES, for the video page and the videos list's detail // pane (server only). // // RELEASE 17: a tiered file is a RELATIVE SYMLINK in `data//` into // `channels//media/`, and a dirent `isFile()` filter hides it — on a // tiered channel the audio would vanish from both pages. So links are listed // too, and BY FILE KIND: // // - a real file is statted directly (the corpus disk); // - a link is `lstat`ed for its time (the hook gives the link the file's own // times, so the corpus disk answers it) and its SIZE is asked of the media // drive — every media link of the directory in ONE `onDrive(mediaDir)` call // when the channel's media is relocated, so a drive that does not answer // costs one budget, not one per file, and a stalled one none at all. A // link whose target is not there (an unmounted or renamed-away drive) or // whose drive does not answer is listed with `size: null` — the file exists; // its bytes are not reachable now. // // The mover's tree walk (relocateDir.ts) keeps its own `isFile()`: over // `data/` it measures the corpus disk's text, over `media/` the real bytes. export async function loadVideoDirFiles( videoDir: string, // `config.mediaDir`, when the channel's media is relocated. Absent: an // in-place `media/` is on the corpus disk and needs no watchdog. mediaDir: string | undefined, ): Promise<{ files: VideoFile[] }> { let entries: Dirent[]; try { entries = await readdir(videoDir, { withFileTypes: true }); } catch { return { files: [] }; } const files: VideoFile[] = []; const links: Dirent[] = []; for (const e of entries) { if (e.name.includes(".tmp-")) continue; if (e.isFile()) { const s = await stat(path.join(videoDir, e.name)).catch(() => null); if (s) files.push({ name: e.name, size: s.size, mtime: s.mtimeMs }); } else if (e.isSymbolicLink()) { links.push(e); } } if (links.length > 0) { const sizes = new Map(); const statLinks = async (kind: "media" | "other") => { for (const e of links) { const isMedia = isTierable(e.name); if ((kind === "media") !== isMedia) continue; const s = await stat(path.join(videoDir, e.name)).catch(() => null); if (s?.isFile()) sizes.set(e.name, s.size); } }; // A link that is not a media name is not the tier's (nothing makes one); // it is statted directly, like the files. await statLinks("other"); try { await (mediaDir ? onDrive(mediaDir, () => statLinks("media")) : statLinks("media")); } catch { /* the drive did not answer: the sizes stay unknown */ } for (const e of links) { const l = await lstat(path.join(videoDir, e.name)).catch(() => null); if (!l) continue; files.push({ name: e.name, size: sizes.get(e.name) ?? null, mtime: l.mtimeMs, }); } } files.sort((a, b) => a.name.localeCompare(b.name)); return { files }; }