Add Seal-parity download options + playlist support (Phase A)

Phase A of the Seal feature-parity roadmap: configurable post-processing via a
shared DownloadOptions model (src/shared/ipc.ts), editable as persisted defaults
in Settings and overridable per-download in the download bar.

- audio formats (mp3/m4a/opus/flac/wav/aac), video container (mp4/mkv/webm),
  preferred-codec sort (-S vcodec)
- subtitles (download/embed/langs/auto), SponsorBlock (remove/mark + categories
  + force-keyframes-at-cuts), embed chapters/metadata/thumbnail, square-crop
  audio artwork
- playlist downloads: probe via `-J --flat-playlist`, inline selection UI
  (checkbox list, select-all/none, live count); each entry enqueued as its own
  single-video download, reusing the existing queue/concurrency/history
- reusable DownloadOptionsForm shared by Settings and the download bar so the
  defaults and per-download override never drift
- ROADMAP.md tracking full Seal parity (phases A-E)

Also includes in-tree security hardening that landed alongside: preload sandbox
(CJS preload output), http(s)-only window-open + blocked renderer navigation,
argv-injection guard for URLs (src/main/url.ts), extension-allowlisted file
open/reveal (src/main/reveal.ts), yt-dlp version-check timeout, and a minimal
window.electron presence marker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 19:32:59 -04:00
parent d4b9a0eefa
commit bb5dd6c438
21 changed files with 1101 additions and 91 deletions
+26
View File
@@ -0,0 +1,26 @@
/**
* Validate that a string is an http(s) URL and return it trimmed.
*
* This is the front-line guard against yt-dlp *argument injection*: the URL is
* passed to yt-dlp as a positional argv element, so a value beginning with `-`
* (e.g. `--config-locations=…`, `--load-info-json=…`, `--exec`) would be parsed
* as an OPTION rather than a URL — which can lead to arbitrary command
* execution. A string that `new URL()` accepts with an http/https protocol
* necessarily begins with its scheme, so it can never be read as an option.
* (Call sites also pass `--` before the positional URL as defence in depth.)
*
* Throws a user-friendly Error on anything that isn't an http(s) URL.
*/
export function assertHttpUrl(raw: string): string {
const trimmed = (raw ?? '').trim()
let u: URL
try {
u = new URL(trimmed)
} catch {
throw new Error('That doesnt look like a valid URL.')
}
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
throw new Error('Only http and https links are supported.')
}
return trimmed
}