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:
+27
-39
@@ -1,6 +1,6 @@
|
||||
import { execFile } from 'child_process'
|
||||
import { existsSync } from 'fs'
|
||||
import { getYtdlpPath, YTDLP_MISSING_MSG } from './binaries'
|
||||
import { execFileAsync } from './lib/exec'
|
||||
import { PROBE_MAX_BUFFER, PROBE_TIMEOUT_MS } from './constants'
|
||||
import { fmtBytes } from '@shared/format'
|
||||
import { cleanError } from './log'
|
||||
@@ -114,52 +114,40 @@ function buildPlaylist(data: RawInfo): PlaylistInfo {
|
||||
* its format list) or, when the URL is a playlist, the flat list of its entries.
|
||||
* `--flat-playlist` is a no-op for a lone video, so one call covers both cases.
|
||||
*/
|
||||
export function probeMedia(url: string): Promise<ProbeResult> {
|
||||
export async function probeMedia(url: string): Promise<ProbeResult> {
|
||||
const ytdlp = getYtdlpPath()
|
||||
if (!existsSync(ytdlp)) {
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
error: YTDLP_MISSING_MSG
|
||||
})
|
||||
return { ok: false, error: YTDLP_MISSING_MSG }
|
||||
}
|
||||
let target: string
|
||||
try {
|
||||
target = assertHttpUrl(url) // normalised form (audit F5)
|
||||
} catch (e) {
|
||||
return Promise.resolve({ ok: false, error: (e as Error).message })
|
||||
return { ok: false, error: (e as Error).message }
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
execFile(
|
||||
ytdlp,
|
||||
// `--` terminates option parsing so the URL can never be read as a flag.
|
||||
['-J', '--flat-playlist', '--no-warnings', '--', target],
|
||||
{ windowsHide: true, maxBuffer: PROBE_MAX_BUFFER, timeout: PROBE_TIMEOUT_MS },
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
// execFile sets `killed` when it terminated the process on timeout.
|
||||
const msg = (err as { killed?: boolean }).killed
|
||||
? 'Timed out fetching video info. Check the link or your connection.'
|
||||
: cleanError(stderr) || err.message
|
||||
resolve({ ok: false, error: msg })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(stdout) as RawInfo
|
||||
if (data._type === 'playlist' || Array.isArray(data.entries)) {
|
||||
const playlist = buildPlaylist(data)
|
||||
if (playlist.count === 0) {
|
||||
resolve({ ok: false, error: 'This playlist has no downloadable entries.' })
|
||||
return
|
||||
}
|
||||
resolve({ ok: true, kind: 'playlist', playlist })
|
||||
} else {
|
||||
resolve({ ok: true, kind: 'video', info: buildInfo(data) })
|
||||
}
|
||||
} catch {
|
||||
resolve({ ok: false, error: 'Could not parse video info from yt-dlp.' })
|
||||
}
|
||||
}
|
||||
)
|
||||
// `--` terminates option parsing so the URL can never be read as a flag.
|
||||
const r = await execFileAsync(ytdlp, ['-J', '--flat-playlist', '--no-warnings', '--', target], {
|
||||
maxBuffer: PROBE_MAX_BUFFER,
|
||||
timeout: PROBE_TIMEOUT_MS
|
||||
})
|
||||
if (!r.ok) {
|
||||
const msg = r.timedOut
|
||||
? 'Timed out fetching video info. Check the link or your connection.'
|
||||
: cleanError(r.stderr) || r.error?.message || ''
|
||||
return { ok: false, error: msg }
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(r.stdout) as RawInfo
|
||||
if (data._type === 'playlist' || Array.isArray(data.entries)) {
|
||||
const playlist = buildPlaylist(data)
|
||||
if (playlist.count === 0) {
|
||||
return { ok: false, error: 'This playlist has no downloadable entries.' }
|
||||
}
|
||||
return { ok: true, kind: 'playlist', playlist }
|
||||
}
|
||||
return { ok: true, kind: 'video', info: buildInfo(data) }
|
||||
} catch {
|
||||
return { ok: false, error: 'Could not parse video info from yt-dlp.' }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user