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 at available quality, with optional PO token and authorized cookie recovery - HLS stream downloads with authentication headers - Handling protected content and troubleshooting common download failures Goal Split: File vs. Text (Read First) Before downloading anything, classify the request: - User wants the video/audio file itself (save it, edit it, watch offline) → continue with the dow…

Referer: https://example.com/\\r\\nUser-Agent: Mozilla/5.0...'\n```\n\n### Handling Separate Audio/Video Streams\n\nSome platforms (like Mux) deliver audio and video separately:\n\n1. **Download audio stream:**\n```bash\nffmpeg -headers \"Referer: https://example.com/\" \\\n -protocol_whitelist file,http,https,tcp,tls,crypto,httpproxy \\\n -i \"https://cdn.example.com/audio/rendition.m3u8\" \\\n -c copy audio.m4a\n```\n\n2. **Download video stream:**\n```bash\nffmpeg -headers \"Referer: https://example.com/\" \\\n -protocol_whitelist file,http,https,tcp,tls,crypto,httpproxy \\\n -i \"https://cdn.example.com/video/rendition.m3u8\" \\\n -c copy video.mp4\n```\n\n3. **Merge streams:**\n```bash\nffmpeg -i video.mp4 -i audio.m4a -c copy merged.mp4\n```\n\n### Troubleshooting HLS Downloads\n\n#### 403 Forbidden Errors\n\n**Cause**: Missing or incorrect authentication headers.\n\n**Solution**:\n1. Verify Referer header matches the video source website\n2. Check if additional headers (Origin, User-Agent) are needed\n3. Ensure the m3u8 URL includes all query parameters from browser\n\n#### yt-dlp Stuck on Cookie Extraction\n\n**Symptom**: `Extracting cookies from chrome` hangs indefinitely.\n\n**Solution**: Use ffmpeg directly instead of yt-dlp for HLS streams.\n\n#### Protocol Not Whitelisted\n\n**Error**: `Protocol 'https' not on whitelist 'file,crypto,data'`\n\n**Solution**: Add `-protocol_whitelist file,http,https,tcp,tls,crypto,httpproxy`\n\n#### Empty Segments or No Streams\n\n**Cause**: Expired signatures in the m3u8 URLs.\n\n**Solution**:\n1. Get fresh URLs from browser DevTools\n2. Download immediately after obtaining URLs\n3. Look for rendition URLs with updated signature parameters\n\n### Performance Tips\n\n- HLS downloads typically run at 10-15x realtime speed\n- No re-encoding with `-c copy` (fastest)\n- Monitor download with real-time progress display\n- Use absolute output paths to avoid directory confusion\n\n## Further Reading\n\n- **PO Token Setup**: See `references/po-token-setup.md` for detailed installation and troubleshooting\n- **Internal SOP**: `references/internal-sop.md` — the short list of yt-dlp failures that look like something else (URLs unquoted in zsh, proxy not reaching the PO Token provider). Read it first when a download fails for no obvious reason\n- **yt-dlp Documentation**: https://github.com/yt-dlp/yt-dlp\n- **Format Selection Guide**: https://github.com/yt-dlp/yt-dlp#format-selection\n\n## Next Step: Text From the Video\n\nTwo situations, two routes:\n\n- **The user's goal is text and nothing has been downloaded yet**: do not download the video just to get text — follow the text-first ladder in `references/text-first.md` (existing archived transcripts → platform transcript APIs → yt-dlp subtitle-only → authorized audio track + ASR).\n- **The user already has the downloaded file and now also wants text**: still check existing transcripts and original subtitles using its known source identity. If those checks yield no usable text, reuse the local file at level L4 without another download. Proactively suggest:\n\n```\nDownload complete: [filename]\n\nIf you need the spoken content as text, I can transcribe it for you.\n\nOptions:\nA) Transcribe with /daymade-audio:asr-transcribe-to-text (Recommended for speech-to-text)\nB) No thanks — I just needed the video file\n```","attachment_filenames":["references/internal-sop.md","references/po-token-setup.md","references/text-first.md","scripts/download_video.py","tests/test_download_video.py"],"attachments":[{"filename":"references/internal-sop.md","content":"# YouTube Downloader Internal SOP\n\n1. Quote URLs in shell commands (`'https://www.youtube.com/watch?v=VIDEO_ID'`). Bind the requested video, output type and quality before downloading.\n2. Check the current yt-dlp installation, supported JavaScript runtime/EJS setup and the intended proxy. Use the native default clients first; do not install a provider or select Android as a prerequisite. Pass `--js-runtimes node` to the wrapper only when using an already-installed Node runtime.\n3. Read the failing diagnostic and available media formats. “Only images”, “Requested format is not available”, 360p-only output and HTTP 403 each need diagnosis; none alone establishes a PO Token requirement. A timeout/SSL failure is not token evidence.\n4. If the failed extraction or media request explicitly names a missing/required PO Token, load [provider setup](po-token-setup.md). Within existing installation/service authorization, execute the wrapper with `--auto-po-token`, then verify the original file request. If setup is not authorized, ask only for that missing permission. Do not stop after suggesting a command when it is already authorized.\n5. If an account or persistent bot check requires cookies, obtain cookie permission before supplying either cookie option. Preserve the authorized session. Leave client selection native unless a compatible targeted retry is needed; never switch a cookie download to Android/iOS.\n6. For an authorized provider branch, use bgutil or browser WPC as documented. Keep the WPC window open while minting tokens and use the same intended network path. Pass that proxy to the provider too; in a bridged container, translate a host-local proxy address to `host.docker.internal`. If browser discovery fails, supply `--wpc-browser-path`. Do not install Docker, restart a shared service, or delete a container as an automatic workaround.\n7. For a media 403/fragment error under a quality preset, allow one progressive-format retry and check whether its lower quality still satisfies the request. Preserve custom format selections. For network failures, repair the diagnosed connection issue before another bounded attempt.\n8. Validate the actual local file with `ffprobe`: streams, duration and resolution/audio properties. Report the observed path, size and quality; provider health, metadata and format listings do not prove media delivery.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2380,"content_sha256":"0f30fab2c838dda304fad200dfcaa51bea6410c93ae785abee556070b6375f2a"},{"filename":"references/po-token-setup.md","content":"# PO Token Setup Guide\n\n## What are PO Tokens?\n\nProof 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.\n\n## When to Enter This Branch\n\nStart with yt-dlp's default clients. Use this guide when the failed extraction or media request explicitly identifies a missing/required PO Token, or the user explicitly requests provider setup. A 403, unavailable format, images-only result or requested 1080p/4K quality alone is insufficient.\n\nCheck the current [extractor guidance](https://github.com/yt-dlp/yt-dlp/wiki/Extractors#youtube), [Token enforcement by client/request type](https://github.com/yt-dlp/yt-dlp/wiki/PO-Token-Guide#current-po-token-enforcement) and [EJS setup](https://github.com/yt-dlp/yt-dlp/wiki/EJS). Treat JavaScript challenge/runtime failures separately from token failures. Do not assume a provider grants every source quality.\n\n## Operational SOP (Internal)\n\nFollow [the download SOP](internal-sop.md) for diagnosis, authorization and file acceptance. Within an already-authorized setup branch, run the preparation and resume the original download yourself. Otherwise obtain only the missing installation/service permission. Cookie extraction requires its own authorization.\n\nFor a failed explicit token requirement, the wrapper's `--auto-po-token` option permits provider setup and a token-assisted retry after its native attempt. `--info` and `-F` never start the helper. Direct provider setup below remains available for an explicit setup request.\n\nUse `mweb` with web tokens for a targeted non-cookie retry; preserve compatible cookie clients (the wrapper uses `web_safari`) when authorized cookies are present. Web tokens are not Android/iOS tokens. Keep a WPC browser window open while it mints tokens. Preserve shared providers on health failure rather than deleting/restarting them automatically.\n\n### Authorized Provider Setup\n\nAfter setup is authorized, reuse a healthy installed provider. If a new Docker provider is needed and Docker is already available:\n\n1. Install the plugin into yt-dlp's Python environment (one-time):\n```bash\n\u003cYTDLP_PYTHON> -m pip install bgutil-ytdlp-pot-provider\n```\nIn China, prefer a local PyPI mirror:\n```bash\n\u003cYTDLP_PYTHON> -m pip install bgutil-ytdlp-pot-provider -i https://pypi.tuna.tsinghua.edu.cn/simple\n```\n\n2. Start the provider (Docker):\n```bash\ndocker run -d --name bgutil-pot-provider -p 4416:4416 --init brainicism/bgutil-ytdlp-pot-provider\n```\n\n3. Retry yt-dlp downloads using a web client (e.g., `mweb`) so PO tokens apply.\n\n### Installation\n\nInstall a PO token provider plugin to handle token generation automatically. The plugin must be installed into yt-dlp's own Python environment.\n\n**Step 1: Locate yt-dlp's Python**\n\n```bash\nhead -1 $(which yt-dlp)\n# Output example: #!/opt/homebrew/Cellar/yt-dlp/2025.10.22/libexec/bin/python\n```\n\n**Step 2: Install Plugin**\n\n```bash\n# For Homebrew-installed yt-dlp (macOS)\n\u003cYTDLP_PYTHON> -m pip install bgutil-ytdlp-pot-provider\n\n# For pip-installed yt-dlp\npython3 -m pip install bgutil-ytdlp-pot-provider --user\n```\n\n**Step 3: Verify Installation**\n\n```bash\nyt-dlp -F \"https://youtu.be/VIDEO_ID\"\n```\n\nInspect verbose yt-dlp output for the selected provider and successful token acquisition. Then download the requested format and verify the file. Format availability alone does not prove which provider supplied it.\n\n## Available PO Token Provider Plugins\n\n### 1. bgutil-ytdlp-pot-provider (Recommended)\n\n- **Installation**: `pip install bgutil-ytdlp-pot-provider`\n- **Requires**: yt-dlp 2025.05.22 or above\n- **Automatic**: Works transparently once installed\n- **Best for**: General use, most reliable\n\n### 2. yt-dlp-get-pot\n\n- **Installation**: `pip install yt-dlp-get-pot`\n- **Requires**: yt-dlp 2025.01.15 or above\n- **Method**: Launches browser to mint tokens\n- **Best for**: Users comfortable with browser automation\n\n### 3. yt-dlp-getpot-wpc (Browser-based, no Docker)\n\n- **Installation**: `pip install yt-dlp-getpot-wpc`\n- **Requires**: yt-dlp 2025.09.26 or above\n- **Method**: Uses a browser window to mint tokens\n- **Best for**: Environments without Docker or restricted networks\n\nIn China, prefer a local PyPI mirror:\n```bash\npip install yt-dlp-getpot-wpc -i https://pypi.tuna.tsinghua.edu.cn/simple\n```\n\n### 4. yt-dlp-get-pot-rustypipe\n\n- **Installation**: `pip install yt-dlp-get-pot-rustypipe`\n- **Method**: Uses rustypipe-botguard\n- **Supports**: All web-based YouTube clients\n- **Best for**: Advanced users, specific client requirements\n\n## Verification Workflow\n\n### Check Available Formats\n\n```bash\nyt-dlp -F \"https://youtu.be/VIDEO_ID\"\n```\n\nCompare the actual format list with the requested media. Format IDs and maximum resolution vary by source and client. Do not infer provider status from a fixed ID list or resolution table.\n\n### Download Best Quality\n\n```bash\n# 1080p max\nyt-dlp -f \"bestvideo[height\u003c=1080]+bestaudio/best\" \"VIDEO_URL\"\n\n# Best available (4K if available)\nyt-dlp -f \"bestvideo+bestaudio/best\" \"VIDEO_URL\"\n```\n\n### Verify Downloaded Quality\n\n```bash\n# Check resolution\nffprobe -v error -select_streams v:0 -show_entries stream=width,height -of default=noprint_wrappers=1 video.mp4\n```\n\n## Troubleshooting\n\n### Plugin Not Working\n\n**Symptom**: The chosen provider reports a token-generation or connection error\n\n**Solution**:\n1. Verify yt-dlp version: `yt-dlp --version` (need 2025.05.22+)\n2. Check plugin installation: `pip list | grep bgutil`\n3. Ensure plugin installed in correct Python environment\n4. Read the provider error and repair its diagnosed dependency or connection within authorization. Do not reinstall or restart a shared service on a resolution-only symptom.\n\n### Warning Messages\n\n```\nWARNING: [youtube] [pot:bgutil:http] Error reaching GET http://127.0.0.1:4416/ping\n```\n\n**Impact**: The named HTTP provider did not answer. Other clients/providers may still complete the download; this message alone does not establish the final result.\n\n**Action**: No action needed if download succeeds with high-quality formats.\n\n## Alternative: Browser Cookies Method\n\nWhen an account or persistent bot check requires an authenticated session, obtain cookie permission before using:\n\n```bash\n# Firefox\nyt-dlp --cookies-from-browser firefox \"VIDEO_URL\"\n\n# Chrome\nyt-dlp --cookies-from-browser chrome \"VIDEO_URL\"\n```\n\n**Benefits**:\n- Access age-restricted content\n- Access members-only content\n- Availability according to that account's actual access; no quality guarantee\n\n**Requirements**:\n- Must be logged into YouTube in the browser\n- Browser and yt-dlp must use same IP address\n\n## Quality and Recovery Boundaries\n\nVerify the downloaded file rather than promising quality by setup type. Cookies do not replace a missing Token; a provider does not fix missing EJS components, network timeouts or absent source formats. Stop escalating once the requested file is verified. Keep a failed provider setup visible and resume from that missing prerequisite after it is repaired.\n\n## References\n\n- [yt-dlp PO Token Guide](https://github.com/yt-dlp/yt-dlp/wiki/PO-Token-Guide)\n- [bgutil-ytdlp-pot-provider](https://github.com/Brainicism/bgutil-ytdlp-pot-provider)\n- [yt-dlp GitHub](https://github.com/yt-dlp/yt-dlp)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7343,"content_sha256":"6197f22d857405f51da67bb699b28692528d55a627cd832b5608b388de12d0b8"},{"filename":"references/text-first.md","content":"# Text-First Ladder: Get the Spoken Content Without Downloading the Video\n\nLoad this when the user's goal is the **text** of a video (transcript, subtitles,\nmeeting notes, archive, \"what was said\") rather than the video/audio file itself.\nWhen the user wants the file, ignore this ladder and use the normal download flow\nin SKILL.md — a text-first miss never blocks a file request.\n\nClimb down the ladder; stop at the first level that yields usable text. Every\nlevel names its own failure exit. Do not batch-download media to hunt for text:\neach level targets only the specific video(s) the user named.\n\n**Identity rule (all levels):** match sources by the video's ID (the `v=` parameter\nor platform equivalent), never by title alone. A title hit is a candidate, not a\nmatch — confirm the archived/API record's video ID equals the requested one before\nreusing its text.\n\n## L1 — Existing archives you already own\n\n**Trigger:** always start here.\n\n**Entry:**\n1. If available, use `skills-search` across owned sources for a skill that already archives this\n content (e.g. favorites/web-archive owners), then query that owner's archive.\n2. If available, use `favorites-search` over the existing favorites/subscription collections for the\n video ID. If either helper or its archive is unavailable, record that route as\n unchecked and continue to L2/L3; do not require installation of an inaccessible\n helper or treat its absence as proof that no transcript exists.\n\nReuse archived text as-is when the record's video ID matches and the snapshot is\ncomplete (full transcript, not a stub). Do not re-download or re-transcribe what\nis already archived.\n\n**Failure =** no archive record with a matching video ID, or the record is a\nstub/pointer without full text. **Exit → L2.**\n\n## L2 — Ready transcripts from existing services\n\n**Trigger:** check the user's already selected transcript services for the video,\nincluding conference talks and other non-podcast videos. This may include Guyu\nknowledge-base transcripts or Podwise episodes; do not require a new subscription\nor processing purchase to complete this level. Use the available service-owning\nSkill and verify its current read route before fetching.\n\n**Podwise owner, when available:** the `podwise-reader` skill (a separate thin bundle: read-only transcript\nretrieval + local export). If it is not installed, treat this level as a pointer\nonly — do not invent CLI invocations or API paths beyond the verified surface below.\n\nVerified read-only surface (from the official Podwise CLI v0.4.2 source): direct\nHTTPS GET against the open API, i.e. `GET /api/open/v1/episodes/\u003cepisode_seq>` and\nits transcript endpoint, using the user's existing token.\n\n**Forbidden for read-only retrieval:** the official CLI v0.4.2 `get transcript` and\n`get summary` commands asynchronously call MarkAsRead server-side after a\nsuccessful GET — they mutate account state. Do not use them when the goal is only\nto read text; use the direct GET methods above or wait for `podwise-reader`.\n\nMatch the episode by its source video ID, never by title. If there is no matching\nepisode, do not auto-process / translate / import anything.\n\n**Failure =** no episode whose source video ID matches, or only mutation-capable\nCLI access is available. **Exit → L3.**\n\n## L3 — Original subtitles via yt-dlp (subtitle-only, no media download)\n\n**Trigger:** L1–L2 missed and the platform exposes subtitle tracks (YouTube and\nmost yt-dlp-supported sites).\n\n**Entry:** list actual tracks first. Select an available original or requested\nlanguage; the English/Chinese expression below is an example, not a requirement\nfor every video. Do not call subtitles missing merely because that example\nexcludes the source language. Flags were checked against yt-dlp 2026.03.17.\n\n```bash\n# 1. List available subtitle tracks (simulates; downloads nothing)\nyt-dlp --skip-download --list-subs \"VIDEO_URL\"\n\n# 2. Fetch subtitles only — --skip-download guarantees no video/audio is written\nyt-dlp --skip-download --write-subs --write-auto-subs \\\n --sub-langs \"en.*,zh-Hans,zh-Hant\" --sub-format \"srt/vtt/best\" --convert-subs srt \\\n -o \"%(title)s-%(id)s.%(ext)s\" \"VIDEO_URL\"\n```\n\n- Prefer uploader-provided tracks (`--write-subs`); auto-generated subs\n (`--write-auto-subs`) are a fallback. Uploaded tracks also do not prove human\n authorship or review; claim a reviewed status only with separate evidence.\n- Auto-generated or uncorrected subtitles archived downstream must be labeled\n \"auto-generated, not human-reviewed (未精校)\" with the source URL and retrieval\n timestamp.\n- Subtitle requests hit the same bot-check wall as downloads: the cookie and\n PO Token rules in SKILL.md's Reliable Download SOP apply unchanged. Do not\n restate or weaken them here.\n\n**Failure =** no subtitle track in the wanted language (neither human nor auto),\nor retrieval still blocked after the standard cookie/PO-token recovery.\n**Exit → L4.**\n\n## L4 — Authorized audio track + ASR after the checked text routes miss\n\n**Trigger:** no usable text was obtained through the checked L1–L3 channels; the\nuser still wants the text. Record unavailable channels as unknown, not proof that\nno transcript exists anywhere.\n\n**Entry:**\n1. Confirm the user has the rights/permission for this content — the same\n authorization, playlist, cookie and PO Token rules as a normal file download\n apply unchanged.\n2. Download the **audio track only**, never the full video:\n\n```bash\nyt-dlp -f bestaudio -x --audio-format mp3 \"VIDEO_URL\"\n# or\nyt-dlp -f bestaudio \"VIDEO_URL\"\n```\n\nRequire an audio-only format: `-x` alone defaults to `bestaudio/best` and can\nfetch a combined video when no separate audio track is offered. An unavailable\n`bestaudio` format leaves this route blocked; do not add a video fallback.\nFor an existing local video/audio file, reuse that file at this level after the\nsame identity-based text checks, without downloading another copy.\n\n3. Hand the local audio file to `/daymade-audio:asr-transcribe-to-text` for\n GPU transcription. CPU inference is a hard stop, not a fallback: if no\n GPU-capable route is available, report blocked instead of transcribing on CPU.\n\n**Failure =** no authorized audio track, or no GPU ASR route available. **Exit →**\nstop and report that no text was obtained; offer the normal file-download flow if\nthe user also wants the video itself.\n\n## Invariants\n\n- This ladder changes nothing about the download flow: defaults, cookie/account\n authorization, playlist confirmation, PO Token recovery, HLS handling, retry and\n proxy behavior in SKILL.md are all untouched.\n- A text-first miss never converts into \"just download the video anyway\" without\n the user asking for the file.\n- Archive results carry their provenance: which level produced the text, source\n URL/video ID, retrieval timestamp, and review status (uploaded, automatic or ASR; unreviewed unless explicitly checked).\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6951,"content_sha256":"7736825ccc5a8638d98eeb0053e882372acc09dcef1b787b8af049375bf0ae97"},{"filename":"scripts/download_video.py","content":"#!/usr/bin/env python3\n\"\"\"\nYouTube video downloader using yt-dlp with robust error handling.\n\nThis script handles common issues like nsig extraction failures and network problems,\nespecially useful for users behind proxies or in regions with YouTube access issues.\n\nRequirements:\n - yt-dlp: Install via `brew install yt-dlp` (macOS) or `pip install yt-dlp` (cross-platform)\n - A supported JavaScript runtime and EJS components for current YouTube extraction\n - Optional PO token provider: see ../references/po-token-setup.md\n\nUsage:\n scripts/download_video.py \"https://youtu.be/VIDEO_ID\"\n scripts/download_video.py \"https://youtu.be/VIDEO_ID\" --audio-only\n scripts/download_video.py \"https://youtu.be/VIDEO_ID\" --quality 1080p\n scripts/download_video.py \"https://youtu.be/VIDEO_ID\" -o ~/Downloads\n\nNote:\n Start with yt-dlp's default clients. --auto-po-token authorizes provider setup\n only after a failed download with an explicit missing-token diagnostic.\n Browser cookies and alternate clients require explicit options.\n\"\"\"\n\nimport argparse\nimport json\nimport subprocess\nimport sys\nimport shutil\nimport time\nimport os\nimport re\nfrom pathlib import Path\nfrom typing import Iterable, Optional\nfrom urllib.parse import quote, urlparse, urlunparse\nfrom urllib.request import urlopen\nfrom urllib.error import URLError\n\n\nPYPI_MIRROR = \"https://pypi.tuna.tsinghua.edu.cn/simple\"\n\nQUALITY_PRESETS = {\n \"best\": \"bestvideo+bestaudio/best\",\n \"1080p\": \"bestvideo[height\u003c=1080]+bestaudio/best\",\n \"720p\": \"bestvideo[height\u003c=720]+bestaudio/best\",\n \"480p\": \"bestvideo[height\u003c=480]+bestaudio/best\",\n \"360p\": \"bestvideo[height\u003c=360]+bestaudio/best\",\n \"worst\": \"worstvideo+worstaudio/worst\",\n}\n\n\ndef build_output_template(output_dir: str, template: Optional[str]) -> str:\n if template:\n template_path = Path(template)\n if template_path.is_absolute():\n return template\n return str(Path(output_dir).expanduser().resolve() / template)\n\n return str(Path(output_dir).expanduser().resolve() / \"%(title)s.%(ext)s\")\n\n\ndef list_files(root: Path) -> set:\n return {path for path in root.rglob(\"*\") if path.is_file()}\n\n\ndef human_size(num_bytes: int) -> str:\n if num_bytes \u003c 1024:\n return f\"{num_bytes} B\"\n for unit in [\"KB\", \"MB\", \"GB\", \"TB\"]:\n num_bytes /= 1024.0\n if num_bytes \u003c 1024:\n return f\"{num_bytes:.1f} {unit}\"\n return f\"{num_bytes:.1f} PB\"\n\n\ndef pick_primary_file(files: Iterable[Path], audio_only: bool) -> Optional[Path]:\n video_exts = {\".mp4\", \".webm\", \".mkv\", \".mov\", \".m4v\"}\n audio_exts = {\".mp3\", \".m4a\", \".opus\", \".aac\", \".flac\", \".wav\"}\n candidates = []\n for path in files:\n if path.suffix.lower() in {\".part\", \".ytdl\", \".tmp\"}:\n continue\n if audio_only:\n if path.suffix.lower() in audio_exts:\n candidates.append(path)\n else:\n if path.suffix.lower() in video_exts:\n candidates.append(path)\n\n if not candidates:\n candidates = [path for path in files if path.suffix.lower() not in {\".part\", \".ytdl\", \".tmp\"}]\n\n if not candidates:\n return None\n\n return max(candidates, key=lambda p: p.stat().st_size)\n\n\ndef get_video_resolution(path: Path) -> Optional[str]:\n check = subprocess.run([\"which\", \"ffprobe\"], capture_output=True, text=True)\n if check.returncode != 0:\n return None\n\n cmd = [\n \"ffprobe\",\n \"-v\",\n \"error\",\n \"-select_streams\",\n \"v:0\",\n \"-show_entries\",\n \"stream=width,height\",\n \"-of\",\n \"csv=p=0:s=x\",\n str(path),\n ]\n result = subprocess.run(cmd, capture_output=True, text=True)\n if result.returncode != 0:\n return None\n value = result.stdout.strip()\n return value or None\n\n\ndef filter_cookie_lines(text: str) -> str:\n if not text:\n return \"\"\n filtered = []\n for line in text.splitlines():\n lowered = line.lower()\n if \"extracting cookies\" in lowered:\n continue\n if \"extracted\" in lowered and \"cookies\" in lowered:\n continue\n filtered.append(line)\n return \"\\n\".join(filtered)\n\n\ndef run_yt_dlp(cmd: list, hide_cookie_logs: bool = False) -> subprocess.CompletedProcess:\n # Retain diagnostics even without cookies, while keeping progress visible.\n with subprocess.Popen(\n cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,\n text=True, encoding=\"utf-8\", errors=\"replace\",\n ) as process:\n lines = []\n for line in process.stdout:\n lines.append(line)\n visible = filter_cookie_lines(line) if hide_cookie_logs else line.rstrip(\"\\n\")\n if visible:\n print(visible, flush=True)\n return subprocess.CompletedProcess(cmd, process.wait(), \"\".join(lines), \"\")\n\n\ndef final_error(result: subprocess.CompletedProcess) -> str:\n \"\"\"Use the terminal failure, not explanatory words in an unused-format warning.\"\"\"\n text = ((result.stdout or \"\") + \"\\n\" + (result.stderr or \"\")).lower()\n errors = [line.strip() for line in text.splitlines() if re.match(\n r\"^(?:error:\\s*\\S|\\[download\\].*error:|http error [45])\", line.strip())]\n return errors[-1] if errors else \"\"\n\n\ndef has_403_error(result: subprocess.CompletedProcess) -> bool:\n text = final_error(result)\n return any(marker in text for marker in (\n \"http error 403\", \"403: forbidden\", \"fragment 1 not found\",\n ))\n\n\ndef has_pot_error(result: subprocess.CompletedProcess) -> bool:\n \"\"\"Identify an explicit missing-token diagnostic, not a generic download error.\"\"\"\n if result.returncode == 0:\n return False\n text = ((result.stdout or \"\") + \"\\n\" + (result.stderr or \"\")).lower()\n # A provider timeout or a warning for an unused client does not explain a\n # failed network request. Leave these failures on the network/recovery path.\n if any(word in text for word in (\"timed out\", \"timeout\", \"ssl\", \"connection reset\", \"network is unreachable\")):\n return False\n token_lines = [\n line for line in text.splitlines()\n if\n re.search(r\"\\bpo[ _-]?tokens?\\b\", line)\n and re.search(r\"\\b(require[ds]?|required|missing|not provided|not supplied|must provide)\\b\", line)\n ]\n failure = final_error(result)\n return bool(token_lines) and (\n failure in token_lines\n or any(marker in failure for marker in (\n \"only images are available\", \"requested format is not available\",\n \"http error 403\", \"403: forbidden\",\n ))\n )\n\n\ndef has_wpc_error(result: subprocess.CompletedProcess) -> bool:\n text = \"\"\n if hasattr(result, \"stdout\") and result.stdout:\n text += result.stdout\n if hasattr(result, \"stderr\") and result.stderr:\n text += result.stderr\n text = text.lower()\n return \"pot:wpc\" in text or \"webpoclient\" in text\n\n\ndef with_player_client(cmd: list, client: str) -> list:\n \"\"\"Replace only the YouTube player_client, retaining provider arguments.\"\"\"\n rebuilt = []\n youtube_args = []\n index = 0\n while index \u003c len(cmd):\n token = cmd[index]\n if token == \"--extractor-args\" and index + 1 \u003c len(cmd):\n value = cmd[index + 1]\n if value.startswith(\"youtube:\"):\n youtube_args.extend(\n part for part in value[len(\"youtube:\"):].split(\";\")\n if part and not part.startswith(\"player_client=\"))\n else:\n rebuilt.extend([token, value])\n index += 2\n continue\n rebuilt.append(token)\n index += 1\n youtube_args.append(f\"player_client={client}\")\n # yt-dlp replaces repeated dictionaries for the same extractor. Keep all\n # YouTube settings together so its real parser retains them.\n rebuilt.extend([\"--extractor-args\", \"youtube:\" + \";\".join(youtube_args)])\n return rebuilt\n\n\ndef get_proxy_settings(proxy_arg: Optional[str]) -> tuple[Optional[str], Optional[str]]:\n if proxy_arg:\n proxy = proxy_arg\n else:\n proxy = (\n os.environ.get(\"ALL_PROXY\")\n or os.environ.get(\"all_proxy\")\n or os.environ.get(\"HTTPS_PROXY\")\n or os.environ.get(\"https_proxy\")\n or os.environ.get(\"HTTP_PROXY\")\n or os.environ.get(\"http_proxy\")\n )\n no_proxy = os.environ.get(\"NO_PROXY\") or os.environ.get(\"no_proxy\")\n return proxy, no_proxy\n\n\ndef normalize_proxy_for_docker(proxy_url: str) -> str:\n parsed = urlparse(proxy_url)\n if parsed.hostname in {\"127.0.0.1\", \"localhost\"}:\n host = \"host.docker.internal\"\n netloc = \"\"\n if parsed.username or parsed.password:\n userinfo = parsed.username or \"\"\n if parsed.password:\n userinfo += f\":{parsed.password}\"\n netloc = f\"{userinfo}@\"\n if parsed.port:\n netloc += f\"{host}:{parsed.port}\"\n else:\n netloc += host\n parsed = parsed._replace(netloc=netloc)\n return urlunparse(parsed)\n return proxy_url\n\n\ndef is_localhost_proxy(proxy_url: str) -> bool:\n parsed = urlparse(proxy_url)\n return parsed.hostname in {\"127.0.0.1\", \"localhost\"}\n\n\ndef find_chrome_path() -> Optional[str]:\n candidates = [\n \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\",\n \"/Applications/Chromium.app/Contents/MacOS/Chromium\",\n ]\n for candidate in candidates:\n if Path(candidate).exists():\n return candidate\n for name in [\"google-chrome\", \"chromium\", \"chromium-browser\", \"chrome\"]:\n path = shutil.which(name)\n if path:\n return path\n return None\n\n\ndef with_wpc_browser(cmd: list, browser_path: Optional[str]) -> list:\n if not browser_path:\n return cmd\n return cmd + [\"--extractor-args\", f\"youtubepot-wpc:browser_path={browser_path}\"]\n\n\ndef provider_ping(url: str = \"http://127.0.0.1:4416/ping\") -> bool:\n try:\n with urlopen(url, timeout=3) as response:\n return response.status == 200\n except (URLError, ConnectionResetError, TimeoutError):\n return False\n\n\ndef docker_available() -> bool:\n if not shutil.which(\"docker\"):\n return False\n result = subprocess.run([\"docker\", \"--version\"], capture_output=True, text=True)\n return result.returncode == 0\n\n\ndef docker_daemon_ready() -> bool:\n result = subprocess.run([\"docker\", \"info\"], capture_output=True, text=True)\n return result.returncode == 0\n\n\ndef wait_for_provider(timeout: int = 10) -> bool:\n deadline = time.time() + timeout\n while time.time() \u003c deadline:\n if provider_ping():\n return True\n time.sleep(1)\n return False\n\n\ndef container_exists(name: str) -> bool:\n result = subprocess.run(\n [\"docker\", \"ps\", \"-a\", \"--filter\", f\"name={name}\", \"--format\", \"{{.Names}}\"],\n capture_output=True,\n text=True,\n )\n return result.returncode == 0 and name in result.stdout.split()\n\n\ndef parse_yt_dlp_version() -> Optional[str]:\n result = subprocess.run([\"yt-dlp\", \"--version\"], capture_output=True, text=True)\n if result.returncode != 0:\n return None\n return result.stdout.strip()\n\n\ndef version_at_least(version: str, minimum: str) -> bool:\n def parse(value: str) -> list:\n return [int(part) for part in value.split(\".\") if part.isdigit()]\n\n current = parse(version)\n required = parse(minimum)\n if not current or not required:\n return False\n while len(current) \u003c len(required):\n current.append(0)\n while len(required) \u003c len(current):\n required.append(0)\n return current >= required\n\n\ndef yt_dlp_python() -> Optional[str]:\n yt_dlp_path = shutil.which(\"yt-dlp\")\n if not yt_dlp_path:\n return None\n try:\n with open(yt_dlp_path, \"r\", encoding=\"utf-8\") as handle:\n first = handle.readline().strip()\n except OSError:\n return None\n if not first.startswith(\"#!\"):\n return None\n shebang = first[2:].strip()\n if shebang.endswith(\"env python3\") or shebang.endswith(\"env python\"):\n return \"python3\"\n return shebang\n\n\ndef ensure_pot_plugin_installed(proxy_url: Optional[str]) -> bool:\n version = parse_yt_dlp_version()\n if not version or not version_at_least(version, \"2025.05.22\"):\n print(\"⚠️ yt-dlp needs to be updated before enabling PO Token provider.\")\n return False\n\n python_bin = yt_dlp_python()\n if not python_bin:\n print(\"⚠️ Unable to locate yt-dlp's Python interpreter for plugin install.\")\n return False\n\n check = subprocess.run(\n [python_bin, \"-m\", \"pip\", \"show\", \"bgutil-ytdlp-pot-provider\"],\n capture_output=True,\n text=True,\n )\n if check.returncode == 0:\n return True\n\n print(\"⚠️ Installing PO Token provider plugin (one-time setup)...\")\n install_cmd = [python_bin, \"-m\", \"pip\", \"install\", \"bgutil-ytdlp-pot-provider\", \"-i\", PYPI_MIRROR]\n if proxy_url:\n install_cmd.extend([\"--proxy\", proxy_url])\n install = subprocess.run(install_cmd, capture_output=True, text=True)\n return install.returncode == 0\n\n\ndef ensure_wpc_provider(proxy_url: Optional[str]) -> bool:\n version = parse_yt_dlp_version()\n if not version or not version_at_least(version, \"2025.09.26\"):\n print(\"⚠️ yt-dlp needs to be updated before enabling the WPC PO Token provider.\")\n return False\n\n python_bin = yt_dlp_python()\n if not python_bin:\n print(\"⚠️ Unable to locate yt-dlp's Python interpreter for WPC provider install.\")\n return False\n\n check = subprocess.run(\n [python_bin, \"-m\", \"pip\", \"show\", \"yt-dlp-getpot-wpc\"],\n capture_output=True,\n text=True,\n )\n if check.returncode == 0:\n return True\n\n print(\"⚠️ Installing WPC PO Token provider (one-time setup)...\")\n install_cmd = [python_bin, \"-m\", \"pip\", \"install\", \"-U\", \"yt-dlp-getpot-wpc\", \"-i\", PYPI_MIRROR]\n if proxy_url:\n install_cmd.extend([\"--proxy\", proxy_url])\n install = subprocess.run(install_cmd, capture_output=True, text=True)\n return install.returncode == 0\n\n\ndef ensure_po_token_provider(proxy_url: Optional[str], no_proxy: Optional[str]) -> Optional[str]:\n if not ensure_pot_plugin_installed(proxy_url):\n return \"wpc\" if ensure_wpc_provider(proxy_url) else None\n\n if provider_ping():\n return \"bgutil\"\n\n if not docker_available():\n print(\"⚠️ Docker is not available. Switching to browser-based PO Token provider...\")\n return \"wpc\" if ensure_wpc_provider(proxy_url) else None\n\n if not docker_daemon_ready():\n print(\"⚠️ Docker daemon is not running. Switching to browser-based PO Token provider...\")\n return \"wpc\" if ensure_wpc_provider(proxy_url) else None\n\n name = \"bgutil-pot-provider\"\n if container_exists(name):\n start = subprocess.run([\"docker\", \"start\", name], capture_output=True, text=True)\n if start.returncode != 0:\n print(\"⚠️ Docker container failed to start. Switching to browser-based PO Token provider...\")\n return \"wpc\" if ensure_wpc_provider(proxy_url) else None\n else:\n env_args = []\n use_host_network = False\n docker_proxy = None\n if proxy_url:\n use_host_network = is_localhost_proxy(proxy_url)\n docker_proxy = proxy_url if use_host_network else normalize_proxy_for_docker(proxy_url)\n env_args.extend(\n [\n \"-e\",\n f\"HTTP_PROXY={docker_proxy}\",\n \"-e\",\n f\"HTTPS_PROXY={docker_proxy}\",\n \"-e\",\n f\"ALL_PROXY={docker_proxy}\",\n \"-e\",\n f\"http_proxy={docker_proxy}\",\n \"-e\",\n f\"https_proxy={docker_proxy}\",\n \"-e\",\n f\"all_proxy={docker_proxy}\",\n ]\n )\n if no_proxy:\n env_args.extend(\n [\n \"-e\",\n f\"NO_PROXY={no_proxy}\",\n \"-e\",\n f\"no_proxy={no_proxy}\",\n ]\n )\n run_cmd = [\"docker\", \"run\", \"-d\", \"--name\", name]\n if use_host_network:\n run_cmd.extend([\"--network\", \"host\"])\n run_cmd.extend(\n [\n \"-p\",\n \"4416:4416\",\n *env_args,\n \"--init\",\n \"brainicism/bgutil-ytdlp-pot-provider\",\n ]\n )\n\n run = subprocess.run(run_cmd, capture_output=True, text=True)\n if run.returncode != 0:\n if use_host_network and proxy_url:\n # Retry without host network using host.docker.internal proxy\n docker_proxy = normalize_proxy_for_docker(proxy_url)\n env_args = [\n \"-e\",\n f\"HTTP_PROXY={docker_proxy}\",\n \"-e\",\n f\"HTTPS_PROXY={docker_proxy}\",\n \"-e\",\n f\"ALL_PROXY={docker_proxy}\",\n \"-e\",\n f\"http_proxy={docker_proxy}\",\n \"-e\",\n f\"https_proxy={docker_proxy}\",\n \"-e\",\n f\"all_proxy={docker_proxy}\",\n ]\n if no_proxy:\n env_args.extend(\n [\n \"-e\",\n f\"NO_PROXY={no_proxy}\",\n \"-e\",\n f\"no_proxy={no_proxy}\",\n ]\n )\n retry = subprocess.run(\n [\n \"docker\",\n \"run\",\n \"-d\",\n \"--name\",\n name,\n \"-p\",\n \"4416:4416\",\n *env_args,\n \"--init\",\n \"brainicism/bgutil-ytdlp-pot-provider\",\n ],\n capture_output=True,\n text=True,\n )\n if retry.returncode == 0:\n run = retry\n else:\n print(\"⚠️ Docker provider failed to start. Switching to browser-based PO Token provider...\")\n return \"wpc\" if ensure_wpc_provider(proxy_url) else None\n else:\n print(\"⚠️ Docker provider failed to start. Switching to browser-based PO Token provider...\")\n return \"wpc\" if ensure_wpc_provider(proxy_url) else None\n\n if wait_for_provider():\n print(\"✓ PO Token provider is running.\")\n return \"bgutil\"\n\n # Do not remove/restart a shared container just because its health probe failed.\n print(\"⚠️ Docker provider did not respond; preserving it and trying the browser provider...\")\n return \"wpc\" if ensure_wpc_provider(proxy_url) else None\n\n\ndef restart_po_token_provider(proxy_url: Optional[str], no_proxy: Optional[str]) -> bool:\n name = \"bgutil-pot-provider\"\n if not docker_available() or not docker_daemon_ready():\n return False\n if container_exists(name):\n subprocess.run([\"docker\", \"rm\", \"-f\", name], capture_output=True, text=True)\n env_args = []\n use_host_network = False\n docker_proxy = None\n if proxy_url:\n use_host_network = is_localhost_proxy(proxy_url)\n docker_proxy = proxy_url if use_host_network else normalize_proxy_for_docker(proxy_url)\n env_args.extend(\n [\n \"-e\",\n f\"HTTP_PROXY={docker_proxy}\",\n \"-e\",\n f\"HTTPS_PROXY={docker_proxy}\",\n \"-e\",\n f\"ALL_PROXY={docker_proxy}\",\n \"-e\",\n f\"http_proxy={docker_proxy}\",\n \"-e\",\n f\"https_proxy={docker_proxy}\",\n \"-e\",\n f\"all_proxy={docker_proxy}\",\n ]\n )\n if no_proxy:\n env_args.extend(\n [\n \"-e\",\n f\"NO_PROXY={no_proxy}\",\n \"-e\",\n f\"no_proxy={no_proxy}\",\n ]\n )\n run_cmd = [\"docker\", \"run\", \"-d\", \"--name\", name]\n if use_host_network:\n run_cmd.extend([\"--network\", \"host\"])\n run_cmd.extend(\n [\n \"-p\",\n \"4416:4416\",\n *env_args,\n \"--init\",\n \"brainicism/bgutil-ytdlp-pot-provider\",\n ]\n )\n\n run = subprocess.run(run_cmd, capture_output=True, text=True)\n if run.returncode != 0 and use_host_network:\n docker_proxy = normalize_proxy_for_docker(proxy_url)\n env_args = [\n \"-e\",\n f\"HTTP_PROXY={docker_proxy}\",\n \"-e\",\n f\"HTTPS_PROXY={docker_proxy}\",\n \"-e\",\n f\"ALL_PROXY={docker_proxy}\",\n \"-e\",\n f\"http_proxy={docker_proxy}\",\n \"-e\",\n f\"https_proxy={docker_proxy}\",\n \"-e\",\n f\"all_proxy={docker_proxy}\",\n ]\n if no_proxy:\n env_args.extend(\n [\n \"-e\",\n f\"NO_PROXY={no_proxy}\",\n \"-e\",\n f\"no_proxy={no_proxy}\",\n ]\n )\n subprocess.run(\n [\n \"docker\",\n \"run\",\n \"-d\",\n \"--name\",\n name,\n \"-p\",\n \"4416:4416\",\n *env_args,\n \"--init\",\n \"brainicism/bgutil-ytdlp-pot-provider\",\n ],\n capture_output=True,\n text=True,\n )\n return wait_for_provider()\n\n\ndef fallback_format_for_quality(quality: Optional[str]) -> str:\n if not quality or quality == \"best\":\n return \"best[protocol!*=m3u8][ext=mp4]/best[protocol!*=m3u8]/best\"\n if quality.endswith(\"p\") and quality[:-1].isdigit():\n height = quality[:-1]\n return (\n f\"best[height\u003c={height}][protocol!*=m3u8][ext=mp4]/\"\n f\"best[height\u003c={height}][protocol!*=m3u8]/best[protocol!*=m3u8]\"\n )\n return \"best[protocol!*=m3u8][ext=mp4]/best[protocol!*=m3u8]/best\"\n\n\ndef print_video_info(cmd_base: list, url: str, hide_cookie_logs: bool = False) -> int:\n info_cmd = cmd_base + [\"--skip-download\", \"--dump-json\", \"--no-playlist\", url]\n result = subprocess.run(info_cmd, capture_output=True, text=True)\n if result.returncode != 0:\n print(\"✗ Failed to fetch video metadata\")\n if result.stderr:\n print(filter_cookie_lines(result.stderr).strip())\n fallback = fetch_oembed_info(url)\n if fallback:\n print(\"\\n✓ Retrieved metadata via YouTube oEmbed (limited fields).\")\n render_oembed_info(fallback)\n return 0\n return result.returncode\n\n first_line = result.stdout.strip().splitlines()[0] if result.stdout else \"\"\n if not first_line:\n print(\"✗ No metadata returned\")\n return 1\n\n try:\n info = json.loads(first_line)\n title = info.get(\"title\", \"Unknown title\")\n uploader = info.get(\"uploader\") or info.get(\"channel\") or \"Unknown uploader\"\n duration = info.get(\"duration\")\n duration_text = f\"{duration}s\" if isinstance(duration, int) else \"Unknown\"\n thumbnail = info.get(\"thumbnail\")\n print(f\"Title: {title}\")\n print(f\"Uploader: {uploader}\")\n print(f\"Duration: {duration_text}\")\n if thumbnail:\n print(f\"Thumbnail: {thumbnail}\")\n except json.JSONDecodeError:\n print(first_line)\n\n return 0\n\n\ndef fetch_oembed_info(url: str) -> Optional[dict]:\n oembed_url = f\"https://www.youtube.com/oembed?url={quote(url, safe='')}&format=json\"\n try:\n with urlopen(oembed_url, timeout=15) as response:\n payload = response.read().decode(\"utf-8\")\n return json.loads(payload)\n except Exception:\n return None\n\n\ndef render_oembed_info(info: dict) -> None:\n title = info.get(\"title\", \"Unknown title\")\n uploader = info.get(\"author_name\", \"Unknown uploader\")\n thumbnail = info.get(\"thumbnail_url\")\n print(f\"Title: {title}\")\n print(f\"Uploader: {uploader}\")\n print(\"Duration: Unknown\")\n if thumbnail:\n print(f\"Thumbnail: {thumbnail}\")\n\n\ndef download_video(\n url: str,\n output_dir: str = \".\",\n format_spec: str = None,\n quality: str = None,\n output_template: str = None,\n merge_format: str = \"mp4\",\n subtitles: bool = False,\n subtitle_lang: str = \"en\",\n cookies_from_browser: str = None,\n cookies_file: str = None,\n player_client: str = None,\n auto_po_token: bool = False,\n proxy: str = None,\n wpc_browser_path: str = None,\n allow_playlist: bool = False,\n use_android_client: bool = False,\n audio_only: bool = False,\n list_formats: bool = False,\n info_only: bool = False,\n js_runtimes: str = None,\n) -> int:\n \"\"\"\n Download a YouTube video using yt-dlp.\n\n Args:\n url: YouTube video URL\n output_dir: Directory to save the downloaded file\n format_spec: Format specification (e.g., \"bestvideo+bestaudio/best\")\n quality: Quality preset (best, 1080p, 720p, 480p, 360p, worst)\n output_template: Output filename template (yt-dlp template syntax)\n merge_format: Merge output container format (e.g., mp4, mkv)\n subtitles: Download subtitles if available\n subtitle_lang: Subtitle languages (comma-separated)\n cookies_from_browser: Load cookies from browser (e.g., chrome, firefox)\n cookies_file: Load cookies from a cookies.txt file\n player_client: Use a specific YouTube player client (e.g., web_safari)\n auto_po_token: Authorize provider setup after a failed explicit PO-token diagnostic\n proxy: Proxy URL for yt-dlp and PO Token provider\n wpc_browser_path: Browser path for WPC PO Token provider\n allow_playlist: Allow playlist downloads (default: False)\n use_android_client: Explicit legacy Android client request (default: False)\n audio_only: Download audio only\n list_formats: List available formats instead of downloading\n info_only: Print video info before exiting\n js_runtimes: Explicit yt-dlp JavaScript runtime configuration (e.g., node)\n\n Returns:\n Exit code (0 for success, non-zero for failure)\n \"\"\"\n # Check if yt-dlp is installed\n check_result = subprocess.run(\n [\"which\", \"yt-dlp\"], capture_output=True, text=True\n )\n if check_result.returncode != 0:\n print(\"✗ Error: yt-dlp is not installed\")\n print(\" Install via: brew install yt-dlp # or: pip install yt-dlp\")\n return 1\n\n # Build yt-dlp command\n cmd = [\"yt-dlp\"]\n\n if cookies_from_browser and cookies_file:\n print(\"✗ Error: Use either --cookies-from-browser or --cookies-file, not both.\")\n return 2\n\n proxy_value, no_proxy = get_proxy_settings(proxy)\n\n has_cookies = bool(cookies_from_browser or cookies_file)\n if has_cookies and player_client:\n clients = set(player_client.split(\",\"))\n if clients & {\"android\", \"android_vr\", \"ios\", \"tv_simply\"}:\n print(\"✗ Error: The selected player client does not support account cookies.\")\n return 2\n # Never make an alternate client the consequence of disabling PO tokens.\n use_android = use_android_client and not (has_cookies or player_client)\n if use_android:\n cmd.extend([\"--extractor-args\", \"youtube:player_client=android\"])\n\n if cookies_from_browser:\n cmd.extend([\"--cookies-from-browser\", cookies_from_browser])\n elif cookies_file:\n cmd.extend([\"--cookies\", cookies_file])\n\n if proxy_value:\n cmd.extend([\"--proxy\", proxy_value])\n if js_runtimes:\n cmd.extend([\"--js-runtimes\", js_runtimes])\n\n if player_client:\n cmd.extend([\"--extractor-args\", f\"youtube:player_client={player_client}\"])\n\n if not allow_playlist:\n cmd.append(\"--no-playlist\")\n\n # List formats if requested\n if list_formats:\n cmd.extend([\"-F\", url])\n result = run_yt_dlp(cmd, hide_cookie_logs=bool(cookies_from_browser))\n\n return result.returncode\n\n if info_only:\n return print_video_info(cmd, url, hide_cookie_logs=bool(cookies_from_browser))\n\n if format_spec and quality:\n print(\"✗ Error: Use either --format or --quality, not both.\")\n return 2\n\n if not format_spec and not quality and not audio_only:\n quality = \"best\"\n\n format_from_quality = False\n if quality:\n format_spec = QUALITY_PRESETS.get(quality)\n if not format_spec:\n print(f\"✗ Error: Unsupported quality preset: {quality}\")\n return 2\n format_from_quality = True\n\n # Set output directory\n output_root = Path(output_dir).expanduser().resolve()\n output_root.mkdir(parents=True, exist_ok=True)\n output_template_final = build_output_template(str(output_root), output_template)\n cmd.extend([\"-o\", output_template_final])\n\n # Handle audio-only downloads\n if audio_only:\n cmd.extend([\"-x\", \"--audio-format\", \"mp3\"])\n if format_spec:\n cmd.extend([\"-f\", format_spec])\n\n if subtitles:\n cmd.extend([\"--write-subs\", \"--write-auto-subs\", \"--sub-lang\", subtitle_lang])\n\n if merge_format:\n cmd.extend([\"--merge-output-format\", merge_format])\n\n # Add URL\n cmd.append(url)\n\n def finalize_download(before_snapshot: set) -> None:\n after_files = list_files(output_root)\n new_files = sorted(after_files - before_snapshot)\n primary = pick_primary_file(new_files, audio_only=audio_only)\n if primary:\n size = human_size(primary.stat().st_size)\n print(f\"\\n✓ Download completed successfully!\")\n print(f\" File: {primary}\")\n print(f\" Size: {size}\")\n if audio_only:\n print(\" Resolution: N/A (audio-only)\")\n else:\n resolution = get_video_resolution(primary)\n if resolution:\n print(f\" Resolution: {resolution}\")\n else:\n print(\" Resolution: Not available\")\n else:\n print(f\"\\n✓ Download completed successfully!\")\n print(f\" Location: {output_root}\")\n\n before_files = list_files(output_root)\n print(f\"Executing: {' '.join(cmd)}\")\n result = run_yt_dlp(cmd, hide_cookie_logs=bool(cookies_from_browser))\n if result.returncode == 0:\n finalize_download(before_files)\n return 0\n\n if has_pot_error(result):\n if auto_po_token:\n token_client = player_client or (\"web_safari\" if has_cookies else \"mweb\")\n if use_android or token_client not in {\"mweb\", \"web\", \"web_safari\", \"web_music\", \"web_creator\"}:\n print(\"✗ Select a compatible web player client before using the web PO Token provider.\")\n return 2\n print(\"\\n⚠️ Explicit PO Token requirement detected. Preparing the authorized provider...\")\n provider_type = ensure_po_token_provider(proxy_value, no_proxy)\n if provider_type:\n cmd = with_player_client(cmd, token_client)\n if provider_type == \"wpc\":\n browser = wpc_browser_path or find_chrome_path()\n cmd = with_wpc_browser(cmd, browser)\n print(f\"Executing: {' '.join(cmd)}\")\n result = run_yt_dlp(cmd, hide_cookie_logs=bool(cookies_from_browser))\n if result.returncode == 0:\n finalize_download(before_files)\n return 0\n diagnostics = ((result.stdout or \"\") + (result.stderr or \"\")).lower()\n if provider_type == \"bgutil\" and \"[pot:bgutil\" in diagnostics and \"error\" in diagnostics:\n print(\"\\n⚠️ The authorized provider failed. Trying WPC without restarting the shared service...\")\n if ensure_wpc_provider(proxy_value):\n provider_type = \"wpc\"\n cmd = with_wpc_browser(cmd, wpc_browser_path or find_chrome_path())\n result = run_yt_dlp(cmd, hide_cookie_logs=bool(cookies_from_browser))\n if result.returncode == 0:\n finalize_download(before_files)\n return 0\n if provider_type == \"wpc\" and has_wpc_error(result):\n print(\"\\n⚠️ Browser verification not ready. Retrying once; keep its window open...\")\n time.sleep(3)\n result = run_yt_dlp(cmd, hide_cookie_logs=bool(cookies_from_browser))\n if result.returncode == 0:\n finalize_download(before_files)\n return 0\n else:\n print(\"✗ Authorized PO Token setup failed; the original download was already attempted.\")\n return result.returncode\n else:\n print(\"\\nPO Token requirement detected. If provider setup is authorized, retry with --auto-po-token.\")\n\n # A 403 alone never authorizes installing a provider or extracting cookies.\n # Retain explicitly supplied cookies on the one compatible-client retry.\n if has_cookies and not player_client and has_403_error(result):\n print(\"\\n⚠️ Retrying the authorized cookie session with web_safari...\")\n cmd = with_player_client(cmd, \"web_safari\")\n result = run_yt_dlp(cmd, hide_cookie_logs=bool(cookies_from_browser))\n if result.returncode == 0:\n finalize_download(before_files)\n return 0\n\n if not audio_only and format_from_quality and has_403_error(result):\n print(\"\\n⚠️ 403/fragment failure. Retrying once with progressive formats; quality may be lower...\")\n retry_cmd = cmd[:]\n retry_format = fallback_format_for_quality(quality)\n retry_cmd[retry_cmd.index(\"-f\") + 1] = retry_format\n result = run_yt_dlp(retry_cmd, hide_cookie_logs=bool(cookies_from_browser))\n if result.returncode == 0:\n finalize_download(before_files)\n return 0\n\n print(f\"\\n✗ Download failed with exit code {result.returncode}\")\n return result.returncode\n\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"Download YouTube videos using yt-dlp with robust error handling\"\n )\n parser.add_argument(\"url\", help=\"YouTube video URL\")\n parser.add_argument(\n \"-o\",\n \"--output-dir\",\n default=\".\",\n help=\"Output directory (default: current directory)\",\n )\n parser.add_argument(\n \"--output-template\",\n help=\"Output template (e.g., '%%(title)s.%%(ext)s')\",\n )\n parser.add_argument(\n \"-f\", \"--format\", help=\"Format specification (e.g., 'bestvideo+bestaudio/best')\"\n )\n parser.add_argument(\n \"-q\",\n \"--quality\",\n choices=sorted(QUALITY_PRESETS.keys()),\n help=\"Quality preset (best, 1080p, 720p, 480p, 360p, worst)\",\n )\n parser.add_argument(\n \"--merge-format\",\n default=\"mp4\",\n help=\"Merge output container format (default: mp4)\",\n )\n parser.add_argument(\n \"--subtitles\",\n action=\"store_true\",\n help=\"Download subtitles if available\",\n )\n parser.add_argument(\n \"--sub-lang\",\n default=\"en\",\n help=\"Subtitle languages (comma-separated, default: en)\",\n )\n parser.add_argument(\n \"--cookies-from-browser\",\n help=\"Load cookies from browser (e.g., chrome, firefox)\",\n )\n parser.add_argument(\n \"--cookies-file\",\n help=\"Load cookies from a cookies.txt file\",\n )\n parser.add_argument(\n \"--player-client\",\n help=\"Use a specific YouTube player client (e.g., web_safari)\",\n )\n parser.add_argument(\n \"--proxy\",\n help=\"Proxy URL for yt-dlp and PO Token provider (e.g., http://127.0.0.1:1082)\",\n )\n parser.add_argument(\n \"--wpc-browser-path\",\n help=\"Browser executable path for WPC PO Token provider\",\n )\n parser.add_argument(\n \"--js-runtimes\",\n help=\"Enable an installed yt-dlp JavaScript runtime (e.g., node or node:/path/to/node)\",\n )\n auto_group = parser.add_mutually_exclusive_group()\n auto_group.add_argument(\n \"--auto-po-token\",\n action=\"store_true\",\n help=\"Authorize provider installation/start after a failed explicit PO Token diagnostic\",\n )\n auto_group.add_argument(\n \"--no-auto-po-token\",\n action=\"store_true\",\n help=\"Do not install/start a PO Token provider (default)\",\n )\n parser.add_argument(\n \"--playlist\",\n action=\"store_true\",\n help=\"Allow playlist downloads (default: single video only)\",\n )\n parser.add_argument(\n \"--no-android-client\",\n action=\"store_true\",\n help=\"Compatibility option: default clients are already used; select Android explicitly with --player-client\",\n )\n parser.add_argument(\n \"-a\", \"--audio-only\", action=\"store_true\", help=\"Download audio only (as MP3)\"\n )\n parser.add_argument(\n \"-F\", \"--list-formats\", action=\"store_true\", help=\"List available formats\"\n )\n parser.add_argument(\n \"--info\",\n action=\"store_true\",\n help=\"Print video metadata (title/uploader/duration) and exit\",\n )\n\n args = parser.parse_args()\n\n exit_code = download_video(\n url=args.url,\n output_dir=args.output_dir,\n format_spec=args.format,\n quality=args.quality,\n output_template=args.output_template,\n merge_format=args.merge_format,\n subtitles=args.subtitles,\n subtitle_lang=args.sub_lang,\n cookies_from_browser=args.cookies_from_browser,\n cookies_file=args.cookies_file,\n player_client=args.player_client,\n auto_po_token=args.auto_po_token,\n proxy=args.proxy,\n wpc_browser_path=args.wpc_browser_path,\n allow_playlist=args.playlist,\n use_android_client=False,\n audio_only=args.audio_only,\n list_formats=args.list_formats,\n info_only=args.info,\n js_runtimes=args.js_runtimes,\n )\n\n sys.exit(exit_code)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":38564,"content_sha256":"425d0568f5ec197e87635972e1b9931e9f8c48a3619b3577cae819e2428587cc"},{"filename":"tests/test_download_video.py","content":"#!/usr/bin/env python3\n\"\"\"Synthetic route controls; live media acceptance is performed separately.\"\"\"\n\nimport importlib.util\nimport io\nimport subprocess\nimport sys\nimport tempfile\nimport unittest\nfrom contextlib import redirect_stdout\nfrom pathlib import Path\nfrom unittest.mock import patch\n\nSPEC = importlib.util.spec_from_file_location(\n \"download_video\", Path(__file__).parents[1] / \"scripts/download_video.py\"\n)\ndownloader = importlib.util.module_from_spec(SPEC)\nSPEC.loader.exec_module(downloader)\n\n\ndef result(code=0, text=\"\"):\n return subprocess.CompletedProcess([], code, text, \"\")\n\n\nTOKEN = (\n \"WARNING: mweb client https formats require a GVS PO Token which was not provided.\\n\"\n \"ERROR: Requested format is not available\"\n)\n\n# Shape emitted by yt-dlp's skipped-format diagnostic, with synthetic identity.\nSKIPPED_TOKEN_WARNING = (\n \"WARNING: [youtube] VIDEO_ID: mweb client https formats require a GVS \"\n \"PO Token which was not provided. They will be skipped as they may \"\n \"yield HTTP Error 403.\"\n)\n\n\nclass DownloadRoutes(unittest.TestCase):\n def invoke(self, outcomes=None, **kwargs):\n with tempfile.TemporaryDirectory() as root, \\\n patch.object(downloader.subprocess, \"run\", return_value=result()) as base, \\\n patch.object(downloader, \"run_yt_dlp\", side_effect=outcomes or [result()]) as run, \\\n patch.object(downloader, \"ensure_po_token_provider\", return_value=\"bgutil\") as provider, \\\n patch.object(downloader, \"print_video_info\", return_value=0) as info, \\\n patch.object(downloader, \"get_proxy_settings\", return_value=(None, None)), \\\n redirect_stdout(io.StringIO()):\n code = downloader.download_video(\"https://youtu.be/VIDEO_ID\", output_dir=root, **kwargs)\n return code, [call.args[0] for call in run.call_args_list], provider.call_count, info.call_args\n\n def test_default_preserves_native_clients_without_provider(self):\n code, calls, providers, _ = self.invoke()\n self.assertEqual((code, len(calls), providers), (0, 1, 0))\n self.assertNotIn(\"--extractor-args\", calls[0])\n self.assertNotIn(\"--cookies-from-browser\", calls[0])\n self.assertIn(\"--no-playlist\", calls[0])\n\n def test_opt_in_does_not_start_provider_for_success(self):\n code, calls, providers, _ = self.invoke(auto_po_token=True)\n self.assertEqual((code, len(calls), providers), (0, 1, 0))\n self.assertNotIn(\"--extractor-args\", calls[0])\n\n def test_explicit_token_failure_uses_authorized_provider_after_first_attempt(self):\n code, calls, providers, _ = self.invoke([result(1, TOKEN), result()], auto_po_token=True)\n self.assertEqual((code, len(calls), providers), (0, 2, 1))\n self.assertNotIn(\"--extractor-args\", calls[0])\n self.assertIn(\"youtube:player_client=mweb\", calls[1])\n\n def test_token_requirement_without_setup_authorization_returns_failure(self):\n code, calls, providers, _ = self.invoke([result(1, TOKEN)])\n self.assertEqual((code, len(calls), providers), (1, 1, 0))\n\n def test_authorized_provider_failure_reaches_wpc_without_service_restart(self):\n with patch.object(downloader, \"ensure_wpc_provider\", return_value=True) as wpc, \\\n patch.object(downloader, \"restart_po_token_provider\") as restart, \\\n patch.object(downloader, \"find_chrome_path\", return_value=\"/synthetic/chrome\"):\n code, calls, providers, _ = self.invoke(\n [result(1, TOKEN), result(1, \"ERROR: [pot:bgutil:http] provider connection failed\"), result()],\n auto_po_token=True)\n self.assertEqual((code, len(calls), providers, wpc.call_count), (0, 3, 1, 1))\n restart.assert_not_called()\n self.assertIn(\"youtubepot-wpc:browser_path=/synthetic/chrome\", calls[2])\n\n def test_provider_setup_failure_preserves_original_exit(self):\n with tempfile.TemporaryDirectory() as root, \\\n patch.object(downloader.subprocess, \"run\", return_value=result()), \\\n patch.object(downloader, \"run_yt_dlp\", return_value=result(7, TOKEN)) as run, \\\n patch.object(downloader, \"ensure_po_token_provider\", return_value=None), \\\n redirect_stdout(io.StringIO()):\n code = downloader.download_video(\"URL\", output_dir=root, auto_po_token=True)\n self.assertEqual((code, run.call_count), (7, 1))\n\n def test_non_token_failures_do_not_install_or_retry_formats(self):\n for diagnostic in (\n \"ERROR: The read operation timed out\",\n TOKEN + \"\\nERROR: Connection timed out\",\n \"ERROR: Requested format is not available\",\n \"WARNING: Only images are available\\nERROR: Requested format is not available\",\n \"ERROR: nsig extraction failed\",\n \"ERROR: Sign in to confirm you're not a bot\",\n \"WARNING: PO Token missing\\nERROR: ffmpeg failed\",\n SKIPPED_TOKEN_WARNING + \"\\nERROR: Postprocessing: ffmpeg not found. Please install or provide the path using --ffmpeg-location\",\n SKIPPED_TOKEN_WARNING + \"\\nERROR: unable to open for writing: Permission denied\",\n SKIPPED_TOKEN_WARNING + \"\\nERROR: HTTP Error 403\\nERROR: Postprocessing: ffmpeg failed\",\n \"ERROR: transport failed\",\n ):\n with self.subTest(diagnostic=diagnostic):\n code, calls, providers, _ = self.invoke([result(1, diagnostic)], auto_po_token=True)\n self.assertEqual((code, len(calls), providers), (1, 1, 0))\n\n def test_real_warning_shape_plus_real_format_failure_can_recover(self):\n code, calls, providers, _ = self.invoke(\n [result(1, SKIPPED_TOKEN_WARNING + \"\\nERROR: Requested format is not available\"), result()],\n auto_po_token=True)\n self.assertEqual((code, len(calls), providers), (0, 2, 1))\n\n def test_403_only_uses_bounded_progressive_retry(self):\n code, calls, providers, _ = self.invoke([result(1, \"HTTP Error 403\"), result()], auto_po_token=True)\n self.assertEqual((code, len(calls), providers), (0, 2, 0))\n self.assertIn(\"protocol!*=m3u8\", calls[1][calls[1].index(\"-f\") + 1])\n\n def test_metadata_and_format_listing_do_not_start_provider_or_force_android(self):\n code, calls, providers, info = self.invoke(info_only=True, auto_po_token=True)\n self.assertEqual((code, len(calls), providers), (0, 0, 0))\n self.assertNotIn(\"--extractor-args\", info.args[0])\n code, calls, providers, _ = self.invoke(list_formats=True, auto_po_token=True)\n self.assertEqual((code, len(calls), providers), (0, 1, 0))\n self.assertNotIn(\"--extractor-args\", calls[0])\n\n def test_browser_and_file_cookies_survive_token_retry(self):\n for option, flag in ((\"cookies_from_browser\", \"--cookies-from-browser\"), (\"cookies_file\", \"--cookies\")):\n with self.subTest(option=option):\n code, calls, providers, _ = self.invoke(\n [result(1, TOKEN), result()], auto_po_token=True, **{option: \"synthetic\"})\n self.assertEqual((code, providers), (0, 1))\n self.assertIn(flag, calls[1])\n self.assertIn(\"youtube:player_client=web_safari\", calls[1])\n\n def test_incompatible_cookie_client_fails_before_download(self):\n for client in (\"android\", \"android_vr\", \"ios\", \"tv_simply\"):\n code, calls, providers, _ = self.invoke(cookies_file=\"synthetic.txt\", player_client=client)\n self.assertEqual((code, len(calls), providers), (2, 0, 0))\n\n def test_explicit_non_web_client_does_not_install_web_token_provider(self):\n code, calls, providers, _ = self.invoke([result(1, TOKEN)], auto_po_token=True, player_client=\"android\")\n self.assertEqual((code, len(calls), providers), (2, 1, 0))\n\n def test_custom_formats_audio_playlist_subtitles_and_runtime_preserved(self):\n code, calls, providers, _ = self.invoke(\n format_spec=\"bestaudio\", audio_only=True, subtitles=True,\n subtitle_lang=\"fr\", allow_playlist=True, js_runtimes=\"node\")\n self.assertEqual(code, 0)\n for expected in (\"-x\", \"mp3\", \"-f\", \"bestaudio\", \"--write-subs\", \"--write-auto-subs\", \"fr\", \"node\"):\n self.assertIn(expected, calls[0])\n self.assertNotIn(\"--no-playlist\", calls[0])\n\n def test_legacy_android_is_explicit_and_cookies_take_precedence(self):\n _, calls, _, _ = self.invoke(use_android_client=True)\n self.assertIn(\"youtube:player_client=android\", calls[0])\n _, calls, _, _ = self.invoke(use_android_client=True, cookies_file=\"synthetic.txt\")\n self.assertNotIn(\"youtube:player_client=android\", calls[0])\n\n def test_client_change_retains_token_and_provider_configuration(self):\n cmd = [\"yt-dlp\", \"--extractor-args\", \"youtube:player_client=mweb;po_token=mweb.gvs+FAKE\",\n \"--extractor-args\", \"youtubepot-wpc:browser_path=/synthetic/chrome\", \"URL\"]\n actual = downloader.with_player_client(cmd, \"web_safari\")\n self.assertIn(\"youtube:po_token=mweb.gvs+FAKE;player_client=web_safari\", actual)\n self.assertIn(\"youtubepot-wpc:browser_path=/synthetic/chrome\", actual)\n self.assertEqual(sum(value.startswith(\"youtube:\") for value in actual), 1)\n\n def test_capture_diagnostics_without_cookies(self):\n with redirect_stdout(io.StringIO()):\n actual = downloader.run_yt_dlp([sys.executable, \"-c\", \"import sys; print('ERROR: missing PO Token', file=sys.stderr); sys.exit(1)\"])\n self.assertEqual(actual.returncode, 1)\n self.assertTrue(downloader.has_pot_error(actual))\n\n def test_success_warning_and_empty_diagnostics_are_not_failures(self):\n self.assertFalse(downloader.has_pot_error(result(0, TOKEN)))\n self.assertFalse(downloader.has_pot_error(subprocess.CompletedProcess([], 1)))\n\n def test_cli_default_and_explicit_authorization(self):\n for flags, expected in (([], False), ([\"--auto-po-token\"], True), ([\"--no-auto-po-token\"], False)):\n with patch.object(sys, \"argv\", [\"download_video.py\", \"URL\", *flags]), \\\n patch.object(downloader, \"download_video\", return_value=0) as download:\n with self.assertRaises(SystemExit) as exited:\n downloader.main()\n self.assertEqual(exited.exception.code, 0)\n self.assertEqual(download.call_args.kwargs[\"auto_po_token\"], expected)\n self.assertFalse(download.call_args.kwargs[\"use_android_client\"])\n\n\nif __name__ == \"__main__\":\n unittest.main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10648,"content_sha256":"bab9ebffd6b7d5816dd32f1673add4f6488db5ef8a32fff97e84dadeeb0e2fe1"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"YouTube Downloader","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Overview","type":"text"}]},{"type":"paragraph","content":[{"text":"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:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"YouTube downloads at available quality, with optional PO token and authorized cookie recovery","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"HLS stream downloads with authentication headers","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Handling protected content and troubleshooting common download failures","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Goal Split: File vs. Text (Read First)","type":"text"}]},{"type":"paragraph","content":[{"text":"Before downloading anything, classify the request:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User wants the video/audio file itself","type":"text","marks":[{"type":"strong"}]},{"text":" (save it, edit it, watch offline) → continue with the download flow below, unchanged.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User wants the spoken content as text","type":"text","marks":[{"type":"strong"}]},{"text":" (transcript, subtitles, meeting notes, archive, \"what was said\") → follow ","type":"text"},{"text":"references/text-first.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" FIRST: existing archived transcripts → platform transcript APIs → yt-dlp subtitle-only → authorized audio track + ASR. Do not download the full video just to obtain text.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"The text-first route never blocks a file request: when the user wants the file, download it with the unchanged flow below.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Non-Technical User Experience (Default)","type":"text"}]},{"type":"paragraph","content":[{"text":"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.","type":"text"}]},{"type":"paragraph","content":[{"text":"Default flow:","type":"text","marks":[{"type":"strong"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ask for the URL (if not provided).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fetch video metadata (title/uploader/duration/thumbnail) and confirm it matches the user's intent.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"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).","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Offer simple choices (video vs. audio-only, quality, subtitles, save location).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Proceed with sensible defaults if the user does not specify:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Video download at best quality","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"MP4 merged output","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Single video only (no playlists)","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Download and report the final file path, file size, and resolution (if video).","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Offer choices in user-friendly terms:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"“Download the video in best quality (default)”","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"“Download audio only (MP3)”","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"“Pick a quality: 1080p / 720p / 480p / 360p”","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"“Include subtitles (if available)”","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"“Save to the Downloads folder (default) or tell me another folder”","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Always render the thumbnail when available:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If metadata includes a thumbnail URL, include it using Markdown image syntax: ","type":"text"},{"text":"![Thumbnail](URL)","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Ask before doing extra work:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Confirm playlist downloads (can be large).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Confirm installing/upgrading dependencies if missing.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ask before extracting browser cookies.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If using cookies, never mention cookie counts or raw cookie details in user-facing responses. Say “used your Chrome login session”.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Set up a PO Token helper only after an explicit token diagnostic or a user request, and within existing installation/service authorization. If that authorization is already present, execute the recovery and resume the download. Otherwise request only the missing permission. Do not install Docker as a fallback; use the authorized browser provider when appropriate.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Legal/Safety reminder (brief):","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Proceed only if the user has the rights or permission to download the content.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Response template (use plain language, no commands):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"![Thumbnail](THUMBNAIL_URL)\n\nTitle: …\nChannel: …\nDuration: …\n\nI can help you:\n1) Download the video (best quality, MP4)\n2) Download audio only (MP3)\n3) Pick a specific quality (1080p/720p/480p/360p)\n4) Include subtitles (if available)\n\nWhere should I save it? (Default: Downloads folder)","type":"text"}]},{"type":"paragraph","content":[{"text":"If the user says “just download”:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Proceed with defaults and confirm when the download finishes.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If blocked, classify the actual diagnostic using the SOP below and execute the applicable authorized recovery.","type":"text"}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Reliable Download SOP (Internal)","type":"text"}]},{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"the download SOP","type":"text","marks":[{"type":"link","attrs":{"href":"references/internal-sop.md","title":null}}]},{"text":" before retrying a failure. Start with the current yt-dlp default clients and the existing network configuration. Do not require a provider, switch to Android, or extract browser cookies merely to begin a download.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Agent Execution Checklist (Internal)","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"scripts/download_video.py URL --info","type":"text","marks":[{"type":"code_inline"}]},{"text":" for metadata. It never starts a provider; its oEmbed fallback supplies limited fields when extraction fails.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Render the thumbnail when available. Use the requested file/audio, quality, subtitles and save location; proceed with defaults when already specified or implied.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run the ordinary download first. With an installed Node runtime, pass ","type":"text"},{"text":"--js-runtimes node","type":"text","marks":[{"type":"code_inline"}]},{"text":" when yt-dlp needs it; runtime installation still follows dependency authorization.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"On failure, read the actual error and ","type":"text"},{"text":"-F","type":"text","marks":[{"type":"code_inline"}]},{"text":" output. An absent requested format, images-only output, 403 or low resolution alone does not establish a token problem.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If a failed request explicitly reports a missing/required PO Token, read ","type":"text"},{"text":"provider setup","type":"text","marks":[{"type":"link","attrs":{"href":"references/po-token-setup.md","title":null}}]},{"text":". Within existing setup authorization, execute ","type":"text"},{"text":"--auto-po-token","type":"text","marks":[{"type":"code_inline"}]},{"text":" and finish the original download; do not stop at giving the user a command. This option attempts the native route first, then permits a bounded provider retry and WPC recovery.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Request browser-cookie permission before supplying either cookie option. Preserve the authorized cookie session and a compatible client throughout retries.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"For an isolated 403/fragment failure, the wrapper can retry a quality preset once with progressive formats. Verify the resulting resolution; the fallback may be lower quality. Custom formats and audio-only requests keep their explicit selection.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify the final file with ","type":"text"},{"text":"ffprobe","type":"text","marks":[{"type":"code_inline"}]},{"text":" and report its path, size and actual resolution or audio properties. Provider health or a format listing alone is not a completed download.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Use This Skill","type":"text"}]},{"type":"paragraph","content":[{"text":"This skill should be invoked when users:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Request downloading YouTube videos or playlists","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Want to extract audio from YouTube videos","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Experience yt-dlp download failures or limited format availability","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Need help with format selection or quality options","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Report only low-quality (360p) formats available","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ask about downloading YouTube content in specific quality (1080p, 4K, etc.)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Need to convert downloaded WebM videos to MP4 format for wider compatibility","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Request downloading HLS streams (m3u8) from platforms like Mux, Vimeo, or other streaming services","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Need to download protected streams that require authentication headers","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. Verify yt-dlp Installation (Run this yourself)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"which yt-dlp\nyt-dlp --version","type":"text"}]},{"type":"paragraph","content":[{"text":"If missing or a current extraction diagnostic requires an update, obtain any missing install/upgrade authorization, then use the existing installation owner:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"brew upgrade yt-dlp # macOS\n# or\npip install --upgrade yt-dlp # Cross-platform","type":"text"}]},{"type":"paragraph","content":[{"text":"Check the current ","type":"text"},{"text":"EJS setup guide","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/yt-dlp/yt-dlp/wiki/EJS","title":null}}]},{"text":" for the JavaScript runtime and challenge-solver requirements of the installed distribution. An installed Node binary may need explicit ","type":"text"},{"text":"--js-runtimes node","type":"text","marks":[{"type":"code_inline"}]},{"text":"; do not treat an EJS/nsig failure as a PO Token failure.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Check Current Quality Access (Run this yourself)","type":"text"}]},{"type":"paragraph","content":[{"text":"Before downloading, check available formats:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"yt-dlp -F \"https://youtu.be/VIDEO_ID\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Compare the listed media streams with the requested quality. If only a low-resolution format appears, inspect extraction/runtime warnings and the source availability before choosing recovery.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"High-Quality Download Workflow","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 1: Try the Native Client Selection","type":"text"}]},{"type":"paragraph","content":[{"text":"Keep ","type":"text"},{"text":"player_client","type":"text","marks":[{"type":"code_inline"}]},{"text":" unset unless there is a specific diagnostic or explicit request. The ","type":"text"},{"text":"official extractor guide","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/yt-dlp/yt-dlp/wiki/Extractors#youtube","title":null}}]},{"text":" describes yt-dlp's default attempt with clients that do not require tokens. Available formats depend on the video, account, client, network and current YouTube enforcement.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 2: Download and Escalate Only When Needed","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Download best quality up to 1080p\nscripts/download_video.py \"VIDEO_URL\" --quality 1080p\n\n# Download best available quality\nscripts/download_video.py \"VIDEO_URL\"\n\n# After an explicit token requirement, with setup already authorized\nscripts/download_video.py \"VIDEO_URL\" --auto-po-token","type":"text"}]},{"type":"paragraph","content":[{"text":"For installation and provider-specific recovery, load ","type":"text"},{"text":"PO Token setup","type":"text","marks":[{"type":"link","attrs":{"href":"references/po-token-setup.md","title":null}}]},{"text":". Do not infer a provider requirement from resolution alone.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 3: Verify Download Quality","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Check video resolution\nffprobe -v error -select_streams v:0 -show_entries stream=width,height,codec_name -of default=noprint_wrappers=1 video.mp4","type":"text"}]},{"type":"paragraph","content":[{"text":"Expected output for 1080p:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"codec_name=vp9\nwidth=1920\nheight=1080","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Authorized Browser Cookies Method","type":"text"}]},{"type":"paragraph","content":[{"text":"Use cookies when the content requires the authorized account or a bot check persists, after cookie permission is granted. Cookies authenticate a session; they do not replace a missing PO Token:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Firefox\nyt-dlp --cookies-from-browser firefox -f \"bestvideo[height\u003c=1080]+bestaudio/best\" \"VIDEO_URL\"\n\n# Chrome\nyt-dlp --cookies-from-browser chrome -f \"bestvideo[height\u003c=1080]+bestaudio/best\" \"VIDEO_URL\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Benefits","type":"text","marks":[{"type":"strong"}]},{"text":": Access to age-restricted and members-only content. ","type":"text"},{"text":"Requirements","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Must be logged into YouTube in the specified browser.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Browser and yt-dlp must use the same IP/proxy.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Preserve yt-dlp's default cookie-compatible clients; use ","type":"text"},{"text":"web_safari","type":"text","marks":[{"type":"code_inline"}]},{"text":" only as a targeted retry. Do not combine account cookies with Android/iOS clients that do not support them.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Tasks","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Audio-Only Download (Run this yourself)","type":"text"}]},{"type":"paragraph","content":[{"text":"Extract audio as MP3:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"yt-dlp -x --audio-format mp3 \"VIDEO_URL\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Custom Output Directory (Run this yourself)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"yt-dlp -P ~/Downloads/YouTube \"VIDEO_URL\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Download with Subtitles (Run this yourself)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"yt-dlp --write-subs --sub-lang en \"VIDEO_URL\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Playlist Download (Run this yourself)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"yt-dlp -f \"bestvideo[height\u003c=1080]+bestaudio/best\" \"PLAYLIST_URL\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Convert WebM to MP4 (Run this yourself)","type":"text"}]},{"type":"paragraph","content":[{"text":"YouTube high-quality downloads often use WebM format (VP9 codec). Convert to MP4 for wider compatibility:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Check if ffmpeg is installed\nwhich ffmpeg || brew install ffmpeg # macOS\n\n# Convert WebM to MP4 with good quality settings\nffmpeg -i \"video.webm\" -c:v libx264 -preset medium -crf 23 -c:a aac -b:a 128k \"video.mp4\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Parameters explained:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-c:v libx264","type":"text","marks":[{"type":"code_inline"}]},{"text":": Use H.264 video codec (widely compatible)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-preset medium","type":"text","marks":[{"type":"code_inline"}]},{"text":": Balance between encoding speed and file size","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-crf 23","type":"text","marks":[{"type":"code_inline"}]},{"text":": Constant Rate Factor for quality (18-28 range, lower = better quality)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-c:a aac","type":"text","marks":[{"type":"code_inline"}]},{"text":": Use AAC audio codec","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-b:a 128k","type":"text","marks":[{"type":"code_inline"}]},{"text":": Audio bitrate 128 kbps","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Tip","type":"text","marks":[{"type":"strong"}]},{"text":": Conversion maintains 1080p resolution and provides ~6x encoding speed on modern hardware.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Troubleshooting Quick Reference","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Missing Quality / Only Images / Requested Format Not Available","type":"text"}]},{"type":"paragraph","content":[{"text":"Inspect ","type":"text"},{"text":"-F","type":"text","marks":[{"type":"code_inline"}]},{"text":", the selected format expression, current yt-dlp/EJS/runtime warnings and source availability. These messages have multiple causes. Enter provider setup only when a failed extraction or media request also names a missing/required PO Token. Do not install a provider because a requested height or format ID is absent.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Sign in to Confirm You’re Not a Bot","type":"text"}]},{"type":"paragraph","content":[{"text":"Check that the intended proxy/network is in use. If the challenge persists and cookies are needed, obtain cookie authorization, retain that session, and retry. Stop with the exact authorization gap if it is unavailable; do not silently read browser cookies.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"nsig / JavaScript Challenge Failure","type":"text"}]},{"type":"paragraph","content":[{"text":"Check the installed yt-dlp version and the ","type":"text"},{"text":"EJS runtime and solver instructions","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/yt-dlp/yt-dlp/wiki/EJS","title":null}}]},{"text":". Repair the diagnosed prerequisite within authorization. Android or AndroidVR can be selected explicitly for a bounded diagnostic, but neither is the default recovery or a guaranteed solution.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"403 / SSL EOF / Fragment Errors","type":"text"}]},{"type":"paragraph","content":[{"text":"Distinguish a media 403 from TLS, timeout, proxy and provider-health errors. A generic 403 does not prove a Token requirement. Check the network path first for SSL/timeout failures; retry only after a relevant change. The wrapper's single progressive retry applies to 403/fragment failures with quality presets, and may reduce quality.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Slow Downloads or Network Errors","type":"text"}]},{"type":"paragraph","content":[{"text":"For users in China or behind restrictive proxies:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Downloads may be slow due to network conditions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Allow sufficient time for completion","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"yt-dlp automatically retries on transient failures","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"PO Token Warning (Harmless)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"WARNING: android client https formats require a GVS PO Token","type":"text"}]},{"type":"paragraph","content":[{"text":"Action","type":"text","marks":[{"type":"strong"}]},{"text":": A successful, verified download needs no repair for an unused-client warning. On failure, read the full diagnostic and actual formats before choosing a token branch.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Bundled Script Reference","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"scripts/download_video.py","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this wrapper for native-client downloads and bounded diagnostic recovery. It does not install/start providers or read cookies by default. Execute it yourself and report the verified file.","type":"text"}]},{"type":"paragraph","content":[{"text":"Basic usage:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"scripts/download_video.py \"VIDEO_URL\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Arguments:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"url","type":"text","marks":[{"type":"code_inline"}]},{"text":" - YouTube video URL (required)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-o, --output-dir","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Output directory","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--output-template","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Output filename template (yt-dlp syntax)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-f, --format","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Format specification","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-q, --quality","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Quality preset (best, 1080p, 720p, 480p, 360p, worst). Default: best (skipped for ","type":"text"},{"text":"--audio-only","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-a, --audio-only","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Extract audio as MP3","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--subtitles","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Download subtitles if available","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--sub-lang","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Subtitle languages (comma-separated, default: en)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--cookies-from-browser","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Load cookies from a browser (e.g., chrome, firefox)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--cookies-file","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Load cookies from a cookies.txt file","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--player-client","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Use a specific YouTube player client (e.g., web_safari)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--auto-po-token","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Authorize provider setup after a failed explicit missing/required-token diagnostic; try native clients first, then Docker bgutil or browser WPC","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--no-auto-po-token","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Disable provider setup (default); does not disable already-installed yt-dlp plugins","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--proxy","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Proxy URL for yt-dlp and the PO Token provider (e.g., http://127.0.0.1:1082)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--wpc-browser-path","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Browser executable path for WPC provider","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-F, --list-formats","type":"text","marks":[{"type":"code_inline"}]},{"text":" - List available formats","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--merge-format","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Merge output container (e.g., mp4, mkv). Default: mp4","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--playlist","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Allow playlist downloads (default: single video only)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--info","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Print title/uploader/duration/thumbnail and exit","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--no-android-client","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Retained compatibility option; the wrapper already leaves native client selection unchanged","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"--js-runtimes","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Pass an installed runtime to yt-dlp, e.g. ","type":"text"},{"text":"node","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"node:/path/to/node","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quality Expectations","type":"text"}]},{"type":"paragraph","content":[{"text":"Do not promise a resolution based on a provider/client/cookie combination. Request the desired quality, inspect available formats, and verify the actual downloaded file. A successful high-resolution download without new provider setup proves only that input and environment; installed plugins or configuration may still contribute. A provider cannot create a resolution absent from the source.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"HLS Stream Downloads (m3u8)","type":"text"}]},{"type":"paragraph","content":[{"text":"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.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Identifying HLS Streams","type":"text"}]},{"type":"paragraph","content":[{"text":"HLS streams use ","type":"text"},{"text":".m3u8","type":"text","marks":[{"type":"code_inline"}]},{"text":" playlist files:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Master playlist: Lists multiple quality options","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Rendition playlist: Contains actual video/audio segment URLs","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Download Workflow","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Step 1: Obtain the Stream URL","type":"text"}]},{"type":"paragraph","content":[{"text":"Get the m3u8 URL from the video source. For protected streams:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Open browser DevTools → Network tab","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Play the video","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Filter for \"m3u8\" to find the playlist URLs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Copy the rendition URL (usually contains quality info like \"rendition.m3u8\")","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Step 2: Identify Required Headers","type":"text"}]},{"type":"paragraph","content":[{"text":"Many CDNs require authentication headers:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Referer","type":"text","marks":[{"type":"strong"}]},{"text":": Origin website (e.g., ","type":"text"},{"text":"https://maven.com/","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Origin","type":"text","marks":[{"type":"strong"}]},{"text":": Same as Referer for CORS","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User-Agent","type":"text","marks":[{"type":"strong"}]},{"text":": Browser identification","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Check the Network tab to see which headers the browser sends.","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Step 3: Download with ffmpeg","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ffmpeg with the ","type":"text"},{"text":"-headers","type":"text","marks":[{"type":"code_inline"}]},{"text":" flag for protected streams:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"ffmpeg -headers \"Referer: https://example.com/\" \\\n -protocol_whitelist file,http,https,tcp,tls,crypto,httpproxy \\\n -i \"https://cdn.example.com/path/rendition.m3u8?params\" \\\n -c copy -bsf:a aac_adtstoasc \\\n output.mp4","type":"text"}]},{"type":"paragraph","content":[{"text":"Key parameters:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-headers","type":"text","marks":[{"type":"code_inline"}]},{"text":": Set HTTP headers (critical for authentication)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-protocol_whitelist","type":"text","marks":[{"type":"code_inline"}]},{"text":": Enable required protocols for HLS","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-c copy","type":"text","marks":[{"type":"code_inline"}]},{"text":": Stream copy (no re-encoding, faster)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"-bsf:a aac_adtstoasc","type":"text","marks":[{"type":"code_inline"}]},{"text":": Fix AAC audio compatibility","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Common header patterns:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Single header\n-headers \"Referer: https://example.com/\"\n\n# Multiple headers\n-headers \"Referer: https://example.com/\" \\\n-headers \"User-Agent: Mozilla/5.0...\"\n\n# Alternative syntax\n-headers

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 at available quality, with optional PO token and authorized cookie recovery - HLS stream downloads with authentication headers - Handling protected content and troubleshooting common download failures Goal Split: File vs. Text (Read First) Before downloading anything, classify the request: - User wants the video/audio file itself (save it, edit it, watch offline) → continue with the dow…

Referer: https://example.com/\\r\\nUser-Agent: Mozilla/5.0...'","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Handling Separate Audio/Video Streams","type":"text"}]},{"type":"paragraph","content":[{"text":"Some platforms (like Mux) deliver audio and video separately:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Download audio stream:","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"ffmpeg -headers \"Referer: https://example.com/\" \\\n -protocol_whitelist file,http,https,tcp,tls,crypto,httpproxy \\\n -i \"https://cdn.example.com/audio/rendition.m3u8\" \\\n -c copy audio.m4a","type":"text"}]},{"type":"ordered_list","attrs":{"order":2,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Download video stream:","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"ffmpeg -headers \"Referer: https://example.com/\" \\\n -protocol_whitelist file,http,https,tcp,tls,crypto,httpproxy \\\n -i \"https://cdn.example.com/video/rendition.m3u8\" \\\n -c copy video.mp4","type":"text"}]},{"type":"ordered_list","attrs":{"order":3,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Merge streams:","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"ffmpeg -i video.mp4 -i audio.m4a -c copy merged.mp4","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Troubleshooting HLS Downloads","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"403 Forbidden Errors","type":"text"}]},{"type":"paragraph","content":[{"text":"Cause","type":"text","marks":[{"type":"strong"}]},{"text":": Missing or incorrect authentication headers.","type":"text"}]},{"type":"paragraph","content":[{"text":"Solution","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify Referer header matches the video source website","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check if additional headers (Origin, User-Agent) are needed","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ensure the m3u8 URL includes all query parameters from browser","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"yt-dlp Stuck on Cookie Extraction","type":"text"}]},{"type":"paragraph","content":[{"text":"Symptom","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"Extracting cookies from chrome","type":"text","marks":[{"type":"code_inline"}]},{"text":" hangs indefinitely.","type":"text"}]},{"type":"paragraph","content":[{"text":"Solution","type":"text","marks":[{"type":"strong"}]},{"text":": Use ffmpeg directly instead of yt-dlp for HLS streams.","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Protocol Not Whitelisted","type":"text"}]},{"type":"paragraph","content":[{"text":"Error","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"Protocol 'https' not on whitelist 'file,crypto,data'","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"Solution","type":"text","marks":[{"type":"strong"}]},{"text":": Add ","type":"text"},{"text":"-protocol_whitelist file,http,https,tcp,tls,crypto,httpproxy","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Empty Segments or No Streams","type":"text"}]},{"type":"paragraph","content":[{"text":"Cause","type":"text","marks":[{"type":"strong"}]},{"text":": Expired signatures in the m3u8 URLs.","type":"text"}]},{"type":"paragraph","content":[{"text":"Solution","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Get fresh URLs from browser DevTools","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Download immediately after obtaining URLs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Look for rendition URLs with updated signature parameters","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Performance Tips","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"HLS downloads typically run at 10-15x realtime speed","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No re-encoding with ","type":"text"},{"text":"-c copy","type":"text","marks":[{"type":"code_inline"}]},{"text":" (fastest)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Monitor download with real-time progress display","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use absolute output paths to avoid directory confusion","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Further Reading","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PO Token Setup","type":"text","marks":[{"type":"strong"}]},{"text":": See ","type":"text"},{"text":"references/po-token-setup.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for detailed installation and troubleshooting","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Internal SOP","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"references/internal-sop.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — the short list of yt-dlp failures that look like something else (URLs unquoted in zsh, proxy not reaching the PO Token provider). Read it first when a download fails for no obvious reason","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"yt-dlp Documentation","type":"text","marks":[{"type":"strong"}]},{"text":": https://github.com/yt-dlp/yt-dlp","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Format Selection Guide","type":"text","marks":[{"type":"strong"}]},{"text":": https://github.com/yt-dlp/yt-dlp#format-selection","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Next Step: Text From the Video","type":"text"}]},{"type":"paragraph","content":[{"text":"Two situations, two routes:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The user's goal is text and nothing has been downloaded yet","type":"text","marks":[{"type":"strong"}]},{"text":": do not download the video just to get text — follow the text-first ladder in ","type":"text"},{"text":"references/text-first.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" (existing archived transcripts → platform transcript APIs → yt-dlp subtitle-only → authorized audio track + ASR).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The user already has the downloaded file and now also wants text","type":"text","marks":[{"type":"strong"}]},{"text":": still check existing transcripts and original subtitles using its known source identity. If those checks yield no usable text, reuse the local file at level L4 without another download. Proactively suggest:","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Download complete: [filename]\n\nIf you need the spoken content as text, I can transcribe it for you.\n\nOptions:\nA) Transcribe with /daymade-audio:asr-transcribe-to-text (Recommended for speech-to-text)\nB) No thanks — I just needed the video file","type":"text"}]}]},"metadata":{"date":"2026-11-02","name":"youtube-downloader","author":"@skillopedia","source":{"stars":1511,"repo_name":"claude-code-skills","origin_url":"https://github.com/daymade/claude-code-skills/blob/HEAD/youtube-downloader/SKILL.md","repo_owner":"daymade","body_sha256":"ba6710e1fdf33c7991460e647774bef04258980a78eadd2fbfdd81019a2cb898","cluster_key":"f6961245d75e4db0f6a16ff778fdd2e39a1abbc391adfa73db8642a7379c936e","clean_bundle":{"format":"clean-skill-bundle-v1","source":"daymade/claude-code-skills/youtube-downloader/SKILL.md","text_bytes":65886,"source_commit":"0633c92cdf1192ebed0b550384ea2a56f0abd520","skills_sh_rank":10129,"attachment_count":5,"text_attachments":5,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"youtube-downloader/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"media-content","category_label":"Media"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"media-content","import_tag":"skills-sh-top20k-2026-11","description":"Fetch existing YouTube transcripts/subtitles or archive spoken video content as text, and download YouTube videos/audio or HLS streams (m3u8) with yt-dlp and ffmpeg. Use for transcript/subtitle retrieval, text-first video archiving, video/audio downloads, protected streams with authentication headers, and download failures including nsig, 403 or browser-cookie issues."}},"renderedAt":1793672020601}

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 at available quality, with optional PO token and authorized cookie recovery - HLS stream downloads with authentication headers - Handling protected content and troubleshooting common download failures Goal Split: File vs. Text (Read First) Before downloading anything, classify the request: - User wants the video/audio file itself (save it, edit it, watch offline) → continue with the dow…