Building a Desktop YouTube Downloader: Cookies, the n-Challenge, and Zero-Install
YouTube doesn't hand you a file, and it fights back. Why a download tool has to be a desktop app, how I got past the bot gate and the JavaScript n-challenge, and how I made it install nothing.
I wanted a free YouTube downloader with no ads, no quotas, and no upload of my links to someone else's server. It sounds like a weekend project: paste a link, pick a quality, save the file. The catch is that YouTube does not hand you a file, and it actively works to stop tools like this. The interesting part of Youloader was never the interface. It was the running fight to make a single download work at all.
Two things shaped the whole build. First, it had to be a desktop app, not a website. Second, getting one video out required clearing a series of walls, each one invisible until the previous fell. Here is the chain, in the order I hit it.
Why It Can't Be a Website
When you watch a video, your browser does not download one MP4. YouTube serves video and audio as separate streams, with URLs scrambled by JavaScript running in its player, and it increasingly answers anonymous requests with "sign in to confirm you're not a bot." Almost nobody re-implements all of that. The de-facto engine is yt-dlp, with ffmpeg merging the streams back together.
The trouble is where that engine runs. Point it at YouTube from a cloud server and you get blocked within hours, because YouTube blocks data-center IPs aggressively. The public downloader sites survive only by paying for residential proxy pools funded by heavy ads, and by rotating domains when they get knocked down. For something free, reliable, and personal, the honest answer is to run the engine on the user's own machine, on their own residential IP. So Youloader is an Electron app: a thin shell over yt-dlp and ffmpeg, running locally.
(I started with Tauri for the smaller binaries, but it needs Rust and the Visual Studio C++ build tools, which I could not verify on the machine I was building on. Electron uses the Node that was already there and every build was reproducible, so it won the trade.)
Wall One: "Sign In to Confirm You're Not a Bot"
The very first resolve failed at the bot gate. The fix is to hand yt-dlp the user's logged-in session as cookies:
yt-dlp --cookies-from-browser firefox "https://youtube.com/watch?v=..."Firefox worked immediately. Chrome and Edge did not: on Windows they lock their cookie database while running and protect it with app-bound encryption, so yt-dlp simply cannot read it ("could not copy cookie database"). Rather than fight that, I added a second path in the app, importing an exported cookies.txt, which is browser-agnostic and works no matter what. The cookies never leave the machine.
Wall Two: Past the Gate, but Only Thumbnails
With cookies accepted, the next error was "Requested format is not available," and the only thing that came back was a handful of storyboard images. This is the part that took the longest to understand.
YouTube protects each stream URL with an n parameter that is scrambled by JavaScript in its player. yt-dlp ships a solver for it, but it can no longer run that solver in pure Python; it needs an external JavaScript engine. Without one, you get images and nothing else. The fix was to give it a runtime:
yt-dlp --js-runtimes node -F "https://youtube.com/watch?v=..."The same video that returned 4 storyboards now returned 143 real formats. The diagnosis only clicked because I ran yt-dlp -F directly, away from my own UI, which separated "my bug" from "YouTube's wall." When an integration misbehaves, talk to the underlying tool first. I also switched yt-dlp to its nightly channel, because tracking a moving target like YouTube is the whole game.
Wall Three: "Nothing to Install" Was a Lie
Beating the n-challenge with --js-runtimes node had a hidden cost: it only worked on machines that already had Node 22 or newer installed. A desktop app that quietly requires Node is not the zero-install tool I was promising.
So why a JavaScript runtime at all, and which one? yt-dlp needs some engine to run its solver, and Deno is the one it recommends: it runs the untrusted YouTube JavaScript in a sandbox with no file, network, or environment access by default, which is safer than handing it a full Node. It also ships as a single self-contained binary. That last part is what made it bundle-able. On first run, the app downloads Deno into the same folder as yt-dlp and ffmpeg, and points yt-dlp at it:
// prefer the bundled Deno, fall back to a system Node if present
const args = ["--js-runtimes", "deno,node" /* ... */]
// the bin folder (yt-dlp + ffmpeg + deno) is prepended to PATHNow yt-dlp, ffmpeg, and Deno all set themselves up the first time you resolve a link. The download is best-effort: if Deno cannot be fetched, the app still runs and falls back to whatever runtime is on the system. There is genuinely nothing for the user to install.
The Unglamorous Wall: Packaging
Two Windows-specific traps cost an afternoon each. First, pnpm's symlinked node_modules confuses electron-builder; a .npmrc with node-linker=hoisted gives it the flat tree it expects. Second, electron-builder downloads a code-signing cache that contains macOS symlinks, and extracting those on Windows needs a privilege that is off by default, so the installer build fails unless Developer Mode is on.
The clean fix was to stop building installers locally at all. The release runs in CI, where the Windows runners have that privilege, and as a bonus the same workflow produces the macOS and Linux builds in one pass.
What I'd Tell Someone Building This
Run where the platform cannot block you. A local app on the user's own IP sidesteps the bans that make a hosted version a money pit. When an integration misbehaves, drop down and talk to the engine directly; yt-dlp -F turned a vague "no formats" into the exact fix in one command. Expect the platform to fight back, and track its nightly builds rather than a stable release. And bundle the messy dependencies, because "nothing to install" is only true if it is actually true.
The result is free, runs entirely on your own machine, updates itself, and will keep needing small fixes as YouTube changes the locks. That last part is not a bug. For this kind of tool, the cat-and-mouse is half the point.
Related posts
Private Image Compression in the Browser: WebAssembly Codecs, and the AVIF That Wouldn't Ship
- Published on
Giving a Portfolio Assistant Real Tools (and Five Models So It Never Goes Down)
- Published on
Streaming AI Chat UIs with the Vercel AI SDK and Provider Switching
- Published on
Building a RAG Chatbot That Cut Support Tickets by 40%
- Published on