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
+139
View File
@@ -0,0 +1,139 @@
import { app, session, BrowserWindow, type Cookie } from 'electron'
import { existsSync, statSync, unlinkSync, writeFileSync } from 'fs'
import { join } from 'path'
import { assertHttpUrl } from './url'
import type { CookiesStatus, CookiesLoginResult } from '@shared/ipc'
/**
* Persisted session AeroFetch's built-in sign-in window uses. Kept separate
* from the main window's (default) session so a logged-in site can't see or
* touch anything the app itself loads.
*/
const PARTITION = 'persist:aerofetch-login'
export function getCookiesFilePath(): string {
return join(app.getPath('userData'), 'cookies.txt')
}
export function getCookiesStatus(): CookiesStatus {
const p = getCookiesFilePath()
if (!existsSync(p)) return { exists: false }
return { exists: true, savedAt: statSync(p).mtimeMs }
}
export async function clearCookies(): Promise<void> {
const p = getCookiesFilePath()
if (existsSync(p)) unlinkSync(p)
await session.fromPartition(PARTITION).clearStorageData()
}
/**
* Render cookies in the Netscape cookie-jar format yt-dlp's `--cookies` reads.
* Columns: domain, includeSubdomains, path, secure, expiry (unix seconds, 0
* for session cookies), name, value.
*/
function toNetscapeCookieFile(cookies: Cookie[]): string {
const lines = ['# Netscape HTTP Cookie File', '# Generated by AeroFetch — do not edit.', '']
for (const c of cookies) {
if (!c.domain || !c.name) continue
const domain = c.hostOnly || c.domain.startsWith('.') ? c.domain : `.${c.domain}`
const includeSubdomains = domain.startsWith('.') ? 'TRUE' : 'FALSE'
const expiry = c.session || !c.expirationDate ? 0 : Math.round(c.expirationDate)
lines.push(
[domain, includeSubdomains, c.path || '/', c.secure ? 'TRUE' : 'FALSE', expiry, c.name, c.value].join(
'\t'
)
)
}
return lines.join('\n') + '\n'
}
let loginWindow: BrowserWindow | null = null
let pendingResolvers: Array<(r: CookiesLoginResult) => void> = []
function exportAndResolve(): void {
session
.fromPartition(PARTITION)
.cookies.get({})
.then((cookies) => {
writeFileSync(getCookiesFilePath(), toNetscapeCookieFile(cookies))
const result: CookiesLoginResult = { ok: true, cookieCount: cookies.length }
pendingResolvers.forEach((resolve) => resolve(result))
})
.catch((e) => {
const result: CookiesLoginResult = { ok: false, error: (e as Error).message }
pendingResolvers.forEach((resolve) => resolve(result))
})
.finally(() => {
pendingResolvers = []
})
}
/**
* Open (or focus + navigate) a sign-in window backed by a persisted session
* partition. The user logs into whatever site they need, then closes the
* window — cookies are exported to a Netscape-format file at that point,
* ready for yt-dlp's `--cookies`. Mirrors Seal's "log in via WebView" feature.
*/
export function openCookieLoginWindow(url: string): Promise<CookiesLoginResult> {
return new Promise((resolve) => {
let validUrl: string
try {
validUrl = assertHttpUrl(url)
} catch (e) {
resolve({ ok: false, error: (e as Error).message })
return
}
if (loginWindow && !loginWindow.isDestroyed()) {
pendingResolvers.push(resolve)
loginWindow.loadURL(validUrl).catch(() => {})
loginWindow.focus()
return
}
pendingResolvers = [resolve]
let win: BrowserWindow
try {
win = new BrowserWindow({
width: 480,
height: 720,
title: 'Sign in — AeroFetch',
autoHideMenuBar: true,
webPreferences: {
partition: PARTITION,
sandbox: true,
contextIsolation: true,
nodeIntegration: false
}
})
} catch (e) {
pendingResolvers = []
resolve({ ok: false, error: (e as Error).message })
return
}
loginWindow = win
// Some sites log in via an OAuth/SSO popup. Let those open as real windows
// sharing the same partition, rather than silently swallowing the click.
win.webContents.setWindowOpenHandler(() => ({
action: 'allow',
overrideBrowserWindowOptions: {
autoHideMenuBar: true,
webPreferences: { partition: PARTITION, sandbox: true, contextIsolation: true, nodeIntegration: false }
}
}))
win.on('closed', () => {
loginWindow = null
})
// Closing the window IS "I'm done" — export whatever the partition
// collected. The partition itself outlives the window, so this is safe
// even though cookies.get() resolves after 'close' has already fired.
win.on('close', exportAndResolve)
win.loadURL(validUrl).catch(() => {
/* navigation errors surface as Chromium's own error page */
})
})
}