feat(diagnostics): persist full stderr detail behind a "Show details" toggle

describeDownloadError condenses a postprocessing failure to one line for
the queue/notification, discarding the rest of yt-dlp's stderr. The
persisted error log now also keeps the fuller non-noise transcript
(stderrDetail) as an optional `detail` field, and the Diagnostics card
gets a collapsible "Show details" expander per row so a failure like
"Postprocessing: Conversion failed!" can be inspected -- and copied
into a bug report -- without re-running the download from a terminal.
This commit is contained in:
2026-07-05 14:56:09 -04:00
parent 6cb9c5ca47
commit 27cae28e12
9 changed files with 155 additions and 17 deletions
+2 -1
View File
@@ -74,7 +74,8 @@ export function isValidErrorLogEntry(o: unknown): o is ErrorLogEntry {
(e.kind === 'video' || e.kind === 'audio') &&
typeof e.error === 'string' &&
typeof e.occurredAt === 'number' &&
isOptionalString(e.title)
isOptionalString(e.title) &&
isOptionalString(e.detail)
)
}
+9 -3
View File
@@ -33,7 +33,7 @@ import {
FILEPATH_MARKER,
DEST_MARKER
} from './core/buildArgs'
import { describeDownloadError } from './log'
import { describeDownloadError, stderrDetail } from './log'
import { addErrorLog } from './errorlog'
import {
STALL_TIMEOUT_MS,
@@ -117,7 +117,12 @@ function notify(wc: WebContents, title: string, body: string, incognito = false)
if (flashWin && !flashWin.isDestroyed() && !flashWin.isFocused()) flashWin.flashFrame(true)
}
function logFailure(opts: StartDownloadOptions, title: string | undefined, error: string): void {
function logFailure(
opts: StartDownloadOptions,
title: string | undefined,
error: string,
detail?: string
): void {
// L136: incognito downloads are never recorded — not even a failure entry, which
// would persist the title + URL in the user-facing diagnostics log.
if (opts.incognito) return
@@ -127,6 +132,7 @@ function logFailure(opts: StartDownloadOptions, title: string | undefined, error
url: opts.url,
kind: opts.kind,
error,
detail,
occurredAt: Date.now()
})
}
@@ -532,7 +538,7 @@ function wireChildProcess(params: {
} else {
const msg = describeDownloadError(stderrTail) || `yt-dlp exited with code ${code}`
send(wc, { type: 'error', id: opts.id, error: msg })
logFailure(opts, getTitle(), msg)
logFailure(opts, getTitle(), msg, stderrDetail(stderrTail))
notify(
wc,
opts.incognito ? 'Download failed' : (getTitle() ?? 'Download failed'),
+17
View File
@@ -53,3 +53,20 @@ export function describeDownloadError(stderr: string): string {
}
return context.length > 0 ? `${headline}${context.join(' · ')}` : headline
}
/**
* Full non-noise stderr, for the persisted error log's "Show details" expander
* (Diagnostics card) — everything describeDownloadError condenses to one line,
* kept in full (progress ticks and delete-bookkeeping still stripped) so a bug
* report carries the real yt-dlp/ffmpeg transcript instead of a paraphrase.
* Returns undefined when there's nothing beyond a single line — the headline
* the caller already shows — so a plain, self-explanatory error doesn't grow a
* details toggle with nothing new in it.
*/
export function stderrDetail(stderr: string): string | undefined {
const lines = stderr
.split('\n')
.map((l) => l.trim())
.filter((l) => l && !CONTEXT_NOISE.test(l))
return lines.length > 1 ? lines.join('\n') : undefined
}