Skip to content

corecompiled/SnagLite

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SnagLite

Paste a URL. Get the file. Highest available quality, fastest sane speed, zero setup.

.NET CLI build Android debug build Windows Android .NET 10 Kotlin

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.

Quick start

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=dQw4w9WgXcQ

First 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+.

What makes it different

  • Genuinely zero-config. No accounts. No tokens. No "configure your downloader" wizard. The first snaglite <url> works.
  • Fast by default. aria2c -x16 -s16 -k1M pulls 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-dlp on a 7-day debounce per cold launch (yt-dlp ships extractor fixes daily). Manual snaglite update is 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.


Reference


SnagLite CLI (Windows)

What it does

  • Takes any URL, downloads the best video+audio merged into a single .mp4.
  • Auto-installs yt-dlp.exe, ffmpeg.exe, aria2c.exe on first run — no manual setup, no PATH wrangling.
  • Uses aria2c with 16 connections for fast parallel chunk downloading.
  • Live progress bar in the terminal (percent, speed, ETA).
  • No prompts, no auth, no tokens, no telemetry.

Supported sites

  • All yt-dlp built-in extractorsyoutube.com and ~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.

Install

  1. Grab snaglite.exe from the Releases page, or from publish/ after running the publish command below.
  2. Drop it anywhere on your PATH (e.g. C:\Tools\snaglite.exe).
  3. First invocation auto-downloads helper binaries to %LOCALAPPDATA%\SnagLite\bin.

Build from source

dotnet publish src/SnagLite/SnagLite.csproj -c Release -r win-x64 --self-contained -o publish

Produces publish/snaglite.exe (single-file, .NET runtime embedded).

Usage

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

Commands

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.

Options (for get / default)

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.

Defaults

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

Paths

Purpose Location
Helper binaries %LOCALAPPDATA%\SnagLite\bin
Config %APPDATA%\SnagLite\config.json
Default downloads %USERPROFILE%\Videos\SnagLite

Troubleshooting

  • 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_client chain (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 manual snaglite 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-bypass is 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-aria2 only if a CDN throttles parallel connections.
  • Tool download fails — corporate proxy, firewall, or GitHub rate limit. Manually drop yt-dlp.exe, ffmpeg.exe, and aria2c.exe into %LOCALAPPDATA%\SnagLite\bin.

Out of scope (CLI)

  • Cookies / login-gated content (no --cookies-from-browser passthrough yet).
  • Playlists / batch input files.
  • Subtitles.
  • Cross-platform (win-x64 only).

SnagLite Android

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.

What it does

  • Paste any video URL, tap Download — saves a merged .mp4 (or .m4a audio-only) to Movies/SnagLite (or Music/SnagLite) via scoped-storage MediaStore.
  • Multi-download queue — up to 3 downloads run in parallel; extra submissions wait as Queued and 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 .part file 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:
    1. Extracts yt-dlp / ffmpeg / aria2c from APK assets to internal storage.
    2. Auto-updates yt-dlp to latest stable.
    3. Primes a YouTube session in a hidden WebView (harvests visitor_data + cookies).
    4. Requests POST_NOTIFICATIONS permission.
  • Subsequent launches: instant. Background re-check of yt-dlp weekly.
  • 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_LOCK so the CPU stays awake while downloads run, and a WIFI_MODE_FULL_HIGH_PERF WifiLock so 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.

Supported sites

  • 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 Range GETs.
  • Iframe wrappers — a few listing-page hosts are auto-unwrapped before resolution so the underlying provider host is what reaches the resolver pipeline.

YouTube auth (automatic)

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.

  1. Silent background yt-dlp updates — on every cold launch (wifi-only, 24 h-debounced) the app checks for a newer yt-dlp and installs it in the background. No dialog. The existing binary keeps serving downloads until the swap completes.
  2. 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).
  3. Auto-recover on 403 / bot / sign-in error — silently re-runs YouTubeUpdater.updateNow() + YouTubeBootstrapper.harvest(), then retries once with the fallback player_client chain (web_safari,android_vr,mweb) and yt-dlp's native HTTP downloader (skipping aria2c on the retry — it can amplify googlevideo URL drift).
  4. 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.

Install

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.

Build from source

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).

Settings

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.

Defaults

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

Paths (on-device)

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)

Permissions

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).

Troubleshooting

  • YouTube 403 Forbidden — extractor staleness vs. current YouTube signature/n-param scheme. The app silently updates yt-dlp on every cold launch (wifi-only, daily debounce). On a 403 it also auto-retries with a visitor_data re-harvest + fallback player_client chain. 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_client chains 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. Pull adb logcat -s ResolverA ResolverB ResolverC Packer DirectDownloader and report the URL. Resolver patterns live under android/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.

Cutting a release

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.

  1. Make sure main builds clean locally:
    dotnet publish src/SnagLite/SnagLite.csproj -c Release -r win-x64 --self-contained -o publish
    cd android; .\gradlew.bat :app:assembleDebug; cd ..
  2. Push everything you want in the release to main (or whichever branch you'll release from).
  3. Open https://github.com/corecompiled/SnagLite/actions/workflows/release.yml in a browser.
  4. Click the grey Run workflow dropdown (top right).
  5. Fill in:
    • Branch — usually main. The chosen commit is what the tag attaches to.
    • Release tag — semver with a v prefix, 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.
  6. Click the green Run workflow button.
  7. Wait 5–10 minutes. The workflow runs three jobs in order: build-win and build-android in parallel, then publish.
  8. When it goes green, the release appears at https://github.com/corecompiled/SnagLite/releases with 5 attached assets:
    • snaglite-<tag>-android-arm64-v8a-release.apk
    • snaglite-<tag>-android-armeabi-v7a-release.apk
    • snaglite-<tag>-android-universal-release.apk
    • snaglite-<tag>-android-x86_64-release.apk
    • snaglite-<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.


Migrating from Snag

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\Snag directories to the new SnagLite names. No manual action needed; if the new directories already exist the move is skipped (idempotent).
  • Android users: the applicationId changed from com.patron.snag to com.patron.snaglite, so the new APK installs alongside the old app rather than upgrading it. Uninstall the old Snag APK after confirming the new one works. On first launch, SnagLite moves any Movies/Snag/* and Music/Snag/* MediaStore rows it can update into Movies/SnagLite/ and Music/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.

Out of scope (Android)

  • Playlists / batch input files.
  • Subtitles.
  • Sharing intent (Send to → SnagLite from YouTube/etc.).
  • Persistence of the download list across process death (in-memory only; queued/paused items are lost if the OS kills the process).

About

Zero-friction video downloader for Windows and Android.

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors