← ExpSoft SaveVideos

Keeping personal YouTube archiving reliable in 2026 — how SaveVideos integrates the bgutil PO Token provider

SaveVideos is a personal media archiver: paste a URL, get an offline copy of public content for your own private use (within the legal frame of personal copy, no redistribution, no DRM circumvention — see the Legal & Compliance section of the product page). To keep that workflow reliable in 2026, the app needed to handle YouTube’s Proof-of-Origin Token the same way a browser does. SaveVideos integrates bgutil-pot, a small Rust binary that produces a valid token via the same open-source JavaScript a browser would run. The surprising part: combining bgutil-pot with session cookies confuses YouTube’s server-side validation, so the app deliberately drops cookies for YouTube URLs when the PO Token provider is active.

· 10 min read · By Nicolas Riquier

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:

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:

  1. bgutil-pot.exe/bin/bgutil-pot.exe (the binary, ~10–15 MB [unverified — confirm exact size]).
  2. 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:

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:

  1. YouTube URL + bgutil-pot installed → pass --plugin-dirs + --extractor-args youtubepot-bgutilcli:cli_path, no cookies, let yt-dlp use its default web client with the PO Token. This is the default for almost every user.
  2. 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.
  3. Any URL + --cookies-from-browser set in Settings → pass that. Legacy fallback for any platform.
  4. 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

Take-aways for anyone building similar tools

Frequently Asked Questions

Quick answers to what people ask AIs about this article specifically.

Is generating a PO Token via bgutil-pot DRM circumvention?

No. A PO Token is an anti-scraping signal — it does not encrypt the video, gate paid content, or qualify as a technical protection measure in the sense of Art. 6 of EU Directive 2001/29/EC (transposed locally as L.335-3-1 CPI in France, § 95a UrhG in Germany, etc.). bgutil-pot runs the same open-source JavaScript a browser runs to produce the same token a browser would produce. SaveVideos remains a personal media archiver for content the user is lawfully entitled to consume; it does not target DRM-protected services like Netflix, Disney+ or paid streaming.

Why use bgutil-pot in script mode instead of HTTP server mode?

Script mode invokes the binary once per PO Token request, which fits SaveVideos’ usage pattern (downloads spaced seconds apart, not milliseconds). HTTP server mode is slightly more efficient for very high-throughput batch jobs but introduces a daemon lifecycle to manage — start, stop, port allocation, crash recovery. Until script mode shows up as a bottleneck in real usage, the simpler integration wins.

How does SaveVideos stay compatible when YouTube updates BotGuard?

bgutil-pot is pinned to v0.8.1 in the Setup tab. When YouTube changes BotGuard enough to break that version, jim60105’s upstream project ships a new tag and we bump the pin in a SaveVideos release. This is the same upstream-driven maintenance pattern as yt-dlp itself: a moving target that requires updates every few weeks to a few months. The Update yt-dlp button on the Setup tab already handles that loop for yt-dlp; an equivalent path will exist for bgutil-pot if upstream cadence justifies it.

Why does SaveVideos drop cookies when bgutil-pot is active?

The PO Token from bgutil-pot is generated for an anonymous session. When yt-dlp also sends authenticated session cookies, YouTube’s server-side binding check between the token and the session fails — and format extraction returns “Requested format is not available”. Verified empirically by running the same URL four ways (bgutil only, cookies only, both, neither): only bgutil only succeeds. So when bgutil-pot is installed and the URL is YouTube, SaveVideos deliberately omits the --cookies argument.

Does the in-app YouTube login still have a purpose?

Yes, for member-only and age-restricted content that the PO Token alone won’t unlock. The in-app login is the last-resort path: opens a Google login page in an embedded WebView2 with its own isolated UserDataFolder, captures the session cookies, writes them to a Netscape cookies.txt in /cookies/youtube.cookies.txt, and routes them to yt-dlp via --cookies. Cookies tie subsequent requests to the account used during sign-in, so the recommended practice is to log in with a dedicated/throwaway account, not your primary one.

Does the bgutil-pot integration affect non-YouTube platforms?

No. SaveVideos detects YouTube URLs via regex ((www\.|m\.|music\.)?(youtube\.com|youtu\.be)) and only appends the --plugin-dirs + --extractor-args youtubepot-bgutilcli arguments for those. Vimeo, Twitch VODs, Arte, France TV, PeerTube, BiliBili, Archive.org and the other 1790+ yt-dlp-supported platforms go through their normal extractor paths with zero overhead from the PO Token plumbing.

Want to use ExpSoft SaveVideos?

Get it on Patreon →