Skip to content

Review of v1.0.7: caption track bugs, gaps and comparison with other tools #3

Closed

Description

@solrevdev

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 test passes 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. and m. 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 head causes 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 -l with 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 for master moving during a run (.github/workflows/publish.yml).
  • It replaces yt_json exactly. It has the same five-field schema, placeholder strings and timestamp format. Output matched yt_json byte 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_json returns 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 warns NU1902: 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) and README.md:177 links to LICENSE, but the file doesn't exist. GitHub reports license: 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.
  • url echoes the input. For a bare ID, url is "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 from Main, 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 url is the only stdin key used. Extra keys such as language are ignored silently.
    • It doesn't say that a positional argument overrides stdin.
    • It doesn't say that -l falls back.
    • It doesn't say that an ID starting with - needs --.
    • -v means version, not verbose.
    • --metadata-only -l fr is accepted without a warning.
  • Stale doc. docs/html-viewer-feature.md describes a web/viewer.html that doesn't exist. Commit 52e08aa added 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, transcript is about 2.9x the size of transcriptRaw.

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

  1. 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 as IpBlocked or NoTranscript.
    • The .NET default proxy already honours HTTPS_PROXY, but this isn't documented.
  2. Language control.
    • Options: --list-languages, preferring or requiring manual tracks, failing when the requested language is missing, and translation.
  3. Output formats. Plain text, SRT, VTT, and a segments array of {start, end, text} for agents.
  4. Metadata. Channel, upload date, duration, chapters, tags, view count and thumbnails. Chapters would let the Markdown be split into sections.
  5. Batch use. Several URLs, a file or NDJSON on stdin, playlist and channel expansion, and concurrency.
  6. Long transcripts. Chunking or pagination by characters or tokens.
  7. An MCP server mode. For example, ytx mcp. This is how Claude Code and Cursor users install tools now.
  8. Optional extras. A Whisper fallback for videos with no captions, comments, and an -o output file flag.

Suggested plan

  1. 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, es and ja cases and for manual versus auto tracks.
  2. Say which track was used. Add videoId, captionLanguage and isAutoGenerated to the output, and a warning on stderr when it falls back.
  3. Report caption status. Add a captionStatus field (ok, none, blocked, skipped), a distinct exit code, or both. Stop catching every exception.
  4. Housekeeping. Upgrade YoutubeExplode to 6.6.2, add LICENSE, and remove or implement the stale viewer doc.
  5. Features. In this order:
    • --proxy and documented HTTPS_PROXY support
    • a segments array
    • --format txt|srt|vtt
    • --list-languages
    • richer metadata
    • ytx mcp

🤖 Generated with Claude Code

https://claude.ai/code/session_01GheLxQMy85bM3Cb7mWY1CT

Activity

  1. solrevdev commented on Oct 8, 2026

    @solrevdev
    OwnerAuthor

    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.

  2. solrevdev commented on Oct 8, 2026

    @solrevdev
    OwnerAuthor

    Done in v1.1.0, now on NuGet.

    Item from this review Where
    Bug 1, auto-generated captions beat human-written ones #8
    Bug 2, short codes matched the wrong language (-l fr returned Afrikaans) #8
    Bug 3, silent fallback #8 adds a stderr warning; #10 adds captionLanguage and captionIsAutoGenerated to the output
    Bug 4, caption failures looked like "no captions" #10 adds captionStatus and captionError, exit code 3, and narrow exception handling
    Outdated YoutubeExplode and NU1902, missing LICENSE, stale viewer doc #9
    Thin output, and url echoing the input #10 adds metadata fields and a canonical url
    No cancellation or timeout #11 adds --timeout and makes Ctrl+C exit 130
      ordering, help and README gaps, --metadata-only -l #11
    Features: proxy, segments, formats, language list #12 adds --proxy, --segments, --format md/txt/srt/vtt and --list-languages
    CI: tag pins, no Dependabot or CodeQL, tests on net8.0 only #13
    Packaging: CHANGELOG, icon, SourceLink and symbols #13 and #14

    These items were checked and deliberately left alone:

    • Overlapping auto-caption cues. YoutubeExplode reads srv3, which has no rolling duplicates. On two auto tracks, 0–3 neighbouring cues overlapped, and those were real repeated speech.
    • Random 11–21 s calls. None appeared in 40 timed runs (1.0–3.0 s each). --timeout now bounds them.
    • Branch protection versus the publish job's push to master. Left off by choice.
    • ytx mcp. Dropped. The CLI's JSON contract already serves agents.
    • Chapters and other rich metadata. YoutubeExplode doesn't expose chapters.

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions