Why a personal media archiver still matters
Public videos go offline. Channels get terminated. Creators delete uploads. National archives don’t cover online video at scale. The simplest case — “I want to keep a copy of this video I’m allowed to watch, so I can rewatch it on a long flight” — is a use case the EU explicitly recognises through the private-copy exception (Art. L.122-5 CPI in France, § 53 UrhG in Germany, RD 1657/2012 in Spain, broadly aligned with Directive 2001/29/EC). SaveVideos exists to make that workflow one paste-and-click on Windows, for content you’re lawfully entitled to consume, kept on your own disk, not redistributed.
That brief stays the same whether the upstream platform is YouTube, Vimeo, Twitch VODs, Arte, France TV, PeerTube, BiliBili, Archive.org, or any of the 1800+ that yt-dlp covers. The technical reality is that platforms increasingly add friction to their request paths. The engineering challenge is keeping the brief possible regardless.
What YouTube expects from a video request in 2026
Around mid-2024, YouTube started requiring a Proof-of-Origin Token (PO Token) on most video format requests. A PO Token is a cryptographic blob, signed by a piece of open-but-obfuscated JavaScript called BotGuard, that proves the request originated from a fully-loaded JavaScript environment. Chrome, Edge and Firefox produce them automatically every time you load a YouTube page — the browser fetches BotGuard, executes it, gets a signed token, and includes it on subsequent media requests. You never see this. It’s part of the HTTP exchange a browser already does to play a video.
This is not DRM. PO Tokens don’t encrypt the video stream, they don’t prevent the user from accessing the content, and they’re not an access-control measure in the legal sense of technical protection measures (Art. 6 of Directive 2001/29/EC, transposed locally as L.335-3-1 CPI etc.). What they are is an anti-scraping signal: YouTube wants to distinguish requests coming from a JS-running client from requests coming from automated scripts, primarily to throttle bots and abusive load patterns on their infrastructure.
The practical implication for a personal archiver: to make a valid video-format request, you need to send a PO Token, just like a browser does. Tools that don’t run a full JavaScript environment can’t produce one on their own. yt-dlp — the engine SaveVideos builds on — doesn’t have BotGuard built in, so without help it surfaces the now-common error: “Sign in to confirm you’re not a bot.”
Three approaches we tried before settling on bgutil-pot
For SaveVideos V1.0.0.0, we explored three paths in order. Each works in some cases; each has UX or technical costs that made it a poor default.
Approach 1 — yt-dlp’s alternative player clients
yt-dlp lets you pass --extractor-args "youtube:player_client=..." to control which client signature it uses. Two of the available clients (web_embedded and android_vr) don’t require a PO Token. Setting a cascade gives you a quiet fallback for many URLs:
--extractor-args "youtube:player_client=default,web_embedded,android_vr"
This works for embeddable videos and non-kid-targeted content. It does not work for the URLs YouTube most stringently checks, which includes a lot of mainstream uploads. We also learned the hard way: if you include mweb in the cascade and it’s tried last, its own PO-Token-requirement failure surfaces as the overall error — even when earlier clients in the cascade would have succeeded. We removed mweb from the fallback for that reason. default,web_embedded,android_vr stays as a no-cookies fallback for users who choose to skip the PO Token provider setup.
Approach 2 — cookies from the user’s daily browser
yt-dlp’s --cookies-from-browser chrome (or edge, firefox, brave ...) reads the user’s logged-in YouTube cookies straight from their daily browser’s SQLite store. It works — sometimes — but has two structural drawbacks that make it a poor default in a desktop tool:
- Chromium browsers keep their cookie database locked while running, even after the window is closed. Background processes (system tray, “continue running background apps” setting) hold the lock alive. yt-dlp surfaces this as “Could not copy Chrome cookie database” — with the word “Chrome” regardless of which Chromium-based browser is actually targeted. Many users don’t know they need to kill
msedge.exeprocesses via Task Manager. - Cookies tie requests to the user’s real Google account. YouTube’s ToS §5 prohibits downloading even within the private-copy frame; the platform sanction is account suspension, not legal action. Risking the user’s primary Google account is a much worse failure mode than “the download didn’t work today”.
SaveVideos still exposes --cookies-from-browser as a Settings option, plus a “Force close browser & free cookies” helper that terminates the matching Chromium processes after an explicit confirmation dialog. It’s no longer the default path though.
Approach 3 — embedded WebView2 login flow
The clean theoretical answer: run a real Chromium engine ourselves. We embed a WebView2 instance with a dedicated UserDataFolder, guide the user through Google’s login flow inside SaveVideos, then dump the session cookies to a Netscape-format cookies.txt for yt-dlp. The browser is real (it’s actual Chromium under the hood), it’s isolated from the user’s daily browser, and the cookies are fresh.
This still ships in SaveVideos under Settings → “▶ YouTube authentication (recommended for member-only content)”. It’s gated by an explicit consent dialog that documents where the cookies are stored, who reads them, and the account-risk implications of using a personal account.
But it has friction the default path shouldn’t have: a user has to sign in, possibly with 2FA, and the resulting requests are tied to whatever account they used. For the simple “keep a copy of this public video” case, that’s a heavy ask. It’s now the last-resort path. Most users will never need it.
How a browser handles this, and how SaveVideos matches it
The right architectural answer existed all along: produce the PO Token the same way a browser does. The yt-dlp community converged on a plugin called bgutil-ytdlp-pot-provider for this. The plugin is a thin Python file that yt-dlp loads from a configured plugin directory; on each YouTube request that needs a PO Token, the plugin spawns a small helper, asks it for a token, and hands the result back to yt-dlp.
For the helper we use jim60105’s Rust port (bgutil-ytdlp-pot-provider-rs). It’s a single Windows .exe that embeds a JavaScript engine via the rustypipe-botguard crate, fetches YouTube’s BotGuard JavaScript, runs it in a minimal browser-shaped environment, and emits the signed token to stdout. The same JavaScript that runs in Chrome, Edge or Firefox runs here — just without the rest of a browser.
From SaveVideos’ point of view, integration is two downloads at Setup time:
bgutil-pot.exe→/bin/bgutil-pot.exe(the binary, ~10–15 MB [unverified — confirm exact size]).bgutil-ytdlp-pot-provider-rs.zip→ extracted to/plugins/yt_dlp_plugins/extractor/(the Python plugin yt-dlp loads).
And two arguments added to every yt-dlp invocation against a YouTube URL:
--plugin-dirs "<root>/plugins"
--extractor-args "youtubepot-bgutilcli:cli_path=<root>/bin/bgutil-pot.exe"
The user sees nothing of this. yt-dlp detects the plugin, calls bgutil-pot.exe --content-binding <video-id> when it needs a token, parses the response, and includes the token in its format request. The protocol exchange is the same one a browser would do; the only thing SaveVideos provides is the JavaScript-running environment that yt-dlp itself doesn’t carry.
The PO Token + cookies interaction
This is the engineering nugget — the bit we didn’t anticipate.
The intuitive assumption when integrating two authentication-adjacent mechanisms is belt and suspenders: pass both cookies and the PO Token, presumably better odds. So the first integration shipped with both active simultaneously when the user was signed in via the in-app WebView2.
Every YouTube URL failed with the same error: “Requested format is not available. Use --list-formats for a list of available formats.”
The error wording is misleading — it sounds like a format-selector problem, not an authentication one. And the situation was confusing because:
- bgutil-pot alone (no cookies) worked on the same URLs.
- Cookies alone (no bgutil) gave the original “Sign in to confirm you’re not a bot” error.
- The analyze step doesn’t even use
-f, so the format-not-available wording can’t literally be about format selection.
What’s actually happening: bgutil-pot produces a PO Token for an anonymous client session. When yt-dlp simultaneously sends authenticated session cookies, YouTube’s server-side check between the token and the session fails — the binding doesn’t resolve, and the server refuses to expose the format list. The error wording then surfaces incorrectly as a format-selector issue.
The fix is one branch in the cookie-args appender:
// bgutil-pot supplies an anonymous PO Token. Mixing it with authenticated
// session cookies makes YouTube reject the request — the token doesn't
// validate against the cookie session, format extraction fails with
// "Requested format is not available". Verified empirically: cookies +
// bgutil → broken; bgutil alone → works. So when bgutil is active for
// a YouTube URL, drop cookies entirely.
if (IsYouTubeUrl(url) && HasBgUtilInstalled()) return;
Verified empirically by running the same yt-dlp command four ways against the same video — bgutil only, cookies only, both, neither. Only bgutil only succeeded.
How SaveVideos composes the layers today
The final auth-path priority, evaluated per URL:
- YouTube URL + bgutil-pot installed → pass
--plugin-dirs+--extractor-args youtubepot-bgutilcli:cli_path, no cookies, let yt-dlp use its defaultwebclient with the PO Token. This is the default for almost every user. - YouTube URL + in-app sign-in cookies → pass
--cookies path/to/cookies.txt. For member-only or age-restricted content that the PO Token alone won’t unlock. - Any URL +
--cookies-from-browserset in Settings → pass that. Legacy fallback for any platform. - None of the above → pass the no-cookies player-client cascade (
default,web_embedded,android_vr). Works for embeddable / non-kid-targeted content. The Setup tab encourages users to install bgutil-pot if they hit this often.
The Run tab log prints the auth path taken for each analysis, so the user gets the diagnostic for free:
Analysing https://www.youtube.com/watch?v=… (PO Token via bgutil-pot)
Analysing https://www.youtube.com/watch?v=… (in-app YouTube auth)
Analysing https://www.youtube.com/watch?v=… (cookies: edge)
Analysing https://www.youtube.com/watch?v=…
What about other platforms?
None of the above applies to Vimeo, Twitch VODs, Arte, France TV, PeerTube, BiliBili, Archive.org, Niconico, Rumble, Odysee or the other 1790+ platforms yt-dlp supports. Those don’t use PO Tokens; they use their own anti-scraping mechanisms, and yt-dlp handles them out of the box. SaveVideos adds --plugin-dirs + --extractor-args youtubepot-bgutilcli only when the URL matches the YouTube regex ((www\.|m\.|music\.)?(youtube\.com|youtu\.be)). For everything else, the arguments are not added — yt-dlp doesn’t pay any overhead and unrelated extractors aren’t affected.
The cookies layer is similarly scoped: in-app YouTube cookies attach only to YouTube URLs; --cookies-from-browser attaches to all of them (because the user’s daily browser may have a session on a different platform too).
What we didn’t ship
- HTTP-mode bgutil server. The Rust binary supports running as a long-lived HTTP server (
bgutil-pot server --port 4416). yt-dlp would talk to it via the HTTP variant of the plugin. Slightly more efficient for very high-throughput batch jobs (one warm process instead of N short-lived ones), but it introduces daemon lifecycle — start, stop, recover, free the port. For SaveVideos’ usage pattern (downloads seconds apart, not milliseconds), script mode comes out ahead overall. - Auto-update of bgutil-pot. The Setup tab pins to
v0.8.1ofjim60105/bgutil-ytdlp-pot-provider-rs. We deliberately review upstream releases manually before bumping the pin — a breaking change in BotGuard handling shouldn’t ship to users silently. The trade-off is that when YouTube updates BotGuard enough to breakv0.8.1, we’ll need to bump and ship a SaveVideos release. The Update yt-dlp button on the Setup tab already covers similar maintenance for yt-dlp itself. - Reimplementing yt-dlp in C#. A tempting thought after a week of wrestling with extractor args. Not viable. yt-dlp inherits roughly a decade of extractor reverse-engineering from
youtube-dlplus its own active maintenance, with weekly releases to track YouTube’s changes tobase.js. SaveVideos provides Windows packaging and integration ergonomics; reinventing the engine would mean signing up for an unwinnable maintenance burden. Stay a wrapper.
Take-aways for anyone building similar tools
- For YouTube in 2026, your tool needs a PO Token. Plan for it from day one.
- Don’t mix a PO Token with cookies on the same request — the server-side validation will reject the combination.
- Keep the user-friction tier (login flows) for member-only or age-restricted edge cases. Most people just want to paste a URL and have it work, for content they’re lawfully consuming.
- Stay a wrapper. yt-dlp does the extraction. ExpSoft adds packaging, integration ergonomics, and a legal frame that’s explicit about what the tool is for — personal copy, not redistribution.