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.
# Migration Guide — youtube-legend-cli


- Upgrade notes for the releases that change something you depend on
- The current version is `0.4.0`, and every section below is written from it looking back
- Languages: [English]MIGRATION.md | [Português Brasileiro]MIGRATION.pt-BR.md


## v0.4.0 — Browser Subsystem Removed
- This is the release that breaks scripts, and it breaks them on purpose
- The two browser-driven providers, `provider-noteey` and `provider-getsubs`, were removed after both were measured broken at the source
- The whole subsystem only they used went with them: the Chromium session, the anti-fingerprint patches, the process lifecycle, the request interception and the traffic observer
- The `auto` chain is now `provider-decopy` followed by `provider-noiz`, and both speak plain HTTP
- MEASURED on 2026-09-04: `--provider provider-noteey` now exits `2`, and so do `--asr`, `--headless`, `--no-fallback` and `config discover`

| Removed | What to do instead |
|---|---|
| `--provider provider-noteey` | `--provider auto`, `--provider provider-decopy` or `--provider provider-noiz` |
| `--provider provider-getsubs` | the same three |
| `--asr` and the `asr` configuration key | nothing, because no surviving provider publishes two tracks of one language to choose between |
| `--headless` | nothing, because no browser is launched any more |
| `--no-fallback` | pin a provider with `--provider`, which never falls back |
| `config discover` | nothing, because it existed to locate Chromium and `Xvfb` |

- These modules left the tree with the subsystem: `provider::getsubs`, `provider::provider_noteey` and `net::observe`
- MEASURED on 2026-09-04: none of the three is defined anywhere under `src/`
- CORRECTED on 2026-09-04: this list used to name `provider::stealth` as a fourth removal, and that module did NOT leave
- `src/provider/mod.rs` still declares `pub mod stealth;`, and `docs/public-surface.txt` records it under `[0.4.0]`
- What the subsystem took from it was the browser-driven interior, and the module itself survives
- Only `provider::provider_noteey` had ever been published, at `v0.3.4`, so it is the single removal that breaks an embedder
- `docs/public-surface.txt` is the record of that surface, and `repo_invariants::public_surface_matches_the_record_and_removals_forced_a_breaking_bump` is the gate that holds it
- Under Cargo rules a `0.x` crate already breaks by growing its MINOR, and `0.3.4 -> 0.4.0` is exactly that step

### Migration Steps for v0.4.0
- Search your scripts for `provider-noteey` and `provider-getsubs` and replace both with `auto` or with one of the two surviving providers
- Delete every `--asr`, `--headless` and `--no-fallback` from your command lines, because each one now exits `2` instead of being ignored
- Delete the `asr` key from your `config.toml`, and confirm the result with `youtube-legend-cli config show`
- Delete any Chromium, `Xvfb` or browser-profile setup from your images and your CI-free runbooks, because none of it is used
- Rebuild any embedder that named `provider::getsubs`, `provider::provider_noteey` or `net::observe`
- Leave any use of `provider::stealth` alone, because that module is still public at `0.4.0`
- Do NOT delete your local cache, because every body an older version wrote there is still readable

### What Survived the Removal on Purpose
- The parser `parse::noteey_to_text` and the variant `SubtitleFormat::NoteeyTranscript` were KEPT, and the name is the only thing about them that is stale
- MEASURED on 2026-09-04: `src/cache/mod.rs` still reads the on-disk hint `noteey-transcript` and maps it to that variant
- Your cache therefore keeps answering after the upgrade, and `--offline` keeps serving the bodies an older version stored
- Removing the variant would not have deleted one file of yours; it would only have made those files unreadable, and that is not a trade this project makes for the sake of a tidy enum
- The user-facing messages no longer name a provider that does not exist, and they describe the SHAPE of the body instead


## v0.3.x — What Changed Before
- v0.3.0 added a YouTube-direct provider and the `--provider` flag, and both are gone from `0.4.0`
- v0.3.1 added a headless browser mode behind `--headless`, and that flag is gone from `0.4.0`
- v0.3.2 consolidated the chain onto `provider-noteey`, which `0.4.0` removed
- v0.3.3 fixed a batch of correctness defects and renamed the JSON body field from `body` to `content`
- v0.3.4 migrated the crates.io owner and the GitHub repository, and changed nothing you invoke
- The provider names from any of those releases are historical, and `--help` is the only authority on the current list
- If you are jumping from `0.2.x` or `0.3.x` straight to `0.4.0`, read the v0.4.0 section above and treat everything here as background


## The JSON Envelope You Get Today
- The success envelope carries `byte_size`, `content`, `delivered_language`, `duration_ms`, `format`, `language`, `provider`, `source_url`, `target_resolved`, `target_source` and `video_id`
- `language` echoes the language you ASKED for, by design
- `delivered_language` is the only field that reports the track actually delivered
- `target_source` distinguishes `argv` from `batch-file`
- The old `body` field is gone, so a filter of `.body` must become `.content`

```json
{
  "provider": "provider-decopy",
  "video_id": "dQw4w9WgXcQ",
  "target_resolved": "https://youtu.be/dQw4w9WgXcQ",
  "target_source": "argv",
  "language": "en",
  "delivered_language": "en",
  "format": "txt",
  "content": "...",
  "byte_size": 2320,
  "duration_ms": 0,
  "source_url": "cache"
}
```

- The error envelope is FLAT, and its discriminator is the boolean `error`
- Its fields sit at the TOP level and are `error`, `code`, `message`, `kind`, `retryable`, `provider`, `video_id`, `target_resolved`, `target_source`, `requested_language`, `available_languages` and `attempts`
- There is no `.error` object and there is no `ok` field, so a filter like `.error.kind` FAILS because you cannot index a boolean
- Both envelopes go to `stdout` under `--json`, and never to `stderr`

```json
{
  "error": true,
  "code": 69,
  "message": "providers unavailable",
  "kind": "provider_unavailable",
  "retryable": false,
  "provider": "provider-noiz",
  "video_id": "aaaaaaaaaaa",
  "target_resolved": "https://youtu.be/aaaaaaaaaaa",
  "target_source": "batch-file",
  "requested_language": "en",
  "attempts": []
}
```

- The authoritative schemas live in `docs/schemas/`, as `success-envelope.schema.json`, `error-envelope.schema.json`, `dry-run-envelope.schema.json` and `config-envelope.schema.json`
- Ask the binary for the same contract at run time with `--print-schema`


## Exit Codes You Can Depend On
- `0` means success
- `2` means invalid usage, and the envelope `kind` reads `invalid_usage`
- `66` is `EX_NOINPUT`, used by `language_unavailable` and by `no_captions`
- `69` is `EX_UNAVAILABLE`, used by `provider_rate_limited`
- `143` means the process was ended by the SECOND signal, and that is deliberate semantics
- Under `--batch` the process exit code is the code of the WORST item, and never the code of the first one


## Configuration Instead of Environment Variables
- The binary reads NO environment variable to govern its behaviour
- `RUST_LOG` was removed on 2026-08-31 and is no longer read, so setting it changes nothing
- Any older guide that taught you an environment variable was wrong, and the replacement is a CLI flag or the `config` TOML file
- `youtube-legend-cli config list-keys` prints the whole registry, with each key, its type and its description
- `youtube-legend-cli config path` prints the file that a run will really read
- `--config <PATH>` points a single run at a different file, which is how you keep a throwaway profile


## Step-by-Step Migration
- Update the binary with `cargo install youtube-legend-cli --locked --force`
- Confirm the install with `youtube-legend-cli --version`, which must report `0.4.0` or newer
- Run `youtube-legend-cli --help` and compare it against every flag your scripts pass, because a removed flag now exits `2` instead of being ignored
- Smoke-test the default behaviour by piping a known URL through the binary and reading the transcript
- Re-run your JSON consumers against the flat error envelope, and fix any filter that still indexes `.error` as an object
- Re-run your batch pipelines and read the process exit code as the WORST item, not the first
- Rebuild any embedder that named `provider::getsubs`, `provider::provider_noteey` or `net::observe`, and leave `provider::stealth` alone because it survives


## Rollback
- Pin the previous version with `cargo install youtube-legend-cli --version 0.3.4 --locked --force`
- Restore the scripts you had, because the flags removed at `0.4.0` come back with the older binary
- Leave the cache alone, because a newer envelope field is ignored rather than fatal on the older binary
- The CLI never auto-upgrades, so the binary on disk is the binary that runs
- Pin the version explicitly at install time whenever a script must not move under you


## See Also
- [CHANGELOG.md]../CHANGELOG.md — the full release history
- [docs/ARCHITECTURE.md]ARCHITECTURE.md — provider pipeline and chain semantics
- [docs/CROSS_PLATFORM.md]CROSS_PLATFORM.md — six declared targets, container recipes, XDG paths
- [docs/TESTING.md]TESTING.md — the local gates that exercise this surface