DEV Community

Rebell Yell
Rebell Yell

Posted on Edited on Originally published at appgrabbit.com

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

Update, September 2026. Grabbit is now AppGrabbit (version 3.2.0) and a few things below are out of date. The Free plan moved from per-feature caps to 5 credits a month (a video, a batch of up to 10 links, a transcript or a converted file is 1 credit; claim 1 more every 48 hours), so the converter is no longer Pro-only. Transcripts no longer depend on YouTube captions: videos without them, and your own files (a recorded class, a meeting, a voice note), are transcribed locally with whisper.cpp, on an NVIDIA card or the CPU, nothing uploaded. Pricing is USD 8.99 a month, monthly only (the annual plan was dropped), or a one-time 7-day pass for USD 3.99. Details: what's new in 3.2 · how the local Whisper setup works · how credits work.

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 that converts between video and image formats entirely on your machine (Pro-only when this was written; open to Free since 3.2.0, 1 credit per file).

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

Users can paste a list of URLs — one per line — and Grabbit queues them all (Free batches are capped at 10 links per batch; Pro has no cap). 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. (Since 3.2.0 this is only the fast path: when a video has no captions, or when you drop a file of your own, the audio is transcribed locally with whisper.cpp; see the update at the top.)


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 (July 2026): 10 single downloads, 1 batch (up to 10 links), 2 transcripts — replaced in 3.2.0 by 5 credits a month
  • Pro: $8.99/month, monthly only (the annual plan was dropped) — unlimited downloads and batches, playlists, 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)