yt-dlp-git and curl-cffi on Termux: Why Stable Is Stale and Fingerprints Matter
Let me tell you how modern media downloading actually works in 2026. It is
not HTTP GET video.mp4, thank you, goodbye. It is an arms race.
On one side: yt-dlp, 2000+ extractors, a volunteer army reverse-engineering
JavaScript players before breakfast. On the other: YouTube, TikTok,
Instagram, Cloudflare, Akamai, DataDome, rotating ciphers, obfuscated
players, nsig throttling, PO tokens, and TLS fingerprinting that spots
your Python script from three packets away and says: no. Bot. Begone.
If you run the stable yt-dlp from your distro repo on a phone, you lose
twice. Once because it is old, once because it looks like a bot. So we
packaged the fix in the Termux repo: yt-dlp-git plus curl-cffi.
Repo: github.com/Twilight0/termux-repo Builder: github.com/Twilight0/curl-cffi-builder
curl -sL https://twilight0.github.io/termux-repo/install.sh | bash
pkg update
pkg install -y yt-dlp-git curl-cffi
Part 1: yt-dlp-git — why stable is not enough
yt-dlp stable releases roughly monthly. Websites hosting media change roughly hourly. You do the math.
What breaks, constantly:
- YouTube player JS: signature cipher (
sig),nsig(throttling bypass), SABR streaming, PO token requirements. Google pushes a newbase.jsand suddenly your month-old stable downloads at 50 KB/s, or getsSign in to confirm you're not a bot, or extracts no formats at all. Fix lands in git master within hours. Stable users wait weeks. - Extractors: TikTok changes API endpoints, Vimeo shuffles JSON,
Instagram requires new
dtoken, Twitch kills an endpoint, Bandcamp renames a field. Each fix is a 20-line extractor patch. Master gets dozens per week. - New features:
--impersonatetargets, new format sort options, SponsorBlock integration tweaks, subtitle fixes, chapter embedding. Stable is a snapshot. Master is the river.
Capabilities reminder, because people forget how absurdly powerful this
tool is: best-video+best-audio auto-merge, format selection
(-F, -f bv+ba/b), subtitles (--write-subs --sub-langs all),
thumbnails, chapters, metadata, SponsorBlock (--sponsorblock-remove),
playlists, archives (--download-archive so you never re-download),
cookies from browser, proxy / IPv4-IPv6 forcing, cache dir for client IDs
and signatures.
On Termux official repos, yt-dlp lags. You get a version from last month
trying to parse this morning’s YouTube. Our yt-dlp-git tracks git master
snapshots, rebuilt weekly via CI. When YouTube breaks on Tuesday night,
you pkg upgrade on Wednesday, not next month.
pkg install -y yt-dlp-git
yt-dlp --version
# 2026.09.xx not 2026.06.xx — that two-month gap is the difference
# between working and whining
Part 2: curl-cffi — optional, until it isn’t
Here is the dirty secret: setting User-Agent: Mozilla/5.0 ... Chrome
does almost nothing. Any half-serious WAF stopped checking User-Agent in
- What they check is how your TLS handshake looks on the wire.
Python’s requests, httpx, urllib3 have a distinct fingerprint:
- TLS version, cipher suite list and order (
4865-4866-4867...) - Extension IDs and order (
0-11-10-35-16-5...), GREASE, ALPN - Supported groups / curves, EC point formats, signature algorithms
- Certificate compression (brotli/zlib), ticket handling, key shares
- HTTP/2 SETTINGS frame (
1:65536;3:1000;4:6291456...), WINDOW_UPDATE, PRIORITY frames, pseudo-header order (:method :authority :scheme :path)
That concatenated blob is JA3 + Akamai HTTP/2 fingerprint. Chrome sends one pattern, Python sends another. Cloudflare sees Python JA3 asking for video segments and bins you: 403, challenge, infinite redirect, empty formats list. Your headers were perfect. Your handshake betrayed you.
curl-impersonate fixes this by patching curl + BoringSSL to byte-mimic
real browsers. curl_cffi brings that to Python via CFFI:
Python binding for curl-impersonate via cffi. Unlike pure-python clients, it can impersonate browsers’ TLS signatures / JA3 fingerprints.
yt-dlp wires it in as an optional dependency:
yt-dlp --list-impersonate-targets
gives you chrome, chrome-110, chrome124, chrome131_android,
edge99, safari15_3, safari17_0, firefox133, etc. Use latest
Chrome / Safari unless you have a reason not to. yt-dlp docs even warn:
forcing impersonation for all requests can hurt speed/stability, so
use it when the site needs it.
Python-side, same idea:
from curl_cffi import requests
r = requests.get(url, impersonate="chrome")
print(r.status_code)
Or low-level, if you enjoy pain:
from curl_cffi import Curl, CurlOpt
c = Curl()
c.setopt(CurlOpt.HTTP2_PSEUDO_HEADERS_ORDER, "masp")
Upstream curl_cffi ships wheels for desktop Linux / macOS / Windows
64-bit. What it does not ship: Android, iOS, 32-bit ARM, Kodi boxes.
Read the PyPI fine print: excluded from Unix zipimport binary,
x86 32-bit, musllinux_aarch64. If you are on a phone, upstream
pretends you don’t exist.
That is why curl-cffi in our repo matters, and why we built it even
for armeabi-v7a when upstream didn’t.
Part 3: how we built curl-cffi for armeabi-v7a anyway
Upstream logic is sound, from a desktop perspective: 32-bit ARM is dying,
Android wheels are hell, iOS wheels are double hell. Our logic is
different: the phones in drawers that we want as workstations are
32-bit ARM. An old Galaxy Tab, a cheap Xiaomi, a TV stick — armeabi-v7a
is still everywhere outside the flagship bubble. Zero-bloat means not
abandoning working hardware because CI is annoying.
So curl-cffi-builder does the annoying part on GitHub Actions:
- NDK toolchain: Android NDK,
armv7a-linux-androideabi21target (plusaarch64-linux-android,x86_64-linux-android, iOSarm64). Matrix builds:ubuntu-latestfor Android/Linux,macos-latestfor iOS,windows-latestfor Windows Kodi. - Native stack from source: BoringSSL (Chrome’s fork, not OpenSSL),
nghttp2, brotli, zstd, then curl-impersonate’s patched curl against
that stack. No system curl. No shortcuts. If one lib assumes 64-bit
long, the 32-bit build dies screaming — patch and retry. - cffi binding fixups: upstream
setup.pyassumes desktop tags, 64-bit pointers, prebuilt.sonames. We rewrite platform tags, fixlong vs int64struct layouts for ARMv7, force soft-float / NEON flags correctly, and produce importable wheels:curl-cffi-android-arm64-v8a.whl,curl-cffi-android-x86_64.whl,curl-cffi-ios-arm64.whl. - Kodi bonus: same pipeline assembles
script.module.curlcffi-*.zip— multi-arch Kodi module with dynamic path injection for Windows / Linux / Android TV / CoreELEC / macOS. Same fingerprint bypass your TV box enjoys. - Termux APT repack: for the Termux repo we don’t ship pip wheels
(pip would reject the platform tag). We ship native
.debs per Termux arch, includingarmeabi-v7a, with properDEBIAN/control,postinstthat drops the.sowhere Python finds it.pkg install curl-cffijust works, even on the 32-bit tablet everyone told you to throw away.
Is it officially supported upstream? No. Does it work? Yes. That’s the whole AliveOS thesis: support the hardware people actually have.
Part 4: download using impersonate — practical recipes
Install once:
pkg update
pkg install -y yt-dlp-git curl-cffi ffmpeg python
Check what your build can fake:
yt-dlp --list-impersonate-targets
Basic: let yt-dlp pick whatever works:
yt-dlp --impersonate="" \
-o "%(title)s [%(id)s].%(ext)s" \
"https://www.youtube.com/watch?v=XXXX"
Explicit Chrome, the workhorse for Cloudflare-fronted sites:
yt-dlp --impersonate chrome \
--merge-output-format mp4 \
-f "bv+ba/b" \
"URL"
Pinned version + OS when a site is picky (some WAFs validate the User-Agent matches the fingerprint generation):
yt-dlp --impersonate chrome-124:windows-10 \
--cookies-from-browser chrome \
-F "URL" # list formats first, then download
Audio-only archival, phone-friendly:
yt-dlp --impersonate safari \
-x --audio-format mp3 --audio-quality 0 \
--write-subs --sub-langs "en.*" \
--download-archive archive.txt \
"PLAYLIST_URL"
Debug when it still fails (it will, sometimes):
yt-dlp -v --impersonate chrome --list-formats "URL"
# -v shows JA3 / TLS errors, 403 vs empty formats,
# nsig / PO token warnings — read it before blaming us
Rules of thumb from the trenches:
- Start without
--impersonate. Add it when you get 403 / challenge /no formats foundon a site that works in real Chrome. - Don’t force impersonation globally if you don’t need it — upstream warns it can hurt speed/stability. Per-site, per-command.
- Update weekly.
pkg upgradepulls newyt-dlp-gitsnapshots. Yesterday’s extractor is today’s 404. - On 32-bit ARM (
armeabi-v7a), keep expectations honest: impersonation costs CPU (BoringSSL + HTTP/2). It works, but don’t parallelize-N 8on a 2016 tablet and wonder why it thermal-throttles. That is physics, not a bug. See our Fermi reclocking notes if you want to know how seriously we take thermals.
Why this belongs in AliveOS-on-Android
Phones are hostile territory: Bionic, seccomp, SELinux, no systemd, battery as bloat detector. The answer isn’t to port the whole desktop. It’s the Dory lesson again — port the workflow, not the furniture. See retiring Dory and AliveOS on Android.
yt-dlp-git + curl-cffi is exactly that: two small packages that turn
a consumption slab back into a tool. Archive lectures, mirror your own
videos, grab audio for offline listening, bypass the fingerprint wall
without a laptop.
Old hardware, new tricks. Zero bloat, full teeth.
pkg install -y yt-dlp-git curl-cffi
yt-dlp --impersonate chrome "URL"
Break it, file issues with device + Android version + uname -a +
verbose log. We can’t test every phone. You are the lab.
See Also
- Termux repo —
wrangler,muse-code,opencode,yt-dlp-git,curl-cffi, install scripts, CI, DNS guides. - curl-cffi-builder —
cross-compilation pipeline for Android / iOS wheels + Kodi
script.module.curlcffi. - AliveOS on Android: Termux, PRoot — the bigger picture: proot Arch, glibc swamp, why phones deserve better.
