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
+80 -3
View File
@@ -1,5 +1,12 @@
import { describe, it, expect } from 'vitest'
import { buildArgs, CROP_SQUARE_PPA, ARIA2C_ARGS, type AccessOptions } from '../src/main/buildArgs'
import {
buildArgs,
parseExtraArgs,
formatCommandLine,
CROP_SQUARE_PPA,
ARIA2C_ARGS,
type AccessOptions
} from '../src/main/buildArgs'
import {
DEFAULT_DOWNLOAD_OPTIONS,
type DownloadOptions,
@@ -25,9 +32,10 @@ function dlo(overrides: Partial<DownloadOptions> = {}): DownloadOptions {
function build(
o: StartDownloadOptions,
d: DownloadOptions,
access: AccessOptions = NO_ACCESS
access: AccessOptions = NO_ACCESS,
extraArgs: string[] = []
): string[] {
return buildArgs(o, OUT, d, BIN, access)
return buildArgs(o, OUT, d, BIN, access, extraArgs)
}
/** True when `seq` appears as a contiguous run inside `argv`. */
@@ -283,3 +291,72 @@ describe('cropThumbnail → --ppa crop', () => {
expect(argv).not.toContain('--embed-thumbnail')
})
})
// --- Custom commands (Phase C): extra args + display formatting ------------
describe('parseExtraArgs', () => {
it('splits on whitespace', () => {
expect(parseExtraArgs('--write-thumbnail --no-mtime')).toEqual([
'--write-thumbnail',
'--no-mtime'
])
})
it('keeps a double-quoted span with spaces as one token', () => {
expect(parseExtraArgs('--proxy "http://example.com:8080"')).toEqual([
'--proxy',
'http://example.com:8080'
])
})
it('keeps a single-quoted span with spaces as one token', () => {
expect(parseExtraArgs("--output-na 'My Title - %(id)s.%(ext)s'")).toEqual([
'--output-na',
'My Title - %(id)s.%(ext)s'
])
})
it('returns an empty array for blank input', () => {
expect(parseExtraArgs('')).toEqual([])
expect(parseExtraArgs(' ')).toEqual([])
})
it('collapses repeated whitespace between tokens', () => {
expect(parseExtraArgs('--verbose --no-progress')).toEqual(['--verbose', '--no-progress'])
})
})
describe('formatCommandLine', () => {
it('joins the exe and args with spaces when nothing needs quoting', () => {
expect(formatCommandLine('yt-dlp.exe', ['-f', 'best', '--', 'https://x.test/v'])).toBe(
'yt-dlp.exe -f best -- https://x.test/v'
)
})
it('quotes an arg containing a space', () => {
expect(formatCommandLine('yt-dlp.exe', ['--ppa', 'EmbedThumbnail+ffmpeg_o:-c:v mjpeg'])).toBe(
'yt-dlp.exe --ppa "EmbedThumbnail+ffmpeg_o:-c:v mjpeg"'
)
})
it('escapes embedded double quotes', () => {
expect(formatCommandLine('yt-dlp.exe', ['say "hi"'])).toBe('yt-dlp.exe "say \\"hi\\""')
})
it('renders an empty-string arg as ""', () => {
expect(formatCommandLine('yt-dlp.exe', [''])).toBe('yt-dlp.exe ""')
})
})
describe('buildArgs extraArgs', () => {
it('appends extraArgs after every other option, still before -- url', () => {
const argv = build(opts(), dlo(), NO_ACCESS, ['--write-thumbnail', '--no-mtime'])
expect(hasSeq(argv, '--write-thumbnail', '--no-mtime', '--', URL)).toBe(true)
})
it('omits nothing extra when extraArgs is empty', () => {
const withEmpty = build(opts(), dlo(), NO_ACCESS, [])
const withoutParam = build(opts(), dlo())
expect(withEmpty).toEqual(withoutParam)
})
})