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:
+22
-4
@@ -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 1–16; 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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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',
|
||||
|
||||
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user