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.
docs.rs failed to build youtube-legend-cli-0.4.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

youtube-legend-cli

English | Português Brasileiro

docs.rs Crates.io v0.4.0 License: MIT OR Apache-2.0 MSRV 1.88.0 Downloads Rust 1.88+

What Is It

  • youtube-legend-cli is a single Rust binary that turns a YouTube URL into a clean subtitle file
  • It is non-interactive, ships no daemon, and never phones home
  • It speaks plain HTTP to third-party subtitle providers, with no browser and no headless runtime
  • The interface is pure Unix: the URL on argv or on stdin, the subtitle body on stdout, logs and progress on stderr
  • The version in Cargo.toml is 0.4.0

Why

  • You want the subtitle text, and you do not want to run a browser to get it
  • You want a command that behaves the same inside a script, a Makefile and an agent loop
  • You want machine-readable failure, so --json gives you a stable kind instead of prose you have to parse
  • You want the process to tell the truth through its exit code, so downstream tooling can branch without reading text
  • You want configuration you can inspect, so config list-keys prints the whole registry instead of a wiki page
  • You want zero surprises about state, so the binary reads NO environment variable at all

Superpowers

  • One positional [URL] accepts the watch, shorts, embed and youtu.be forms
  • Three output formats behind --format: txt, srt and vtt
  • Language negotiation through --lang, in ISO 639-1 or BCP 47 form
  • Two real providers plus an auto chain that walks them in cost order
  • A local file cache with a TTL you control through --cache-ttl
  • Batch mode that reads one URL per line from stdin, with --jobs concurrency and --resume
  • A structured JSON envelope on stdout under --json, for success AND for failure
  • Agent-native output shaping with --select, --filter, --limit, --sort, --dedupe-by and --count-only
  • Output budgets with --truncate-content and --max-output-bytes, so an agent never drowns in a transcript
  • A self-describing surface through --print-schema, config list-keys, completions and man
  • Unicode NFC normalisation on the emitted transcript text
  • Cooperative signal handling: SIGINT exits 130 and SIGTERM exits 143, both declared in src/main.rs
  • Zero telemetry, zero analytics, zero call home

Quick Start

# Install from crates.io
cargo install youtube-legend-cli

# Subtitles for one video, plain text on stdout
youtube-legend-cli "https://youtu.be/dQw4w9WgXcQ" > subtitle.txt

# Keep the timestamps
youtube-legend-cli --format srt "https://youtu.be/dQw4w9WgXcQ" > subtitle.srt

# Brazilian Portuguese
youtube-legend-cli --lang pt-BR "https://youtu.be/dQw4w9WgXcQ"

# Structured JSON on stdout
youtube-legend-cli --json "https://youtu.be/dQw4w9WgXcQ"

# Batch mode, one URL per line on stdin
youtube-legend-cli --batch < urls.txt > subtitles.txt

Installation

# From crates.io
cargo install youtube-legend-cli

# From a local checkout
cargo install --path .

# Verify
youtube-legend-cli --version
  • The Minimum Supported Rust Version is 1.88.0, declared by rust-version in Cargo.toml
  • rust-toolchain.toml pins the local development toolchain, which is a different and newer channel
  • This project has NO CI, so nothing enforces the MSRV for you: build and test on that version yourself
  • The crate ships exactly one binary, named youtube-legend-cli

Usage

Streams

  • stdout carries the subtitle body, or the --json envelope
  • stderr carries logs, progress and human-readable error messages
  • MEASURED on 2026-09-04 with --json against not-a-url: the JSON error object leaves on stdout and the exit code is 65
  • stdin accepts one URL, or a batch of one URL per line under --batch
  • --no-input refuses stdin and fails fast instead of blocking

Input And Output Flags

  • [URL] is the positional argument, in watch, shorts, embed or youtu.be form
  • --lang <LANG> sets the preferred subtitle language, ISO 639-1 or BCP 47, default en
  • --ui-lang <LANG> sets the interface language for messages on stderr
  • --format <FORMAT> picks txt, srt or vtt, default txt
  • --json emits structured JSON on stdout
  • --batch reads multiple URLs from stdin, one per line
  • --resume skips URLs whose output a previous --batch run already emitted
  • --jobs <N> sets how many batch items run concurrently, default 0, which derives the number from the host
  • --no-input refuses stdin and fails fast
  • --yes assumes yes for any confirmation prompt
  • --print-schema prints the JSON Schema of every output surface and exits

Network And Cache Flags

  • --timeout <SECONDS> caps the WHOLE operation, not one HTTP request, default 300
  • --provider <PROVIDER> accepts auto, provider-decopy or provider-noiz, default auto
  • --user-agent <STRING> sets a custom User-Agent for HTTP requests
  • --cache-ttl <HOURS> sets the local cache TTL, default 24
  • --no-cache disables reads from the local cache
  • --dry-run skips network I/O and serves reads from cache only
  • --offline refuses every outbound request and serves reads from the local cache only

Logging And Presentation Flags

  • --verbose emits tracing events to stderr
  • --quiet suppresses all stderr output except errors
  • --log-level <LEVEL> accepts error, warn, info, debug or trace, default warn
  • --log-format <FORMAT> accepts text or json, default text
  • --color <WHEN> accepts auto, always or never, default auto
  • --no-progress suppresses progress bars on stderr
  • --config <PATH> points at a TOML config file

Agent-Native Shaping Flags

  • --select <KEYS> keeps only these comma-separated dotted keys, and carries the alias --fields
  • --filter <EXPR> filters elements by key=value, key!=value or key~substring
  • --limit <N> emits at most N elements
  • --sort <KEY> sorts ascending by a dotted key
  • --dedupe-by <KEY> drops later elements repeating that key's value
  • --count-only replaces the payload with a count
  • --truncate-content <N> shortens strings above N characters, never bytes
  • --max-output-bytes <N> caps the envelope at N bytes by dropping whole elements

Help Flags

  • -h and --help print help
  • -V and --version print the version

Providers

  • provider-decopy pins the decopy.ai provider, which serves the native track only and offers no language choice
  • provider-noiz pins the noiz.io provider, which is subject to a five-per-day quota
  • auto is the default and walks the whole chain in cost order
  • There are exactly TWO providers, and no browser is involved in either
# Default, auto walks the whole chain
youtube-legend-cli "https://youtu.be/dQw4w9WgXcQ"

# Pin one provider
youtube-legend-cli --provider provider-decopy "https://youtu.be/dQw4w9WgXcQ"

Commands

  • The binary exposes exactly THREE subcommands
  • config inspects and edits the configuration file
  • completions <SHELL> prints the shell completion script on stdout
  • man prints the section 1 manual page on stdout, in roff format

config

  • config path prints the absolute path of the configuration file
  • config show prints every key that is set, in dotted form
  • config get <KEY> prints the value of one key
  • config set <KEY> <VALUE> sets one key, validated against the registry
  • config unset <KEY> removes one key, restoring the compiled default
  • config list-keys prints the whole registry: key, type and description
youtube-legend-cli config path
youtube-legend-cli config list-keys
youtube-legend-cli config set lang pt-BR
youtube-legend-cli config get lang
youtube-legend-cli config unset lang
youtube-legend-cli config show

completions

  • completions accepts exactly FIVE shells: bash, elvish, fish, powershell and zsh
  • The script is derived from the clap command tree, so it never drifts from the flags above
youtube-legend-cli completions zsh > _youtube-legend-cli
youtube-legend-cli completions bash > youtube-legend-cli.bash

man

  • man prints the section 1 page in roff format on stdout, derived from the same command tree
youtube-legend-cli man > youtube-legend-cli.1

Configuration

  • The binary reads ZERO environment variables to govern its behaviour
  • RUST_LOG was the fallback for --log-level until 2026-08-31 and is NOT read any more
  • Any document that teaches you an environment variable for this CLI is wrong
  • Configuration happens in exactly two places: a CLI flag, and the TOML file that config owns
  • Run config path to find that file, config list-keys to see every key it accepts, and config set to change one
  • A CLI flag always wins over the file
  • --config <PATH> points the run at a different TOML file

Exit Codes

  • The CLI follows the BSD sysexits.h convention, mapped in src/error.rs
  • 0 means success
  • 2 means the command line was refused before the run started, with kind equal to invalid_usage
  • 64 is EX_USAGE, for an invalid usage or an empty stdin
  • 65 is EX_DATAERR, for an invalid URL or invalid input
  • 66 is EX_NOINPUT, for language_unavailable and for no_captions
  • 69 is EX_UNAVAILABLE, for a provider that is unavailable or rate limited
  • 70 is EX_SOFTWARE, reserved for defects that are ours
  • 74 is EX_IOERR, for a local I/O failure
  • 75 is EX_TEMPFAIL, for the operation timeout
  • 76 is EX_PROTOCOL, for a malformed upstream answer
  • 78 is EX_CONFIG, for a configuration error
  • 130 means the run was cut short by SIGINT
  • 141 means the reader of stdout closed the pipe
  • 143 means the run was cut short by SIGTERM
  • Under --batch the process exit is the code of the WORST item, never of the first one

JSON Envelopes

Success Envelope

  • --json prints one JSON object on stdout
  • The measured keys are byte_size, content, delivered_language, duration_ms, format, language, provider, source_url, target_resolved, target_source and video_id
  • language echoes what you ASKED for, by design
  • delivered_language is the only field that reports the track actually delivered
  • target_source tells argv apart from batch-file

Error Envelope

  • A failure also prints one JSON object on stdout, never on stderr
  • The discriminant is error, whose value is the boolean true
  • There is NO nested .error object and there is NO ok field, so a filter like .error.kind fails against a boolean
  • The top-level fields are error, code, message, kind, retryable, provider, video_id, target_resolved, target_source, requested_language, available_languages and attempts
  • message is localised by --ui-lang and must NEVER be parsed
  • Branch on kind, which is a stable English identifier for the failure class
  • Branch on retryable to decide whether repeating the same call can help
youtube-legend-cli --json "not-a-url" | jaq -c '{kind, code, retryable}'

Integration Patterns

  • Pipe the plain body straight into a file, because stdout carries nothing else
  • Branch on the exit code first, and only then parse stdout
  • Shape the envelope in the CLI instead of post-processing it, so an agent pays for fewer tokens
  • Cap the payload with --truncate-content and --max-output-bytes when a transcript would blow your context
  • Use --offline in a sandbox that must never open a socket
  • Use --no-input in a non-interactive runner, so a missing URL fails fast instead of hanging on stdin
  • Use --resume to make a long batch restartable
# Only the fields an agent needs
youtube-legend-cli --json --select video_id,language,byte_size "https://youtu.be/dQw4w9WgXcQ"

# A batch of URLs, four at a time, restartable
youtube-legend-cli --batch --jobs 4 --resume --json < urls.txt > out.ndjson

# Branch on the exit code, then on kind
if ! out=$(youtube-legend-cli --json "$url"); then
  printf '%s\n' "$out" | jaq -r '.kind'
fi

# Never open a socket
youtube-legend-cli --offline --json "https://youtu.be/dQw4w9WgXcQ"

Performance

  • Three micro-benchmarks live in benches/cache_bench.rs
  • cache_path measures composing the cache file path
  • srt_to_text measures converting a SubRip body into plain text
  • noteey_to_text measures converting a provider transcript body into plain text
  • Run them with cargo bench --bench cache_bench
  • No timing figure is published here, because this repository carries no reproducible baseline to cite
  • The default URL length ceiling is 2048 characters, declared in src/cli/mod.rs
  • The default whole-operation ceiling is 300 seconds, and it wraps the entire provider chain and every retry

Troubleshooting FAQ

The command exits 66 and prints nothing on stdout

  • Exit 66 is EX_NOINPUT, which covers no_captions and language_unavailable
  • Run again with --json and read available_languages to see what the video really offers
  • Ask for one of those languages with --lang

The command exits 69

  • Exit 69 is EX_UNAVAILABLE, which covers a provider that is down or rate limited
  • provider-noiz carries a five-per-day quota, so a quiet day is not the same as a broken install
  • --offline also refuses the network by design, so check whether you passed it

The command exits 2 instead of 64

  • Exit 2 means clap refused the command line before the run started
  • Read the message on stderr, fix the flag spelling, and try again

My JSON filter on .error.kind returns nothing

  • error is a boolean, not an object, so you cannot index it
  • Read .kind at the top level instead

Setting RUST_LOG changes nothing

  • The binary reads NO environment variable
  • Use --log-level on the command line, or set log_level through config set

The run hangs with no URL

  • Without a URL the CLI waits on stdin by design
  • Pass --no-input to fail fast instead

A batch run returned a failure exit even though most URLs worked

  • Under --batch the process exit is the code of the worst item
  • Read the per-item envelopes on stdout to see which ones failed

Contributing

  • Read CONTRIBUTING.md for the development workflow, the MSRV expectation, the style rules and the no Co-authored-by policy
  • This project has NO CI, so you run the gates locally before you claim a change is green

Security

  • Read SECURITY.md for the supported versions, the threat model and the private disclosure channel
  • The secret_endpoints module is declared pub(crate), so upstream hostnames never enter the published rustdoc

Changelog

License