Verify it yourself
- Source repository — Public and MIT licensed. Electron main process, React renderer, binary fetch script and electron-builder config.
- Releases — v0.1.0 and v0.2.0, published May 2026, with macOS Apple Silicon and Intel installers.
- v0.2.0 release notes — Batch downloads, channel support, the concurrency limiter and the packaged-build preload fix.
Introduction
Most of my work is web and backend. This one is a desktop app, and it exists because almost every free YouTube downloader on the internet is a thin wrapper around yt-dlp that adds ads, a paywall, telemetry, or a prompt to hand over your browser cookies. I wanted the wrapper without the tax on the user.
The Challenge
yt-dlp and ffmpeg already do the hard media work, and they do it better than anything I would write. The gap is not extraction, it is that both are command-line tools with a large flag surface, and most people will not open a terminal to type a format selector. The obvious fix is a GUI, but a GUI immediately inherits a distribution problem: the app is useless without two native binaries that differ per operating system and per CPU architecture. That is the actual engineering constraint on this project, not the download logic.
The Solution
I built a three-layer app. A React renderer, styled with Tailwind, holds the URL input, the video preview and the options panel. The Electron main process owns everything privileged: it resolves binary paths, builds the yt-dlp argument list, spawns the process, parses progress from stdout and pushes typed events back to the UI over IPC. A separate Node script fetches the correct yt-dlp and ffmpeg builds for a given platform and architecture into a gitignored directory, and electron-builder copies that directory into the packaged app as an extra resource. The binaries are never committed, and the same command can prepare a Windows or Linux payload from a Mac.
Technical Deep Dive
Binary resolution is layered rather than assumed. electron/binaries.ts checks process.resourcesPath/bin when the app is packaged, then a local resources/bin during development, then falls back to which or where on PATH. If neither yt-dlp nor ffmpeg resolves, the app raises a message telling the user to run npm run fetch-binaries or install the tool, instead of failing silently mid-download.
scripts/fetch-binaries.mjs takes --platform and --arch flags and maps them to real upstream artifacts: yt-dlp.exe for Windows, yt-dlp_macos for macOS, and the linux, aarch64 or armv7l builds for Linux. ffmpeg comes from a different source per platform, with a fallback URL for macOS, and is unpacked with unzip, PowerShell Expand-Archive or tar -xJf depending on where the script is running.
The format selector is a cascade, not a single expression. For video the main process builds bv*[height<=N] with optional [fps>=50] and [dynamic_range~=HDR] clauses, then chains progressively looser fallbacks so an HDR request on a source without HDR still resolves to the best available stream rather than erroring out. Output is merged and remuxed to mp4.
Progress comes from parsing yt-dlp's stdout with --newline and --progress, against a regex that pulls percent, total size, speed and ETA. Line prefixes such as [download] Destination:, [Merger], [ExtractAudio] and [SponsorBlock] are used to infer a stage, so the UI can say whether it is fetching video, fetching audio, merging or post-processing rather than showing one bar that resets.
The queue caps concurrency at two downloads. Extra jobs sit in a pending array and start as slots free up, and cancelling a job that has not started yet removes it from the queue without ever spawning a process. Cancelling a running job sends SIGTERM and escalates to SIGKILL after 1.5 seconds on Unix.
The renderer runs with contextIsolation on, nodeIntegration off, a Content Security Policy meta tag, and a window-open handler that denies in-app navigation and hands external URLs to the system browser. Everything the UI can do is defined by the preload script's typed window.app surface: get defaults, choose a folder, fetch info, start, cancel, reveal in folder, subscribe to events.
Packaging exposed a real Electron trap. The preload script has to be CommonJS in this configuration, so the Vite build emits it as preload.cjs while the rest of the app stays ESM. Getting that wrong produced a black window in packaged builds that never appeared in development, which is documented in the v0.2.0 release notes along with the CSP relaxation needed for file:// loading.
Key Features
Quality presets with source-aware options
Presets from 360p up to 2160p, with HDR and 60fps toggles that are enabled only when the fetched format list actually contains them. The app reads yt-dlp's JSON dump, collects available heights, dynamic range and fps, and disables what the source cannot deliver.
Audio-only export
MP3, M4A or Opus, with a bitrate passed through to yt-dlp's --audio-quality flag. The panel defaults MP3 and M4A to 192 kbps and Opus to 160. Extraction runs through the ffmpeg path the app resolved, handed to yt-dlp with --ffmpeg-location.
SponsorBlock marking or removal
Eight categories including sponsor, self-promo, intro, outro, preview, filler, subscribe begs and non-music. Segments can be written as chapters or cut out entirely, mapping to yt-dlp's --sponsorblock-mark and --sponsorblock-remove flags. Off is a supported mode.
Batch, playlist and channel handling
Paste multiple URLs separated by newlines or commas and the app fetches metadata for all of them in parallel. Channel and playlist URLs are detected by pattern and loaded with --flat-playlist so large channels resolve fast, with a limit control that defaults to 25 items.
Live download queue
Per-job progress percent, speed, ETA, stage, cancel and reveal-in-folder, driven by IPC events rather than polling. Closing the app cancels every pending and running job so no orphaned yt-dlp process is left behind.
Privacy by construction
No analytics SDK is installed, so there is nothing to disable. There is no account system, no remote config and no update check. yt-dlp runs with --no-call-home and --ignore-config, and --cookies-from-browser is never used, so browser cookies are not read.
Results & Impact
- ✓Two tagged releases are public: v0.1.0 and v0.2.0, both published in May 2026, shipping macOS installers for Apple Silicon and Intel. Windows NSIS, portable and Linux AppImage and deb targets are configured in electron-builder.yml and can be built from source.
- ✓The whole app runs on three runtime npm dependencies: react, react-dom and lucide-react. Everything else is a dev dependency or a bundled CLI binary, which is what makes the no-telemetry claim checkable rather than a marketing line.
- ✓A single fetch-binaries command prepares the yt-dlp and ffmpeg payload for any supported platform and architecture from any development machine, so producing a Windows or Linux installer does not require a Windows or Linux box to obtain the binaries.
- ✓The source is MIT licensed and public on GitHub, with the contribution rules written into the README: privacy stays a feature, and dependencies stay thin.
- ✓One limitation is stated openly in the README and the release notes rather than hidden: the macOS builds are unsigned and not notarized, because notarization needs a paid Apple Developer account this project does not have. First launch requires right-click then Open.
Lessons Learned
"The hard part of a desktop app that shells out to CLI tools is not the shelling out. It is deciding where the binaries live, how they get there, and what happens when they are missing. I spent more time on binary resolution and the fetch script than on the download logic, and that ratio was correct."
"Packaged Electron behaves differently enough from dev Electron that you have to test the packaged build early. The preload module format and the CSP under file:// both worked perfectly in dev and produced a blank window when packaged. Anything that only breaks after electron-builder runs will cost you a whole debugging session if you find it late."
"Making a format selector fall back gracefully beats making it precise. Users ask for HDR at 4K on videos that have neither. A chained selector that degrades to the best available stream produces a file; a strict one produces an error message the user cannot act on."
"Privacy is easier to keep as an architectural constraint than as a policy. Once there is no analytics package, no login flow and no update server in the build, there is nothing to accidentally re-enable. The README's contribution rule exists to keep it that way."
Conclusion
This project is deliberately small. It is a clean interface over two excellent tools, packaged so that a non-technical user gets the benefit without a terminal, a signup or a tracker. The interesting engineering is in packaging and binary distribution, which is usually where cross-platform desktop projects quietly fall over.
Frequently asked questions
- Is Free YT Downloader open source and free to use?
- Yes. The application code is MIT licensed and the full source is public on GitHub. There is no paid tier, no trial and no account. The bundled yt-dlp and ffmpeg binaries keep their own upstream licenses, which are noted in the README's acknowledgments section.
- Does the app collect any data or read my browser cookies?
- No. There is no analytics package in the dependency list, no crash reporter, no login and no update beacon. yt-dlp runs with --no-call-home and --ignore-config, and the --cookies-from-browser flag is never used. The only outbound traffic is yt-dlp fetching the video and optional SponsorBlock API calls.
- Which platforms does it run on, and are installers available for all of them?
- One codebase targets Windows, macOS and Linux, with NSIS, portable, dmg, AppImage and deb targets configured in electron-builder. Published releases so far include macOS Apple Silicon and Intel dmg files. Windows and Linux installers can be built from source after fetching the matching binaries.
- How does it handle yt-dlp and ffmpeg without asking me to install them?
- A Node script downloads the correct yt-dlp and ffmpeg build for your platform and architecture into a local resources folder, and electron-builder copies that into the packaged app. At runtime the app looks in the packaged resources first, then falls back to anything already on your PATH.
- Why does macOS block the app the first time I open it?
- The macOS builds are not code-signed or notarized, which Gatekeeper treats as untrusted. Notarization requires a paid Apple Developer account this project does not have. Right-click the app in Applications, choose Open, and confirm once. After that it launches normally, and this is stated in both the README and the release notes.
- Can it download whole playlists or channels without flooding my queue?
- Yes. Playlist and channel URLs are detected by pattern and loaded with --flat-playlist so metadata resolves quickly even on large channels. A limit control appears whenever a collection is queued and defaults to the first 25 items, and the downloader itself runs at most two jobs in parallel.
- Can I fork this and adapt it for my own tooling?
- Yes, that is what the MIT license is for. The architecture separates cleanly: the renderer is plain React state, the main process owns argument building and process spawning, and the yt-dlp wrapper is a single file. If you want similar desktop or automation work built for your stack, the full-stack development service page is the place to start.
Related work and reading
Interested in a Similar Project?
Let's discuss how I can help bring your ideas to life.
Get in Touch