yt-dlp/yt-dlp
github.com/yt-dlp/yt-dlp ↗ · 1229 files · 11.4 MB
yt-dlp is a feature-rich command-line audio/video downloader with support for thousands of sites.
Overview
yt-dlp is a Python 3.10+ command-line audio/video downloader (also usable as the `yt_dlp` library) that routes URLs through a registry of ~1750 site-specific InfoExtractor classes to produce metadata 'info dicts', then dispatches downloads to protocol-specific FileDownloaders and runs ffmpeg-based post-processors. It is a youtube-dl fork distinguished by zero required dependencies, a plugin system, pluggable network request handlers (urllib/requests/curl_cffi/websockets) with browser impersonation, and a built-in self-updater that pulls verified binaries from GitHub releases.
System map
yt-dlp is a single-process Python CLI/package. Execution enters through `yt_dlp.main()` (console script or `python -m yt_dlp`), parses optparse options plus config files into a `ydl_opts` params dict, then constructs one `YoutubeDL` orchestrator that owns the extractor table, post-processor lists, cookiejar, cache, and the RequestDirector. The codebase is layered: `extractor/` produces info dicts, `networking/` abstracts HTTP/WebSocket transports, `downloader/` writes bytes to disk per protocol, and `postprocessor/` runs ffmpeg and other mutations. Shared mutable state lives in `globals.py` `Indirect` cells, which is also how plugins merge into the core registries. Build artifacts (zipapp, PyInstaller binaries, lazy_extractors.py) are produced by the Makefile and bundle/devscripts.
- CLI entrypoint chain: the `yt-dlp` console script maps to `yt_dlp:main`; `python -m yt_dlp` runs `__main__.py` which calls `yt_dlp.main()`; `main()` sets `IN_CLI` and calls `_real_main()`.
- `_real_main()` orchestrates the whole run: `setproctitle`, `parse_options`, plugin loading via `plugin_dirs`/`_load_all_plugins`, `Updater` self-update, then `ydl.download(all_urls)` or `download_with_info_file`, returning `_download_retcode`.
- `YoutubeDL` (4554-line god object) is the central runtime boundary: its `params` dict is the configuration, and it owns `_ies` extractor instances, `_pps` post-processor lists keyed by POSTPROCESS_WHEN, the cookiejar, `Cache`, and the request director.
- The extractor registry is `yt_dlp/extractor/_extractors.py` importing ~1752 IE classes; `extractor/extractors.py` prefers a generated `lazy_extractors.py` (`_CLASS_LOOKUP`) and otherwise eagerly builds the lookup, ordering YouTube extractors first for match performance and `GenericIE` last as the catch-all.
- All HTTP/WebSocket traffic goes through `RequestDirector.send()`, which preference-sorts registered `RequestHandler` subclasses in the module-level `_REQUEST_HANDLERS` dict (Urllib always; Requests, Websockets, CurlCffi imported defensively).
- Downloads are dispatched by protocol: `get_suitable_downloader`/`_get_suitable_downloader` consult `PROTOCOL_MAP` (HlsFD, DashSegmentsFD, F4mFD, RtmpFD, HttpFD, YoutubeLiveChatFD, etc.) and can delegate to external tools (aria2c, wget, curl, axel, httpie, ffmpeg) via `ExternalFD` subclasses.
- Post-processors register into the `postprocessors` Indirect dict (all classes ending in 'PP'); FFmpeg work is executed by `real_run_ffmpeg` building an argv and calling `Popen.run`.
- Cross-module shared state is held in `Indirect` cells in `globals.py`: `extractors`, `postprocessors`, `plugin_dirs` (default `['default']`), `plugin_ies`, `plugin_pps`, `supported_js_runtimes`, `supported_remote_components`, `IN_CLI`, `LAZY_EXTRACTORS`.
- The package has zero hard dependencies (`dependencies = []`); extras `default`, `curl-cffi`, `secretstorage`, `deno` add requests/urllib3/websockets/brotli/mutagen/pycryptodomex/certifi/yt-dlp-ejs, with exact-pinned `pin-*` groups for reproducible builds.
- On-disk state: `Cache` writes JSON envelopes (`yt-dlp_version` + data) under `cachedir` (default `$XDG_CACHE_HOME` or `~/.cache/yt-dlp`) keyed by section/key; the download archive is a text file of `extractor_key video_id` lines appended under `locked_file`.
- Self-update subsystem `Updater` targets three GitHub repos (stable/nightly/master), downloads the `_update_spec` and `SHA2-256SUMS` release assets, sha256-verifies the new binary, swaps `.new`/`.old` files, and re-executes via `Popen` in `restart()`.
- Python >=3.10 is enforced twice: `requires-python` in pyproject and an `ImportError` raised at import time in `yt_dlp/__init__.py`.
Feature catalog
The feature surface is one large optparse CLI plus the `YoutubeDL` Python API. Options are organized into ~15 groups (General, Network, Geo, Selection, Download, Filesystem, Thumbnail, Verbosity, Workarounds, Video Format, Subtitles, Authentication, Post-Processing, SponsorBlock, Extractor). Major features: per-site extraction with playlist/search support, format selection DSL, output templates, download archive dedup, browser-cookie and netrc authentication, TLS/browser impersonation, ffmpeg post-processing incl. SponsorBlock, external downloaders, plugins, JS runtimes/remote components for YouTube challenge solving, self-update, and extensive listing/simulation/JSON dump modes.
- The CLI is defined in `create_parser()` using optparse OptionGroups: General, Network, Geo-restriction, Video Selection, Download, Filesystem, Thumbnail, Internet Shortcut, Verbosity/Simulation, Workarounds, Video Format, Subtitle, Authentication, Post-Processing, SponsorBlock, Extractor Options.
- Config files are auto-loaded: `parseOpts` searches user/system config dirs for `yt-dlp.conf`, `config`, `config.txt` and supports `--ignore-config`; URLs come from argv, `-a/--batch-file` (including '-' for stdin).
- Format selection uses a DSL parsed by `build_format_selector` (e.g. 'b/bv', the test default) combined with `FormatSorter` sorting in `sort_formats`.
- Output naming is a template engine (`outtmpl`) supporting `%(field)s` expansion, defaults per type, sanitization, and literal `%%` — implemented in `parse_outtmpl`/`prepare_outtmpl`/`evaluate_outtmpl`.
- `--download-archive` deduplicates across runs: `_make_archive_id` builds `extractor_key video_id`, `in_download_archive` checks the in-memory set (plus `_old_archive_ids`), `record_download_archive` appends under `locked_file`.
- Authentication features: `--cookies-from-browser` supports brave/chrome/chromium/edge/opera/vivaldi/whale/firefox/safari with OS keyring decryption, `--cookies` Netscape files, and per-extractor username/password or netrc via `_NETRC_MACHINE`/`_perform_login`.
- Browser impersonation (`--impersonate`, `--list-impersonate-targets`) maps targets like chrome/safari/firefox/edge/tor to the curl_cffi request handler, with a rendered availability table in `_real_main`.
- Post-processing features include FFmpeg merge/extract-audio/remux/embed-subs/thumbnail & subtitle conversion/fixups/chapter splitting, SponsorBlock removal, metadata parsing, MoveFilesAfterDownload, and xattr writing.
- `--exec [WHEN:]CMD` runs user shell commands after chosen stages via `ExecPP`, which validates template expansion safety unless the `allow-unsafe-exec-expansion` compat opt is set.
- Self-update `-U/--update` supports channels stable/nightly/master (`UPDATE_SOURCES`) and `--update-to CHANNEL@TAG` for pinning.
- Listing/simulation outputs: `--list-extractors`, `--list-extractor-descriptions`, `-F/--list-formats` (render_formats_table), list-thumbnails/subtitles, `--simulate`, `--dump-json`/`--dump-single-json` via `sanitize_info`, and forced `--print`/`--print-to-file` machinery.
- Geo-restriction bypass fakes `X-Forwarded-For` with a random IP from a country or CIDR block (`--geo-bypass-country`, `--geo-bypass-ip-block`), initialized per-extractor and retried once on GeoRestrictedError.
Data flow
Data flows: CLI args + config files -> `ydl_opts` params dict -> URL list -> extractor selection by `_VALID_URL` regex -> `ie.extract()` performs HTTP via the shared RequestDirector and returns an info dict -> `process_ie_result` normalizes and recursively resolves playlists/URL results -> format selection computes `requested_formats` -> `process_info` writes sidecars and calls `dl()` -> a protocol FileDownloader streams bytes to `.part`/final files -> post-processors mutate files -> archive/cache/sidecar JSON are the persistence outputs. The info dict is the single mutable data structure threaded through the whole pipeline.
- Input boundary: `get_urls` merges argv URLs with batch-file/stdin lines; `parse_options` then produces the flat `ydl_opts` params dict consumed by `YoutubeDL`.
- Extractor dispatch: `YoutubeDL.extract_info` iterates `self._ies` and picks the first extractor whose `suitable(url)` — a `_match_valid_url` regex match on `_VALID_URL` — returns true; no match yields 'No suitable extractor'.
- `InfoExtractor.extract` runs `initialize()` (pre-login, `_perform_login`, geo-bypass setup, `_real_initialize`) then `_real_extract(url)`, and can retry once with a fake IP on GeoRestrictedError; errors are normalized into `ExtractorError`.
- All extractor HTTP flows through `self._downloader.urlopen` -> `_request_director.send`, which applies basic-auth extraction, `sanitize_url`, proxy/header cleanup, then per-handler `validate`/`send` in preference order.
- After extraction, `add_default_extra_info` stamps `webpage_url`, `original_url`, `extractor`, `extractor_key` onto the info dict; `process_ie_result` then dispatches on `_type` (video/playlist/url/compat_list) and `__process_playlist`/`__process_iterable_entry` recurse into entries via `extract_info`.
- Format selection: `build_format_selector` parses the `-f` spec into selector functions applied in `process_video_result`, producing `requested_formats` (split A/V) or a single format on the info dict.
- `process_info` computes filenames via `prepare_filename`, writes description/subtitles/thumbnails sidecars, then calls `self.dl(temp_filename, info_dict)`; multi-format downloads iterate `requested_formats` into separate files later merged.
- `dl()` picks a FileDownloader via `get_suitable_downloader`, attaches progress hooks, and calls `fd.download(name, new_info)`; `FileDownloader.download` handles skip-if-present, sleep intervals, then `real_download` writes to `filename + '.part'` with resume metadata in `filename + '.ytdl'`.
- Post-processing is staged: `run_all_pps` iterates `self._pps[key]` for each `POSTPROCESS_WHEN` stage ('pre_process','after_filter','video','before_dl','post_process','after_move','after_video','playlist'), each `pp.run(infodict)` returning files to delete/move.
- Output boundaries: media files plus optional sidecars (`.info.json` via `_write_info_json`, `.description`, subtitle files, thumbnails), the download archive line append, and stdout JSON from `sanitize_info` (strips `__private` keys and non-serializable objects to repr).
- Cookies enter via `load_cookies` (browser extraction or Netscape file) into a `YoutubeDLCookieJar`, are attached to every request handler in `build_request_director`, and persisted on `YoutubeDL.__exit__`/`save_cookies`.
- Error flow: `_handle_extraction_exceptions` wraps extraction — `ReExtractInfo` loops, `GeoRestrictedError` reports with VPN hint, `ExtractorError` reports with optional traceback, `ignoreerrors` downgrades to report-only; the CLI exit code is `ydl._download_retcode` (e.g. 101 for DownloadCancelled).
Behavior traces
Three end-to-end flows were traced in code. Startup: `__main__` -> `main` -> `_real_main` (options, plugins, updater, YoutubeDL init). Primary action: `download` -> `extract_info` -> IE regex match -> `ie.extract` -> `process_ie_result` -> `process_video_result` -> `process_info` -> `dl` -> FileDownloader -> post_process -> archive write. Persistence/self-update: `Updater.query_update` fetches `_update_spec` + `SHA2-256SUMS`, verifies sha256, atomically swaps the executable, and `restart()` re-invokes the original argv.
- Startup trace: `__main__.py` -> `yt_dlp.main()` sets `IN_CLI` -> `_real_main` calls `setproctitle('yt-dlp')` -> `parse_options` (optparse + config files) -> `print_extractor_information` handles `--list-*` -> `plugin_dirs`/`_load_all_plugins` -> `YoutubeDL(ydl_opts)` -> `Updater` if `-U` -> `ydl.download` or `download_with_info_file`.
- YoutubeDL construction order: `__init__` stores params, initializes `_ies`/`_pps`, loads plugins if not loaded (`all_plugins_loaded`), resolves color policy and `_out_files`, applies js_runtimes/remote_components/compat_opts/http_headers, then `print_debug_header` when auto_init.
- Per-URL trace: `download(url_list)` wraps `extract_info` in `__download_wrapper` (per-URL error containment, dump_single_json) -> `extract_info` scans `self._ies` for `ie.suitable(url)` -> `__extract_info` applies header cookies, calls `ie.extract(url)`, adds default extra info, calls `process_ie_result`.
- Inside `ie.extract`: `initialize()` -> `_initialize_pre_login` -> optional `_perform_login(username, password)` from `_get_login_info` -> `_real_initialize` -> `_real_extract(url)`; GeoRestrictedError triggers `__maybe_fake_ip_and_retry` once.
- Video persistence trace: `process_ie_result` (video) -> `process_video_result` -> `process_info` -> `pre_process` PPs -> `prepare_filename`/`_ensure_dir_exists` -> `_write_description`/`_write_subtitles`/`_write_thumbnails` -> `dl()` -> `fd.download` -> `real_download` -> `post_process` PPs -> `record_download_archive`.
- Playlist trace: `process_ie_result` sees `_type` playlist -> `__process_playlist` builds `PlaylistEntries` (lazy iteration) -> `__process_iterable_entry` per entry -> recursive `extract_info` on each entry URL with `extra_info` carrying playlist metadata.
- HTTP request trace: extractor calls `_request_webpage`/`_download_webpage` -> `_create_request` builds a `Request` -> `YoutubeDL.urlopen` sanitizes URL, injects basic auth, cleans proxies/headers -> `RequestDirector.send` -> handlers sorted by preference -> `handler.validate`/`handler.send` -> `Response` returned to extractor for parsing.
- Self-update trace: `Updater.update` -> `query_update` -> `_download_update_spec`/`_process_update_spec` resolve tag -> `_download_asset('SHA2-256SUMS')` yields checksum -> `_download_asset(binary)` -> sha256 compare -> write `.new`, rename current to `.old`, rename `.new` -> `restart()` re-execs `self.cmd` via `Popen.run`.
- Cookie-load trace: `--cookies-from-browser`/`--cookies` -> `YoutubeDL` cookiejar property -> `load_cookies` -> `_parse_browser_specification` -> `extract_cookies_from_browser` decrypts the browser store (AES via `aes.py`, keyrings via secretstorage) -> jar shared with every RequestHandler; saved back on `__exit__` via `save_cookies`.
- Plugin-load trace: each subsystem calls `register_plugin_spec(PluginSpec(module_name, suffix, ...))` which inserts a `PluginFinder` on `sys.meta_path`; `_real_main` calls `_load_all_plugins` -> `load_plugins` iterates `yt_dlp_plugins.<module>` submodules -> classes ending in the suffix are merged (plugins first) into the subsystem's Indirect dict.
- Live-wait trace: for upcoming streams `_wait_for_video` computes a wait from `release_timestamp` (or randomized range), sleeps in a countdown loop, then raises `ReExtractInfo`, which `_handle_extraction_exceptions` catches to re-run the whole extraction.
Testing signals
Tests live in `test/` (pytest + unittest hybrid). Two tiers: 'core' tests (utils, networking, jsinterp, postprocessors, options) run offline under `-m 'not download'`, and 'download' tests that hit live sites, generated dynamically from each extractor's `_TESTS`/`_WEBPAGE_TESTS`. CI runs core tests across CPython 3.10-3.15 and PyPy on Linux+Windows with reruns; lint is ruff + autopep8. Coverage is explicitly unimplemented. The extractor download tests depend on live third-party sites and are therefore inherently flaky and mostly excluded from core CI.
- pytest is the test runner (`pytest~=9.0`, `pytest-rerunfailures`), configured with `-ra`, `--tb=short`, `strict_markers`, and a custom `download` marker.
- `test_download.py` dynamically generates `TestDownload` unittest methods from `gettestcases()`/`getwebpagetestcases()`, which iterate every extractor's `_TESTS`/`_WEBPAGE_TESTS`; tests download real URLs and verify info dicts plus md5 checksums.
- `devscripts/run_tests.py` is the canonical entrypoint: `core` => `pytest -m 'not download'`, `download` => `-m download`, and bare names map to `test/test_download.py::TestDownload::test_<IE>`; it falls back to `python -m unittest` when pytest is absent.
- Core CI (`core.yml`) runs on ubuntu+windows across py3.11-3.15 and pypy-3.11 (py3.10 on Windows + quick-test.yml on Linux), skipping `yt_dlp/extractor/**` changes except youtube/common/extractors, and adds `--flaky` only when networking files changed.
- `test_all_urls.py` asserts URL->extractor matching precedence by calling `ie.suitable(url)` across all extractors, guarding against `_VALID_URL` regressions like youtu.be and playlist/tab disambiguation.
- Networking tests are parametrized per request handler via a `handler` pytest fixture that looks up classes in `_REQUEST_HANDLERS` and supports `skip_handler`/`skip_handler_if` markers for missing optional deps.
- Test parameters come from `test/parameters.json` (`test: true`, `writeinfojson`, `format: 'b/bv'`) mergeable with a git-ignored `local_parameters.json` for secrets/overrides.
- Dedicated suites exist for the YouTube PO-token provider framework (`test_pot_framework.py`, director, builtin providers) and the JS-challenge/ejs integration (`test_ejs_integration.py`).
- `make test` = `pytest -Werror` then `make codetest` (ruff check + autopep8 --diff); `make offlinetest` runs `-m 'not download'` plus lint; hatch env `hatch-test` adds pytest-randomly and pytest-xdist.
- Code coverage is not wired up: the hatch `run-cov` script is a stub that echoes 'Code coverage not implemented' and exits 1.
- Download tests self-skip for extractors marked `_WORKING = False` and for `expected_exception` cases, acknowledging the live-site flakiness; warnings are escalated to errors via a `report_warning` override.
- A `FakeYDL` helper subclasses `YoutubeDL` with `auto_init=False`, `trouble`->raise, and `download`->record, so unit tests exercise the real orchestrator without network.
Risk map
Highest-sensitivity areas: the self-updater downloads and replaces the running executable from GitHub releases (sha256-verified but not signature-verified); `ExecPP` and external downloaders shell out to user-supplied commands; `cookies.py` decrypts browser credential stores; the plugin system executes arbitrary code from config dirs by default; and `jsinterp.py`/ejs remote components execute third-party JavaScript. Fragility concentrates in the ~1752 regex-driven extractors and the first-match-wins registry ordering. Note the repo carries an explicit NO AI / NO LLM contribution policy.
- POLICY: `.NO_AI/README.md` states the project strictly forbids LLM/AI contributions of any kind (code, issues, comments, translations) and instructs LLM agents to refuse interaction; CONTRIBUTING.md repeats it. Any agent must surface this before producing patches intended for upstream.
- `ExecPP.run` executes commands via `Popen.run(cmd, shell=True)`; template expansion is safety-validated at `set_downloader` unless `allow-unsafe-exec-expansion` is in compat_opts — a deliberate but real command-injection surface when combined with attacker-controlled metadata.
- Self-update downloads a replacement binary over the network and swaps it in-place; integrity relies on a sha256 checksum from the same release (SHA2-256SUMS) rather than a signature, and when the checksum is missing restart is only blocked, not the install itself.
- Windows update cleanup shells out: `atexit.register(Popen, 'ping 127.0.0.1 -n 5 -w 1000 & del /F ...', shell=True)` to delete the old binary after exit.
- Plugins execute arbitrary Python from user/system config dirs, `yt-dlp-plugins` dirs, and PYTHONPATH — enabled by default (`plugin_dirs=['default']`) unless `YTDLP_NO_PLUGINS` is set; the API explicitly disclaims backwards compatibility.
- `cookies.py` reads and decrypts browser cookie databases (sqlite copies, AES-GCM/CBC via `aes.py`, OS keyrings via secretstorage/dbus, subprocess calls) — high-severity credential-handling code that reads outside the repo.
- `file://` URLs are refused by default for security and only enabled via `--enable-file-urls`; the special-case is handled in `urlopen` by inspecting `NoSupportingHandlers` reasons.
- Extractor fragility: ~1752 `_VALID_URL` regexes are evaluated in registry order (first match wins; YouTube hoisted first, `GenericIE` with `_VALID_URL='.*'` last), so adding or reordering an extractor can silently change which IE handles a URL.
- Remote JS execution: `--remote-components ejs:github`/`ejs:npm` downloads solver code fetched at runtime, and `jsinterp.py` interprets remote JavaScript for signature deciphering — remote-code supply-chain surface gated behind explicit options.
- ffmpeg invocation builds argv programmatically and forces `file:`-prefixed filename arguments to defend against ffmpeg protocol tricks — the comment documents the attack class (filenames containing ':' or starting with '-').
- Mutable global `Indirect` registries mean plugin classes are merged ahead of built-ins (`merge_dicts(regular_classes, destination)`), so a plugin can shadow a core extractor/postprocessor and behavior depends on load order. (inferred)
- Core CI excludes `yt_dlp/extractor/**` changes (except youtube/common/extractors), and download tests hit live third-party sites — so most extractor regressions are only caught by the flaky download tier, not the core gate. (inferred)
Extension points
yt-dlp is built around registries. New site support = a new `InfoExtractor` subclass ending in `IE` registered by importing it in `_extractors.py`. Post-processors are `PP`-suffixed classes in the `postprocessors` dict. Request handlers are `RH`-suffixed classes auto-registered in `_REQUEST_HANDLERS` with optional preference functions. Third parties plug in via `yt_dlp_plugins` namespace packages (extractor/postprocessor), the documented YouTube PO-token provider framework (PTP/PCP/PCSP suffixes), JS challenge providers (JCP), external downloader FDs, and JS runtime registrations. API hooks (progress/post/close/postprocessor) support library embedding.
- InfoExtractor contract: subclass `InfoExtractor`, define `_VALID_URL` (regex or list; `False` for embed-only), implement `_real_extract(url)`; optional hooks `_initialize_pre_login`, `_perform_login` (gated on `_NETRC_MACHINE`), `_real_initialize`, `_GEO_COUNTRIES`/`_GEO_IP_BLOCKS`, `_TESTS`.
- Built-in extractor registration is manual: add an import to `yt_dlp/extractor/_extractors.py`; `extractors.py` then populates the global `extractors` Indirect dict where iteration order determines match precedence.
- Plugin extractors/post-processors: any `yt_dlp_plugins.extractor`/`yt_dlp_plugins.postprocessor` module exposing a public class ending in `IE`/`PP` is discovered via the `PluginFinder` meta-path finder and prepended into the core registries.
- New request handlers: subclass `RequestHandler` (name must end in 'RH'), implement `validate`/`_send`; module-level `_REQUEST_HANDLERS` registration asserts unique `RH_KEY`, and `@register_preference` adds per-request ordering functions.
- YouTube PO-token provider framework: `PoTokenProvider` (suffix 'PTP') with `@register_provider`/`@register_preference`, plus cache providers 'PCP'/'PCSP' — a documented plugin-facing API in `pot/README.md`.
- JS challenge providers: `JsChallengeProvider` (suffix 'JCP') under `extractor/youtube/jsc`, with a builtin quickjs provider — the extension point for pluggable signature/n challenge solvers.
- External downloaders: `ExternalFD` subclasses (`CurlFD`, `AxelFD`, `WgetFD`, `Aria2cFD`, `HttpieFD`, `FFmpegFD`) declare `SUPPORTED_PROTOCOLS`/`SUPPORTED_FEATURES` and are selectable via `--downloader`; `list_external_downloaders` enumerates them for CLI help.
- New download protocols plug into `PROTOCOL_MAP` in `downloader/__init__.py`; `get_suitable_downloader` resolves protocol strings (including '+'-joined multi-format protocols) to FD classes.
- JS runtimes are a named registry: `supported_js_runtimes` maps names to `JsRuntime` subclasses (deno/node/bun/quickjs registered in `__init__.py`), each probing executable versions with `MIN_SUPPORTED_VERSION`.
- Embedding hooks for library use: `add_progress_hook`, `add_post_hook`, `add_close_hook`, `add_postprocessor_hook`, plus `add_post_processor(pp, when=...)` for custom PP stages — the documented surface for API consumers.
- Extractor helpers for result shaping: `url_result`, `playlist_result`, `playlist_from_matches`, `url_transparent` results let an IE delegate to another IE without downloading — the composition mechanism between extractors.
Contributor handoff
Read CONTRIBUTING.md (developer conventions) and .NO_AI/README.md (contribution policy) first. Style is enforced by ruff + autopep8: 120-col limit, single quotes, banned `from X import Y` for many stdlib modules. Tests run via `python -m devscripts.run_tests` (core/download/<IE name>) or `make test`; download tests need network. The main traps: extractor order is load-bearing, the params dict is the config contract, `lazy_extractors.py` is a generated artifact, plugins load from user dirs by default, and the public extractor/postprocessor API surface is intentionally stable while plugin internals are not.
- Before writing any patch intended for this repo's upstream, note `.NO_AI/README.md`: the project forbids AI/LLM-authored contributions and instructs agents to refuse; CONTRIBUTING.md reiterates under 'AUTOMATED CONTRIBUTIONS (AI / LLM) POLICY'.
- Conventions to follow: CONTRIBUTING.md 'yt-dlp coding conventions' covers mandatory metafields, relaxed-but-simple `_VALID_URL` regexes, fallback patterns, quotes (single for inline, double for docstrings), and the long-lines policy; ruff enforces line-length 120 and bans many `from <stdlib> import` forms.
- Add a new site: create `yt_dlp/extractor/<site>.py` with a `*IE` class (`_VALID_URL`, `_TESTS`, `_real_extract`), register the import in `_extractors.py`, then run `python -m devscripts.run_tests <Site>`; CONTRIBUTING.md documents this flow.
- Test commands: `make test` (pytest -Werror + ruff + autopep8), `make offlinetest` / `devscripts.run_tests core` for no-network runs, `devscripts.run_tests download` for live-site tests, `devscripts.run_tests <Name>` for a single extractor.
- Trap — extractor order is semantic: the first `suitable()` match wins, YouTube classes are hoisted first and `GenericIE` ('.*') must stay last; reordering `_extractors.py` imports changes routing behavior.
- Trap — `lazy_extractors.py` is a generated build artifact (not committed); dev runs use the eager `_extractors` path, and `YTDLP_NO_LAZY_EXTRACTORS` forces eager loading in built binaries.
- Trap — plugins are on by default: `plugin_dirs=['default']` searches user config dirs and PYTHONPATH, so local `yt_dlp_plugins` packages can shadow behavior during debugging; use `YTDLP_NO_PLUGINS` or `--no-plugin-dirs` to isolate.
- Config contract: CLI options are defined in `options.py` `create_parser`, mapped into the `ydl_opts` params dict in `parse_options` (`__init__.py`), and consumed everywhere via `self.params.get(...)`; new options need edits in both places plus README regeneration via `make README.md`.
- Public API boundary: `yt_dlp.__all__` exports `YoutubeDL`, `gen_extractors`, `list_extractors`, `main`, `parse_options`; the pot/jsc provider READMEs explicitly mark everything outside the listed modules as internal-only.
- Debuggability: `-v/--verbose` prints the debug header (`print_debug_header` reports version/python/env/ffmpeg), `write_debug` emits `[debug]` traces, and `debug_printtraffic` makes RequestDirector log handler selection per request.
- Broken-site convention: set `_WORKING = False` on the IE (surfaced as '(CURRENTLY BROKEN)' in --list-extractors and skipped in download tests) rather than deleting the extractor.
Languages
| Language | Files | Share of source |
|---|---|---|
| Python | 1126 | 100% |
| YAML | 32 | docs/config |
| Markdown | 10 | docs/config |
| JSON | 3 | docs/config |
| Shell | 3 | 0% |
| TOML | 1 | 0% |
Start reading here
Probable source entrypoint: yt_dlp/__init__.py — inferred from the directory tree and manifest, not the upstream docs.
Source roots
yt_dlp/ (1045 files) devscripts/ (30 files) bundle/ (23 files)
Source layout — yt_dlp/
| Path | Files |
|---|---|
yt_dlp/__pyinstaller/ | 2 |
yt_dlp/compat/ | 8 |
yt_dlp/dependencies/ | 2 |
yt_dlp/downloader/ | 17 |
yt_dlp/extractor/ | 322 |
yt_dlp/networking/ | 10 |
yt_dlp/postprocessor/ | 10 |
yt_dlp/utils/ | 10 |
yt_dlp/__init__.py | 1 |
yt_dlp/aes.py | 1 |
yt_dlp/cache.py | 1 |
yt_dlp/cookies.py | 1 |
yt_dlp/globals.py | 1 |
yt_dlp/options.py | 1 |
Repository structure
| Top-level path | Files |
|---|---|
yt_dlp/ | 1045 |
test/ | 81 |
.github/ | 31 |
devscripts/ | 30 |
bundle/ | 23 |
.NO_AI/ | 1 |
Docs found: CONTRIBUTING.mdREADME.md
Package
pyproject.toml — yt-dlp, 53 declared dependencies.
defaultbrotlibrotlicfficertifi"mutagen"pycryptodomex"requests>=2.32.2,<3"urllib3>=2.0.2,<3"websockets>=13.0"yt-dlp-ejs==0.8.0"]curl-cffi
CI & config entrypoints
README.md— Repository readme.github/workflows/challenge-tests.yml— GitHub Actions workflow.github/workflows/release.yml— GitHub Actions workflow.github/workflows/label-handler.yml— GitHub Actions workflow.github/workflows/core.yml— GitHub Actions workflow.github/workflows/test-workflows.yml— GitHub Actions workflow.github/workflows/codeql.yml— GitHub Actions workflow.github/workflows/sanitize-comment.yml— GitHub Actions workflow.github/workflows/issue-lockdown.yml— GitHub Actions workflow.github/workflows/release-master.yml— GitHub Actions workflow
Structural health signals
Average score 5.9/10 across 96 analyzed source files; 94 hotspots surfaced.
yt_dlp/extractor/vimeo.py— 1139 lines, score 4.7 (Large file, Deep nesting)test/test_networking.py— 1057 lines, score 4.7 (Large file, Deep nesting)yt_dlp/extractor/soundcloud.py— 1110 lines, score 4.7 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
c7fb478d21e92026-09-16 — [ie/youtube] Use Safari UA for web_embedded client (#17684)bbc809a1161d2026-08-30 — [utils] `devalue`: Improve binary type parsing (#16934)9caaf45401492026-08-30 — [ie] Do not warn on intentional generic results (#16683)2f3929ba1a792026-08-29 — [ie/applepodcasts] Fix token caching (#17567)f1df65d5d7a92026-08-30 — [utils]`download_range_func`: Update `__eq__` and `__repr__` to include `from_info` (#16393)