feat: Phase P reliability (YouTube extractor-args plumbing, signing + smoke-test docs)
- YouTube reliability: Settings.youtubePlayerClient + youtubePoToken -> one --extractor-args "youtube:player_client=...;po_token=..." group (accessArgs, unit-tested); advanced fields in Settings -> Network. Automatic WebView PO-token minting deferred (documented). - docs/SMOKE-TEST.md: release-gate checklist for all shipped-but-untested OS wiring (cookies window, protocol/Send-to, scheduled sync, aria2c, proxy, tray/taskbar/pause-resume/terminal) — needs a human to run. - docs/SIGNING.md: code-signing guide (CSC_LINK/CSC_KEY_PASSWORD, OV/EV, CI, HSM hook, verification). Build is signing-ready; needs a cert. - i18n remains deferred (not faked). typecheck + test (195) + build all clean. Roadmap: Phase P code shipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+22
-20
@@ -391,28 +391,30 @@ All in the main process. typecheck + test + `npm run build` clean.
|
|||||||
live by matching each card's text (DOM `display` toggle keyed off the root's children — no
|
live by matching each card's text (DOM `display` toggle keyed off the root's children — no
|
||||||
per-card refactor), with a "no settings match" hint.*
|
per-card refactor), with a "no settings match" hint.*
|
||||||
|
|
||||||
## Phase P — Reliability, longevity & trust
|
## Phase P — Reliability, longevity & trust ✅ (code shipped; cert + manual passes still required)
|
||||||
|
|
||||||
The least glamorous, highest-leverage track: make sure what's *already shipped* actually works,
|
The least glamorous, highest-leverage track. The buildable code is done (typecheck + test +
|
||||||
keep YouTube downloads working as the platform tightens, and remove first-run friction.
|
build clean); the parts that inherently need a human or a paid cert are delivered as artifacts.
|
||||||
|
|
||||||
- [ ] **PO Token generation.** YTDLnis mints YouTube tokens in a WebView to dodge throttling
|
- [x] **PO Token / YouTube reliability plumbing.** *Done: `Settings.youtubePlayerClient` +
|
||||||
and "Sign in to confirm you're not a bot." This is becoming **required** for YouTube to
|
`youtubePoToken` → one `--extractor-args "youtube:player_client=…;po_token=…"` group
|
||||||
keep working at all. → reuse the existing persisted Electron `BrowserWindow`
|
(`accessArgs` in `buildArgs.ts`, unit-tested), with "advanced" fields in Settings → Network.
|
||||||
(`persist:aerofetch-login` in `src/main/cookies.ts`) to host a token-minting script, or
|
Lets a power user switch extraction client (`web_safari`/`tv`/`mweb`) or paste a token.*
|
||||||
support a PO-token provider, passed via `--extractor-args youtube:po_token=…`. A
|
**Deferred: automatic WebView token minting** (the YTDLnis headline) — the hard part;
|
||||||
longevity hedge more than a feature.
|
this is the argv plumbing it would feed. Cookies remain the easier bot-check fix.
|
||||||
- [ ] **Verify the shipped-but-untested OS wiring** (flagged across both parity tracks as
|
- [x] **Verify the shipped-but-untested OS wiring.** *Can't be executed here (no real install).
|
||||||
"reasoning + typecheck only"). A single real NSIS-install smoke-test pass over: the
|
Delivered as a release-gate checklist — [docs/SMOKE-TEST.md](docs/SMOKE-TEST.md) —
|
||||||
cookies sign-in window export · the `aerofetch://` protocol + Explorer "Send to" · the
|
enumerating every untested path (cookies window, `aerofetch://` + Send-to, scheduled
|
||||||
scheduled `--sync` + RSS fast-check (Phase J) · aria2c · proxy. Converts a lot of
|
`--sync` + RSS, aria2c, proxy, plus the new tray/taskbar/pause-resume/terminal) with exact
|
||||||
"probably works" into "verified."
|
steps + expected results. **Still needs a human to run it.**
|
||||||
- [ ] **Code signing.** The unsigned `.exe` triggers SmartScreen "Run anyway" — the single
|
- [x] **Code signing.** *Build is signing-ready: electron-builder picks the cert up from
|
||||||
biggest first-run friction for anyone you share it with. electron-builder `win` signing
|
`CSC_LINK` / `CSC_KEY_PASSWORD` with no yml change (unset = unsigned, as today). Documented
|
||||||
(OV/EV cert); EV builds SmartScreen reputation fastest.
|
in [docs/SIGNING.md](docs/SIGNING.md) (OV vs EV, local + CI, HSM/EV hook, signature
|
||||||
- [ ] **i18n / language setting.** Carried over from Phase E (deferred). YTDLnis and Seal both
|
verification). **Actual signing needs a purchased certificate.***
|
||||||
ship many languages. Large but optional — needs an i18n library + string extraction, with
|
- [ ] **i18n / language setting.** Still deferred — deliberately not faked. Shipping a language
|
||||||
translations arriving incrementally.
|
picker that doesn't change anything would be misleading; doing it properly means wiring an
|
||||||
|
i18n library + extracting every string, with translations arriving incrementally. Tracked,
|
||||||
|
not started.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# AeroFetch — Windows code signing
|
||||||
|
|
||||||
|
Unsigned builds run, but Windows SmartScreen shows a blue **"Windows protected your PC"**
|
||||||
|
prompt the first time each user runs the installer (they must click *More info → Run anyway*).
|
||||||
|
This is the single biggest first-run friction when sharing AeroFetch. Code signing removes it
|
||||||
|
(EV certificates remove it immediately; OV certificates build reputation over time/installs).
|
||||||
|
|
||||||
|
**This step needs a certificate that can't live in the repo**, so it isn't wired on by default.
|
||||||
|
The build is otherwise ready for it — electron-builder picks the cert up from environment
|
||||||
|
variables with no `electron-builder.yml` changes required.
|
||||||
|
|
||||||
|
## What you need
|
||||||
|
|
||||||
|
- An **Authenticode code-signing certificate** for Windows:
|
||||||
|
- **OV (Organization Validation)** — cheaper; SmartScreen reputation accrues as more users
|
||||||
|
install, so early downloads still see the prompt.
|
||||||
|
- **EV (Extended Validation)** — pricier, usually on a hardware token / cloud HSM; clears
|
||||||
|
SmartScreen immediately. Token-based EV can't be fed as a plain `.pfx` (see "HSM/EV" below).
|
||||||
|
- The cert as a password-protected `.pfx`/`.p12` (for OV), or an HSM/cloud-signing setup (EV).
|
||||||
|
|
||||||
|
## Signing an OV `.pfx` locally
|
||||||
|
|
||||||
|
electron-builder reads these env vars automatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PowerShell
|
||||||
|
$env:CSC_LINK = "C:\path\to\codesign.pfx" # or a base64 string of the .pfx
|
||||||
|
$env:CSC_KEY_PASSWORD = "<pfx password>"
|
||||||
|
npm run build:win
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# bash / CI
|
||||||
|
export CSC_LINK="$(base64 -w0 codesign.pfx)" # base64 is convenient for CI secrets
|
||||||
|
export CSC_KEY_PASSWORD="$PFX_PASSWORD"
|
||||||
|
npm run build:win
|
||||||
|
```
|
||||||
|
|
||||||
|
electron-builder then signs `AeroFetch Setup <ver>.exe`, the portable `.exe`, and the app
|
||||||
|
binary. No changes to `electron-builder.yml` are needed; if `CSC_LINK` is unset, the build
|
||||||
|
proceeds **unsigned** (current behavior).
|
||||||
|
|
||||||
|
## CI (GitHub Actions sketch)
|
||||||
|
|
||||||
|
Store the cert + password as encrypted secrets and export them before the build:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Build (signed)
|
||||||
|
env:
|
||||||
|
CSC_LINK: ${{ secrets.WINDOWS_CSC_LINK }} # base64 of the .pfx
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CSC_KEY_PASSWORD }}
|
||||||
|
run: npm run build:win
|
||||||
|
```
|
||||||
|
|
||||||
|
## HSM / EV certificates
|
||||||
|
|
||||||
|
EV certs (and many newer OV certs) ship on a hardware token or cloud HSM and can't be exported
|
||||||
|
to a `.pfx`. Sign via a custom signing hook instead — set `win.sign` in `electron-builder.yml`
|
||||||
|
to a script that invokes your provider's tool (Azure Trusted Signing, DigiCert KeyLocker,
|
||||||
|
SSL.com eSigner, or `signtool` with the token). Each provider documents the exact `signtool`
|
||||||
|
invocation; electron-builder calls your hook once per artifact with the file path.
|
||||||
|
|
||||||
|
## Verifying a signature
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-AuthenticodeSignature ".\dist\AeroFetch Setup <ver>.exe" | Format-List
|
||||||
|
# Status should be 'Valid'; SignerCertificate should be your cert.
|
||||||
|
signtool verify /pa /v ".\dist\AeroFetch Setup <ver>.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The LGPL ffmpeg + yt-dlp binaries shipped in `resources/bin` are third-party; signing the
|
||||||
|
AeroFetch artifacts doesn't (and needn't) re-sign them.
|
||||||
|
- Timestamp the signature (electron-builder does by default) so it stays valid after the cert
|
||||||
|
expires.
|
||||||
|
- Signing does **not** replace the smoke test — see [SMOKE-TEST.md](SMOKE-TEST.md).
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# AeroFetch — manual smoke-test checklist
|
||||||
|
|
||||||
|
Some features can only be exercised against a **real install** (or a real yt-dlp run) —
|
||||||
|
they're main-process / Electron / OS-integration code that the Vite browser preview and the
|
||||||
|
unit suite can't reach. They're implemented and typecheck/test/build clean, but the roadmaps
|
||||||
|
flag them as needing a hands-on pass. Run this checklist after building
|
||||||
|
(`npm run build:win`) and installing the NSIS artifact (or launching the portable `.exe`).
|
||||||
|
|
||||||
|
Mark each ✅/❌ and note anything surprising.
|
||||||
|
|
||||||
|
## Downloads & post-processing (Phases A, L, M)
|
||||||
|
|
||||||
|
- [ ] **Basic download** — paste a URL, Fetch, pick a format, Download → file lands in
|
||||||
|
Documents\Video (or your folder), progress + speed update, "Completed".
|
||||||
|
- [ ] **Trim / cut** (Phase L) — set a section like `0:10-0:20`, download → output is ~10s and
|
||||||
|
starts/ends near the cut points (`--download-sections` + `--force-keyframes-at-cuts`).
|
||||||
|
- [ ] **Split by chapters** (Phase L) — on a video with chapters, enable it → per-chapter files
|
||||||
|
appear alongside the full file.
|
||||||
|
- [ ] **Pause / resume** (Phase M) — start a large download, Pause (process stops, `.part`
|
||||||
|
stays), Resume → it **continues** from where it stopped, not from 0% (yt-dlp `--continue`).
|
||||||
|
- [ ] **Scheduling** (Phase M) — schedule a download a couple of minutes out → it sits "Saved /
|
||||||
|
Scheduled", then auto-starts at the time (only while the app is running).
|
||||||
|
|
||||||
|
## Access & networking (Phase B, P)
|
||||||
|
|
||||||
|
- [ ] **Cookies — sign-in window** — Settings → Cookies → "Sign-in window", log into a site,
|
||||||
|
close it → "cookies saved" with a timestamp; a members-only/age-gated video then downloads.
|
||||||
|
- [ ] **Cookies — from browser** — pick an installed browser → a gated video downloads.
|
||||||
|
- [ ] **Proxy** — set a working proxy → downloads route through it (verify via the proxy logs).
|
||||||
|
- [ ] **aria2c** — drop `aria2c.exe` into `resources/bin`, enable it → multi-connection download
|
||||||
|
works; remove it → silently falls back to yt-dlp's downloader.
|
||||||
|
- [ ] **YouTube client / PO token** (Phase P) — set `youtubePlayerClient` (e.g. `web_safari`) →
|
||||||
|
the run includes `--extractor-args youtube:player_client=web_safari` (check Terminal/Preview).
|
||||||
|
|
||||||
|
## OS integration (Phase E, J, O)
|
||||||
|
|
||||||
|
- [ ] **`aerofetch://` protocol** — from a browser, open `aerofetch://download?url=<encoded>` →
|
||||||
|
AeroFetch focuses and the URL appears in the "Link received" banner.
|
||||||
|
- [ ] **Explorer "Send to AeroFetch"** — drag a tab to the desktop to make a `.url`, right-click →
|
||||||
|
Send to → AeroFetch → the link is received.
|
||||||
|
- [ ] **Scheduled sync** (Phase J) — Settings → enable the daily sync → a Windows Task Scheduler
|
||||||
|
task exists; run it (or `AeroFetch.exe --sync`) → watched sources index and new items enqueue.
|
||||||
|
- [ ] **RSS fast-check** (Phase J) — "Check for new" on a watched channel → newly-posted videos
|
||||||
|
show as new without a full re-index.
|
||||||
|
- [ ] **System tray** (Phase O) — enable "Keep running in the tray", close the window → it hides
|
||||||
|
to the tray; tray click reopens; tray → Quit actually exits.
|
||||||
|
- [ ] **Taskbar progress** (Phase O) — during downloads the taskbar icon shows a progress bar;
|
||||||
|
it turns red if one fails and clears when the queue goes idle.
|
||||||
|
- [ ] **Jump list** (Phase O) — right-click the taskbar icon → "Open AeroFetch" focuses the app.
|
||||||
|
|
||||||
|
## Terminal (Phase N)
|
||||||
|
|
||||||
|
- [ ] Enable "Run custom commands", open Terminal, run `--version` → the version prints; run
|
||||||
|
`-F <url>` → the format table streams in; Stop cancels a long run.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
If anything here fails, capture the Settings → Diagnostics "Copy full report" output and the
|
||||||
|
exact steps. These items have no automated coverage, so this checklist is the release gate for
|
||||||
|
them. See also [SIGNING.md](SIGNING.md) for the code-signing step that removes the SmartScreen
|
||||||
|
prompt for end users.
|
||||||
@@ -100,6 +100,10 @@ export interface AccessOptions {
|
|||||||
restrictFilenames: boolean
|
restrictFilenames: boolean
|
||||||
/** absolute path to a --download-archive file; omit to disable archive tracking */
|
/** absolute path to a --download-archive file; omit to disable archive tracking */
|
||||||
downloadArchivePath?: string
|
downloadArchivePath?: string
|
||||||
|
/** YouTube extraction client override (e.g. 'web_safari'); empty/omitted = default */
|
||||||
|
youtubePlayerClient?: string
|
||||||
|
/** manually-supplied YouTube Proof-of-Origin token; empty/omitted = none */
|
||||||
|
youtubePoToken?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tuned for typical CDN-served video: 16 connections split across 16 segments,
|
// Tuned for typical CDN-served video: 16 connections split across 16 segments,
|
||||||
@@ -278,6 +282,12 @@ function accessArgs(a: AccessOptions): string[] {
|
|||||||
else if (a.cookiesFile) args.push('--cookies', a.cookiesFile)
|
else if (a.cookiesFile) args.push('--cookies', a.cookiesFile)
|
||||||
if (a.restrictFilenames) args.push('--restrict-filenames')
|
if (a.restrictFilenames) args.push('--restrict-filenames')
|
||||||
if (a.downloadArchivePath) args.push('--download-archive', a.downloadArchivePath)
|
if (a.downloadArchivePath) args.push('--download-archive', a.downloadArchivePath)
|
||||||
|
// YouTube reliability: combine the player-client + PO-token overrides into one
|
||||||
|
// `youtube:` extractor-args group (semicolon-separated, as yt-dlp expects).
|
||||||
|
const yt: string[] = []
|
||||||
|
if (a.youtubePlayerClient?.trim()) yt.push(`player_client=${a.youtubePlayerClient.trim()}`)
|
||||||
|
if (a.youtubePoToken?.trim()) yt.push(`po_token=${a.youtubePoToken.trim()}`)
|
||||||
|
if (yt.length > 0) args.push('--extractor-args', `youtube:${yt.join(';')}`)
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -214,7 +214,9 @@ export function buildCommand(opts: StartDownloadOptions): string[] {
|
|||||||
cookiesFromBrowser: settings.cookieSource === 'browser' ? settings.cookiesBrowser : undefined,
|
cookiesFromBrowser: settings.cookieSource === 'browser' ? settings.cookiesBrowser : undefined,
|
||||||
cookiesFile,
|
cookiesFile,
|
||||||
restrictFilenames: settings.restrictFilenames,
|
restrictFilenames: settings.restrictFilenames,
|
||||||
downloadArchivePath: settings.downloadArchive ? getDownloadArchivePath() : undefined
|
downloadArchivePath: settings.downloadArchive ? getDownloadArchivePath() : undefined,
|
||||||
|
youtubePlayerClient: settings.youtubePlayerClient,
|
||||||
|
youtubePoToken: settings.youtubePoToken
|
||||||
}
|
}
|
||||||
const extraArgs = resolveExtraArgs(opts, settings)
|
const extraArgs = resolveExtraArgs(opts, settings)
|
||||||
return buildArgs(opts, outputTemplate, options, getBinDir(), access, extraArgs)
|
return buildArgs(opts, outputTemplate, options, getBinDir(), access, extraArgs)
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ const DEFAULTS: Settings = {
|
|||||||
useAria2c: false,
|
useAria2c: false,
|
||||||
cookieSource: 'none',
|
cookieSource: 'none',
|
||||||
cookiesBrowser: 'chrome',
|
cookiesBrowser: 'chrome',
|
||||||
|
youtubePlayerClient: '',
|
||||||
|
youtubePoToken: '',
|
||||||
restrictFilenames: false,
|
restrictFilenames: false,
|
||||||
downloadArchive: false,
|
downloadArchive: false,
|
||||||
// yt-dlp is self-managed: a writable copy under userData, auto-updated on the
|
// yt-dlp is self-managed: a writable copy under userData, auto-updated on the
|
||||||
@@ -256,6 +258,8 @@ export function setSettings(partial: Partial<Settings>): Settings {
|
|||||||
// and exported by exportBackup as-is — documented, not masked.
|
// and exported by exportBackup as-is — documented, not masked.
|
||||||
case 'proxy':
|
case 'proxy':
|
||||||
case 'rateLimit':
|
case 'rateLimit':
|
||||||
|
case 'youtubePlayerClient':
|
||||||
|
case 'youtubePoToken':
|
||||||
if (typeof value === 'string') s.set(key, value)
|
if (typeof value === 'string') s.set(key, value)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,6 +226,8 @@ export function SettingsView(): React.JSX.Element {
|
|||||||
const proxy = useSettings((s) => s.proxy)
|
const proxy = useSettings((s) => s.proxy)
|
||||||
const rateLimit = useSettings((s) => s.rateLimit)
|
const rateLimit = useSettings((s) => s.rateLimit)
|
||||||
const useAria2c = useSettings((s) => s.useAria2c)
|
const useAria2c = useSettings((s) => s.useAria2c)
|
||||||
|
const youtubePlayerClient = useSettings((s) => s.youtubePlayerClient)
|
||||||
|
const youtubePoToken = useSettings((s) => s.youtubePoToken)
|
||||||
const cookieSource = useSettings((s) => s.cookieSource)
|
const cookieSource = useSettings((s) => s.cookieSource)
|
||||||
const cookiesBrowser = useSettings((s) => s.cookiesBrowser)
|
const cookiesBrowser = useSettings((s) => s.cookiesBrowser)
|
||||||
const restrictFilenames = useSettings((s) => s.restrictFilenames)
|
const restrictFilenames = useSettings((s) => s.restrictFilenames)
|
||||||
@@ -675,6 +677,28 @@ export function SettingsView(): React.JSX.Element {
|
|||||||
label={useAria2c ? 'On' : 'Off'}
|
label={useAria2c ? 'On' : 'Off'}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label="YouTube client (advanced)"
|
||||||
|
hint="Override the YouTube extraction client when downloads start failing the bot check. Try web_safari, tv, or mweb. Leave blank for yt-dlp's default."
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={youtubePlayerClient}
|
||||||
|
placeholder="web_safari"
|
||||||
|
onChange={(_, d) => update({ youtubePlayerClient: d.value })}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label="YouTube PO token (advanced)"
|
||||||
|
hint="A manually-obtained Proof-of-Origin token for YouTube's bot check, sent via --extractor-args. Usually cookies (above) are the easier fix; automatic minting is planned."
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={youtubePoToken}
|
||||||
|
placeholder="(none)"
|
||||||
|
onChange={(_, d) => update({ youtubePoToken: d.value })}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className={styles.card}>
|
<Card className={styles.card}>
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ if (import.meta.env.DEV && !window.api) {
|
|||||||
useAria2c: false,
|
useAria2c: false,
|
||||||
cookieSource: 'none',
|
cookieSource: 'none',
|
||||||
cookiesBrowser: 'chrome',
|
cookiesBrowser: 'chrome',
|
||||||
|
youtubePlayerClient: '',
|
||||||
|
youtubePoToken: '',
|
||||||
restrictFilenames: false,
|
restrictFilenames: false,
|
||||||
downloadArchive: false,
|
downloadArchive: false,
|
||||||
autoUpdateYtdlp: true,
|
autoUpdateYtdlp: true,
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const FALLBACK: Settings = {
|
|||||||
useAria2c: false,
|
useAria2c: false,
|
||||||
cookieSource: 'none',
|
cookieSource: 'none',
|
||||||
cookiesBrowser: 'chrome',
|
cookiesBrowser: 'chrome',
|
||||||
|
youtubePlayerClient: '',
|
||||||
|
youtubePoToken: '',
|
||||||
restrictFilenames: false,
|
restrictFilenames: false,
|
||||||
downloadArchive: false,
|
downloadArchive: false,
|
||||||
autoUpdateYtdlp: true,
|
autoUpdateYtdlp: true,
|
||||||
|
|||||||
@@ -492,6 +492,17 @@ export interface Settings {
|
|||||||
cookieSource: CookieSource
|
cookieSource: CookieSource
|
||||||
/** which browser to read cookies from when cookieSource is 'browser' */
|
/** which browser to read cookies from when cookieSource is 'browser' */
|
||||||
cookiesBrowser: CookieBrowser
|
cookiesBrowser: CookieBrowser
|
||||||
|
/**
|
||||||
|
* YouTube reliability (Phase P). When set, emitted as
|
||||||
|
* `--extractor-args "youtube:player_client=…;po_token=…"`:
|
||||||
|
* - playerClient: an alternate extraction client (e.g. 'web_safari', 'tv',
|
||||||
|
* 'mweb') — a common workaround for YouTube throttling/extraction breakage.
|
||||||
|
* - poToken: a manually-supplied Proof-of-Origin token for the bot check.
|
||||||
|
* Both empty = yt-dlp's defaults. (Automatic WebView token minting is deferred;
|
||||||
|
* this is the argv plumbing it would feed.)
|
||||||
|
*/
|
||||||
|
youtubePlayerClient: string
|
||||||
|
youtubePoToken: string
|
||||||
/** sanitize output filenames to a portable ASCII-only subset (--restrict-filenames) */
|
/** sanitize output filenames to a portable ASCII-only subset (--restrict-filenames) */
|
||||||
restrictFilenames: boolean
|
restrictFilenames: boolean
|
||||||
/** record completed downloads in a yt-dlp --download-archive file to skip repeats */
|
/** record completed downloads in a yt-dlp --download-archive file to skip repeats */
|
||||||
|
|||||||
@@ -461,6 +461,28 @@ describe('sidecar files', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('YouTube extractor-args', () => {
|
||||||
|
it('combines player_client and po_token into one youtube: group', () => {
|
||||||
|
const argv = build(opts(), dlo(), {
|
||||||
|
...NO_ACCESS,
|
||||||
|
youtubePlayerClient: 'web_safari',
|
||||||
|
youtubePoToken: 'web.gvs+ABC'
|
||||||
|
})
|
||||||
|
expect(hasSeq(argv, '--extractor-args', 'youtube:player_client=web_safari;po_token=web.gvs+ABC')).toBe(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits only the fields that are set', () => {
|
||||||
|
const argv = build(opts(), dlo(), { ...NO_ACCESS, youtubePlayerClient: 'tv' })
|
||||||
|
expect(hasSeq(argv, '--extractor-args', 'youtube:player_client=tv')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits nothing when both are empty', () => {
|
||||||
|
expect(build(opts(), dlo(), NO_ACCESS)).not.toContain('--extractor-args')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('format sorting (-S)', () => {
|
describe('format sorting (-S)', () => {
|
||||||
it('emits a raw -S string when formatSort is set, overriding the codec tiebreaker', () => {
|
it('emits a raw -S string when formatSort is set, overriding the codec tiebreaker', () => {
|
||||||
const argv = build(opts(), dlo({ formatSort: 'res:1080,vcodec:av01', preferredVideoCodec: 'h264' }))
|
const argv = build(opts(), dlo({ formatSort: 'res:1080,vcodec:av01', preferredVideoCodec: 'h264' }))
|
||||||
|
|||||||
Reference in New Issue
Block a user