Paste a URL. Get the file. Highest available quality, fastest sane speed, zero setup.
SnagLite is a tiny video downloader that does the obvious thing well: you give it a link, it saves the best-quality file to your downloads folder. Under the hood it wraps the three best tools in the space — yt-dlp, ffmpeg, and aria2c — and handles the parts that usually trip people up: finding the binaries, picking sane formats, gluing video and audio together, retrying when a site changes its mind. You shouldn't have to think about any of that, and with SnagLite you don't.
There are two builds in this repo:
snaglite.exe— a single-file Windows CLI (src/SnagLite/).- SnagLite Android — a Kotlin + Jetpack Compose app (
android/) with a foreground download service, a download queue, pause/resume, and a one-time setup screen that bootstraps everything on first launch.
Both builds share the same philosophy: no accounts, no telemetry, no nags, no PATH wrangling. They support the ~1800 sites yt-dlp supports out of the box, plus a small generic resolver pipeline that fills in for a few common iframe-wrapper hosts the bundled extractors don't recognise.
Windows CLI — grab snaglite.exe from the Releases page (or build it yourself), drop it on your PATH, and:
snaglite https://www.youtube.com/watch?v=dQw4w9WgXcQFirst invocation auto-downloads yt-dlp.exe, ffmpeg.exe, and aria2c.exe to %LOCALAPPDATA%\SnagLite\bin. From then on it's instant.
Android — download an APK from the Releases page (or build it yourself with ./gradlew :app:assembleDebug), sideload, paste a URL into the input box, tap Download. The setup screen on first launch handles the rest. Files land in Movies/SnagLite or Music/SnagLite and show up in your gallery without any storage permission prompts on Android 10+.
- Genuinely zero-config. No accounts. No tokens. No "configure your downloader" wizard. The first
snaglite <url>works. - Fast by default.
aria2c -x16 -s16 -k1Mpulls files in parallel chunks — usually a 3–8× speedup over single-connection HTTP. - Handles awkward sites. When yt-dlp can't crack a host, SnagLite retries once with the generic extractor, and a small custom resolver pipeline handles a handful of common iframe-wrapper sites the bundled extractors miss.
- Android background reliability. Holds a partial wake lock + high-perf Wi-Fi lock for the duration of a download, and prompts to add itself to the battery-optimisation exemption list on first launch so long downloads survive doze mode.
- Stays current. Both builds silently auto-update
yt-dlpon a 7-day debounce per cold launch (yt-dlp ships extractor fixes daily). Manualsnaglite updateis also available on the CLI. - No telemetry, no accounts, no nags. Ever.
Inspired by ytdlnis. Previously known as Snag — see Migrating from Snag below for the rename details.
- Takes any URL, downloads the best video+audio merged into a single
.mp4. - Auto-installs
yt-dlp.exe,ffmpeg.exe,aria2c.exeon first run — no manual setup, no PATH wrangling. - Uses
aria2cwith 16 connections for fast parallel chunk downloading. - Live progress bar in the terminal (percent, speed, ETA).
- No prompts, no auth, no tokens, no telemetry.
- All yt-dlp built-in extractors —
youtube.comand ~1800 others. - Sites without a dedicated extractor are auto-retried with yt-dlp's generic extractor (HTML scrape + HLS/DASH/iframe sniff).
- A few listing-page hosts that embed third-party players via
<iframe>are auto-unwrapped before extraction so the underlying provider host is what reaches the resolver pipeline.
If a site's player obfuscates the stream URL beyond what the bundled extractors can find, yt-dlp's stderr is surfaced verbatim. Run snaglite update first — yt-dlp ships extractor fixes daily.
- Grab
snaglite.exefrom the Releases page, or frompublish/after running the publish command below. - Drop it anywhere on your
PATH(e.g.C:\Tools\snaglite.exe). - First invocation auto-downloads helper binaries to
%LOCALAPPDATA%\SnagLite\bin.
dotnet publish src/SnagLite/SnagLite.csproj -c Release -r win-x64 --self-contained -o publishProduces publish/snaglite.exe (single-file, .NET runtime embedded).
snaglite <url> # download with all defaults
snaglite <url> -o D:\Downloads # custom output dir for this download
snaglite <url> --audio-only # extract audio only (m4a)
snaglite <url> -f "bv[height<=1080]+ba" # custom format spec (yt-dlp syntax)
snaglite <url> --no-aria2 # use yt-dlp's native downloader instead| Command | Description |
|---|---|
snaglite <url> |
Download URL with defaults. Same as snaglite get <url>. |
snaglite get <url> [opts] |
Explicit form. |
snaglite update |
Re-pull latest yt-dlp.exe. |
snaglite where |
Show resolved tool paths and current output dir. |
snaglite config show |
Print saved config. |
snaglite config set-output <PATH> |
Persist a new default output directory. |
| Flag | Description |
|---|---|
-o, --output <PATH> |
One-shot output directory override. |
--audio-only |
Audio-only (m4a). |
-f, --format <SPEC> |
Custom yt-dlp format selector. |
--no-aria2 |
Disable aria2c, fall back to yt-dlp native downloader. |
| Setting | Value |
|---|---|
| Output dir | %USERPROFILE%\Videos\SnagLite (override with --output or snaglite config set-output) |
| Format | bv*+ba/b, merged to .mp4 |
| Filename | <title> [<id>].mp4 (sanitized) |
| Downloader | aria2c -x16 -s16 -k1M |
| Purpose | Location |
|---|---|
| Helper binaries | %LOCALAPPDATA%\SnagLite\bin |
| Config | %APPDATA%\SnagLite\config.json |
| Default downloads | %USERPROFILE%\Videos\SnagLite |
Unsupported URL— auto-retries once with the generic extractor before surfacing the error.- YouTube 403 / sign-in error — auto-recovers: silently re-updates yt-dlp, then retries once with a fallback
player_clientchain (web_safari,android_vr,mweb) and yt-dlp's native HTTP downloader (skipping aria2c, which can amplify googlevideo URL drift). Persistent failure usually means yt-dlp itself needs a manualsnaglite update; cookies / sign-in are out of scope for the CLI by design (see Android for the WebView sign-in fallback). - Geo-blocked — yt-dlp's
--geo-bypassis on by default; if a site requires actual VPN-level bypass it will still fail. Use a VPN. - Slow downloads — confirm aria2c is in use via
snaglite where. Pass--no-aria2only if a CDN throttles parallel connections. - Tool download fails — corporate proxy, firewall, or GitHub rate limit. Manually drop
yt-dlp.exe,ffmpeg.exe, andaria2c.exeinto%LOCALAPPDATA%\SnagLite\bin.
- Cookies / login-gated content (no
--cookies-from-browserpassthrough yet). - Playlists / batch input files.
- Subtitles.
- Cross-platform (win-x64 only).
Kotlin + Jetpack Compose APK. Wraps the same yt-dlp + ffmpeg + aria2c engine via the youtubedl-android library, with native resolvers bolted on for embed hosts the generic extractor can't crack.
- Paste any video URL, tap Download — saves a merged
.mp4(or.m4aaudio-only) toMovies/SnagLite(orMusic/SnagLite) via scoped-storage MediaStore. - Multi-download queue — up to 3 downloads run in parallel; extra submissions wait as
Queuedand are promoted automatically as slots free. Each download card shows a 16:9 thumbnail (fetched lazily from yt-dlp metadata) with an overlaid progress ring while running, plus the title, uploader, duration, file size, and a thin linear progress bar with the latest speed/ETA line. Swipe a card to remove it (with an optional delete-from-device checkbox). - Pause / resume — tap the pause icon on any active download. yt-dlp picks up the
.partfile on resume; direct-host downloads from the custom resolver pipeline restart from byte 0 (no per-chunk progress is persisted). - One-time setup screen on first launch:
- Extracts
yt-dlp/ffmpeg/aria2cfrom APK assets to internal storage. - Auto-updates
yt-dlpto latest stable. - Primes a YouTube session in a hidden WebView (harvests
visitor_data+ cookies). - Requests
POST_NOTIFICATIONSpermission.
- Extracts
- Subsequent launches: instant. Background re-check of
yt-dlpweekly. - Live progress bar on the download card (percent + last yt-dlp/aria2c stdout line).
- Foreground service keeps long downloads alive when the app is backgrounded.
- Background reliability — SnagLite holds a
PARTIAL_WAKE_LOCKso the CPU stays awake while downloads run, and aWIFI_MODE_FULL_HIGH_PERFWifiLockso the Wi-Fi radio doesn't sleep mid-stream when the screen is off. On first launch the app prompts to add itself to the battery-optimization exemption list — this prevents doze-mode throttling on Android 6+ and is what keeps a long download going while the phone is locked. - No telemetry. No accounts.
- All yt-dlp built-in extractors (~1800 sites) — same list as the CLI.
- Native resolvers — a small pipeline of generic resolvers bypasses yt-dlp entirely for a handful of common iframe-wrapper hosts the bundled extractors don't handle. The resolvers download via OkHttp + 8-chunk parallel
RangeGETs. - Iframe wrappers — a few listing-page hosts are auto-unwrapped before resolution so the underlying provider host is what reaches the resolver pipeline.
YouTube actively blocks anonymous requests. SnagLite handles this transparently — the user never sees a sign-in prompt unless a specific video genuinely needs an account.
- Silent background yt-dlp updates — on every cold launch (wifi-only, 24 h-debounced) the app checks for a newer
yt-dlpand installs it in the background. No dialog. The existing binary keeps serving downloads until the swap completes. - Per-request injection of
--user-agent,--cookies,--add-header Accept-Language, and--extractor-args youtube:player_client=tv_simply,default,mweb;formats=missing_pot;visitor_data=…(harvested via hidden WebView during setup, refreshed automatically on failure). - Auto-recover on 403 / bot / sign-in error — silently re-runs
YouTubeUpdater.updateNow()+YouTubeBootstrapper.harvest(), then retries once with the fallbackplayer_clientchain (web_safari,android_vr,mweb) and yt-dlp's native HTTP downloader (skipping aria2c on the retry — it can amplify googlevideo URL drift). - CTAs on the error card — only if recovery still fails. Update engine & retry for 403-class failures, Sign in to YouTube for genuine account-required errors (members-only, age-restricted, "Sign in to confirm…"). Sign-in opens a full-screen WebView at Google login; on success SnagLite exports the cookies and auto-retries.
Most public videos work without any user action. The sign-in surface only appears on the rare videos that actually need a logged-in YouTube account.
Pre-built release-signed APKs are attached to each entry on the Releases page — pick the variant for your device and sideload. To build locally instead, build artifacts land in android/app/build/outputs/apk/:
| APK | Use case |
|---|---|
app-arm64-v8a-{debug,release}.apk |
64-bit modern devices (recommended) |
app-armeabi-v7a-{debug,release}.apk |
32-bit / older devices |
app-x86_64-{debug,release}.apk |
Emulators / x86_64 Chromebooks |
app-universal-{debug,release}.apk |
All ABIs in one APK (~150 MB) — sideload-friendly fallback |
Sideload via adb install or transfer + install.
cd android
.\gradlew.bat :app:assembleDebug # debug APKs in app/build/outputs/apk/debug/
.\gradlew.bat :app:assembleRelease # release APKs in app/build/outputs/apk/release/The release build is signed with a release keystore if four env vars are set; otherwise it falls back to the local debug keystore (with a Gradle warning). R8 + resource shrinking are enabled for release; keep rules live in android/app/proguard-rules.pro.
| Env var | Purpose |
|---|---|
SNAGLITE_KEYSTORE_PATH |
Absolute path to .jks / .keystore. |
SNAGLITE_KEYSTORE_PASS |
Store password. |
SNAGLITE_KEY_ALIAS |
Key alias. |
SNAGLITE_KEY_PASS |
Key password. |
Generate a keystore once with keytool -genkeypair -v -keystore $HOME\snaglite-release.jks -keyalg RSA -keysize 2048 -validity 36500 -alias snaglite.
Requires JDK 17 + Android SDK (API 34).
Tap the gear icon on the main screen.
| Action | Effect |
|---|---|
| Update download engine | Force-pulls latest stable yt-dlp. Normally not needed — the app updates silently on launch. |
| Re-run initial setup | Wipes setup flag; returns to first-launch SetupScreen. |
| Delete file from device when removing | Toggles the default state of the "also delete file" checkbox in the swipe-to-remove dialog. Per-swipe override always available. |
Sign-in to YouTube isn't surfaced in Settings — it's offered automatically (as an error-card CTA) only on the rare videos that need an account.
| Setting | Value |
|---|---|
| Output dir | Movies/SnagLite (video) or Music/SnagLite (audio-only), via MediaStore |
| Format | bv*+ba/b, merged to .mp4 (video) / ba/b extracted to .m4a (audio) |
| Filename | `<title> [].(mp4 |
| Downloader (yt-dlp path) | aria2c -x16 -s16 -k1M |
| Downloader (resolver path) | OkHttp 8-chunk parallel Range GETs |
| Cache dir | <cacheDir>/snaglite-dl/<download-id>/ (per-download subdir; kept across pause/resume, removed when the item is dismissed) |
| Concurrency | 3 active downloads; extras queued |
| Purpose | Location |
|---|---|
| Native binaries | /data/data/com.patron.snaglite/files/ (libpython, ffmpeg, aria2c, yt-dlp.zip) |
| YouTube cookies | <filesDir>/yt-cookies.txt (Netscape format) |
| Prefs | getSharedPreferences("snaglite_yt", MODE_PRIVATE) + getSharedPreferences("snaglite_app", MODE_PRIVATE) |
INTERNET, ACCESS_NETWORK_STATE, POST_NOTIFICATIONS (Android 13+, requested at first-launch setup), FOREGROUND_SERVICE + FOREGROUND_SERVICE_DATA_SYNC, WAKE_LOCK, ACCESS_WIFI_STATE (for the high-perf Wi-Fi lock during downloads), REQUEST_IGNORE_BATTERY_OPTIMIZATIONS (prompted on first launch — see Background reliability above), WRITE_EXTERNAL_STORAGE (Android ≤9 only). No storage permission needed on Android 10+ (scoped storage / MediaStore).
- YouTube 403 Forbidden — extractor staleness vs. current YouTube signature/n-param scheme. The app silently updates
yt-dlpon every cold launch (wifi-only, daily debounce). On a 403 it also auto-retries with avisitor_datare-harvest + fallbackplayer_clientchain. If both still fail, the error card shows Update engine & retry; if that fails too, hit Settings → Update download engine and retry. - YouTube fails with "Please sign in" / 400 errors — primary + fallback
player_clientchains both regressed. Settings → Update download engine, then retry. If a video genuinely needs an account (age-restricted / members-only / "Sign in to confirm…"), the error card surfaces a Sign in to YouTube CTA. Unsupported URL— auto-retries with--force-generic-extractor. If still fails, the embed host has no resolver yet — open an issue with the URL.Couldn't extract video from <host>— host changed its template. Pulladb logcat -s ResolverA ResolverB ResolverC Packer DirectDownloaderand report the URL. Resolver patterns live underandroid/app/src/main/java/com/patron/snaglite/download/resolvers/and updates land in minutes.- First-launch setup hangs at "Updating yt-dlp" — slow network or GitHub throttling. Failure is non-fatal; the bundled yt-dlp will still work. Retry from Settings later.
- Saved file not visible in gallery — MediaStore indexes asynchronously; takes a few seconds. Pull to refresh in the gallery app.
- App "keeps stopping" on launch — the uncaught-exception handler writes a full stack trace to
Android/data/com.patron.snaglite/files/last_crash.txt. Pull that file via any file manager (Files by Google, the system Files app, USB MTP) and share it; that's the fastest path to a fix.
Releases (snaglite.exe + 4 debug APKs, attached to a tag at https://github.com/corecompiled/SnagLite/releases) are cut by a manually-triggered GitHub Actions workflow. There is no auto-publish on tag push — every release is an explicit decision.
- Make sure
mainbuilds clean locally:dotnet publish src/SnagLite/SnagLite.csproj -c Release -r win-x64 --self-contained -o publish cd android; .\gradlew.bat :app:assembleDebug; cd ..
- Push everything you want in the release to
main(or whichever branch you'll release from). - Open https://github.com/corecompiled/SnagLite/actions/workflows/release.yml in a browser.
- Click the grey Run workflow dropdown (top right).
- Fill in:
- Branch — usually
main. The chosen commit is what the tag attaches to. - Release tag — semver with a
vprefix, e.g.v0.1.0. Must not already exist. - Mark as pre-release? — tick for alpha / beta, leave unticked for stable.
- Release notes — leave blank to auto-generate from commits since the previous tag, or paste your own Markdown.
- Branch — usually
- Click the green Run workflow button.
- Wait 5–10 minutes. The workflow runs three jobs in order:
build-winandbuild-androidin parallel, thenpublish. - When it goes green, the release appears at https://github.com/corecompiled/SnagLite/releases with 5 attached assets:
snaglite-<tag>-android-arm64-v8a-release.apksnaglite-<tag>-android-armeabi-v7a-release.apksnaglite-<tag>-android-universal-release.apksnaglite-<tag>-android-x86_64-release.apksnaglite-<tag>-windows-x64.exe
To delete a release (e.g. typo in the tag): on the Releases page, click the release title → trash icon. Then on the Tags page, delete the matching tag — otherwise re-running the workflow with the same tag fails with a tag-already-exists error.
APKs are release-signed with a stable certificate, so in-place upgrades work across releases going forward. Anyone who has an older debug-signed APK installed (from a release cut before the signing flip) must uninstall it first — Android will refuse to upgrade across a signature change with
INSTALL_FAILED_UPDATE_INCOMPATIBLE. One-time pain; subsequent upgrades are seamless.
The project was previously called Snag; the rebrand to SnagLite is purely a naming change — engine, features, and supported sites are unchanged.
- CLI users (Windows): on first run of
snaglite.exe, the binary auto-moves your existing%APPDATA%\Snag,%LOCALAPPDATA%\Snag, and%USERPROFILE%\Videos\Snagdirectories to the newSnagLitenames. No manual action needed; if the new directories already exist the move is skipped (idempotent). - Android users: the
applicationIdchanged fromcom.patron.snagtocom.patron.snaglite, so the new APK installs alongside the old app rather than upgrading it. Uninstall the oldSnagAPK after confirming the new one works. On first launch, SnagLite moves anyMovies/Snag/*andMusic/Snag/*MediaStore rows it can update intoMovies/SnagLite/andMusic/SnagLite/(best-effort — rows owned by the old package may stay in place; this is harmless). Existing app preferences and the YouTube sign-in cookie are not carried over — you'll see the first-launch setup screen and battery-optimization prompt again.
- Playlists / batch input files.
- Subtitles.
- Sharing intent (
Send to → SnagLitefrom YouTube/etc.). - Persistence of the download list across process death (in-memory only; queued/paused items are lost if the OS kills the process).