refactor(main): execFileAsync helper — unify the 5 spawn-and-read sites

Replace the hand-rolled `new Promise((resolve) => execFile(…, cb))` wrappers in
probe / ytdlp (×2) / ffmpeg / indexer.probeFlat / download.probeMeta with one
lib/exec.ts execFileAsync returning a normalized
{ok,stdout,stderr,timedOut,error} that never rejects. Each caller keeps its own
error mapping (timedOut -> "Timed out …" vs stderr) and JSON parsing; behaviour
is unchanged. windowsHide defaults on; per-call timeout/maxBuffer preserved.

Foundation for CC4/CC5 — the stdout line-buffer half lands with SIMP5, the
net.request half with SIMP16, at which point those checkboxes close.

Unit-tested (exec.test.ts) against the node binary: ok / stderr+nonzero-exit /
missing-binary / timeout. typecheck + 262 tests + eslint + prettier green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 09:06:08 -04:00
parent 66eaaac8fc
commit 3baba3b7bc
7 changed files with 192 additions and 151 deletions
+18 -22
View File
@@ -9,9 +9,9 @@
* this module is the impure shell that spawns yt-dlp and writes to disk.
*/
import { execFile } from 'child_process'
import { existsSync } from 'fs'
import { getYtdlpPath, YTDLP_MISSING_MSG } from './binaries'
import { execFileAsync } from './lib/exec'
import { INDEX_MAX_BUFFER, INDEX_TIMEOUT_MS } from './constants'
import { cleanError } from './log'
import { assertHttpUrl } from './url'
@@ -46,28 +46,24 @@ interface FlatInfo {
* flat upload list can be a few MB of JSON; the timeout is generous for the same
* reason. `--` terminates option parsing so the URL can't be read as a flag.
*/
function probeFlat(url: string): Promise<FlatInfo> {
return new Promise((resolve, reject) => {
execFile(
getYtdlpPath(),
['-J', '--flat-playlist', '--no-warnings', '--', url],
{ windowsHide: true, maxBuffer: INDEX_MAX_BUFFER, timeout: INDEX_TIMEOUT_MS },
(err, stdout, stderr) => {
if (err) {
const msg = (err as { killed?: boolean }).killed
? 'Timed out indexing source. Check the link or your connection.'
: cleanError(stderr) || err.message
reject(new Error(msg))
return
}
try {
resolve(JSON.parse(stdout) as FlatInfo)
} catch {
reject(new Error('Could not parse source info from yt-dlp.'))
}
}
async function probeFlat(url: string): Promise<FlatInfo> {
const r = await execFileAsync(
getYtdlpPath(),
['-J', '--flat-playlist', '--no-warnings', '--', url],
{ maxBuffer: INDEX_MAX_BUFFER, timeout: INDEX_TIMEOUT_MS }
)
if (!r.ok) {
throw new Error(
r.timedOut
? 'Timed out indexing source. Check the link or your connection.'
: cleanError(r.stderr) || r.error?.message || ''
)
})
}
try {
return JSON.parse(r.stdout) as FlatInfo
} catch {
throw new Error('Could not parse source info from yt-dlp.')
}
}
/** Probe a channel tab (e.g. /videos, /playlists); never throws — returns null. */