DEV Community

Rebell Yell
Rebell Yell

Posted on • Originally published at appgrabbit.com

I rebuilt my Windows video downloader from scratch — yt-dlp + FFmpeg + PyInstaller, 1,700 downloads later

Three months ago I posted here about Grabbit, a desktop video downloader I built for Windows. The app has grown a lot since then — new engine, local file converter, batch mode, transcript extraction — so I wanted to write a proper update with the actual technical details.

What Grabbit does (2026 edition)

Grabbit is a Windows 10/11 desktop app that downloads videos from 8+ platforms — YouTube, TikTok, Instagram, Facebook, Twitter/X, Pinterest, Twitch, SoundCloud — up to 4K quality. Everything runs locally. No cloud, no account required, no file upload anywhere.

It also ships with a local FFmpeg converter (Pro) that converts between video and image formats entirely on your machine.

Supported downloads: YouTube to MP3, TikTok, Instagram Reels, Pinterest, YouTube Shorts, SoundCloud, and more.

Supported conversions: Video → MP3, WebM → MP4, HEIC → JPG, WebP → JPG, Video → ProRes HQ, Video → DNxHR HQ.


The stack (unchanged from v1, for a reason)

Backend:      Python + Flask + yt-dlp + FFmpeg
Frontend:     Vanilla JS (no framework)
Auth/DB:      Supabase (PostgreSQL + Edge Functions)
Payments:     Stripe → Supabase Edge Function → Resend
Installer:    PyInstaller + Inno Setup
Distribution: GitHub Releases (auto-update via version check)
Enter fullscreen mode Exit fullscreen mode

I kept the same stack because it works. The temptation to rewrite in Electron or Tauri was real but I resisted it. Adding complexity wouldn't have shipped any of the features below.


What's new: the FFmpeg Converter

The biggest addition since April is the local Converter. Users can drop a local file (MP4, MKV, AVI, MOV, WebM, HEIC, WebP) and convert it to another format — entirely on their machine. No upload, no size limit.

The trickiest part: bundling FFmpeg and pillow-heif with PyInstaller.

For FFmpeg I use imageio-ffmpeg which ships its own binary:

import imageio_ffmpeg
ffmpeg_path = imageio_ffmpeg.get_ffmpeg_exe()
Enter fullscreen mode Exit fullscreen mode

This gives you a reliable path whether you're running from source or from a frozen PyInstaller bundle. No which ffmpeg nonsense, no PATH issues on Windows.

For HEIC support (pillow-heif), PyInstaller doesn't collect the native DLLs automatically. You need to force it in the .spec file:

# grabbit.spec
from PyInstaller.utils.hooks import collect_all
datas_heif, binaries_heif, hiddenimports_heif = collect_all('pillow_heif')
Enter fullscreen mode Exit fullscreen mode

Without that, HEIC conversion silently fails at runtime on the packaged exe — no error, just nothing happens. Took an embarrassing amount of time to debug.


What's new: Batch mode

Pro users can paste up to 50 URLs at once — one per line — and Grabbit queues them all. The implementation is straightforward but the UX took iteration: users need to know which URLs failed and why, without it feeling like a wall of errors.

I ended up with a per-URL status row that shows queued → downloading → done/error in real time. The progress streaming still holds — polling every 600ms against a lightweight endpoint works well enough that I haven't needed WebSockets.


What's new: Transcript mode

Grabbit can extract transcripts from YouTube videos — useful for summarizing, repurposing content, or accessibility. It uses yt-dlp's subtitle extraction under the hood:

ydl_opts = {
    'writesubtitles': True,
    'writeautomaticsub': True,
    'subtitleslangs': ['en'],
    'skip_download': True,
}
Enter fullscreen mode Exit fullscreen mode

Auto-generated captions aren't perfect but for most YouTube content they're good enough for summarization use cases.


What's new: self-updating yt-dlp

yt-dlp breaks constantly as platforms change their APIs. Rather than shipping a new app version every time, Grabbit updates yt-dlp silently in the background on launch:

import subprocess, sys

def update_ytdlp():
    subprocess.run(
        [sys.executable, '-m', 'pip', 'install', '--upgrade', 'yt-dlp'],
        capture_output=True
    )
Enter fullscreen mode Exit fullscreen mode

When frozen with PyInstaller, sys.executable points to the bundled Python — so pip updates the bundled yt-dlp, not a system one. This means users always have a working downloader even when a platform changes something overnight.


Numbers after 3 months

  • 1,700+ installs via GitHub Releases
  • Top countries: Mexico, US, Peru, Spain, Argentina, Colombia
  • Top traffic sources: Direct, Google Organic, ChatGPT referral
  • Free plan: 10 single downloads, 1 batch (up to 10 links), 2 transcripts
  • Pro: $8.99/month or $59.99/year — unlimited downloads, batch up to 50, all converter formats

The LATAM tilt was unexpected but makes sense in hindsight — there's a real gap for a clean Spanish-language download tool. I shipped 19 new SEO landing pages in Spanish last week targeting that.


What I'd do differently

Ship the Chrome Extension earlier. The extension puts a one-click Grab button directly on video pages while you're watching. It's one of the highest-retention features and I waited too long to build it.

Don't fight PyInstaller — embrace the spec file. I spent weeks trying to make things work without touching the .spec. The right answer is always: open the spec, add what you need, move on.

Ignore the "just use Electron" advice. Flask + Vanilla JS served from localhost is genuinely fine for a tool like this. The bundle is smaller, startup is faster, and I understand every line of it.


If you're building something similar — local desktop tool, Python backend, freemium model — happy to answer questions in the comments.

If you want to try it: appgrabbit.com — Windows only, free plan available, no account required.

Top comments (0)