/** * Pure identification of a download's orphaned yt-dlp intermediate files (R4). * * When a running download is canceled, yt-dlp is tree-killed and leaves partial * files behind in the output folder: the growing `.part`, fragment pieces * (`.part-Frag*`) and the `.ytdl` fragment-state file for fragmented formats, plus * any already-finished per-stream file (`.fNNN.` — e.g. the video half * of a video+audio download whose audio was still in flight). A plain cancel never * removes these, so they accumulate. download.ts captures the resolved FINAL output * path from the before_dl `dest|` print; this module turns that path + a folder * listing into the exact set to delete. * * Kept free of any electron/node-runtime chain (only `path.parse`) so it's * unit-testable in isolation, the same posture as lib/formatters.ts (L37). * * SAFETY: it returns only unambiguous yt-dlp intermediates, matched on the output * stem. It never returns a bare `.` completed file — so a pre-existing * same-title download the user kept in the same folder is safe — nor any file * belonging to a different download (different stem). This is the guard against the * audit's "delete the wrong file if the destination capture is even slightly off". */ import { parse } from 'path' export function orphanPartials(outputPath: string, entries: string[]): string[] { // `name` is the basename without the final extension; a title containing dots is // preserved (only the trailing `.` is stripped), so the stem stays exact. const stem = parse(outputPath).name if (!stem) return [] const stemDot = `${stem}.` return entries.filter((entry) => { if (!entry.startsWith(stemDot)) return false return ( entry.endsWith('.part') || // ….part — in-progress stream or merge entry.includes('.part-Frag') || // fragment pieces (HLS/DASH) entry.endsWith('.ytdl') || // fragment-download state file /\.f\d+\.[^.]+$/i.test(entry) // finished per-stream file, e.g. .f399.mp4 ) }) }