
Youtube Downloader
- 907 installs
- 1.3k repo stars
- Updated August 4, 2026
- daymade/claude-code-skills
youtube-downloader is an agent playbook skill that runs repeatable yt-dlp download steps for developers who hit PO token, proxy, cookie, or zsh URL quoting failures.
About
youtube-downloader is an internal SOP skill from daymade/claude-code-skills that standardizes yt-dlp downloads inside AI agent shell workflows. The playbook enforces quoted YouTube URLs in zsh where ? triggers glob expansion, aligns HTTP_PROXY and HTTPS_PROXY across yt-dlp and PO Token providers, handles bot-check prompts with browser cookies, and starts PO Token providers—preferring Docker bgutil with browser fallback. A gitleaks security scan is documented in the skill source. Developers reach for youtube-downloader when automated video or audio extraction fails mid-agent-run and manual trial-and-error would waste turns on known failure modes.
- Eight-step internal SOP covering quoted URLs, proxy parity, cookie consent, and PO token client selection (web_safari vs
- PO Token setup with Docker bgutil preferred and browser WPC fallback when Docker is unavailable
- Explicit recovery paths for bot sign-in prompts, "Only images are available", SSL EOF, and fragment errors
- Requires HTTP_PROXY/HTTPS_PROXY/ALL_PROXY alignment across yt-dlp and token minting
- Security scan passed (gitleaks + pattern validation) per bundled ingest metadata
Youtube Downloader by the numbers
- 907 all-time installs (skills.sh)
- +53 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #101 of 550 CLI & Terminal skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/daymade/claude-code-skills --skill youtube-downloaderAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 907 |
|---|---|
| repo stars | ★ 1.3k |
| Security audit | 1 / 3 scanners passed |
| Last updated | August 4, 2026 |
| Repository | daymade/claude-code-skills ↗ |
Why does yt-dlp fail with bot checks or zsh glob errors?
Run a repeatable agent playbook for yt-dlp downloads when PO tokens, proxies, cookies, or zsh URL quoting would otherwise break the job.
Who is it for?
Developers automating YouTube downloads via yt-dlp inside zsh-based agent or CI shell sessions.
Skip if: Developers who only need one-off manual browser downloads without shell automation or PO token setup.
When should I use this skill?
yt-dlp returns bot-check errors, zsh glob failures on ? in URLs, or PO token provider connection errors.
What you get
Successful yt-dlp downloads with quoted URLs, active proxies, loaded cookies, and a running PO Token provider.
- Downloaded media files
- Documented retry steps for failed yt-dlp runs
Files
YouTube Downloader
Overview
Enable reliable video and audio downloads from YouTube and HLS streaming platforms (Mux, Vimeo, etc.) using yt-dlp and ffmpeg. This skill provides workflows for:
- YouTube downloads (up to 4K) using PO token providers or browser cookies
- HLS stream downloads with authentication headers
- Handling protected content and troubleshooting common download failures
Non-Technical User Experience (Default)
Assume the user is non-technical. Do not ask them to run commands. Execute everything yourself and report progress in plain language. Avoid mentioning tooling unless the user asks.
Default flow: 1. Ask for the URL (if not provided). 2. Fetch video metadata (title/uploader/duration/thumbnail) and confirm it matches the user's intent.
- If yt-dlp is blocked by “confirm you’re not a bot”, fall back to YouTube oEmbed for title/uploader/thumbnail (duration may be unknown).
3. Offer simple choices (video vs. audio-only, quality, subtitles, save location). 4. Proceed with sensible defaults if the user does not specify:
- Video download at best quality
- MP4 merged output
- Single video only (no playlists)
5. Download and report the final file path, file size, and resolution (if video).
Offer choices in user-friendly terms:
- “Download the video in best quality (default)”
- “Download audio only (MP3)”
- “Pick a quality: 1080p / 720p / 480p / 360p”
- “Include subtitles (if available)”
- “Save to the Downloads folder (default) or tell me another folder”
Always render the thumbnail when available:
- If metadata includes a thumbnail URL, include it using Markdown image syntax:
.
Ask before doing extra work:
- Confirm playlist downloads (can be large).
- Confirm installing/upgrading dependencies if missing.
- Ask before extracting browser cookies.
- If using cookies, never mention cookie counts or raw cookie details in user-facing responses. Say “used your Chrome login session”.
- If verification is required, automatically set up a local PO Token helper (no user actions). If Docker is missing or fails, do not attempt to install Docker—switch to the browser-based PO Token provider instead.
Legal/Safety reminder (brief):
- Proceed only if the user has the rights or permission to download the content.
Response template (use plain language, no commands):

Title: …
Channel: …
Duration: …
I can help you:
1) Download the video (best quality, MP4)
2) Download audio only (MP3)
3) Pick a specific quality (1080p/720p/480p/360p)
4) Include subtitles (if available)
Where should I save it? (Default: Downloads folder)If the user says “just download”:
- Proceed with defaults and confirm when the download finishes.
- If blocked by a 403, automatically set up the verification helper and retry.
Reliable Download SOP (Internal)
Follow this SOP to avoid common failures and confusion:
1. Quote URLs in shell commands (zsh treats ? as a glob). Example: 'https://www.youtube.com/watch?v=VIDEO_ID'. 2. Ensure proxy is active for both yt-dlp and PO Token providers (HTTP_PROXY/HTTPS_PROXY/ALL_PROXY). 3. If you see “Sign in to confirm you’re not a bot”, request permission and use browser cookies. Do not proceed without cookies. 4. Start a PO Token provider before downloading (fail fast if it cannot start).
- Use Docker bgutil provider when available.
- If Docker is missing or fails, switch to browser-based WPC provider.
5. If cookies are in use, prefer the web_safari player client. Otherwise prefer mweb for PO tokens. 6. Keep the browser window open while WPC is minting tokens. Ensure Chrome can reach YouTube through the same proxy. 7. If you get “Only images are available” or “Requested format is not available”, treat it as a PO Token failure and retry after fixing token provider/browser state. 8. If you get SSL EOF or fragment errors, treat it as a proxy/network issue. Retry with progressive formats and/or a better proxy.
Agent Execution Checklist (Internal)
- Run
scripts/download_video.py URL --info(add--cookies-from-browser chromeif permission granted) to fetch metadata and thumbnail. - If yt-dlp metadata fails, rely on the script’s oEmbed fallback for title/uploader/thumbnail and note that duration may be unavailable.
- If a thumbnail URL is present, render it in the response with Markdown image syntax.
- Ask the user to choose video vs. audio-only and (optionally) a quality preset.
- Use a friendly default save location (Downloads folder) unless the user specifies a folder.
- For subtitles, run with
--subtitlesand the requested--sub-lang. - After download, report file name, size, and resolution (if video) in plain language.
- If download fails with 403/fragment errors, retry once with non-m3u8 progressive formats.
- If “Sign in to confirm you’re not a bot” appears, request cookie access and retry with cookies +
web_safari. - If “Only images are available” appears, treat it as PO Token failure and retry after fixing provider/browser state.
- Start the PO Token provider before downloads (
--auto-po-tokendefault). Fail fast if it cannot start. - If Docker-based provider fails (common in China), automatically fall back to the browser-based WPC provider (it may briefly open a browser window).
- If the WPC provider is used, keep the browser window open until download starts. If the browser fails to launch, set the Chrome path explicitly.
- If the PO Token provider times out, restart it once and retry.
- If a system proxy is configured, pass it into the provider container. If the proxy points to 127.0.0.1/localhost, rewrite it to
host.docker.internalfor Docker.
When to Use This Skill
This skill should be invoked when users:
- Request downloading YouTube videos or playlists
- Want to extract audio from YouTube videos
- Experience yt-dlp download failures or limited format availability
- Need help with format selection or quality options
- Report only low-quality (360p) formats available
- Ask about downloading YouTube content in specific quality (1080p, 4K, etc.)
- Need to convert downloaded WebM videos to MP4 format for wider compatibility
- Request downloading HLS streams (m3u8) from platforms like Mux, Vimeo, or other streaming services
- Need to download protected streams that require authentication headers
Prerequisites
1. Verify yt-dlp Installation (Run this yourself)
which yt-dlp
yt-dlp --versionIf not installed or outdated (< 2025.10.22):
brew upgrade yt-dlp # macOS
# or
pip install --upgrade yt-dlp # Cross-platformCritical: Outdated yt-dlp versions cause nsig extraction failures and missing formats.
2. Check Current Quality Access (Run this yourself)
Before downloading, check available formats:
yt-dlp -F "https://youtu.be/VIDEO_ID"If only format 18 (360p) appears: PO token provider setup needed for high-quality access.
High-Quality Download Workflow
Step 1: Install PO Token Provider (One-time Setup)
For 1080p/1440p/4K access, install a PO token provider plugin into yt-dlp's Python environment:
# Find yt-dlp's Python path (interpreter used by yt-dlp)
head -1 $(which yt-dlp)
# Install plugin using the interpreter from the line above
<YTDLP_PYTHON> -m pip install bgutil-ytdlp-pot-providerVerification: Run yt-dlp -F "VIDEO_URL" again. Look for formats 137 (1080p), 271 (1440p), or 313 (4K).
See references/po-token-setup.md for detailed setup instructions and troubleshooting.
Step 2: Download with Best Quality
Once PO token provider is installed:
# Download best quality up to 1080p
yt-dlp -f "bestvideo[height<=1080]+bestaudio/best" "VIDEO_URL"
# Download best available quality (4K if available)
yt-dlp -f "bestvideo+bestaudio/best" "VIDEO_URL"Step 3: Verify Download Quality
# Check video resolution
ffprobe -v error -select_streams v:0 -show_entries stream=width,height,codec_name -of default=noprint_wrappers=1 video.mp4Expected output for 1080p:
codec_name=vp9
width=1920
height=1080Alternative: Browser Cookies Method
If PO token provider setup is problematic, use browser cookies:
# Firefox
yt-dlp --cookies-from-browser firefox -f "bestvideo[height<=1080]+bestaudio/best" "VIDEO_URL"
# Chrome
yt-dlp --cookies-from-browser chrome -f "bestvideo[height<=1080]+bestaudio/best" "VIDEO_URL"Benefits: Access to age-restricted and members-only content. Requirements:
- Must be logged into YouTube in the specified browser.
- Browser and yt-dlp must use the same IP/proxy.
- Do not use Android client with cookies (Android client does not support cookies).
Common Tasks
Audio-Only Download (Run this yourself)
Extract audio as MP3:
yt-dlp -x --audio-format mp3 "VIDEO_URL"Custom Output Directory (Run this yourself)
yt-dlp -P ~/Downloads/YouTube "VIDEO_URL"Download with Subtitles (Run this yourself)
yt-dlp --write-subs --sub-lang en "VIDEO_URL"Playlist Download (Run this yourself)
yt-dlp -f "bestvideo[height<=1080]+bestaudio/best" "PLAYLIST_URL"Convert WebM to MP4 (Run this yourself)
YouTube high-quality downloads often use WebM format (VP9 codec). Convert to MP4 for wider compatibility:
# Check if ffmpeg is installed
which ffmpeg || brew install ffmpeg # macOS
# Convert WebM to MP4 with good quality settings
ffmpeg -i "video.webm" -c:v libx264 -preset medium -crf 23 -c:a aac -b:a 128k "video.mp4"Parameters explained:
-c:v libx264: Use H.264 video codec (widely compatible)-preset medium: Balance between encoding speed and file size-crf 23: Constant Rate Factor for quality (18-28 range, lower = better quality)-c:a aac: Use AAC audio codec-b:a 128k: Audio bitrate 128 kbps
Tip: Conversion maintains 1080p resolution and provides ~6x encoding speed on modern hardware.
Troubleshooting Quick Reference
Only 360p Available (Format 18)
Cause: Missing PO token provider or outdated yt-dlp.
Solution: 1. Update yt-dlp: brew upgrade yt-dlp 2. Install PO token provider (see Step 1 above) 3. Or use browser cookies method
Sign in to Confirm You’re Not a Bot
Cause: YouTube requires authentication to proceed.
Solution: 1. Request permission and use browser cookies (--cookies-from-browser chrome). 2. Ensure the browser and yt-dlp use the same IP/proxy. 3. Retry with web_safari client if needed.
Only Images Available / Requested Format Not Available
Cause: PO tokens not applied or provider/browser verification failed.
Solution: 1. Verify PO Token provider is running before download. 2. Keep the browser window open if using WPC. 3. If cookies are in use, prefer web_safari client and retry.
nsig Extraction Failed
Symptoms:
WARNING: [youtube] nsig extraction failed: Some formats may be missingSolution: 1. Update yt-dlp to latest version 2. Install PO token provider 3. If still failing and PO tokens are disabled, use Android client: yt-dlp --extractor-args "youtube:player_client=android" "VIDEO_URL"
SSL EOF / Fragment Errors
Cause: Proxy or network instability.
Solution: 1. Retry with progressive formats (non-m3u8). 2. Switch to a more stable proxy/node. 3. Avoid closing the PO token browser window during download.
Slow Downloads or Network Errors
For users in China or behind restrictive proxies:
- Downloads may be slow due to network conditions
- Allow sufficient time for completion
- yt-dlp automatically retries on transient failures
PO Token Warning (Harmless)
WARNING: android client https formats require a GVS PO TokenAction: Ignore if download succeeds. This indicates Android client has limited format access without PO tokens.
Bundled Script Reference
scripts/download_video.py
Use this convenience wrapper to auto-start a PO Token provider by default for high-quality downloads. Use it yourself and report results to the user without asking them to run commands.
Basic usage:
scripts/download_video.py "VIDEO_URL"Arguments:
url- YouTube video URL (required)-o, --output-dir- Output directory--output-template- Output filename template (yt-dlp syntax)-f, --format- Format specification-q, --quality- Quality preset (best, 1080p, 720p, 480p, 360p, worst). Default: best (skipped for--audio-only)-a, --audio-only- Extract audio as MP3--subtitles- Download subtitles if available--sub-lang- Subtitle languages (comma-separated, default: en)--cookies-from-browser- Load cookies from a browser (e.g., chrome, firefox)--cookies-file- Load cookies from a cookies.txt file--player-client- Use a specific YouTube player client (e.g., web_safari)--auto-po-token- Auto-start PO Token provider (default; uses Docker if available, otherwise switches to browser-based provider)--no-auto-po-token- Disable auto PO Token setup--proxy- Proxy URL for yt-dlp and the PO Token provider (e.g., http://127.0.0.1:1082)--wpc-browser-path- Browser executable path for WPC provider-F, --list-formats- List available formats--merge-format- Merge output container (e.g., mp4, mkv). Default: mp4--playlist- Allow playlist downloads (default: single video only)--info- Print title/uploader/duration/thumbnail and exit--no-android-client- Disable Android client fallback
Note: Use the Android client only when PO tokens are disabled. Keep PO tokens enabled for high quality.
Quality Expectations
| Setup | 360p | 720p | 1080p | 1440p | 4K |
|---|---|---|---|---|---|
| Auto PO token (default) | ✓ | ✓ | ✓ | ✓ | ✓ |
| Android client only | ✓ | ✗ | ✗ | ✗ | ✗ |
| PO token provider (manual) | ✓ | ✓ | ✓ | ✓ | ✓ |
| Browser cookies | ✓ | ✓ | ✓ | ✓ | ✓ |
HLS Stream Downloads (m3u8)
For streaming platforms like Mux, Vimeo, and other HLS-based services, use ffmpeg as the primary tool. These streams often require authentication headers that yt-dlp may not handle correctly.
Identifying HLS Streams
HLS streams use .m3u8 playlist files:
- Master playlist: Lists multiple quality options
- Rendition playlist: Contains actual video/audio segment URLs
Download Workflow
Step 1: Obtain the Stream URL
Get the m3u8 URL from the video source. For protected streams: 1. Open browser DevTools → Network tab 2. Play the video 3. Filter for "m3u8" to find the playlist URLs 4. Copy the rendition URL (usually contains quality info like "rendition.m3u8")
Step 2: Identify Required Headers
Many CDNs require authentication headers:
- Referer: Origin website (e.g.,
https://maven.com/) - Origin: Same as Referer for CORS
- User-Agent: Browser identification
Check the Network tab to see which headers the browser sends.
Step 3: Download with ffmpeg
Use ffmpeg with the -headers flag for protected streams:
ffmpeg -headers "Referer: https://example.com/" \
-protocol_whitelist file,http,https,tcp,tls,crypto,httpproxy \
-i "https://cdn.example.com/path/rendition.m3u8?params" \
-c copy -bsf:a aac_adtstoasc \
output.mp4Key parameters:
-headers: Set HTTP headers (critical for authentication)-protocol_whitelist: Enable required protocols for HLS-c copy: Stream copy (no re-encoding, faster)-bsf:a aac_adtstoasc: Fix AAC audio compatibility
Common header patterns:
# Single header
-headers "Referer: https://example.com/"
# Multiple headers
-headers "Referer: https://example.com/" \
-headers "User-Agent: Mozilla/5.0..."
# Alternative syntax
-headers $'Referer: https://example.com/\r\nUser-Agent: Mozilla/5.0...'Handling Separate Audio/Video Streams
Some platforms (like Mux) deliver audio and video separately:
1. Download audio stream:
ffmpeg -headers "Referer: https://example.com/" \
-protocol_whitelist file,http,https,tcp,tls,crypto,httpproxy \
-i "https://cdn.example.com/audio/rendition.m3u8" \
-c copy audio.m4a2. Download video stream:
ffmpeg -headers "Referer: https://example.com/" \
-protocol_whitelist file,http,https,tcp,tls,crypto,httpproxy \
-i "https://cdn.example.com/video/rendition.m3u8" \
-c copy video.mp43. Merge streams:
ffmpeg -i video.mp4 -i audio.m4a -c copy merged.mp4Troubleshooting HLS Downloads
403 Forbidden Errors
Cause: Missing or incorrect authentication headers.
Solution: 1. Verify Referer header matches the video source website 2. Check if additional headers (Origin, User-Agent) are needed 3. Ensure the m3u8 URL includes all query parameters from browser
yt-dlp Stuck on Cookie Extraction
Symptom: Extracting cookies from chrome hangs indefinitely.
Solution: Use ffmpeg directly instead of yt-dlp for HLS streams.
Protocol Not Whitelisted
Error: Protocol 'https' not on whitelist 'file,crypto,data'
Solution: Add -protocol_whitelist file,http,https,tcp,tls,crypto,httpproxy
Empty Segments or No Streams
Cause: Expired signatures in the m3u8 URLs.
Solution: 1. Get fresh URLs from browser DevTools 2. Download immediately after obtaining URLs 3. Look for rendition URLs with updated signature parameters
Performance Tips
- HLS downloads typically run at 10-15x realtime speed
- No re-encoding with
-c copy(fastest) - Monitor download with real-time progress display
- Use absolute output paths to avoid directory confusion
Further Reading
- PO Token Setup: See
references/po-token-setup.mdfor detailed installation and troubleshooting - yt-dlp Documentation: https://github.com/yt-dlp/yt-dlp
- Format Selection Guide: https://github.com/yt-dlp/yt-dlp#format-selection
Next Step: Transcribe Downloaded Audio/Video
After downloading, if the user's goal involves getting text from the video (transcription, subtitles, meeting notes), proactively suggest:
Download complete: [filename]
If you need the spoken content as text, I can transcribe it for you.
Options:
A) Transcribe with /daymade-audio:asr-transcribe-to-text (Recommended for speech-to-text)
B) No thanks — I just needed the video fileSecurity scan passed
Scanned at: 2025-11-19T01:20:50.532287
Tool: gitleaks + pattern-based validation
Content hash: 6dc9ae011eb2d8ca1ba73ceb2c6d8aa031979f7e3bb9b31bfe78fdb7283f7f66
YouTube Downloader Internal SOP
Use this SOP to avoid common yt-dlp failures and confusion:
1. Quote YouTube URLs in shell commands (zsh treats ? as glob). Example: 'https://www.youtube.com/watch?v=VIDEO_ID'. 2. Ensure proxy is active for both yt-dlp and PO Token providers (HTTP_PROXY/HTTPS_PROXY/ALL_PROXY). 3. If you see "Sign in to confirm you're not a bot", request cookie permission and use browser cookies. 4. Start the PO Token provider before downloading. Prefer Docker bgutil; fall back to browser-based WPC when Docker is unavailable or fails. 5. Use web_safari client when cookies are present; otherwise use mweb for PO tokens. 6. Keep the browser window open while WPC is minting tokens and make sure it can reach YouTube through the same proxy. 7. If you see "Only images are available" or "Requested format is not available", treat it as PO token failure and retry after fixing provider/browser state. 8. If you see SSL EOF or fragment errors, treat it as proxy instability. Retry with progressive formats or switch to a more stable proxy.
PO Token Setup Guide
What are PO Tokens?
Proof of Origin (PO) Tokens are cryptographic attestations required by YouTube for certain clients and request types. Without them, requests for affected format URLs may return HTTP Error 403 or result in restricted format access.
Why PO Tokens Matter
As of late 2024/early 2025, YouTube increasingly requires PO tokens for high-quality video formats (1080p, 1440p, 4K). Without PO token support:
- Android client: Only 360p available (format 18)
- Web client: nsig extraction failures, missing formats
- iOS client: Similar restrictions
With PO token provider: Full access to all quality levels including 4K.
Recommended Solution: PO Token Provider Plugin
Operational SOP (Internal)
Use this checklist to prevent common failures:
1. Quote URLs in shell commands to avoid zsh globbing ('https://www.youtube.com/watch?v=VIDEO_ID'). 2. Ensure proxy is active for yt-dlp and token providers (HTTP_PROXY/HTTPS_PROXY/ALL_PROXY). 3. If YouTube asks to confirm you’re not a bot, use browser cookies. Do not proceed without cookies. 4. Start the PO token provider before downloading.
- Prefer Docker bgutil when available.
- Fall back to WPC (browser) if Docker is missing or fails.
5. Use web_safari when cookies are present; use mweb otherwise for PO tokens. 6. Keep the browser window open during WPC token minting. 7. If you see “Only images are available” or “Requested format is not available”, treat it as PO token failure and retry after fixing provider/browser state. 8. If you see SSL EOF/fragment errors, treat it as proxy instability and retry with progressive formats or a better proxy.
Automatic Setup (Preferred for non-technical users)
If Docker is available, you can start the PO token provider automatically:
1. Install the plugin into yt-dlp's Python environment (one-time):
<YTDLP_PYTHON> -m pip install bgutil-ytdlp-pot-providerIn China, prefer a local PyPI mirror:
<YTDLP_PYTHON> -m pip install bgutil-ytdlp-pot-provider -i https://pypi.tuna.tsinghua.edu.cn/simple2. Start the provider (Docker):
docker run -d --name bgutil-pot-provider -p 4416:4416 --init brainicism/bgutil-ytdlp-pot-provider3. Retry yt-dlp downloads using a web client (e.g., mweb) so PO tokens apply.
Installation
Install a PO token provider plugin to handle token generation automatically. The plugin must be installed into yt-dlp's own Python environment.
Step 1: Locate yt-dlp's Python
head -1 $(which yt-dlp)
# Output example: #!/opt/homebrew/Cellar/yt-dlp/2025.10.22/libexec/bin/pythonStep 2: Install Plugin
# For Homebrew-installed yt-dlp (macOS)
<YTDLP_PYTHON> -m pip install bgutil-ytdlp-pot-provider
# For pip-installed yt-dlp
python3 -m pip install bgutil-ytdlp-pot-provider --userStep 3: Verify Installation
yt-dlp -F "https://youtu.be/VIDEO_ID"Look for high-quality formats (137, 248, 271, 313) in the output. If present, the plugin is working.
Available PO Token Provider Plugins
1. bgutil-ytdlp-pot-provider (Recommended)
- Installation:
pip install bgutil-ytdlp-pot-provider - Requires: yt-dlp 2025.05.22 or above
- Automatic: Works transparently once installed
- Best for: General use, most reliable
2. yt-dlp-get-pot
- Installation:
pip install yt-dlp-get-pot - Requires: yt-dlp 2025.01.15 or above
- Method: Launches browser to mint tokens
- Best for: Users comfortable with browser automation
3. yt-dlp-getpot-wpc (Browser-based, no Docker)
- Installation:
pip install yt-dlp-getpot-wpc - Requires: yt-dlp 2025.09.26 or above
- Method: Uses a browser window to mint tokens
- Best for: Environments without Docker or restricted networks
In China, prefer a local PyPI mirror:
pip install yt-dlp-getpot-wpc -i https://pypi.tuna.tsinghua.edu.cn/simple4. yt-dlp-get-pot-rustypipe
- Installation:
pip install yt-dlp-get-pot-rustypipe - Method: Uses rustypipe-botguard
- Supports: All web-based YouTube clients
- Best for: Advanced users, specific client requirements
Verification Workflow
Check Available Formats
yt-dlp -F "https://youtu.be/VIDEO_ID"Without PO token provider:
ID EXT RESOLUTION
18 mp4 640x360 # Only low qualityWith PO token provider:
ID EXT RESOLUTION
137 mp4 1920x1080 # 1080p available
248 webm 1920x1080
271 webm 2560x1440 # 1440p available
313 webm 3840x2160 # 4K availableDownload Best Quality
# 1080p max
yt-dlp -f "bestvideo[height<=1080]+bestaudio/best" "VIDEO_URL"
# Best available (4K if available)
yt-dlp -f "bestvideo+bestaudio/best" "VIDEO_URL"Verify Downloaded Quality
# Check resolution
ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of default=noprint_wrappers=1 video.mp4Troubleshooting
Plugin Not Working
Symptom: Still only seeing format 18 after plugin installation
Solution: 1. Verify yt-dlp version: yt-dlp --version (need 2025.05.22+) 2. Check plugin installation: pip list | grep bgutil 3. Ensure plugin installed in correct Python environment 4. Try reinstalling: pip uninstall bgutil-ytdlp-pot-provider && pip install bgutil-ytdlp-pot-provider
Warning Messages
WARNING: [youtube] [pot:bgutil:http] Error reaching GET http://127.0.0.1:4416/pingImpact: This warning is usually harmless if formats are available. The plugin uses HTTP as fallback.
Action: No action needed if download succeeds with high-quality formats.
Alternative: Browser Cookies Method
If PO token providers don't work, use browser cookies for authentication:
# Firefox
yt-dlp --cookies-from-browser firefox "VIDEO_URL"
# Chrome
yt-dlp --cookies-from-browser chrome "VIDEO_URL"Benefits:
- Access age-restricted content
- Access members-only content
- Better quality selection
Requirements:
- Must be logged into YouTube in the browser
- Browser and yt-dlp must use same IP address
Quality Comparison
| Method | 360p | 720p | 1080p | 1440p | 4K |
|---|---|---|---|---|---|
| Default (no workaround) | ✗ | ✗ | ✗ | ✗ | ✗ |
| Android client only | ✓ | ✗ | ✗ | ✗ | ✗ |
| PO token provider | ✓ | ✓ | ✓ | ✓ | ✓ |
| Browser cookies | ✓ | ✓ | ✓ | ✓ | ✓ |
References
#!/usr/bin/env python3
"""
YouTube video downloader using yt-dlp with robust error handling.
This script handles common issues like nsig extraction failures and network problems,
especially useful for users behind proxies or in regions with YouTube access issues.
Requirements:
- yt-dlp: Install via `brew install yt-dlp` (macOS) or `pip install yt-dlp` (cross-platform)
- For high-quality downloads (1080p+): Install PO token provider
See ../references/po-token-setup.md for setup instructions
Usage:
scripts/download_video.py "https://youtu.be/VIDEO_ID"
scripts/download_video.py "https://youtu.be/VIDEO_ID" --audio-only
scripts/download_video.py "https://youtu.be/VIDEO_ID" --quality 1080p
scripts/download_video.py "https://youtu.be/VIDEO_ID" -o ~/Downloads
Note:
This script auto-starts a PO Token provider for high-quality downloads.
If PO tokens are disabled, it can fall back to the Android client (360p only).
"""
import argparse
import json
import subprocess
import sys
import shutil
import time
import os
from pathlib import Path
from typing import Iterable, Optional
from urllib.parse import quote, urlparse, urlunparse
from urllib.request import urlopen
from urllib.error import URLError
PYPI_MIRROR = "https://pypi.tuna.tsinghua.edu.cn/simple"
QUALITY_PRESETS = {
"best": "bestvideo+bestaudio/best",
"1080p": "bestvideo[height<=1080]+bestaudio/best",
"720p": "bestvideo[height<=720]+bestaudio/best",
"480p": "bestvideo[height<=480]+bestaudio/best",
"360p": "bestvideo[height<=360]+bestaudio/best",
"worst": "worstvideo+worstaudio/worst",
}
def build_output_template(output_dir: str, template: Optional[str]) -> str:
if template:
template_path = Path(template)
if template_path.is_absolute():
return template
return str(Path(output_dir).expanduser().resolve() / template)
return str(Path(output_dir).expanduser().resolve() / "%(title)s.%(ext)s")
def list_files(root: Path) -> set:
return {path for path in root.rglob("*") if path.is_file()}
def human_size(num_bytes: int) -> str:
if num_bytes < 1024:
return f"{num_bytes} B"
for unit in ["KB", "MB", "GB", "TB"]:
num_bytes /= 1024.0
if num_bytes < 1024:
return f"{num_bytes:.1f} {unit}"
return f"{num_bytes:.1f} PB"
def pick_primary_file(files: Iterable[Path], audio_only: bool) -> Optional[Path]:
video_exts = {".mp4", ".webm", ".mkv", ".mov", ".m4v"}
audio_exts = {".mp3", ".m4a", ".opus", ".aac", ".flac", ".wav"}
candidates = []
for path in files:
if path.suffix.lower() in {".part", ".ytdl", ".tmp"}:
continue
if audio_only:
if path.suffix.lower() in audio_exts:
candidates.append(path)
else:
if path.suffix.lower() in video_exts:
candidates.append(path)
if not candidates:
candidates = [path for path in files if path.suffix.lower() not in {".part", ".ytdl", ".tmp"}]
if not candidates:
return None
return max(candidates, key=lambda p: p.stat().st_size)
def get_video_resolution(path: Path) -> Optional[str]:
check = subprocess.run(["which", "ffprobe"], capture_output=True, text=True)
if check.returncode != 0:
return None
cmd = [
"ffprobe",
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=width,height",
"-of",
"csv=p=0:s=x",
str(path),
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
return None
value = result.stdout.strip()
return value or None
def filter_cookie_lines(text: str) -> str:
if not text:
return ""
filtered = []
for line in text.splitlines():
lowered = line.lower()
if "extracting cookies" in lowered:
continue
if "extracted" in lowered and "cookies" in lowered:
continue
filtered.append(line)
return "\n".join(filtered)
def run_yt_dlp(cmd: list, hide_cookie_logs: bool = False) -> subprocess.CompletedProcess:
if not hide_cookie_logs:
return subprocess.run(cmd)
result = subprocess.run(cmd, capture_output=True, text=True)
stdout = filter_cookie_lines(result.stdout)
stderr = filter_cookie_lines(result.stderr)
if stdout:
print(stdout)
if stderr:
print(stderr, file=sys.stderr)
return result
def has_403_error(result: subprocess.CompletedProcess) -> bool:
text = ""
if hasattr(result, "stdout") and result.stdout:
text += result.stdout
if hasattr(result, "stderr") and result.stderr:
text += result.stderr
text = text.lower()
return "http error 403" in text or "403: forbidden" in text or "fragment 1 not found" in text
def has_pot_error(result: subprocess.CompletedProcess) -> bool:
text = ""
if hasattr(result, "stdout") and result.stdout:
text += result.stdout
if hasattr(result, "stderr") and result.stderr:
text += result.stderr
text = text.lower()
return "pot" in text and "error" in text
def has_wpc_error(result: subprocess.CompletedProcess) -> bool:
text = ""
if hasattr(result, "stdout") and result.stdout:
text += result.stdout
if hasattr(result, "stderr") and result.stderr:
text += result.stderr
text = text.lower()
return "pot:wpc" in text or "webpoclient" in text
def with_player_client(cmd: list, client: str) -> list:
rebuilt = []
skip_next = False
for token in cmd:
if skip_next:
skip_next = False
continue
if token == "--extractor-args":
skip_next = True
continue
rebuilt.append(token)
rebuilt.extend(["--extractor-args", f"youtube:player_client={client}"])
return rebuilt
def get_proxy_settings(proxy_arg: Optional[str]) -> tuple[Optional[str], Optional[str]]:
if proxy_arg:
proxy = proxy_arg
else:
proxy = (
os.environ.get("ALL_PROXY")
or os.environ.get("all_proxy")
or os.environ.get("HTTPS_PROXY")
or os.environ.get("https_proxy")
or os.environ.get("HTTP_PROXY")
or os.environ.get("http_proxy")
)
no_proxy = os.environ.get("NO_PROXY") or os.environ.get("no_proxy")
return proxy, no_proxy
def normalize_proxy_for_docker(proxy_url: str) -> str:
parsed = urlparse(proxy_url)
if parsed.hostname in {"127.0.0.1", "localhost"}:
host = "host.docker.internal"
netloc = ""
if parsed.username or parsed.password:
userinfo = parsed.username or ""
if parsed.password:
userinfo += f":{parsed.password}"
netloc = f"{userinfo}@"
if parsed.port:
netloc += f"{host}:{parsed.port}"
else:
netloc += host
parsed = parsed._replace(netloc=netloc)
return urlunparse(parsed)
return proxy_url
def is_localhost_proxy(proxy_url: str) -> bool:
parsed = urlparse(proxy_url)
return parsed.hostname in {"127.0.0.1", "localhost"}
def find_chrome_path() -> Optional[str]:
candidates = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
]
for candidate in candidates:
if Path(candidate).exists():
return candidate
for name in ["google-chrome", "chromium", "chromium-browser", "chrome"]:
path = shutil.which(name)
if path:
return path
return None
def with_wpc_browser(cmd: list, browser_path: Optional[str]) -> list:
if not browser_path:
return cmd
return cmd + ["--extractor-args", f"youtubepot-wpc:browser_path={browser_path}"]
def provider_ping(url: str = "http://127.0.0.1:4416/ping") -> bool:
try:
with urlopen(url, timeout=3) as response:
return response.status == 200
except (URLError, ConnectionResetError, TimeoutError):
return False
def docker_available() -> bool:
result = subprocess.run(["docker", "--version"], capture_output=True, text=True)
return result.returncode == 0
def docker_daemon_ready() -> bool:
result = subprocess.run(["docker", "info"], capture_output=True, text=True)
return result.returncode == 0
def wait_for_provider(timeout: int = 10) -> bool:
deadline = time.time() + timeout
while time.time() < deadline:
if provider_ping():
return True
time.sleep(1)
return False
def container_exists(name: str) -> bool:
result = subprocess.run(
["docker", "ps", "-a", "--filter", f"name={name}", "--format", "{{.Names}}"],
capture_output=True,
text=True,
)
return result.returncode == 0 and name in result.stdout.split()
def parse_yt_dlp_version() -> Optional[str]:
result = subprocess.run(["yt-dlp", "--version"], capture_output=True, text=True)
if result.returncode != 0:
return None
return result.stdout.strip()
def version_at_least(version: str, minimum: str) -> bool:
def parse(value: str) -> list:
return [int(part) for part in value.split(".") if part.isdigit()]
current = parse(version)
required = parse(minimum)
if not current or not required:
return False
while len(current) < len(required):
current.append(0)
while len(required) < len(current):
required.append(0)
return current >= required
def yt_dlp_python() -> Optional[str]:
yt_dlp_path = shutil.which("yt-dlp")
if not yt_dlp_path:
return None
try:
with open(yt_dlp_path, "r", encoding="utf-8") as handle:
first = handle.readline().strip()
except OSError:
return None
if not first.startswith("#!"):
return None
shebang = first[2:].strip()
if shebang.endswith("env python3") or shebang.endswith("env python"):
return "python3"
return shebang
def ensure_pot_plugin_installed(proxy_url: Optional[str]) -> bool:
version = parse_yt_dlp_version()
if not version or not version_at_least(version, "2025.05.22"):
print("⚠️ yt-dlp needs to be updated before enabling PO Token provider.")
return False
python_bin = yt_dlp_python()
if not python_bin:
print("⚠️ Unable to locate yt-dlp's Python interpreter for plugin install.")
return False
check = subprocess.run(
[python_bin, "-m", "pip", "show", "bgutil-ytdlp-pot-provider"],
capture_output=True,
text=True,
)
if check.returncode == 0:
return True
print("⚠️ Installing PO Token provider plugin (one-time setup)...")
install_cmd = [python_bin, "-m", "pip", "install", "bgutil-ytdlp-pot-provider", "-i", PYPI_MIRROR]
if proxy_url:
install_cmd.extend(["--proxy", proxy_url])
install = subprocess.run(install_cmd, capture_output=True, text=True)
return install.returncode == 0
def ensure_wpc_provider(proxy_url: Optional[str]) -> bool:
version = parse_yt_dlp_version()
if not version or not version_at_least(version, "2025.09.26"):
print("⚠️ yt-dlp needs to be updated before enabling the WPC PO Token provider.")
return False
python_bin = yt_dlp_python()
if not python_bin:
print("⚠️ Unable to locate yt-dlp's Python interpreter for WPC provider install.")
return False
check = subprocess.run(
[python_bin, "-m", "pip", "show", "yt-dlp-getpot-wpc"],
capture_output=True,
text=True,
)
if check.returncode == 0:
return True
print("⚠️ Installing WPC PO Token provider (one-time setup)...")
install_cmd = [python_bin, "-m", "pip", "install", "-U", "yt-dlp-getpot-wpc", "-i", PYPI_MIRROR]
if proxy_url:
install_cmd.extend(["--proxy", proxy_url])
install = subprocess.run(install_cmd, capture_output=True, text=True)
return install.returncode == 0
def ensure_po_token_provider(proxy_url: Optional[str], no_proxy: Optional[str]) -> Optional[str]:
if not ensure_pot_plugin_installed(proxy_url):
return "wpc" if ensure_wpc_provider(proxy_url) else None
if provider_ping():
return "bgutil"
if not docker_available():
print("⚠️ Docker is not available. Switching to browser-based PO Token provider...")
return "wpc" if ensure_wpc_provider(proxy_url) else None
if not docker_daemon_ready():
print("⚠️ Docker daemon is not running. Switching to browser-based PO Token provider...")
return "wpc" if ensure_wpc_provider(proxy_url) else None
name = "bgutil-pot-provider"
if container_exists(name):
start = subprocess.run(["docker", "start", name], capture_output=True, text=True)
if start.returncode != 0:
print("⚠️ Docker container failed to start. Switching to browser-based PO Token provider...")
return "wpc" if ensure_wpc_provider(proxy_url) else None
else:
env_args = []
use_host_network = False
docker_proxy = None
if proxy_url:
use_host_network = is_localhost_proxy(proxy_url)
docker_proxy = proxy_url if use_host_network else normalize_proxy_for_docker(proxy_url)
env_args.extend(
[
"-e",
f"HTTP_PROXY={docker_proxy}",
"-e",
f"HTTPS_PROXY={docker_proxy}",
"-e",
f"ALL_PROXY={docker_proxy}",
"-e",
f"http_proxy={docker_proxy}",
"-e",
f"https_proxy={docker_proxy}",
"-e",
f"all_proxy={docker_proxy}",
]
)
if no_proxy:
env_args.extend(
[
"-e",
f"NO_PROXY={no_proxy}",
"-e",
f"no_proxy={no_proxy}",
]
)
run_cmd = ["docker", "run", "-d", "--name", name]
if use_host_network:
run_cmd.extend(["--network", "host"])
run_cmd.extend(
[
"-p",
"4416:4416",
*env_args,
"--init",
"brainicism/bgutil-ytdlp-pot-provider",
]
)
run = subprocess.run(run_cmd, capture_output=True, text=True)
if run.returncode != 0:
if use_host_network and proxy_url:
# Retry without host network using host.docker.internal proxy
docker_proxy = normalize_proxy_for_docker(proxy_url)
env_args = [
"-e",
f"HTTP_PROXY={docker_proxy}",
"-e",
f"HTTPS_PROXY={docker_proxy}",
"-e",
f"ALL_PROXY={docker_proxy}",
"-e",
f"http_proxy={docker_proxy}",
"-e",
f"https_proxy={docker_proxy}",
"-e",
f"all_proxy={docker_proxy}",
]
if no_proxy:
env_args.extend(
[
"-e",
f"NO_PROXY={no_proxy}",
"-e",
f"no_proxy={no_proxy}",
]
)
retry = subprocess.run(
[
"docker",
"run",
"-d",
"--name",
name,
"-p",
"4416:4416",
*env_args,
"--init",
"brainicism/bgutil-ytdlp-pot-provider",
],
capture_output=True,
text=True,
)
if retry.returncode == 0:
run = retry
else:
print("⚠️ Docker provider failed to start. Switching to browser-based PO Token provider...")
return "wpc" if ensure_wpc_provider(proxy_url) else None
else:
print("⚠️ Docker provider failed to start. Switching to browser-based PO Token provider...")
return "wpc" if ensure_wpc_provider(proxy_url) else None
if wait_for_provider():
print("✓ PO Token provider is running.")
return "bgutil"
# If container started but not responding, recreate with proxy settings
if restart_po_token_provider(proxy_url, no_proxy) and provider_ping():
print("✓ PO Token provider is running.")
return "bgutil"
print("⚠️ Docker-based provider failed. Switching to browser-based PO Token provider...")
return "wpc" if ensure_wpc_provider(proxy_url) else None
def restart_po_token_provider(proxy_url: Optional[str], no_proxy: Optional[str]) -> bool:
name = "bgutil-pot-provider"
if not docker_available() or not docker_daemon_ready():
return False
if container_exists(name):
subprocess.run(["docker", "rm", "-f", name], capture_output=True, text=True)
env_args = []
use_host_network = False
docker_proxy = None
if proxy_url:
use_host_network = is_localhost_proxy(proxy_url)
docker_proxy = proxy_url if use_host_network else normalize_proxy_for_docker(proxy_url)
env_args.extend(
[
"-e",
f"HTTP_PROXY={docker_proxy}",
"-e",
f"HTTPS_PROXY={docker_proxy}",
"-e",
f"ALL_PROXY={docker_proxy}",
"-e",
f"http_proxy={docker_proxy}",
"-e",
f"https_proxy={docker_proxy}",
"-e",
f"all_proxy={docker_proxy}",
]
)
if no_proxy:
env_args.extend(
[
"-e",
f"NO_PROXY={no_proxy}",
"-e",
f"no_proxy={no_proxy}",
]
)
run_cmd = ["docker", "run", "-d", "--name", name]
if use_host_network:
run_cmd.extend(["--network", "host"])
run_cmd.extend(
[
"-p",
"4416:4416",
*env_args,
"--init",
"brainicism/bgutil-ytdlp-pot-provider",
]
)
run = subprocess.run(run_cmd, capture_output=True, text=True)
if run.returncode != 0 and use_host_network:
docker_proxy = normalize_proxy_for_docker(proxy_url)
env_args = [
"-e",
f"HTTP_PROXY={docker_proxy}",
"-e",
f"HTTPS_PROXY={docker_proxy}",
"-e",
f"ALL_PROXY={docker_proxy}",
"-e",
f"http_proxy={docker_proxy}",
"-e",
f"https_proxy={docker_proxy}",
"-e",
f"all_proxy={docker_proxy}",
]
if no_proxy:
env_args.extend(
[
"-e",
f"NO_PROXY={no_proxy}",
"-e",
f"no_proxy={no_proxy}",
]
)
subprocess.run(
[
"docker",
"run",
"-d",
"--name",
name,
"-p",
"4416:4416",
*env_args,
"--init",
"brainicism/bgutil-ytdlp-pot-provider",
],
capture_output=True,
text=True,
)
return wait_for_provider()
def fallback_format_for_quality(quality: Optional[str]) -> str:
if not quality or quality == "best":
return "best[protocol!*=m3u8][ext=mp4]/best[protocol!*=m3u8]/best"
if quality.endswith("p") and quality[:-1].isdigit():
height = quality[:-1]
return (
f"best[height<={height}][protocol!*=m3u8][ext=mp4]/"
f"best[height<={height}][protocol!*=m3u8]/best[protocol!*=m3u8]"
)
return "best[protocol!*=m3u8][ext=mp4]/best[protocol!*=m3u8]/best"
def print_video_info(cmd_base: list, url: str, hide_cookie_logs: bool = False) -> int:
info_cmd = cmd_base + ["--skip-download", "--dump-json", "--no-playlist", url]
result = subprocess.run(info_cmd, capture_output=True, text=True)
if result.returncode != 0:
print("✗ Failed to fetch video metadata")
if result.stderr:
print(filter_cookie_lines(result.stderr).strip())
fallback = fetch_oembed_info(url)
if fallback:
print("\n✓ Retrieved metadata via YouTube oEmbed (limited fields).")
render_oembed_info(fallback)
return 0
return result.returncode
first_line = result.stdout.strip().splitlines()[0] if result.stdout else ""
if not first_line:
print("✗ No metadata returned")
return 1
try:
info = json.loads(first_line)
title = info.get("title", "Unknown title")
uploader = info.get("uploader") or info.get("channel") or "Unknown uploader"
duration = info.get("duration")
duration_text = f"{duration}s" if isinstance(duration, int) else "Unknown"
thumbnail = info.get("thumbnail")
print(f"Title: {title}")
print(f"Uploader: {uploader}")
print(f"Duration: {duration_text}")
if thumbnail:
print(f"Thumbnail: {thumbnail}")
except json.JSONDecodeError:
print(first_line)
return 0
def fetch_oembed_info(url: str) -> Optional[dict]:
oembed_url = f"https://www.youtube.com/oembed?url={quote(url, safe='')}&format=json"
try:
with urlopen(oembed_url, timeout=15) as response:
payload = response.read().decode("utf-8")
return json.loads(payload)
except Exception:
return None
def render_oembed_info(info: dict) -> None:
title = info.get("title", "Unknown title")
uploader = info.get("author_name", "Unknown uploader")
thumbnail = info.get("thumbnail_url")
print(f"Title: {title}")
print(f"Uploader: {uploader}")
print("Duration: Unknown")
if thumbnail:
print(f"Thumbnail: {thumbnail}")
def download_video(
url: str,
output_dir: str = ".",
format_spec: str = None,
quality: str = None,
output_template: str = None,
merge_format: str = "mp4",
subtitles: bool = False,
subtitle_lang: str = "en",
cookies_from_browser: str = None,
cookies_file: str = None,
player_client: str = None,
auto_po_token: bool = True,
proxy: str = None,
wpc_browser_path: str = None,
allow_playlist: bool = False,
use_android_client: bool = True,
audio_only: bool = False,
list_formats: bool = False,
info_only: bool = False,
) -> int:
"""
Download a YouTube video using yt-dlp.
Args:
url: YouTube video URL
output_dir: Directory to save the downloaded file
format_spec: Format specification (e.g., "bestvideo+bestaudio/best")
quality: Quality preset (best, 1080p, 720p, 480p, 360p, worst)
output_template: Output filename template (yt-dlp template syntax)
merge_format: Merge output container format (e.g., mp4, mkv)
subtitles: Download subtitles if available
subtitle_lang: Subtitle languages (comma-separated)
cookies_from_browser: Load cookies from browser (e.g., chrome, firefox)
cookies_file: Load cookies from a cookies.txt file
player_client: Use a specific YouTube player client (e.g., web_safari)
auto_po_token: Attempt to auto-start PO Token provider on 403 errors
proxy: Proxy URL for yt-dlp and PO Token provider
wpc_browser_path: Browser path for WPC PO Token provider
allow_playlist: Allow playlist downloads (default: False)
use_android_client: Use Android client to avoid nsig extraction issues
audio_only: Download audio only
list_formats: List available formats instead of downloading
info_only: Print video info before exiting
Returns:
Exit code (0 for success, non-zero for failure)
"""
# Check if yt-dlp is installed
check_result = subprocess.run(
["which", "yt-dlp"], capture_output=True, text=True
)
if check_result.returncode != 0:
print("✗ Error: yt-dlp is not installed")
print(" Install via: brew install yt-dlp # or: pip install yt-dlp")
return 1
# Build yt-dlp command
cmd = ["yt-dlp"]
if cookies_from_browser and cookies_file:
print("✗ Error: Use either --cookies-from-browser or --cookies-file, not both.")
return 2
proxy_value, no_proxy = get_proxy_settings(proxy)
use_po_token = auto_po_token and not info_only
provider_type = None
wpc_available = False
wpc_retried = False
if use_po_token:
provider_type = ensure_po_token_provider(proxy_value, no_proxy)
if not provider_type:
print("✗ PO Token provider could not be started. Aborting download.")
return 2
wpc_available = provider_type == "wpc"
if not wpc_browser_path and use_po_token:
wpc_browser_path = find_chrome_path()
# Use Android client by default only when PO tokens are disabled and no custom client/cookies
use_android = use_android_client and not (
use_po_token or cookies_from_browser or cookies_file or player_client
)
if use_android_client and not use_android:
if use_po_token:
print("ℹ︎ Note: Disabling Android client because PO Token provider is enabled.")
else:
print("ℹ︎ Note: Disabling Android client because cookies or player client are in use.")
if use_android:
cmd.extend(["--extractor-args", "youtube:player_client=android"])
if cookies_from_browser:
cmd.extend(["--cookies-from-browser", cookies_from_browser])
elif cookies_file:
cmd.extend(["--cookies", cookies_file])
if proxy_value:
cmd.extend(["--proxy", proxy_value])
po_token_client = None
if use_po_token:
po_token_client = "mweb"
if cookies_from_browser or cookies_file:
po_token_client = "web_safari"
if player_client:
cmd.extend(["--extractor-args", f"youtube:player_client={player_client}"])
elif po_token_client:
cmd.extend(["--extractor-args", f"youtube:player_client={po_token_client}"])
if not allow_playlist:
cmd.append("--no-playlist")
# List formats if requested
if list_formats:
cmd.extend(["-F", url])
result = run_yt_dlp(cmd, hide_cookie_logs=bool(cookies_from_browser))
# Check if PO token provider might be needed
if result.returncode == 0 and use_android_client:
print("\n💡 Tip: Using Android client (360p only).")
print(" For 1080p/4K, install PO token provider:")
print(" See ../references/po-token-setup.md for instructions")
return result.returncode
if info_only:
return print_video_info(cmd, url, hide_cookie_logs=bool(cookies_from_browser))
if format_spec and quality:
print("✗ Error: Use either --format or --quality, not both.")
return 2
if not format_spec and not quality and not audio_only:
quality = "best"
format_from_quality = False
if quality:
format_spec = QUALITY_PRESETS.get(quality)
if not format_spec:
print(f"✗ Error: Unsupported quality preset: {quality}")
return 2
format_from_quality = True
# Set output directory
output_root = Path(output_dir).expanduser().resolve()
output_root.mkdir(parents=True, exist_ok=True)
output_template_final = build_output_template(str(output_root), output_template)
cmd.extend(["-o", output_template_final])
# Handle audio-only downloads
if audio_only:
cmd.extend(["-x", "--audio-format", "mp3"])
elif format_spec:
cmd.extend(["-f", format_spec])
if subtitles:
cmd.extend(["--write-subs", "--write-auto-subs", "--sub-lang", subtitle_lang])
if merge_format:
cmd.extend(["--merge-output-format", merge_format])
# Add URL
cmd.append(url)
def finalize_download(before_snapshot: set) -> None:
after_files = list_files(output_root)
new_files = sorted(after_files - before_snapshot)
primary = pick_primary_file(new_files, audio_only=audio_only)
if primary:
size = human_size(primary.stat().st_size)
print(f"\n✓ Download completed successfully!")
print(f" File: {primary}")
print(f" Size: {size}")
if audio_only:
print(" Resolution: N/A (audio-only)")
else:
resolution = get_video_resolution(primary)
if resolution:
print(f" Resolution: {resolution}")
else:
print(" Resolution: Not available")
else:
print(f"\n✓ Download completed successfully!")
print(f" Location: {output_root}")
retry_client = player_client or po_token_client
if wpc_available and wpc_browser_path:
cmd = with_wpc_browser(cmd, wpc_browser_path)
# Execute download
before_files = list_files(output_root)
print(f"Executing: {' '.join(cmd)}")
result = run_yt_dlp(cmd, hide_cookie_logs=bool(cookies_from_browser))
if result.returncode == 0:
finalize_download(before_files)
else:
if use_po_token and provider_type == "bgutil" and has_pot_error(result):
print("\n⚠️ PO Token provider did not respond. Restarting it and retrying...")
if restart_po_token_provider(proxy_value, no_proxy):
retry_cmd = with_player_client(cmd, retry_client or "mweb")
before_retry = list_files(output_root)
print(f"Executing: {' '.join(retry_cmd)}")
retry_result = run_yt_dlp(retry_cmd, hide_cookie_logs=bool(cookies_from_browser))
if retry_result.returncode == 0:
finalize_download(before_retry)
return 0
result = retry_result
if has_pot_error(result):
print("\n⚠️ Docker provider still failing. Switching to browser-based PO Token provider...")
if ensure_wpc_provider(proxy_value):
retry_cmd = with_player_client(cmd, retry_client or "mweb")
retry_cmd = with_wpc_browser(retry_cmd, wpc_browser_path)
before_retry = list_files(output_root)
print(f"Executing: {' '.join(retry_cmd)}")
retry_result = run_yt_dlp(retry_cmd, hide_cookie_logs=bool(cookies_from_browser))
if retry_result.returncode == 0:
finalize_download(before_retry)
return 0
result = retry_result
wpc_retried = True
if use_po_token and has_wpc_error(result):
print("\n⚠️ Browser verification not ready. Keeping Chrome open and retrying once...")
time.sleep(3)
retry_cmd = with_player_client(cmd, retry_client or "mweb")
retry_cmd = with_wpc_browser(retry_cmd, wpc_browser_path)
before_retry = list_files(output_root)
print(f"Executing: {' '.join(retry_cmd)}")
retry_result = run_yt_dlp(retry_cmd, hide_cookie_logs=bool(cookies_from_browser))
if retry_result.returncode == 0:
finalize_download(before_retry)
return 0
result = retry_result
wpc_retried = True
if use_po_token and has_pot_error(result) and not wpc_retried:
print("\n⚠️ PO Token provider failed. Switching to browser-based PO Token provider...")
if ensure_wpc_provider(proxy_value):
retry_cmd = with_player_client(cmd, retry_client or "mweb")
retry_cmd = with_wpc_browser(retry_cmd, wpc_browser_path)
before_retry = list_files(output_root)
print(f"Executing: {' '.join(retry_cmd)}")
retry_result = run_yt_dlp(retry_cmd, hide_cookie_logs=bool(cookies_from_browser))
if retry_result.returncode == 0:
finalize_download(before_retry)
return 0
result = retry_result
if cookies_from_browser and not player_client and has_403_error(result):
print("\n⚠️ Download failed with 403 errors. Retrying with web_safari client...")
retry_cmd = with_player_client(cmd, "web_safari")
before_retry = list_files(output_root)
print(f"Executing: {' '.join(retry_cmd)}")
retry_result = run_yt_dlp(retry_cmd, hide_cookie_logs=bool(cookies_from_browser))
if retry_result.returncode == 0:
finalize_download(before_retry)
return 0
result = retry_result
if not audio_only and format_from_quality:
print("\n⚠️ Download failed. Retrying with non-m3u8 progressive formats...")
retry_cmd = cmd[:]
retry_format = fallback_format_for_quality(quality)
if "-f" in retry_cmd:
format_index = retry_cmd.index("-f") + 1
if format_index < len(retry_cmd):
retry_cmd[format_index] = retry_format
else:
retry_cmd.extend(["-f", retry_format])
before_retry = list_files(output_root)
print(f"Executing: {' '.join(retry_cmd)}")
retry_result = run_yt_dlp(retry_cmd, hide_cookie_logs=bool(cookies_from_browser))
if retry_result.returncode == 0:
finalize_download(before_retry)
return 0
print(f"\n✗ Download failed with exit code {retry_result.returncode}")
return retry_result.returncode
print(f"\n✗ Download failed with exit code {result.returncode}")
return result.returncode
def main():
parser = argparse.ArgumentParser(
description="Download YouTube videos using yt-dlp with robust error handling"
)
parser.add_argument("url", help="YouTube video URL")
parser.add_argument(
"-o",
"--output-dir",
default=".",
help="Output directory (default: current directory)",
)
parser.add_argument(
"--output-template",
help="Output template (e.g., '%%(title)s.%%(ext)s')",
)
parser.add_argument(
"-f", "--format", help="Format specification (e.g., 'bestvideo+bestaudio/best')"
)
parser.add_argument(
"-q",
"--quality",
choices=sorted(QUALITY_PRESETS.keys()),
help="Quality preset (best, 1080p, 720p, 480p, 360p, worst)",
)
parser.add_argument(
"--merge-format",
default="mp4",
help="Merge output container format (default: mp4)",
)
parser.add_argument(
"--subtitles",
action="store_true",
help="Download subtitles if available",
)
parser.add_argument(
"--sub-lang",
default="en",
help="Subtitle languages (comma-separated, default: en)",
)
parser.add_argument(
"--cookies-from-browser",
help="Load cookies from browser (e.g., chrome, firefox)",
)
parser.add_argument(
"--cookies-file",
help="Load cookies from a cookies.txt file",
)
parser.add_argument(
"--player-client",
help="Use a specific YouTube player client (e.g., web_safari)",
)
parser.add_argument(
"--proxy",
help="Proxy URL for yt-dlp and PO Token provider (e.g., http://127.0.0.1:1082)",
)
parser.add_argument(
"--wpc-browser-path",
help="Browser executable path for WPC PO Token provider",
)
auto_group = parser.add_mutually_exclusive_group()
auto_group.add_argument(
"--auto-po-token",
action="store_true",
help="Automatically start a PO Token provider (default)",
)
auto_group.add_argument(
"--no-auto-po-token",
action="store_true",
help="Disable automatic PO Token provider setup",
)
parser.add_argument(
"--playlist",
action="store_true",
help="Allow playlist downloads (default: single video only)",
)
parser.add_argument(
"--no-android-client",
action="store_true",
help="Disable Android client workaround",
)
parser.add_argument(
"-a", "--audio-only", action="store_true", help="Download audio only (as MP3)"
)
parser.add_argument(
"-F", "--list-formats", action="store_true", help="List available formats"
)
parser.add_argument(
"--info",
action="store_true",
help="Print video metadata (title/uploader/duration) and exit",
)
args = parser.parse_args()
exit_code = download_video(
url=args.url,
output_dir=args.output_dir,
format_spec=args.format,
quality=args.quality,
output_template=args.output_template,
merge_format=args.merge_format,
subtitles=args.subtitles,
subtitle_lang=args.sub_lang,
cookies_from_browser=args.cookies_from_browser,
cookies_file=args.cookies_file,
player_client=args.player_client,
auto_po_token=not args.no_auto_po_token,
proxy=args.proxy,
wpc_browser_path=args.wpc_browser_path,
allow_playlist=args.playlist,
use_android_client=not args.no_android_client,
audio_only=args.audio_only,
list_formats=args.list_formats,
info_only=args.info,
)
sys.exit(exit_code)
if __name__ == "__main__":
main()
Related skills
How it compares
Use youtube-downloader when agent-driven yt-dlp runs hit auth, proxy, or shell quoting failures rather than writing ad hoc download commands.
FAQ
Why must YouTube URLs be quoted in zsh for yt-dlp?
youtube-downloader requires single-quoted YouTube URLs because zsh treats ? as a glob character. An unquoted https://www.youtube.com/watch?v=VIDEO_ID can expand or fail before yt-dlp runs.
How does youtube-downloader handle PO Token requirements?
youtube-downloader starts a PO Token provider before downloading, preferring Docker bgutil and falling back to a browser-based provider. Proxy environment variables must be active for both yt-dlp and the token provider.
Is Youtube Downloader safe to install?
skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.