41 lines
1.7 KiB
TypeScript
41 lines
1.7 KiB
TypeScript
import { link, mkdir, copyFile } from "node:fs/promises";
|
|
import { dirname } from "node:path";
|
|
import { run } from "./util/exec";
|
|
|
|
export interface Tools { ffmpeg: string; ffprobe: string }
|
|
|
|
export async function probeDuration(t: Tools, file: string): Promise<number | undefined> {
|
|
try {
|
|
const out = await run([t.ffprobe, "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", file]);
|
|
const s = Number(out.trim());
|
|
return Number.isFinite(s) && s > 0 ? Math.round(s) : undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/** One JPEG frame at `fraction` of the clip, 640px wide. */
|
|
export async function extractFrame(t: Tools, file: string, out: string, fraction = 0.2): Promise<void> {
|
|
await mkdir(dirname(out), { recursive: true });
|
|
const dur = (await probeDuration(t, file)) ?? 0;
|
|
const at = Math.max(0, dur * fraction);
|
|
await run([t.ffmpeg, "-v", "error", "-y", "-ss", String(at), "-i", file, "-frames:v", "1", "-vf", "scale=640:-2", "-q:v", "4", out]);
|
|
}
|
|
|
|
/**
|
|
* Puts a large file into the course. "copy" uses `cp --reflink=auto` (a CoW clone where the filesystem supports it,
|
|
* a plain copy elsewhere); "link" makes a hard link and falls back to copying across filesystems.
|
|
* Either way the original is only read, and the course folder stays self-contained (the media route refuses symlinks that leave it).
|
|
*/
|
|
export async function putFile(src: string, dest: string, mode: "copy" | "link" = "copy"): Promise<void> {
|
|
await mkdir(dirname(dest), { recursive: true });
|
|
if (mode === "link") {
|
|
try { await link(src, dest); return; } catch { /* cross-device or unsupported: copy */ }
|
|
}
|
|
try {
|
|
await run(["cp", "--reflink=auto", "--", src, dest]);
|
|
} catch {
|
|
await copyFile(src, dest);
|
|
}
|
|
}
|