Repository navigation
Description
Summary
ytx is a good tool with a real niche. The code is small and tidy, it parses URLs well, it outputs clean JSON, and it is 2–3x faster than the yt_json shell function it replaces. The main problem is caption track choice. Two bugs there can silently return the wrong transcript, with exit 0 and nothing on stderr.
This review covers v1.0.7 (commit 07d0e12). It combines three passes:
- Source review:
dotnet testpasses all 26 tests. - Live testing: about 40 cases against YouTube on macOS (osx-arm64).
- Competitor survey: run on 8 Oct 2026.
Line numbers refer to src/Ytx/Program.cs unless a file is named.
What's good
- It handles every URL form tested. Watch URLs, bare IDs, youtu.be (including
?si=&t=),/shorts/,/embed/,/live/,music.andm.links, URLs without a scheme, and links with&t=,&list=or&index=all work. Playlist and channel URLs fail cleanly with exit 2. - It's safe to pipe. Stdout is pure JSON with no BOM and a trailing newline, and errors go to stderr. Closing the pipe early with
headcauses no error. - Exit codes are clear. 0 means success, 1 means the video is unavailable, and 2 means a usage error.
- It's fast. A typical run takes about 1.4s, compared with 3.1–4.9s for
yt_json. A 5h15m podcast took about 2.2s, compared with 7.2s, and produced 1.1 MB of JSON. - The argument parser is careful. It supports
--, rejects duplicate URLs and unknown flags, and validates-lwith no value (lines 193–256). - Releases are automated. On each push to
master, CI publishes to NuGet with Trusted Publishing (NuGet/login@v1), then bumps the version, tags it and creates a GitHub Release. It also checks for retries and formastermoving during a run (.github/workflows/publish.yml). - It replaces
yt_jsonexactly. It has the same five-field schema, placeholder strings and timestamp format. Output matchedyt_jsonbyte for byte where the chosen caption track was the same. - The code is in good shape. Nullable reference types are enabled and used correctly, and the README is good.
Bugs, in order of impact
1. Auto-generated captions beat human-written ones
ThenByDescending(t => t.IsAutoGenerated) (line 131) ranks speech-recognition tracks first. An auto track is named something like "English (auto-generated)", which also contains "English", so it wins whenever both tracks exist.
- TED talk
iG9CE55wbtY: ytx returns "[Music] good morning how are you…". The manual track reads "Good morning. How are you? (Audience) Good." - Lex Fridman #501: ytx returns 8,144 auto-caption lines.
yt_jsonreturns 4,773 manual lines.
This affects the default English path, so it hits most users. Fix: use ThenBy.
2. Short language codes match the wrong language
LanguageMatches (lines 267–270) accepts a language name that merely contains the preference (name.Contains(preference)). All matches rank the same, and auto tracks win ties. Confirmed live on iG9CE55wbtY, where exact fr, es and ja tracks all exist:
| Flag | Track returned |
|---|---|
-l fr |
Afrikaans ("Goeie môre") |
-l es |
Chinese |
-l ja |
Azerbaijani |
-l en can also match French, Slovenian or Armenian. The existing tests (tests/Ytx.Tests/ProgramTests.cs:130–143) only cover cases that should match, so they missed this.
Fix: match in this order: exact code, exact name, code prefix (en matching en-*), then substring.
3. Unknown languages fall back silently
When nothing matches, the first track in any language is used (line 132). -l xx, -l Klingon, -l zh and -l pt (only pt-BR and pt-PT exist) all return English. Each exits 0, writes nothing to stderr, and nothing in the output says which track was chosen.
4. Caption failures look like "no captions"
A bare catch (lines 161–164) turns every caption error into an italic placeholder inside the transcript field and exits 0. Network failures, YouTube bot checks and parse errors all look the same as a video with no captions. Scripts have to compare against placeholder text such as _No transcript/captions available for this video._ to detect them.
Smaller issues
- Outdated dependency. YoutubeExplode is pinned at 6.5.6 (
src/Ytx/Ytx.csproj:30), and the latest is 6.6.2. Restore warnsNU1902: the transitive AngleSharp 1.3.0 has a moderate advisory (GHSA-pgww-w46g-26qg). Whether ytx keeps working when YouTube changes depends on this library staying current. - No LICENSE file. The package declares
MIT(Ytx.csproj:22) andREADME.md:177links toLICENSE, but the file doesn't exist. GitHub reportslicense: null. - Thin output. There is no
videoId, canonical URL, caption language, auto or manual flag, channel, duration or upload date. YoutubeExplode already returns most of these from the calls ytx makes. urlechoes the input. For a bare ID,urlis"aCi6BhrvjhM", and tracking parameters such as?si=are kept. The transcript links do use the canonical watch URL.- Thin test coverage. Track selection is an inline lambda in
Main, so it can't be tested. Nothing tests stdin JSON, the output shape or exit codes fromMain, and YoutubeExplode isn't mocked. - No cancellation or timeout. There is no
CancellationToken, no Ctrl+C handling and no timeout. - Random slow calls. About 1 in 8 calls took about 11.5s instead of about 1.5s, and one took 21.7s, on any URL form. Nothing is written to stderr while it happens. It may be a retry after a timeout inside YoutubeExplode or the HTTP stack, but this hasn't been checked.
- Timestamps lose precision. Times are cut to whole seconds (lines 149 and 272–278), and caption end times are never used. That is fine for links, but SRT or VTT output would need the full times.
- Minimal text cleanup.
is replaced after whitespace is collapsed, so double spaces can survive (lines 280–285). Overlapping auto-caption cues are not deduplicated, and the flat transcript loses paragraph structure. - Error messages.
- "Video '…' is not available" doesn't say whether the video is private, deleted or region-blocked.
- Malformed stdin shows System.Text.Json internals (
'a' is an invalid start of a value… Path: $ | LineNumber…).
- Help text gaps.
- It doesn't say that
urlis the only stdin key used. Extra keys such aslanguageare ignored silently. - It doesn't say that a positional argument overrides stdin.
- It doesn't say that
-lfalls back. - It doesn't say that an ID starting with
-needs--. -vmeans version, not verbose.--metadata-only -l fris accepted without a warning.
- It doesn't say that
- Stale doc.
docs/html-viewer-feature.mddescribes aweb/viewer.htmlthat doesn't exist. Commit52e08aaadded only the doc. - CI.
- Actions are pinned by tag, not commit SHA.
- There is no Dependabot or CodeQL, and no CI run for pushes that don't release.
- The test project targets only net8.0.
- The publish job pushes the version bump directly to
master(line 211), so a branch protection rule would break it.
- Packaging. There is no CHANGELOG, NuGet icon, SourceLink or symbols package.
- Output size. For long videos,
transcriptis about 2.9x the size oftranscriptRaw.
Comparison with yt_json
| ytx | yt_json | |
|---|---|---|
| Fields | url, title, description, transcriptRaw, transcript |
Same, in the same order |
| Typical time | 1.3–1.6s | 3.1–4.9s |
| 5h15m video | about 2.2s | 7.2s |
| Default English track | Auto-generated when a manual track exists (bug 1) | Manual |
| Clean stdout | Yes | No when run with zsh -ic: it adds iTerm escape codes and writes can't change option: zle to stderr |
Against the competition
| Tool | Stars and downloads | Strengths | Weaknesses |
|---|---|---|---|
| youtube-transcript-api (Python) | 8.4k stars, about 15.8M PyPI downloads a month | De facto standard. Supports --languages, --exclude-generated, --translate, --list-transcripts, --format json|srt|webvtt|text, several IDs at once, and Webshare or generic proxies |
No metadata. README says cookie auth is currently broken |
| yt-dlp (Python) | about 196k stars | Fullest metadata (-J, including chapters and tracks) and the best anti-blocking tools: cookies from the browser, --proxy, PO token providers |
No clean transcript text. Users have to parse and deduplicate the rolling VTT |
| YoutubeExplode (.NET library) | 3.7k stars | ytx's base. Captions and metadata | A library, not a CLI. No built-in proxy or PO token support |
| Fabric (Go) | 44k stars | fabric -y with transcript, timestamps, comments and metadata |
A large framework |
| jkawamoto/mcp-youtube-transcript (Python) | 487 stars | Closest match in features: transcript plus metadata, 50k-character chunking, proxies | MCP only |
| kimtaeyoon83/mcp-server-youtube-transcript (TypeScript) | 600 stars | Language fallback, optional timestamps | MCP only |
| zemse/yt (Rust) | New | yt analyze returns transcript and metadata as JSON for agents. Supports --format json|srt |
Few users so far |
| steipete/summarize (TypeScript) | 6.7k stars | Falls back to Whisper when a video has no captions | Built around summarising |
| Hosted APIs (Supadata, TranscriptAPI, Apify) | Work from cloud IPs | Paid | |
| Browser extensions (Glasp, Tactiq, NoteGPT, Harpa) | Run on the user's home IP and signed-in browser | Not scriptable |
There is a name clash: koguchic/ytx is a different Rust CLI, installed with cargo install ytx-cli, that only outputs plain text.
The gap ytx fills
- The only maintained .NET option. No other maintained .NET tool returns metadata and an LLM-ready transcript as JSON in one call. The only C# port of youtube-transcript-api, BobLd/youtube-transcript-api-sharp, was last updated in 2021.
- Unique output shape. Clickable
&t=timestamp links in Markdown, raw text and metadata in one document is a combination no competitor offers. - No runtime to install. A single native binary would suit agents and CI. See the separate AOT and performance issue.
- A stable JSON contract. Most competitors print loose text, so a documented, versioned schema would set ytx apart.
What users expect that ytx lacks
- Ways round blocking. YouTube blocks most cloud and datacenter IPs, so ytx fails on CI runners and servers.
- Options:
--proxy, a cookies file, and clear typed errors such asIpBlockedorNoTranscript. - The .NET default proxy already honours
HTTPS_PROXY, but this isn't documented.
- Options:
- Language control.
- Options:
--list-languages, preferring or requiring manual tracks, failing when the requested language is missing, and translation.
- Options:
- Output formats. Plain text, SRT, VTT, and a
segmentsarray of{start, end, text}for agents. - Metadata. Channel, upload date, duration, chapters, tags, view count and thumbnails. Chapters would let the Markdown be split into sections.
- Batch use. Several URLs, a file or NDJSON on stdin, playlist and channel expansion, and concurrency.
- Long transcripts. Chunking or pagination by characters or tokens.
- An MCP server mode. For example,
ytx mcp. This is how Claude Code and Cursor users install tools now. - Optional extras. A Whisper fallback for videos with no captions, comments, and an
-ooutput file flag.
Suggested plan
- Fix track selection.
- Change line 131 to
ThenBy. - Match exact code, then exact name, then prefix, then substring.
- Move selection into a testable method and add tests for the
fr,esandjacases and for manual versus auto tracks.
- Change line 131 to
- Say which track was used. Add
videoId,captionLanguageandisAutoGeneratedto the output, and a warning on stderr when it falls back. - Report caption status. Add a
captionStatusfield (ok,none,blocked,skipped), a distinct exit code, or both. Stop catching every exception. - Housekeeping. Upgrade YoutubeExplode to 6.6.2, add
LICENSE, and remove or implement the stale viewer doc. - Features. In this order:
--proxyand documentedHTTPS_PROXYsupport- a
segmentsarray --format txt|srt|vtt--list-languages- richer metadata
ytx mcp
🤖 Generated with Claude Code
Activity
solrevdev commentedon Oct 8, 2026
Follow-up: the performance side of this review, covering NativeAOT builds and allocation profiling, is in #4. It found that ytx's own code is about 2% of run time, so cutting allocations won't make it noticeably faster. A NativeAOT build starts in 8.6 ms instead of 38 ms and uses about 6x less CPU per call, once one JSON change is made.
The caption-track bugs above are still open and separate from that work.
solrevdev commentedon Oct 8, 2026
Done in v1.1.0, now on NuGet.
-l frreturned Afrikaans)captionLanguageandcaptionIsAutoGeneratedto the outputcaptionStatusandcaptionError, exit code 3, and narrow exception handlingurlechoing the inputurl--timeoutand makes Ctrl+C exit 130 ordering, help and README gaps,--metadata-only -l--proxy,--segments,--format md/txt/srt/vttand--list-languagesThese items were checked and deliberately left alone:
--timeoutnow bounds them.master. Left off by choice.ytx mcp. Dropped. The CLI's JSON contract already serves agents.The full story, for a blog post, is in docs/investigations/2026-10-ytx-review-to-1.1.0.md. NativeAOT continues in #4 and #6.
🤖 Generated with Claude Code