Add Phase B: cookies, aria2c, proxy/rate-limit, restrict-filenames & archive

Implements the remaining Phase B (Access & networking) items from
ROADMAP.md: cookie sources (browser cookie-store import, or a built-in
sign-in window that exports a Netscape cookie file via a persisted Electron
session partition), the aria2c external downloader, proxy/rate-limit,
restrict-filenames, and a download-archive to skip repeats.

Wires download.ts to the buildArgs() module added in the previous commit
(it wasn't hooked up yet) and renames its options param
NetworkOptions -> AccessOptions to reflect the wider scope now that cookies
and filename/archive flags joined proxy/rate-limit/aria2c.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 20:53:18 -04:00
parent 26cd7fc7b0
commit 67133f13b5
15 changed files with 683 additions and 196 deletions
+96 -3
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { buildArgs, CROP_SQUARE_PPA } from '../src/main/buildArgs'
import { buildArgs, CROP_SQUARE_PPA, ARIA2C_ARGS, type AccessOptions } from '../src/main/buildArgs'
import {
DEFAULT_DOWNLOAD_OPTIONS,
type DownloadOptions,
@@ -11,6 +11,7 @@ import {
const BIN = 'C:/fake/bin'
const OUT = 'C:/out/%(title)s.%(ext)s'
const URL = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
const NO_ACCESS: AccessOptions = { proxy: '', rateLimit: '', restrictFilenames: false }
function opts(overrides: Partial<StartDownloadOptions> = {}): StartDownloadOptions {
return { id: 't', url: URL, kind: 'video', quality: '1080p', ...overrides }
@@ -21,8 +22,12 @@ function audio(overrides: Partial<StartDownloadOptions> = {}): StartDownloadOpti
function dlo(overrides: Partial<DownloadOptions> = {}): DownloadOptions {
return { ...DEFAULT_DOWNLOAD_OPTIONS, ...overrides }
}
function build(o: StartDownloadOptions, d: DownloadOptions): string[] {
return buildArgs(o, OUT, d, BIN)
function build(
o: StartDownloadOptions,
d: DownloadOptions,
access: AccessOptions = NO_ACCESS
): string[] {
return buildArgs(o, OUT, d, BIN, access)
}
/** True when `seq` appears as a contiguous run inside `argv`. */
@@ -48,6 +53,94 @@ describe('buildArgs scaffolding', () => {
})
})
// --- Network: proxy / rate limit / aria2c -----------------------------------
describe('network options', () => {
it('emits nothing when proxy/rateLimit are empty and aria2c is unset', () => {
const argv = build(opts(), dlo(), NO_ACCESS)
expect(argv).not.toContain('--proxy')
expect(argv).not.toContain('--limit-rate')
expect(argv).not.toContain('--downloader')
})
it('emits --proxy with the trimmed value', () => {
const argv = build(opts(), dlo(), { ...NO_ACCESS, proxy: ' socks5://127.0.0.1:1080 ' })
expect(hasSeq(argv, '--proxy', 'socks5://127.0.0.1:1080')).toBe(true)
})
it('emits --limit-rate with the trimmed value', () => {
const argv = build(opts(), dlo(), { ...NO_ACCESS, rateLimit: ' 2M ' })
expect(hasSeq(argv, '--limit-rate', '2M')).toBe(true)
})
it('emits --downloader <path> --downloader-args when an aria2c path is given', () => {
const argv = build(opts(), dlo(), { ...NO_ACCESS, aria2cPath: 'C:/fake/bin/aria2c.exe' })
expect(hasSeq(argv, '--downloader', 'C:/fake/bin/aria2c.exe', '--downloader-args', ARIA2C_ARGS)).toBe(
true
)
})
it('omits --downloader when aria2cPath is not set', () => {
const argv = build(opts(), dlo(), NO_ACCESS)
expect(argv).not.toContain('--downloader-args')
})
})
// --- Cookies: browser store vs. sign-in window's exported file --------------
describe('cookies', () => {
it('emits nothing when no cookie source is configured', () => {
const argv = build(opts(), dlo(), NO_ACCESS)
expect(argv).not.toContain('--cookies-from-browser')
expect(argv).not.toContain('--cookies')
})
it('emits --cookies-from-browser <name> when cookiesFromBrowser is set', () => {
const argv = build(opts(), dlo(), { ...NO_ACCESS, cookiesFromBrowser: 'firefox' })
expect(hasSeq(argv, '--cookies-from-browser', 'firefox')).toBe(true)
expect(argv).not.toContain('--cookies')
})
it('emits --cookies <path> when only cookiesFile is set', () => {
const argv = build(opts(), dlo(), { ...NO_ACCESS, cookiesFile: 'C:/userdata/cookies.txt' })
expect(hasSeq(argv, '--cookies', 'C:/userdata/cookies.txt')).toBe(true)
expect(argv).not.toContain('--cookies-from-browser')
})
it('prefers cookiesFromBrowser over cookiesFile when both are set', () => {
const argv = build(opts(), dlo(), {
...NO_ACCESS,
cookiesFromBrowser: 'chrome',
cookiesFile: 'C:/userdata/cookies.txt'
})
expect(hasSeq(argv, '--cookies-from-browser', 'chrome')).toBe(true)
expect(argv).not.toContain('--cookies')
})
})
// --- Restrict filenames / download archive -----------------------------------
describe('restrictFilenames / downloadArchivePath', () => {
it('emits --restrict-filenames when set', () => {
expect(build(opts(), dlo(), { ...NO_ACCESS, restrictFilenames: true })).toContain(
'--restrict-filenames'
)
})
it('omits --restrict-filenames by default', () => {
expect(build(opts(), dlo(), NO_ACCESS)).not.toContain('--restrict-filenames')
})
it('emits --download-archive <path> when a path is given', () => {
const argv = build(opts(), dlo(), { ...NO_ACCESS, downloadArchivePath: 'C:/userdata/archive.txt' })
expect(hasSeq(argv, '--download-archive', 'C:/userdata/archive.txt')).toBe(true)
})
it('omits --download-archive when no path is given', () => {
expect(build(opts(), dlo(), NO_ACCESS)).not.toContain('--download-archive')
})
})
// --- Audio extraction format ------------------------------------------------
describe('audio: -x --audio-format <fmt>', () => {
+3 -2
View File
@@ -21,6 +21,7 @@ const RUN = process.env.AEROFETCH_REAL_DOWNLOAD === '1'
const BIN_DIR = resolve('resources/bin')
const YTDLP = join(BIN_DIR, 'yt-dlp.exe')
const FFMPEG = join(BIN_DIR, 'ffmpeg.exe')
const NO_ACCESS = { proxy: '', rateLimit: '', restrictFilenames: false }
interface RunResult {
status: number | null
@@ -105,7 +106,7 @@ describe.skipIf(!RUN)('real yt-dlp downloads (buildArgs end-to-end)', () => {
embedThumbnail: true,
cropThumbnail: true
}
const argv = buildArgs(opts, join(outDir, '%(id)s.%(ext)s'), options, BIN_DIR)
const argv = buildArgs(opts, join(outDir, '%(id)s.%(ext)s'), options, BIN_DIR, NO_ACCESS)
console.log('[crop] argv:', JSON.stringify(argv))
const res = runYtdlp(argv)
@@ -146,7 +147,7 @@ describe.skipIf(!RUN)('real yt-dlp downloads (buildArgs end-to-end)', () => {
sponsorBlockMode: 'remove' as const,
sponsorBlockCategories: ['music_offtopic' as const]
}
const argv = buildArgs(opts, join(outDir, '%(id)s.%(ext)s'), options, BIN_DIR)
const argv = buildArgs(opts, join(outDir, '%(id)s.%(ext)s'), options, BIN_DIR, NO_ACCESS)
console.log('[sb] argv:', JSON.stringify(argv))
const res = runYtdlp(argv)