Complete Phase C (custom commands & yt-dlp updater) and Phase D (library, UX & notifications)

Phase C had been implemented in the working tree but never committed:
named custom-command templates (CRUD + per-download override), command
preview, and the in-app yt-dlp self-updater.

Phase D adds: history search/kind-filter/multi-select/bulk-delete/
re-download; native OS notifications on completion/failure; a private/
incognito download mode that skips history; settings+template backup/
restore via JSON; and a persistent error log surfaced as a Diagnostics
report in Settings.

See ROADMAP.md for the full per-item breakdown.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 06:19:45 -04:00
parent 67133f13b5
commit a822a2bb52
24 changed files with 1678 additions and 129 deletions
+36 -1
View File
@@ -103,6 +103,35 @@ export interface AccessOptions {
// each at least 1MB so tiny files don't get split pointlessly.
export const ARIA2C_ARGS = 'aria2c:-x 16 -s 16 -k 1M'
/**
* Shell-like split of a raw "extra yt-dlp args" string (Phase C custom-command
* templates) into argv tokens. Supports single- and double-quoted spans so a
* flag value containing spaces (e.g. a --ppa recipe) can be typed as one
* token; no escape-character or nested-quote support beyond that.
*/
export function parseExtraArgs(raw: string): string[] {
const args: string[] = []
const re = /"([^"]*)"|'([^']*)'|(\S+)/g
let m: RegExpExecArray | null
while ((m = re.exec(raw)) !== null) {
args.push(m[1] ?? m[2] ?? m[3])
}
return args
}
// Wrap a single argv token for human-readable display only (Phase C command
// preview) — never used to build the real argv that gets spawned.
function quoteForDisplay(arg: string): string {
if (arg === '') return '""'
if (/[\s"]/.test(arg)) return `"${arg.replace(/"/g, '\\"')}"`
return arg
}
/** Render a binary + argv as a single copy-pasteable command line. */
export function formatCommandLine(exe: string, args: string[]): string {
return [exe, ...args].map(quoteForDisplay).join(' ')
}
function accessArgs(a: AccessOptions): string[] {
const args: string[] = []
if (a.proxy.trim()) args.push('--proxy', a.proxy.trim())
@@ -152,7 +181,8 @@ export function buildArgs(
outputTemplate: string,
o: DownloadOptions,
binDir: string,
access: AccessOptions
access: AccessOptions,
extraArgs: string[] = []
): string[] {
const args = [
'--newline',
@@ -194,6 +224,11 @@ export function buildArgs(
if (o.embedThumbnail && o.videoContainer !== 'webm') args.push('--embed-thumbnail')
}
// Custom-command extra args go last, immediately before the `--` URL
// terminator, so a user-supplied flag can override anything chosen above
// (yt-dlp takes the last occurrence of most options).
if (extraArgs.length > 0) args.push(...extraArgs)
// `--` terminates option parsing so the URL can never be read as a flag.
args.push('--', opts.url)
return args