import { execa } from "execa"; export type ProbeMediaDurationOptions = { ffprobeBin: string; file: string; signal?: AbortSignal; onLog?: (s: string) => void; }; // Measure a media file's container duration (seconds) via ffprobe. Returns null // when ffprobe is missing, errors, or prints something unparseable — callers // treat null as "couldn't measure" and skip the duration guard rather than // failing a download. Distinct from probeAudioStream (ffmpegStreamProbe), which // is a pass/fail corruption check and yields no duration. export async function probeMediaDurationSec( opts: ProbeMediaDurationOptions, ): Promise { const args = [ "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", opts.file, ]; opts.onLog?.(`$ ${opts.ffprobeBin} ${args.join(" ")}\n`); try { const result = await execa(opts.ffprobeBin, args, { cancelSignal: opts.signal, reject: false, }); if (result.exitCode !== 0) { opts.onLog?.( `ffprobe exited ${result.exitCode}; skipping duration check.\n`, ); return null; } const sec = Number.parseFloat(String(result.stdout).trim()); if (!Number.isFinite(sec) || sec <= 0) return null; return sec; } catch (err) { opts.onLog?.( `ffprobe failed (${(err as Error).message}); skipping duration check.\n`, ); return null; } } // The height, in pixels, of a media file's first video stream, or null when // ffprobe is missing, errors, times out or the file has no video. Reads the // container header only, so it is cheap enough to answer "how tall is the file // already on disk" on a cache hit; `timeoutMs` bounds it for a caller that // must answer promptly (default 5 s). export async function probeVideoHeight(opts: { ffprobeBin: string; file: string; timeoutMs?: number; }): Promise { try { const result = await execa( opts.ffprobeBin, [ "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=height", "-of", "default=noprint_wrappers=1:nokey=1", opts.file, ], { reject: false, timeout: opts.timeoutMs ?? 5000 }, ); if (result.exitCode !== 0) return null; const h = Number.parseInt(String(result.stdout).trim(), 10); return Number.isInteger(h) && h > 0 ? h : null; } catch { return null; } }