_ _ _ _
| | | | | | |
| |_| |_ _____ | | |_ ____ ____ ___ ___ _____ _ _ ___
|_ _) ___ | | | | _ \ / ___) _ \/ __|/ ___ | | |/ _ \
| |_| ____|| | | | |_) ) | | | __/\__ \ (___| |_| __/
\__)_____) \_)_) __/ |_| \___||___/\____)____/\___|
|_| v2.2 - Battle Tested
YouTube broke downloads in two ways:
Problem 1 π SABR streaming (2024~) β forces 360p regardless of quality setting
Problem 2 π« Bot detection (2025~) β blocks datacenter/cloud IPs entirely
This repo fixes both. Battle-tested in production serving real users.
β‘ Quick Fix Β β’Β π€ Bot Bypass Β β’Β βοΈ Cloud Deploy Β β’Β π Commands Β β’Β πΎ Config Β β’Β π οΈ Troubleshoot
Every quality = same 9MB file (360p) π |
Each quality = correct file size π |
Tested with the same video, same URL, different quality settings:
Quality File Size Visualization
------- --------- ----------------------------------------
360p 9 MB ##
480p 15 MB ####
720p 21 MB ######
1080p 34 MB #########
4K 244 MB ##################################
Before fix: ALL qualities -> 9 MB (360p) [BROKEN]
After fix: Each quality -> correct size [FIXED]
Copy-paste this. It just works.
yt-dlp \
--extractor-args "youtube:player_client=tv,web_embedded;player_skip=webpage" \
-f "bestvideo*+bestaudio/best" \
-S "res:1080" \
--force-ipv4 \
--merge-output-format mp4 \
"https://youtube.com/watch?v=VIDEO_ID"| Flag | What it does |
|---|---|
player_client=tv,web_embedded |
Most reliable combo β bypasses SABR, gets full format list (144p~4K) |
player_skip=webpage |
Skips webpage request β fewer HTTP calls = less rate limiting |
--force-ipv4 |
Prevents IPv6 routing issues on cloud servers |
-S "res:1080" |
Picks best format matching your target resolution |
-f "bestvideo*+bestaudio/best" |
The * adds progressive fallback for reliability |
π‘ That's literally it. The rest of this README covers bot detection, cloud deployment, and advanced usage.
π Click to expand the full technical explanation
Starting in late 2024, YouTube forced SABR (Server ABR) on the web client:
+---------------------------------------------+
| YouTube's SABR Change |
+---------------------------------------------+
| |
| Before (2024): |
| web client -> 20+ DASH formats |
| (144p, 240p, ... 4K) |
| |
| After (2025+): |
| web client -> 1 progressive format |
| (360p only!) |
| |
+---------------------------------------------+
+------------------+ +------------------+
| --dump-json | | Actual Download |
| says: 137 | ==X==> | 137 not found! |
| available | | (SABR replaced) |
+------------------+ +------------------+
- IDs CHANGE between requests
- YouTube A/B tests swap formats
- SABR may list but block downloads
Solution: Use -S (sort) instead β yt-dlp picks the best match at download time.
| Client | Status | Quality Range | Token Needed | Notes |
|---|---|---|---|---|
web (default) |
β SABR only | 360p | PO Token | Broken for quality selection |
android_vr |
β Full DASH | 144p ~ 4K | None | May intermittently return 360p only |
tv_downgraded |
β Full DASH | 144p ~ 4K | None | Most reliable fallback |
web_creator |
β Full DASH | 144p ~ 4K | None | YouTube Studio client |
mweb |
β Full DASH | 144p ~ 4K | None | Mobile web client |
web_embedded |
β Full DASH | 144p ~ 4K | None | Embedded player client |
By combining multiple clients, yt-dlp gets the full format catalog from whichever responds.
π¨ New in 2025-2026: YouTube aggressively blocks datacenter IPs and suspicious requests with "Sign in to confirm you're not a bot" errors.
If one client gets blocked, rotate to another:
# Primary strategy
yt-dlp --extractor-args "youtube:player_client=web,android_vr,tv_downgraded" URL
# Blocked? Try these alternatives in order:
yt-dlp --extractor-args "youtube:player_client=tv_downgraded,web,android_vr" URL
yt-dlp --extractor-args "youtube:player_client=android_vr,tv_downgraded,web" URL
yt-dlp --extractor-args "youtube:player_client=web_creator,tv_downgraded,android_vr" URL
yt-dlp --extractor-args "youtube:player_client=mweb,tv_downgraded,web" URLπ― This is the #1 fix that actually solved our production issues. Without proper PO Token setup, datacenter servers get blocked even with client rotation.
PO Tokens (Proof-of-Origin) prove to YouTube that you're not a bot. There are two modes:
Mode A: Plugin Only (for personal use)
# Install the PO Token provider plugin
pip install bgutil-ytdlp-pot-provider
# Use with yt-dlp (Node.js required)
yt-dlp --js-runtimes node \
--remote-components ejs:github \
--extractor-args "youtube:player_client=web,android_vr,tv_downgraded" \
URLMode B: Dedicated PO Token Server (for cloud/production - RECOMMENDED)
# 1. Clone and build the PO Token server
git clone https://github.com/Brainicism/bgutil-ytdlp-pot-provider.git
cd bgutil-ytdlp-pot-provider/server
npm ci && npx tsc
# 2. Start the server (runs on port 4416)
node build/main.js &
# 3. CRITICAL: Set this env var so yt-dlp connects to the server
export YT_DLP_POT_PROVIDER_URL="http://127.0.0.1:4416"
# 4. Now yt-dlp automatically uses the PO Token server
yt-dlp --extractor-args "youtube:player_client=web,android_vr,tv_downgraded" URLπ¨ Common mistake: Running the PO Token server WITHOUT setting
YT_DLP_POT_PROVIDER_URL. The server runs fine but yt-dlp doesn't know it exists! This single env var was the fix that solved our production bot-blocking issues.
YouTube's challenge solver responses go stale:
# Clear cache when downloads start failing
yt-dlp --rm-cache-dir
# Then retry
yt-dlp --extractor-args "youtube:player_client=web,android_vr,tv_downgraded" URL# Auto-extract from browser (easiest)
yt-dlp --cookies-from-browser chrome URL
yt-dlp --cookies-from-browser firefox URL
# Or export manually with a browser extension
yt-dlp --cookies cookies.txt URL
β οΈ Cookies expire and need periodic re-export. The strategies above are preferred for automation.
Running yt-dlp on Railway, Heroku, AWS, GCP, or any cloud server? YouTube blocks datacenter IPs at the network level. Here's how to fix it permanently.
Your Server (Datacenter IP)
|
v
+-------------------+
| YouTube |
| |
| IP Reputation DB |-----> BLOCKED
| "Datacenter IP |
| detected" |
+-------------------+
Route yt-dlp through Cloudflare's network. YouTube doesn't block Cloudflare IPs.
Using wireproxy (userspace WireGuard, no root/NET_ADMIN needed):
# 1. Install wgcf (WARP credential generator)
curl -fsSL -o /usr/local/bin/wgcf \
"https://github.com/ViRb3/wgcf/releases/download/v2.2.22/wgcf_2.2.22_linux_amd64"
chmod +x /usr/local/bin/wgcf
# 2. Register free WARP account + generate WireGuard config
wgcf register --accept-tos
wgcf generate
# 3. Download wireproxy
# https://github.com/pufferffish/wireproxy/releases
# 4. Add SOCKS5 section to config
cat >> wgcf-profile.conf << 'EOF'
[Socks5]
BindAddress = 127.0.0.1:1080
EOF
# 5. Start proxy + use with yt-dlp
wireproxy -c wgcf-profile.conf &
yt-dlp --proxy socks5://127.0.0.1:1080 URLUsing Docker (one-liner):
# Pre-built WARP proxy images
docker run -d -p 1080:1080 ghcr.io/kingcc/warproxy:latest
# or
docker run -d -p 9091:9091 ghcr.io/mon-ius/docker-warp-socks:v5
# Then use with yt-dlp
yt-dlp --proxy socks5://127.0.0.1:1080 URLThis is the exact architecture running in production, serving real users worldwide.
+-------------------------------------------------------+
| Docker Container |
| |
| +------------------+ +--------------------+ |
| | bgutil PO Token |--->| HTTP :4416 | |
| | Server (Node.js) | | Auto-generates | |
| | | | YouTube auth tokens | |
| +------------------+ +--------------------+ |
| | | |
| | YT_DLP_POT_PROVIDER_URL | |
| | = http://127.0.0.1:4416 | |
| v v |
| +------------------+ +--------------------+ |
| | yt-dlp |--->| 5 client strategies| |
| | + PO Token plugin| | + free proxy pool | |
| +------------------+ +--------------------+ |
| | |
| +------------------+ +--------------------+ |
| | Flask / Gunicorn |--->| HTTP :8080 | |
| | | | Web Interface | |
| +------------------+ +--------------------+ |
| |
+-------------------------------------------------------+
π‘ The critical connection:
YT_DLP_POT_PROVIDER_URLenv var links yt-dlp to the PO Token server. Without it, the server runs but yt-dlp ignores it.
6-Layer Defense System:
| Layer | Defense | Description |
|---|---|---|
| 1 | YouTube oEmbed API | Gets video metadata (title, thumbnail, author) without yt-dlp β never blocked, unlimited |
| 2 | PO Token Server | bgutil generates auth tokens + YT_DLP_POT_PROVIDER_URL connects to yt-dlp |
| 3 | Client Rotation | 4 player_client strategies (tv,web_embedded first), auto-fallback on failure |
| 4 | Request Serialization | Semaphore prevents parallel YouTube API calls + 10-min video info cache |
| 5 | Free Proxy Pool | Auto-fetches proxies from ProxyScrape as last resort |
| 6 | --force-ipv4 + player_skip=webpage |
Reduces HTTP calls to YouTube, prevents IPv6 routing issues |
π¨ If you're building a web app, don't use yt-dlp for video metadata. YouTube rate-limits datacenter IPs after just 1-2 requests. Use the free, unlimited oEmbed API instead:
import urllib.request, json
def get_video_info(youtube_url):
"""Get title, author, thumbnail β never blocked by YouTube."""
oembed = f"https://www.youtube.com/oembed?url={youtube_url}&format=json"
with urllib.request.urlopen(oembed, timeout=10) as r:
return json.loads(r.read())
# Returns: {"title": "...", "author_name": "...", "thumbnail_url": "...", ...}
# Use yt-dlp ONLY for the actual download, not for metadata.This single change eliminated 90% of our "temporarily blocking requests" errors in production.
# Just change the number after "res:" !
# 4K (best available)
yt-dlp --extractor-args "youtube:player_client=web,android_vr,tv_downgraded" \
-f "bestvideo*+bestaudio/best" --merge-output-format mp4 "URL"
# 1080p (Full HD)
yt-dlp --extractor-args "youtube:player_client=web,android_vr,tv_downgraded" \
-f "bestvideo*+bestaudio/best" -S "res:1080" --merge-output-format mp4 "URL"
# 720p (HD)
yt-dlp --extractor-args "youtube:player_client=web,android_vr,tv_downgraded" \
-f "bestvideo*+bestaudio/best" -S "res:720" --merge-output-format mp4 "URL"
# 480p (SD)
yt-dlp --extractor-args "youtube:player_client=web,android_vr,tv_downgraded" \
-f "bestvideo*+bestaudio/best" -S "res:480" --merge-output-format mp4 "URL"
# 360p (Low)
yt-dlp --extractor-args "youtube:player_client=web,android_vr,tv_downgraded" \
-f "bestvideo*+bestaudio/best" -S "res:360" --merge-output-format mp4 "URL"# MP3 (192kbps - standard)
yt-dlp --extractor-args "youtube:player_client=web,android_vr,tv_downgraded" \
-x --audio-format mp3 --audio-quality 192 "URL"
# MP3 (320kbps - highest quality)
yt-dlp --extractor-args "youtube:player_client=web,android_vr,tv_downgraded" \
-x --audio-format mp3 --audio-quality 320 "URL"
# FLAC (lossless)
yt-dlp --extractor-args "youtube:player_client=web,android_vr,tv_downgraded" \
-x --audio-format flac "URL"yt-dlp --extractor-args "youtube:player_client=web,android_vr,tv_downgraded" -F "URL"yt-dlp \
--extractor-args "youtube:player_client=web,android_vr,tv_downgraded" \
--js-runtimes node \
--remote-components ejs:github \
--rm-cache-dir \
-f "bestvideo*+bestaudio/best" \
-S "res:1080" \
--merge-output-format mp4 \
--retries 3 \
--fragment-retries 3 \
"URL"Tired of typing long commands? Save this once, use forever.
Create ~/.config/yt-dlp/config:
# yt-dlp-rescue config v2.0 (March 2026)
# https://github.com/bravomylife-lab/yt-dlp-rescue
# Fix YouTube SABR quality selection
--extractor-args youtube:player_client=web,android_vr,tv_downgraded
# Best video + audio with progressive fallback
-f bestvideo*+bestaudio/best
# Output format
--merge-output-format mp4
# Bot detection bypass: PO Token auto-generation
# Requires: pip install bgutil-ytdlp-pot-provider
# Requires: Node.js installed
# --js-runtimes node
# --remote-components ejs:github
# Reliability settings
--retries 3
--fragment-retries 3
--socket-timeout 30Now just run:
yt-dlp -S "res:1080" "URL" # That's it!π€ Still getting 360p
# 1. Update yt-dlp to latest
pip install -U yt-dlp
# 2. Clear stale cache
yt-dlp --rm-cache-dir
# 3. Try different client order
yt-dlp --extractor-args "youtube:player_client=tv_downgraded,web_creator,mweb" \
-f "bestvideo*+bestaudio/best" -S "res:1080" "URL"
# 4. Check what formats are actually available
yt-dlp --extractor-args "youtube:player_client=tv_downgraded" -F "URL"π€ "Sign in to confirm you're not a bot"
This means YouTube flagged your IP. Try in order:
# 1. Clear cache + different client
yt-dlp --rm-cache-dir
yt-dlp --extractor-args "youtube:player_client=tv_downgraded,web,android_vr" "URL"
# 2. Enable PO Token generation (install once)
pip install bgutil-ytdlp-pot-provider
yt-dlp --js-runtimes node --remote-components ejs:github "URL"
# 3. Use WARP proxy (if on cloud/datacenter)
yt-dlp --proxy socks5://127.0.0.1:1080 "URL"
# 4. Last resort: browser cookies
yt-dlp --cookies-from-browser chrome "URL"π« "HTTP Error 403: Forbidden"
# Your IP is blocked. Rotate user agent + client:
yt-dlp --user-agent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
--extractor-args "youtube:player_client=tv_downgraded,web_creator" "URL"
# If on cloud server, use WARP proxy (see Cloud Deployment section)π§ ffmpeg not found
# macOS
brew install ffmpeg
# Ubuntu / Debian
sudo apt install ffmpeg
# Windows
winget install ffmpeg
# Verify
ffmpeg -versionWithout ffmpeg, you're limited to progressive formats. Install ffmpeg for the full quality range.
π‘ What does the * in bestvideo* mean?
-f "bestvideo*+bestaudio/best"
^
+-- This asterisk = safety net!
bestvideo -> video-only streams
bestvideo* -> video-only + progressive (video+audio) streams
If DASH merge fails, the * lets yt-dlp fall back to
a single-file format instead of failing completely.
π» Works locally, fails on my server
Your server has a datacenter IP which YouTube blocks at the network level. This cannot be fixed with cookies or PO Tokens alone.
β‘οΈ See the Cloud/Server Deployment section for the WARP proxy solution.
+------------------------------------------------------------------+
| yt-dlp-rescue v2.2 |
+------------------------------------------------------------------+
| |
| QUALITY FIX (SABR bypass): |
| --extractor-args |
| "youtube:player_client=tv,web_embedded;player_skip=webpage" |
| -f "bestvideo*+bestaudio/best" |
| -S "res:1080" --force-ipv4 |
| |
| BOT DETECTION FIX: |
| pip install bgutil-ytdlp-pot-provider |
| export YT_DLP_POT_PROVIDER_URL="http://127.0.0.1:4416" |
| |
| WEB APP METADATA (no rate limit): |
| youtube.com/oembed?url=VIDEO_URL&format=json |
| |
| WHEN THINGS BREAK: |
| yt-dlp --rm-cache-dir |
| pip install -U "yt-dlp[default] @ https://github.com/ |
| yt-dlp/yt-dlp-nightly-builds/releases/latest/...tar.gz" |
| |
+------------------------------------------------------------------+
| Version | Date | Changes |
|---|---|---|
| v2.2.0 | 2026-03-13 | oEmbed API for metadata, player_skip=webpage + --force-ipv4, request serialization + caching, yt-dlp nightly support |
| v2.1.0 | 2026-03-13 | PO Token server mode (YT_DLP_POT_PROVIDER_URL), free proxy auto-rotation, production-tested fix |
| v2.0.0 | 2026-03-13 | Bot detection bypass, cloud deployment guide, WARP proxy, 5-layer defense, client rotation |
| v1.0.0 | 2026-03-01 | Initial release: SABR quality fix, format sort, multi-client strategy |
YouTube changes things constantly. Found a better fix? Something stopped working?
Open an Issue Β /Β Send a PR
It helps others find this fix too.
Keywords: yt-dlp fix, youtube 360p fix, youtube sabr fix, yt-dlp quality fix, youtube download broken, yt-dlp bot detection, youtube sign in to confirm, yt-dlp cloud server, yt-dlp datacenter blocked, yt-dlp proxy, yt-dlp warp, youtube downloader fix 2025, youtube downloader fix 2026, yt-dlp player_client, yt-dlp format sort, yt-dlp rescue, youtube quality stuck 360p, youtube oembed api, yt-dlp temporarily blocking, youtube rate limit bypass, yt-dlp po token server, player_skip webpage, yt-dlp nightly, yt-dlp railway deploy, youtube bot bypass free
Made with determination by @bravomylife-lab