Harden audit findings: correctness, type-safety, Windows conventions & polish
Audit-pass over CODE-AUDIT.md (~48 items closed this pass; all verified — typecheck + 234 tests + eslint + prettier green). Correctness / bugs: - B3: match the release checksum to the asset's filename line (no wrong-hash verify) - B4: newline-safe metadata probe (one --print with a unit-separator delimiter) - B5 / L88: guard the meta event against canceled items; progress no longer promotes a queued item outside pump() - B7: cookie-login promise always resolves (handles destroy-without-close) - L146: trim parser rejects >2 colon-group times; M36: Library selection counts only actionable rows - L11 / L50 / L156 / L57 / L159 / L15 / L3: live queue count, empty-cookie message, schedule picker min, dead-code/comment cleanup Type safety: - Enable noUncheckedIndexedAccess + noFallthroughCasesInSwitch (15 real edge cases fixed) Resilience / Windows / metadata: - R5: settings write failure handled (no unhandled IPC rejection; reconciles to truth) - W1 / W5 / W6: min window size, seeded folder picker, parented sign-in window; L147 dead macOS branches removed - CL1: shared stdout markers; package/builder metadata (license, homepage, repository, copyright, tsbuildinfo glob) Copy / docs / tests: - M37 / SR9 dev-jargon cleanup in hints; M8 / M25 / M26 / L66 / L80 / L81 reconciled - New unit tests for L35 (isValidMediaItem) and L36 (compareVersions) This commit also checkpoints the previously-uncommitted feat/tray-background-clipboard work it builds on: background running + auto-download, library clipboard detection, tray, binary management & library scale, credential encryption at rest, the shared jsonStore and ui/ primitives, and the eslint/prettier tooling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+47
-13
@@ -42,9 +42,7 @@ function videoFormat(quality: string): string {
|
||||
*/
|
||||
function videoSelector(opts: StartDownloadOptions): string {
|
||||
if (opts.formatId && opts.formatId !== BEST_FORMAT_ID) {
|
||||
return opts.formatHasAudio
|
||||
? opts.formatId
|
||||
: `${opts.formatId}+bestaudio/${opts.formatId}`
|
||||
return opts.formatHasAudio ? opts.formatId : `${opts.formatId}+bestaudio/${opts.formatId}`
|
||||
}
|
||||
return videoFormat(opts.quality)
|
||||
}
|
||||
@@ -62,13 +60,19 @@ function audioQuality(quality: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
// Stdout line markers, shared by the emit side (the --print templates here) and
|
||||
// the parse side (download.ts). Defined once so changing a marker can't silently
|
||||
// break the parser that splits on it (CL1).
|
||||
export const PROGRESS_MARKER = 'prog|'
|
||||
export const FILEPATH_MARKER = 'path|'
|
||||
|
||||
// The progress line yt-dlp emits (one per --newline tick). Note the leading
|
||||
// `download:` is the progress-template TYPE selector and is consumed by yt-dlp
|
||||
// (it does NOT appear in the output). The literal `prog|` that follows is our
|
||||
// (it does NOT appear in the output). The PROGRESS_MARKER that follows is our
|
||||
// own marker, so we can tell progress lines apart from the after-move filepath
|
||||
// print on the same stdout stream.
|
||||
export const PROGRESS_TEMPLATE =
|
||||
'download:prog|%(progress.status)s|%(progress.downloaded_bytes)s|' +
|
||||
const PROGRESS_TEMPLATE =
|
||||
`download:${PROGRESS_MARKER}%(progress.status)s|%(progress.downloaded_bytes)s|` +
|
||||
'%(progress.total_bytes)s|%(progress.total_bytes_estimate)s|' +
|
||||
'%(progress.speed)s|%(progress.eta)s'
|
||||
|
||||
@@ -77,8 +81,7 @@ export const PROGRESS_TEMPLATE =
|
||||
// double quotes and leaving ffmpeg single-quoted crop expressions whose commas are
|
||||
// protected from ffmpeg's filtergraph separator. Side = min(width, height).
|
||||
export const CROP_SQUARE_PPA =
|
||||
'EmbedThumbnail+ffmpeg_o:-c:v mjpeg -vf ' +
|
||||
'crop="\'if(gt(ih,iw),iw,ih)\':\'if(gt(iw,ih),ih,iw)\'"'
|
||||
'EmbedThumbnail+ffmpeg_o:-c:v mjpeg -vf ' + "crop=\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\""
|
||||
|
||||
/**
|
||||
* Phase B "Access & networking" settings — not per-download options, always
|
||||
@@ -127,7 +130,9 @@ export function parseExtraArgs(raw: string): string[] {
|
||||
const re = /"([^"]*)"|'([^']*)'|(\S+)/g
|
||||
let m: RegExpExecArray | null
|
||||
while ((m = re.exec(raw)) !== null) {
|
||||
args.push(m[1] ?? m[2] ?? m[3])
|
||||
// Exactly one of the three alternation groups matches; the '' fallback only
|
||||
// satisfies the type checker (noUncheckedIndexedAccess) and never fires.
|
||||
args.push(m[1] ?? m[2] ?? m[3] ?? '')
|
||||
}
|
||||
return args
|
||||
}
|
||||
@@ -197,7 +202,10 @@ export function matchesUrl(pattern: string, url: string): boolean {
|
||||
*/
|
||||
export function parseTrimSections(raw: string | undefined): string[] {
|
||||
if (!raw) return []
|
||||
const TIME = String.raw`\d+(?::\d{1,2})*(?:\.\d+)?`
|
||||
// A time is SS, M:SS, or H:MM:SS — at most two colon-separated groups. The old
|
||||
// `*` quantifier accepted nonsense like `1:2:3:4`, which then failed inside
|
||||
// yt-dlp's --download-sections rather than being rejected up front (L146).
|
||||
const TIME = String.raw`\d+(?::\d{1,2}){0,2}(?:\.\d+)?`
|
||||
const RANGE = new RegExp(`^${TIME}-${TIME}$`)
|
||||
const out: string[] = []
|
||||
for (const piece of raw.split(/[,\n]/)) {
|
||||
@@ -244,7 +252,11 @@ export function sanitizeDirSegment(name: string): string {
|
||||
// byte ever appears in this source file.
|
||||
let s = Array.from(name ?? '', (ch) => (ch.charCodeAt(0) < 0x20 ? ' ' : ch)).join('')
|
||||
s = s.replace(/[<>:"/\\|?*]/g, ' ')
|
||||
s = s.replace(/\s+/g, ' ').trim().replace(/[. ]+$/, '').replace(/^[. ]+/, '')
|
||||
s = s
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.replace(/[. ]+$/, '')
|
||||
.replace(/^[. ]+/, '')
|
||||
// Reserved device names are reserved even WITH an extension ('CON.txt' is still
|
||||
// the CON device), so match an optional trailing '.<ext>' too (audit T3).
|
||||
if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i.test(s)) s = `_${s}`
|
||||
@@ -328,7 +340,23 @@ function postProcessArgs(opts: StartDownloadOptions, o: DownloadOptions): string
|
||||
// Split into one file per chapter. yt-dlp also keeps the full file; the
|
||||
// per-chapter files use yt-dlp's default `chapter:` output template.
|
||||
if (o.splitChapters) args.push('--split-chapters')
|
||||
if (o.embedMetadata) args.push('--embed-metadata')
|
||||
|
||||
// Metadata embedding + optional per-field overrides.
|
||||
// --replace-in-metadata FIELD REGEX REPLACE is used instead of --parse-metadata
|
||||
// FROM:TO because the replacement arg is a separate element (no colon-splitting
|
||||
// on values like "Foo: Bar"). ^.*$ matches any string including empty. The only
|
||||
// escaping Python's re.sub needs in the replacement string is backslash.
|
||||
const metaOverrides: [string, string | undefined][] = [
|
||||
['title', o.metadataTitle],
|
||||
['artist', o.metadataArtist],
|
||||
['album', o.metadataAlbum]
|
||||
]
|
||||
const hasOverrides = metaOverrides.some(([, v]) => v?.trim())
|
||||
if (o.embedMetadata || hasOverrides) args.push('--embed-metadata')
|
||||
const escRepl = (s: string): string => s.replace(/\\/g, '\\\\')
|
||||
for (const [field, val] of metaOverrides) {
|
||||
if (val?.trim()) args.push('--replace-in-metadata', field, '^.*$', escRepl(val.trim()))
|
||||
}
|
||||
|
||||
// Media-server sidecar files (Phase K) — written next to the output file so
|
||||
// Jellyfin/Plex/Kodi can ingest metadata, poster art, and the description.
|
||||
@@ -351,6 +379,12 @@ export function buildArgs(
|
||||
'--newline',
|
||||
'--no-color',
|
||||
'--no-playlist',
|
||||
// Bound a dead/hung connection so yt-dlp aborts (and retries, then exits) a
|
||||
// stalled socket instead of hanging forever and pinning a concurrency slot
|
||||
// with no recovery. The app-side idle watchdog in download.ts is the backstop
|
||||
// for a fully-wedged process. (B1)
|
||||
'--socket-timeout',
|
||||
'30',
|
||||
// --print (below) implies --quiet, which would suppress progress; --progress
|
||||
// forces the progress template to emit anyway.
|
||||
'--progress',
|
||||
@@ -362,7 +396,7 @@ export function buildArgs(
|
||||
PROGRESS_TEMPLATE,
|
||||
// Print the final path after post-processing/move so we can open it later.
|
||||
'--print',
|
||||
'after_move:path|%(filepath)s',
|
||||
`after_move:${FILEPATH_MARKER}%(filepath)s`,
|
||||
'--no-simulate'
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user