feat(audit): L51 + L64 — daily-sync time picker + aria2c connection tuning

Two small self-contained features from the audit's deferred list:

- L51: the Task Scheduler daily sync is no longer pinned to 09:00. New
  Settings.syncTime (24h HH:MM) with a native time input beside the Library
  "Daily sync" switch — persists on change, re-registers the task on blur
  (schtasks /Create /F overwrite is the time-change mechanism). The value
  reaches the schtasks /ST argv, so a shared isValidSyncTime guards it at
  both the settings write and in schedule.ts (unit-tested incl.
  injection-shaped input). Live-verified against the machine's real task.

- L64: aria2c connection count (-x/-s) is user-tunable. New
  Settings.aria2cConnections (1-16, aria2c's own ceiling) exposed as a
  SpinButton in Settings -> Network while the aria2c toggle is on.
  ARIA2C_ARGS became the pure aria2cArgs(n?) with a defensive clamp;
  threaded through AccessOptions. The -k 1M split floor stays fixed — a
  free-text field would bypass the custom-command consent gate.

typecheck + 309 tests + eslint + production build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 22:57:20 -04:00
parent e19f988c72
commit 96800d5a17
12 changed files with 199 additions and 21 deletions
+22 -4
View File
@@ -104,6 +104,8 @@ export interface AccessOptions {
rateLimit: string
/** absolute path to aria2c.exe; omit to use yt-dlp's built-in downloader */
aria2cPath?: string
/** aria2c parallel connections (-x/-s), clamped to 116; omitted = 16 (L64) */
aria2cConnections?: number
/** read auth cookies from an installed browser's own cookie store */
cookiesFromBrowser?: CookieBrowser
/** absolute path to a Netscape-format cookie file (the sign-in window's export) */
@@ -118,9 +120,23 @@ export interface AccessOptions {
youtubePoToken?: string
}
// Tuned for typical CDN-served video: 16 connections split across 16 segments,
// each at least 1MB so tiny files don't get split pointlessly.
export const ARIA2C_ARGS = 'aria2c:-x 16 -s 16 -k 1M'
/**
* The yt-dlp `--downloader-args` value for aria2c: N connections split across N
* segments, each at least 1MB so tiny files don't get split pointlessly. The
* connection count is user-tunable (Settings → Network, L64) and clamped here to
* aria2c's own -x/-s ceiling of 16 — settings validation clamps too, but this is
* pure-module defence for any other caller. Non-finite input → the default 16.
*/
export function aria2cArgs(connections?: number): string {
const n =
connections !== undefined && Number.isFinite(connections)
? Math.min(16, Math.max(1, Math.round(connections)))
: 16
return `aria2c:-x ${n} -s ${n} -k 1M`
}
/** The default tuning (16 connections) — kept for tests/docs that reference it. */
export const ARIA2C_ARGS = aria2cArgs()
/**
* Shell-like split of a raw "extra yt-dlp args" string (Phase C custom-command
@@ -295,7 +311,9 @@ function accessArgs(a: AccessOptions): string[] {
const args: string[] = []
if (a.proxy.trim()) args.push('--proxy', a.proxy.trim())
if (a.rateLimit.trim()) args.push('--limit-rate', a.rateLimit.trim())
if (a.aria2cPath) args.push('--downloader', a.aria2cPath, '--downloader-args', ARIA2C_ARGS)
if (a.aria2cPath) {
args.push('--downloader', a.aria2cPath, '--downloader-args', aria2cArgs(a.aria2cConnections))
}
// cookiesFromBrowser and cookiesFile are mutually exclusive sources — the UI
// only ever sets one (driven by Settings.cookieSource), but browser wins if
// a caller somehow sets both.
+1
View File
@@ -214,6 +214,7 @@ export function buildCommand(opts: StartDownloadOptions, cookiesFile?: string):
proxy: settings.proxy,
rateLimit: settings.rateLimit,
aria2cPath,
aria2cConnections: settings.aria2cConnections,
// L136/M6: incognito attaches no cookies from the browser either.
cookiesFromBrowser:
!opts.incognito && settings.cookieSource === 'browser' ? settings.cookiesBrowser : undefined,
+4 -1
View File
@@ -270,7 +270,10 @@ export function registerIpcHandlers(getMainWindow: () => BrowserWindow | null):
})
)
ipcMain.handle(IpcChannels.scheduledSyncGet, () => getScheduledSync())
ipcMain.handle(IpcChannels.scheduledSyncSet, (_e, enabled: boolean) => setScheduledSync(enabled))
// `time` (24h HH:MM) is validated inside setScheduledSync before touching schtasks (L51).
ipcMain.handle(IpcChannels.scheduledSyncSet, (_e, enabled: boolean, time?: string) =>
setScheduledSync(enabled, time)
)
// Reflect overall queue progress on the Windows taskbar and window title (SR8).
ipcMain.handle(IpcChannels.taskbarProgress, (_e, p: TaskbarProgress) => {
+11 -4
View File
@@ -11,7 +11,7 @@
import { execFile } from 'child_process'
import { getSystem32Path } from './binaries'
import type { ScheduledSyncStatus } from '@shared/ipc'
import { isValidSyncTime, DEFAULT_SETTINGS, type ScheduledSyncStatus } from '@shared/ipc'
const TASK_NAME = 'AeroFetchDailySync'
/** The argv flag the scheduled task passes so startup knows it's a sync launch. */
@@ -46,10 +46,17 @@ export async function getScheduledSync(): Promise<ScheduledSyncStatus> {
/**
* Register or remove the daily-sync scheduled task. Creating runs AeroFetch with
* `--sync` every day at 09:00 (overwriting any prior task of the same name).
* `--sync` every day at the given 24h 'HH:MM' time (overwriting any prior task of
* the same name — which is also how a time change re-registers, L51). The time is
* renderer-supplied over IPC, so it's validated here before reaching the schtasks
* argv; anything malformed falls back to the default rather than failing the toggle.
*/
export async function setScheduledSync(enabled: boolean): Promise<ScheduledSyncStatus> {
export async function setScheduledSync(
enabled: boolean,
time?: string
): Promise<ScheduledSyncStatus> {
if (enabled) {
const startTime = isValidSyncTime(time) ? time : DEFAULT_SETTINGS.syncTime
const tr = `"${process.execPath}" ${SYNC_FLAG}`
const r = await schtasks([
'/Create',
@@ -57,7 +64,7 @@ export async function setScheduledSync(enabled: boolean): Promise<ScheduledSyncS
'/SC',
'DAILY',
'/ST',
'09:00',
startTime,
'/TN',
TASK_NAME,
'/TR',
+12
View File
@@ -14,6 +14,7 @@ import {
DEFAULT_DOWNLOAD_OPTIONS,
DEFAULT_SETTINGS,
isYtdlpUpdateChannel,
isValidSyncTime,
type Settings,
type DownloadOptions,
type SponsorBlockCategory,
@@ -266,6 +267,17 @@ function applySettings(s: Store<Settings>, partial: Partial<Settings>): void {
if (Number.isFinite(n)) s.set('maxConcurrent', Math.min(5, Math.max(1, Math.round(n))))
break
}
case 'aria2cConnections': {
// aria2c caps -x/-s at 16; clamp like maxConcurrent rather than reject (L64).
const n = Number(value)
if (Number.isFinite(n)) s.set('aria2cConnections', Math.min(16, Math.max(1, Math.round(n))))
break
}
case 'syncTime':
// Reaches `schtasks /ST` when the daily sync is (re)registered — only a
// well-formed 24h HH:MM is ever stored (L51).
if (isValidSyncTime(value)) s.set('syncTime', value)
break
case 'clipboardWatch':
case 'useAria2c':
case 'restrictFilenames':