youtube-legend-cli 0.4.0

Non-interactive Rust CLI that downloads YouTube subtitles through third-party providers, using a native Unix stdin/stdout interface.
# Architecture — youtube-legend-cli
- [English]ARCHITECTURE.md | [Português Brasileiro]ARCHITECTURE.pt-BR.md
- Last reviewed on 2026-09-04, against version `0.4.0`
- Scope: a high-level map of the crate, for newcomers and for LLM-assisted contributors
- The rustdoc published on docs.rs is the authoritative reference, and this file is only the map
- The authoritative list of public modules lives in `docs/public-surface.txt`, section `[0.4.0]`


## Bird's-eye view
- `youtube-legend-cli` is a single-binary CLI that turns a YouTube URL into a clean subtitle file
- It speaks the native Unix `stdin` and `stdout` contract
- It exposes a JSON envelope under `--json`
- It NEVER blocks on a TUI and it NEVER blocks on an interactive prompt
- The process is one-shot: it is born, it executes, and it dies inside the same invocation
- There is no daemon, no listening socket, and no background task that outlives `main`
- The two browser-driven providers and the whole browser subsystem were removed on 2026-09-04
- Both remaining providers are plain HTTP

```mermaid
flowchart LR
  A[CLI args / stdin] --> B[Cli::parse]
  B --> C[config: XDG merge]
  C --> D[i18n: resolve interface locale]
  D --> E[commands::run]
  E --> F[ProviderChain]
  F --> G[decopy / noiz]
  G --> H[HTTP]
  H --> I[parse + NFC]
  I --> J[cache]
  J --> K[surface: reduce payload]
  K --> L[stdout envelope]
```


## Module map
- The table names only modules the tree defines today
- Every public path below appears in `docs/public-surface.txt`, section `[0.4.0]`

| Module | Role |
|---|---|
| `cli` | clap-derived parser, `Cli` struct, `config` subcommand, language and provider vocabulary |
| `config` | XDG discovery, key registry, typed get/set/unset, precedence of CLI over file over built-in default |
| `i18n` | `Language` and `Message` enums, one exhaustive catalogue per locale, `OnceLock` resolution |
| `surface` | agent-native payload reduction: filter, sort, dedupe, limit, select, count, truncate, byte cap |
| `commands` | top-level dispatch and provider-chain assembly |
| `commands::extract` | the single-URL path |
| `commands::batch` | the list-input path behind `--batch` |
| `commands::config_cmd` | the six verbs of the `config` subcommand |
| `commands::gen` | `completions` and `man`, both derived from the clap command tree |
| `commands::schema` | `--print-schema`, emitting the six schemas compiled from `docs/schemas/` |
| `provider` | `Provider` trait with `list_tracks`, plus throttle and degraded-error classification |
| `provider::decopy` | HTTP, native track only, no language choice, first in the chain |
| `provider::noiz` | HTTP, a five-per-day quota, last in the chain |
| `provider::robots` | optional robots.txt gate, OFF by default |
| `provider::stealth` | seeded pseudo-random source shared by the HTTP providers |
| `net` | network layer that owns WAF classification and session fingerprint |
| `net::session` | persistent cookie jar and a coherent request-header fingerprint |
| `net::waf` | web-application-firewall classification and retry escalation |
| `parse` | subtitle parsing entry points and Unicode NFC normalisation |
| `parse::video_id` | `extract_video_id` over the watch, shorts, embed and `youtu.be` forms |
| `parse::srv3` | SRV3 and JSON3 caption bodies |
| `parse::player_response` | caption-track classifier over `ytInitialPlayerResponse`, never a download path |
| `cache` | TTL cache keyed on video, language and format, written atomically via rename |
| `retry` | `retry_with_backoff` and `CircuitBreaker` |
| `io` | stdin, stdout and TTY helpers, plus broken-pipe classification |
| `error` | `AppError`, `AppResult`, `NoSubtitleReason` |
| `error::sysexits` | the sysexits mapping every exit code resolves through |
| `logging` | tracing subscriber setup |

- `text`, `secret_endpoints`, `provider::chain` and `provider::health` are internal
- Those four are `pub(crate)` or private, and they NEVER belong to the public surface


## Stream contract
- `stdout` carries only the subtitle body or the JSON envelope
- `stderr` carries every log line, every progress line and every human error message
- `stdin` accepts a single URL, or one URL per line under `--batch`
- Payload reduction happens before serialisation, so the large envelope never exists in memory


## Target designation
- Every execution envelope reports `target_resolved` and `target_source`
- The source is `argv`, `stdin` or `batch-file`, as `TargetSource` in `commands` defines it
- A side-effecting verb NEVER resolves its subject from ambient state without saying so
- An ambiguous invocation fails closed with a usage code instead of acting somewhere else


## Provider pipeline
- `commands::run` dispatches to `commands::extract` for a single URL
- `commands::run` dispatches to `commands::batch` for list input
- `build_provider_chain` assembles the chain, and under `auto` the order is decopy, then noiz
- The chain records every try in a ledger, and `attempts` in the error envelope is that ledger
- The `attempts` ledger may additionally carry `watch-page`, which is a probe over the watch-page HTML
- The `watch-page` probe only names the real cause after the chain fails, and it NEVER delivers a caption
- The chain honours a minimum interval between calls and classifies every failure
- HTTP 429, HTTP 5xx and quota exhaustion are degraded, so the chain walks past them to the next provider
- An environment failure takes precedence over `NoSubtitle`
- `list_tracks` reports the tracks an upstream actually publishes
- The requested `--lang` is negotiated against that list with likely-subtag expansion
- `pt-BR` and `pt-PT` stay distinct, and so do `zh-Hans` and `zh-Hant`
- A language the upstream does not carry yields `NoSubtitle(LanguageUnavailable)`, never `NotPublished`
- The body text is cleaned, normalised to Unicode NFC, written to the cache and handed to the output layer


## Cancellation and exit codes
- `SIGINT` and `SIGTERM` are wired through `tokio_util::CancellationToken` in `main.rs`
- `main.rs` defines `EXIT_SIGINT` as `130`, `EXIT_SIGTERM` as `143` and `EXIT_BROKEN_PIPE` as `141`
- A run cut short by a SECOND signal exits `143`, and that semantics is deliberate
- A broken pipe on stdout exits `141` instead of surfacing as a generic IO failure
- Invalid usage exits `2`, with `kind` equal to `invalid_usage`
- `language_unavailable` and `no_captions` exit `66`, which is `EX_NOINPUT`
- `provider_rate_limited` exits `69`, which is `EX_UNAVAILABLE`
- Under `--batch` the process exit is the WORST item, never the first
- Remaining codes follow the table in `error::sysexits`


## MSRV
- `1.88.0`, declared in the `rust-version` field of `Cargo.toml`
- The local toolchain pinned by `rust-toolchain.toml` may be newer
- The MSRV in `Cargo.toml` is the contract with users


## See Also
- [README]../README.md — user-facing entry point
- [CHANGELOG]../CHANGELOG.md — release history
- [llms.txt]../llms.txt and [llms-full.txt]../llms-full.txt — LLM-friendly excerpts
- [docs/decisions/]decisions/ — ADRs in MADR format
- [docs/agent-teams-workflow.md]agent-teams-workflow.md — playbook used to deliver multi-agent releases
- [docs/COOKBOOK.md]COOKBOOK.md — recipes for scripting the CLI
- [docs/HOW_TO_USE.md]HOW_TO_USE.md — day-one operator guide
- [docs/TESTING.md]TESTING.md — test architecture and gates
- [docs/MIGRATION.md]MIGRATION.md — upgrade notes per release
- [docs/schemas/]schemas/ — machine-readable output contracts
- [docs/public-surface.txt]public-surface.txt — the pinned public module surface