fix(download): name the failing step in postprocessing errors

yt-dlp collapses an ffmpeg postprocessing failure to a bare
"Postprocessing: Conversion failed!" with no indication of which step
died, and without --verbose the underlying ffmpeg reason is never
emitted at all. describeDownloadError() appends the last non-noise
trailing lines (e.g. the [ModifyChapters] SponsorBlock re-encode) so
the error log and completion notification point at the actual cause
instead of a dead end.
This commit is contained in:
2026-07-05 14:39:29 -04:00
parent 1be2708d15
commit 5de4b40c1d
3 changed files with 121 additions and 2 deletions
+2 -2
View File
@@ -33,7 +33,7 @@ import {
FILEPATH_MARKER,
DEST_MARKER
} from './core/buildArgs'
import { cleanError } from './log'
import { describeDownloadError } from './log'
import { addErrorLog } from './errorlog'
import {
STALL_TIMEOUT_MS,
@@ -530,7 +530,7 @@ function wireChildProcess(params: {
opts.incognito
)
} else {
const msg = cleanError(stderrTail) || `yt-dlp exited with code ${code}`
const msg = describeDownloadError(stderrTail) || `yt-dlp exited with code ${code}`
send(wc, { type: 'error', id: opts.id, error: msg })
logFailure(opts, getTitle(), msg)
notify(
+41
View File
@@ -12,3 +12,44 @@ export function cleanError(stderr: string): string {
const errLine = [...lines].reverse().find((l) => /^error/i.test(l))
return (errLine ?? lines[lines.length - 1] ?? '').replace(/^error:\s*/i, '').trim()
}
// Lines that carry no diagnostic value as post-processing context: per-tick
// download progress and the "Deleting original file" cleanup bookkeeping yt-dlp
// prints between merge steps.
const CONTEXT_NOISE = /^\[download\]|^Deleting original file/i
// A single context line, capped so a long output path can't bloat the message.
const CONTEXT_LINE_MAX = 200
/**
* Richer error for the DOWNLOAD failure path (probe/indexer/updater keep the
* concise cleanError line).
*
* yt-dlp collapses an ffmpeg post-processing failure to a bare
* "ERROR: Postprocessing: Conversion failed!" that names neither the step that
* died nor the reason — and without `--verbose` the underlying ffmpeg detail is
* never emitted at all. What yt-dlp DOES print is a tagged progress line per
* post-processor ([Merger], [ModifyChapters], [ExtractAudio], …; its phase tags
* like [download]/[info] are lowercase). So for that generic wrapper we append
* the last couple of non-noise trailing lines, which name the failing step (e.g.
* the SponsorBlock re-encode) and surface any ffmpeg diagnostic when one is
* present. Self-explanatory errors (a private video, a 403) are returned
* unchanged.
*/
export function describeDownloadError(stderr: string): string {
const headline = cleanError(stderr)
if (!headline || !/conversion failed|postprocessing/i.test(headline)) return headline
const lines = stderr
.split('\n')
.map((l) => l.trim())
.filter(Boolean)
const context: string[] = []
for (const line of [...lines].reverse()) {
if (context.length >= 2) break
if (CONTEXT_NOISE.test(line) || /^error/i.test(line)) continue
// Skip anything already conveyed by the headline (avoids echoing it back).
if (headline.includes(line) || line.includes(headline)) continue
const capped = line.length > CONTEXT_LINE_MAX ? `${line.slice(0, CONTEXT_LINE_MAX - 1)}` : line
if (!context.includes(capped)) context.unshift(capped)
}
return context.length > 0 ? `${headline}${context.join(' · ')}` : headline
}