skills/ytdlp-ops/SKILL.md
yt-dlp operations - the media ACQUISITION layer that feeds ffmpeg-ops: format selection (-S sort vs -f filters) that avoids post-download transcodes, --download-sections clip-at-download, audio-only extraction for STT pipelines (-x --audio-format opus), playlists + --download-archive incremental channel syncs, cookies/auth (--cookies-from-browser), rate limiting and politeness, SponsorBlock mark/remove, output templates (-o), subtitle download (--write-subs/--write-auto-subs), remux-vs-recode doctrine, and failure triage (403s, throttling, geo blocks, the nsig-extraction class that means yt-dlp is outdated). Triggers on: yt-dlp, ytdlp, youtube-dl, download video, download youtube, download from youtube, download playlist, download channel, archive channel, channel sync, rip audio, youtube to mp3, youtube to mp4, save video, grab video, video downloader, download subtitles, download transcript, clip from youtube, download section, sponsorblock, cookies-from-browser, download-archive, nsig, requested format is not available, sign in to confirm, download livestream, record stream, live-from-start, premiere, impersonate.
npx skillsauth add 0xDarkMatter/claude-mods ytdlp-opsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Operational expertise for yt-dlp as the acquisition layer: get the right bytes onto disk in the right codec, politely, resumably — then hand off. Anything that re-encodes, cuts precisely, grades, or packages after download is ffmpeg-ops territory; AI-driven editing of what you acquired (transcript → EDL → final cut) is cutcraft — the full chain is acquire → process → edit.
yt-dlp vs the platforms is an arms race. Releases land near-monthly and extractors break between them — the majority of "yt-dlp is broken" reports are a stale binary. Before debugging anything, check staleness:
bash skills/ytdlp-ops/scripts/check-ytdlp-version.sh --live # vs latest GitHub release
bash skills/ytdlp-ops/scripts/check-ytdlp-version.sh --live --json | jq '.data.days_behind'
Exit 10 = installed build is >60 days behind latest (or a smoke extraction
failed) → update before any other triage:
uv tool upgrade yt-dlp # pip/uv-managed install (preferred)
yt-dlp -U # standalone binary self-update only
Second rule: pick codecs at download time. The default "best" on YouTube is
VP9/AV1 + Opus in WebM/MKV. If the destination needs H.264 MP4, stating that in
-S costs nothing — discovering it after download costs a full transcode.
-S over -f)# Declarative sort (-S) — PREFER this. States preferences in priority order and
# always degrades gracefully to the nearest available. h264 + m4a merges
# natively into mp4: zero post-download transcode.
yt-dlp -S "res:1080,vcodec:h264,acodec:m4a" --merge-output-format mp4 URL
# Hard filter (-f) — exact control, but FAILS ("Requested format is not
# available") when nothing matches. Use only for genuine hard requirements,
# always with a / fallback chain:
yt-dlp -f "bv*[height<=1080][vcodec^=avc1]+ba[ext=m4a]/b[height<=1080]/b" URL
# Survey what the extractor actually offers before arguing with selectors:
yt-dlp -F URL
# Smallest acceptable file (bandwidth/storage constrained; + prefix = ascending):
yt-dlp -S "res:480,+size,+br" URL
# Best quality regardless of codec (archival source for later ffmpeg-ops work):
yt-dlp -S "res,fps,hdr:12,vcodec,acodec" --merge-output-format mkv URL
Sort-field reference, filter grammar, per-destination presets: references/format-selection.md + assets/format-presets.json.
--download-sections)# Download ONLY 10:00-12:30 — ranged requests, not a full download + trim:
yt-dlp --download-sections "*10:00-12:30" -S "res:1080,vcodec:h264" URL
# Frame-accurate cut points (re-encodes around the cuts only):
yt-dlp --download-sections "*10:00-12:30" --force-keyframes-at-cuts URL
# Last 5 minutes / by chapter-title regex / multiple sections:
yt-dlp --download-sections "*-5:00-inf" URL
yt-dlp --download-sections "Intro" --download-sections "Outro" URL
Same physics as ffmpeg copy-cuts: without --force-keyframes-at-cuts the section
boundaries snap to keyframes (can be seconds off). Need many precise cuts from
one source? Download once, then use the ffmpeg-ops EDL workflow.
# THE STT acquisition command. YouTube's best audio IS Opus — asking for opus
# means -x COPIES the stream out (no transcode, no quality loss):
yt-dlp -x --audio-format opus -o "%(id)s.%(ext)s" URL
# Zero-processing alternative — native container, no ffmpeg step at all:
yt-dlp -f "ba" -o "%(id)s.%(ext)s" URL
# Whole channel's audio for a transcription pipeline (archive = resumable):
yt-dlp -x --audio-format opus --download-archive stt-archive.txt \
-o "%(channel)s/%(id)s.%(ext)s" CHANNEL_URL
Do NOT --audio-format mp3 for STT — that's a lossy→lossy transcode that helps
nothing. Whisper-prep (16 kHz mono PCM) is the next stage:
ffmpeg-ops stt-whisper.
# Playlist with ID-correlated filenames + archive file (resumable, dedup-safe):
yt-dlp --download-archive archive.txt \
-o "%(playlist)s/%(playlist_index)03d - %(title).100B [%(id)s].%(ext)s" PLAYLIST_URL
# Incremental channel sync (cron-friendly): stop at the first already-archived
# video instead of re-walking the entire channel every run:
yt-dlp --download-archive archive.txt --break-on-existing --lazy-playlist \
-S "res:1080,vcodec:h264,acodec:m4a" CHANNEL_URL
# Subset selection / list without downloading:
yt-dlp -I 1:10 PLAYLIST_URL
yt-dlp --flat-playlist --print "%(id)s %(title)s" PLAYLIST_URL
# DRY-RUN any batch before committing to it — preview every output filename
# (--print implies --simulate; nothing downloads):
yt-dlp --print filename -o "%(playlist_index)03d - %(title).100B [%(id)s].%(ext)s" PLAYLIST_URL
Archive format, sync-job patterns, when --break-on-existing misfires
(non-chronological playlists):
references/playlists-archives.md.
# Capture a livestream from its BEGINNING, not from "now" (YouTube keeps a
# rolling live buffer; without this you get the moment you pressed enter):
yt-dlp --live-from-start URL
# Scheduled premiere/stream: poll (1-10 min between retries) and start when live:
yt-dlp --wait-for-video 60-600 URL
Live capture caveats: a crashed live download is not resumable like a VOD
(fragments expire) — write to fast local disk (-P temp:), not a network share.
For archival quality, prefer re-downloading the VOD after the stream ends; the
live manifest often caps below the post-processed VOD.
# Manual subs, English variants, skip live-chat pseudo-subs, as SRT:
yt-dlp --write-subs --sub-langs "en.*,-live_chat" --convert-subs srt --skip-download URL
# Auto-generated (ASR) captions — exist for most videos when manual subs don't:
yt-dlp --write-auto-subs --sub-langs en --convert-subs srt --skip-download URL
# Embed into the media file instead of a sidecar:
yt-dlp --embed-subs --sub-langs en URL
Sub formats, language matching, transcript-only workflows (subs as cheap STT): references/subtitles-metadata.md.
# Mark segments as chapters — LOSSLESS and reversible. Prefer this:
yt-dlp --sponsorblock-mark all URL
# Cut segments out of the media — modifies the file, re-encodes at boundaries:
yt-dlp --sponsorblock-remove sponsor,selfpromo URL
Category list, mark-vs-remove trade-offs, interaction with --download-sections:
references/sponsorblock.md.
# Pull cookies from a browser profile (private/members/age-gated content):
yt-dlp --cookies-from-browser firefox URL
# Chrome 127+ on Windows uses app-bound cookie encryption — extraction usually
# FAILS. Use Firefox, or export a Netscape cookies.txt and pass it directly:
yt-dlp --cookies cookies.txt URL
Account-ban warning: authenticated bulk downloading is the fastest way to get an account flagged. Use a throwaway account, always pair cookies with the politeness flags below. Details + browser matrix: references/auth-cookies.md.
# The polite-bulk baseline — cap bandwidth, space out requests, retry patiently:
yt-dlp --limit-rate 4M --sleep-requests 1 \
--sleep-interval 5 --max-sleep-interval 15 \
--retries 10 --fragment-retries 10 URL
# Speed (single video, host not throttling you): parallel fragment download:
yt-dlp --concurrent-fragments 4 URL
Politeness is self-interest: 429s and IP flags cost more time than sleeps do.
# Remux: container change only — lossless, near-instant. yt-dlp's job:
yt-dlp -S "vcodec:h264,acodec:m4a" --remux-video mp4 URL
# Recode: a FULL TRANSCODE. Almost never yt-dlp's job — you give up ffmpeg-ops'
# CRF/preset/pix_fmt control for a blind default encode. If codecs must change:
yt-dlp -S "res,vcodec,acodec" URL # 1. acquire best-native
# 2. then transcode with the ffmpeg-ops web-compatible H.264 recipe.
Rule: --remux-video whenever the codecs already fit the target container;
--recode-video only for throwaway one-offs where quality control doesn't matter.
# ID-in-brackets convention — survives renames, correlates with archive files:
yt-dlp -o "%(uploader)s/%(upload_date)s - %(title).100B [%(id)s].%(ext)s" URL
# Cross-filesystem safety (strips spaces/unicode to ASCII-safe names):
yt-dlp --restrict-filenames -o "%(title)s [%(id)s].%(ext)s" URL
# Split destination and scratch space (-P): fragments go to temp, final to home:
yt-dlp -P "D:/media" -P "temp:C:/tmp/ytdlp" URL
%(title).100B truncates at 100 bytes (UTF-8 safe — CJK titles break
char-based truncation). Full field catalog and per-type templates:
references/output-templates.md.
# Self-describing files — metadata, thumbnail and chapters travel with the media:
yt-dlp --embed-metadata --embed-thumbnail --embed-chapters URL
yt-dlp ships ~1,800 extractors (yt-dlp --list-extractors); everything in this
skill except the YouTube-specific parts (nsig, player clients) applies unchanged
to Twitch, Vimeo, SoundCloud, TikTok, and the rest. yt-dlp -v URL names the
extractor in use. For sites with no dedicated extractor, the generic extractor
sniffs direct media/HLS URLs out of the page. When a non-YouTube site returns
403 to yt-dlp but plays fine in a browser, it's usually TLS-fingerprint
blocking — --impersonate fixes it (see
failure-triage).
| Footgun | The trap | The rule |
|---|---|---|
| Default format selection | YouTube "best" = VP9/AV1+Opus in WebM/MKV; downstream tooling expecting MP4 forces a transcode you could have avoided | State codecs at download: -S "vcodec:h264,acodec:m4a" --merge-output-format mp4 |
| -f best | Selects best single pre-merged file — caps at ~720p on YouTube; modern high-res is always video+audio merged | Drop the -f entirely or use -S; b only as the tail of a / fallback chain |
| -f hard filters | "Requested format is not available" the moment an extractor stops offering that exact combo | Prefer -S (degrades gracefully); always end -f chains with /b |
| --recode-video casually | Full blind transcode — no CRF/preset/pix_fmt control, big quality/time cost | --remux-video when codecs fit; real transcodes via ffmpeg-ops |
| --download-sections w/o --force-keyframes-at-cuts | Clip boundaries snap to keyframes — seconds of slop | Add the flag when cuts must be exact (re-encodes at cuts only) |
| Channel sync w/o --break-on-existing | Every cron run re-walks the entire channel (thousands of metadata requests) | --download-archive + --break-on-existing --lazy-playlist |
| No %(id)s in filename | Title changes/dupes make files impossible to correlate with the archive | Always [%(id)s] in the template |
| --cookies-from-browser chrome on Windows | Chrome 127+ app-bound encryption — extraction fails | Use firefox, or export cookies.txt |
| Authenticated bulk runs, no sleeps | Account flagged/banned; IP rate-limited | Throwaway account + --sleep-requests/--sleep-interval always |
| Throttled to ~50-100 KB/s | Looks like a network problem; it's the nsig arms race | Update yt-dlp FIRST (check-ytdlp-version.sh --live) |
| "nsig extraction failed" / "unable to extract" | Debugging the command/network when the binary is stale | Same — update first; these errors mean outdated, not broken usage |
| Raw %(title)s filenames | Emoji/colons/slashes break on Windows and some CI filesystems | --restrict-filenames or .100B-truncated fields + [%(id)s] |
| Thin format list on a fresh machine | No JS runtime — YouTube player JS now needs one (EJS); runtime-less extraction is deprecated and may offer only low-res premuxed | Install deno, or --js-runtimes node; see failure-triage |
| git-bash (MSYS) path mangling | /tmp/...-style args convert per-arg — templates containing %(...)s skip conversion while plain paths convert, scattering outputs | Use Windows-style paths (X:/dir/...) for -o/-P/--download-archive under git-bash |
| pip-installed yt-dlp -U | Self-update doesn't work for pip/uv installs (silently a no-op with a warning) | uv tool upgrade yt-dlp; -U is for the standalone binary only |
The ladder — run in order, stop at the first fix:
check-ytdlp-version.sh --live → exit 10 → update. This
closes most "nsig extraction failed", missing-format, and throttling cases.yt-dlp -v URL — read the actual extractor error,
don't guess from the summary line.--cookies-from-browser firefox, or a different network/IP.--concurrent-fragments, back off and resume later (archive files
make every run resumable).--proxy URL through an
allowed region; the old --geo-bypass header tricks rarely work anymore.Full decision tree with error-message → cause mapping, --extractor-args
escape hatches, and when to file upstream:
references/failure-triage.md.
Follows the Skill Resource Protocol:
--help with examples, stdout = data only, --json envelope
(claude-mods.ytdlp-ops.version-check/v1), semantic exit codes (0 clean,
2 usage, 7 network/yt-dlp unavailable — advisory, 10 drift finding).
| Script | Job | Worked invocation |
|---|---|---|
| check-ytdlp-version.sh | Staleness verifier: --offline structural (CI gate), --live = installed-version age vs latest GitHub release + documented-flag existence in yt-dlp --help + metadata-only smoke extraction | check-ytdlp-version.sh --live --json \| jq '.data.days_behind' — exit 10 = >60 days behind, a documented flag vanished, or smoke failed; 7 = network/API unreachable (advisory) |
Load on demand — one concept per file:
| Reference | Load when |
|---|---|
| format-selection.md | Any -f/-S decision, codec targeting, filter grammar, avoiding transcodes |
| playlists-archives.md | Playlists, channels, --download-archive, incremental sync jobs |
| auth-cookies.md | Private/members/age-gated content, browser cookie matrix, ban avoidance |
| output-templates.md | -o field catalog, paths, sanitization, per-type routing |
| subtitles-metadata.md | Sub download/convert/embed, transcript workflows, metadata/thumbnail embedding |
| sponsorblock.md | SponsorBlock categories, mark vs remove, chapter workflows |
| failure-triage.md | Any download failure — 403/429/geo/nsig/throttling decision tree |
Assets: format-presets.json — canonical, date-stamped
-S/flag presets per destination (web MP4, STT audio, archival, clip, mobile-small).
bash skills/ytdlp-ops/tests/run.sh # fully offline; no network, no yt-dlp needed
Structural assertions plus the verifier's 60-day age logic exercised through its
CM_YTDLP_INSTALLED/CM_YTDLP_LATEST test seams. Real --live runs happen only
in the scheduled freshness workflow — a network blip must never fail a PR.
tools
Comprehensive ffmpeg/ffprobe operations - probe-first media processing: transcode and compress (H.264/H.265/AV1/Opus), frame-accurate cut/trim/concat, EDL-driven editing, color grading and .cube LUTs, audio loudnorm and mixing, STT/Whisper audio prep, subtitles, GIF and thumbnails, HLS packaging, hardware encoding (NVENC/QSV/AMF/VideoToolbox), restoration, scene and silence detection, VMAF quality gates, screen capture, yt-dlp interop. Triggers on: ffmpeg, ffprobe, transcode, convert video, compress video, encode video, extract audio, trim video, cut video, concat videos, video to gif, thumbnail, contact sheet, burn subtitles, watermark, resize video, crop video, change fps, slow motion, timelapse, loudnorm, normalize audio, audio for whisper, transcription prep, scene detection, silence detection, remove silence, color grade, LUT, tonemap HDR, vmaf, nvenc, hardware encode, hls, remux, faststart, deinterlace, stabilize video, denoise video, screen record, EDL, keyframes.
development
Payload CMS 3 (Next.js-native) architecture - collections, globals, fields, access control, hooks, Local API, storage adapters, and database (Postgres/MongoDB/SQLite). Use for: payload, payloadcms, payload cms, payload 3, collection config, access control, payload hooks, local api, payload fields, multi-tenant payload, payload nextjs, payload s3, payload r2, payloadcms architecture, headless cms typescript.
testing
Cypress end-to-end and component testing operations - selector/retry-ability strategy, cy.intercept network stubbing, cy.session auth, component vs e2e, flake diagnosis, CI, Test Replay. Use for: cypress, e2e test, component test, cy.get, cy.intercept, cy.session, data-cy, data-test, retry-ability, flake, flaky test, cypress.config, cy.mount, Test Replay, custom commands, fixtures.
tools
Craft CMS 5 development - content modeling, Twig templating, element queries, GraphQL, plugins, and the Craft 4-to-5 Matrix-as-entries change. Use for: craft cms, craftcms, craft 5, twig, pixel & tonic, matrix field, entry types, sections, element query, eager loading, blitz, project config, headless craft, craft graphql, craft plugin, craft 4 to 5 upgrade.