Collapsed history snapshot — represents the cumulative state of TubeSage through 1.2.21. Earlier commit history (covering versions 1.2.0 through 1.2.20) was rewritten into this single root commit to remove tracking of a personal deploy script that referenced local filesystem paths. The current release content is unchanged. See manifest.json for the plugin version. Older tags (1.2.0 through 1.2.20) have been retired. Features in this release line: - YouTube transcript extraction (ScrapeCreators API + local fallbacks) - LLM-driven summarisation (OpenAI, Anthropic, Google, Ollama) - Per-provider always-visible API key rows - Selected-provider model controls with registry-aware overrides - Custom-model parameter overrides with synchronous panel refresh - Max tokens field with blur-commit and reset-on-blank - Settings de-pollution migration on plugin load - Summary-callout split for multi-section model output - Security patches: fast-xml-parser >=5.7.0, uuid >=14.0.0
7.2 KiB
YouTube Transcript Extraction (TubeSage) — Findings & Next Steps
TL;DR
TubeSage’s “local transcript” extraction relied on YouTube’s private youtubei endpoints.
- Phase 1 (WEB / ScrapeCreators method): call
youtubei/v1/nextto discover transcript params, then callyoutubei/v1/get_transcript. - Phase 2 (ANDROID spoof): call
youtubei/v1/playeras an Android client to obtaincaptionTracks[].baseUrl, then download captions via that URL. - Current state (as of 2026-03-05): both
youtubei/v1/player(ANDROID) andyoutubei/v1/get_transcript(WEB) are failing withHTTP 400FAILED_PRECONDITION(“Precondition check failed.”). This appears to be YouTube tightening request integrity/session requirements, making the private API approach brittle.
This document captures what we implemented, how it evolved, what’s failing now, and the recommended path forward.
Context
TubeSage extracts transcripts to:
- create timestamped notes in Obsidian
- feed the transcript into LLM summarization
- support multiple fallbacks when YouTube transcript retrieval fails
All network requests go through Obsidian’s requestUrl via the shim:
src/utils/fetch-shim.ts
The transcript code lives in:
src/youtube-transcript.ts
Phase 1 — WEB transcript extraction (ScrapeCreators two-step approach)
Reference article (conceptual basis, not copied here):
How it works (TubeSage implementation)
-
Fetch the watch page (
/watch?v=VIDEO_ID) to extract dynamic config:INNERTUBE_API_KEYINNERTUBE_CLIENT_VERSIONVISITOR_DATA
Implemented in
YouTubeTranscriptExtractor.getYouTubeConfig():src/youtube-transcript.ts
-
Call
youtubei/v1/nextto get transcript “params”:- POST
https://www.youtube.com/youtubei/v1/next?prettyPrint=false - body includes
{ context: { client: { clientName: "WEB", clientVersion, visitorData } }, videoId } - then recursively search the JSON for
getTranscriptEndpoint.params
- POST
-
Call
youtubei/v1/get_transcriptwith those params:- POST
https://www.youtube.com/youtubei/v1/get_transcript?prettyPrint=false - body includes
{ context: { client: { clientName: "WEB", clientVersion, visitorData } }, params } - parse transcript segments from the returned structure (
cueGroups,initialSegments, etc.)
- POST
Why we moved away from this approach
This is a private API and historically has been prone to returning HTTP 400 responses (even when next succeeds). The code comments already acknowledge this brittleness:
- “WEB client with ScrapeCreators method … often fails with HTTP 400”
Phase 2 — ANDROID client spoof (Player API → captionTracks → timedtext download)
Why this was added
When the WEB get_transcript flow became unreliable, we added a more reliable path:
- call
youtubei/v1/playeras an ANDROID client - extract
captions.playerCaptionsTracklistRenderer.captionTracks[].baseUrl - download captions directly from that
baseUrl(forcingfmt=json3)
This avoids get_transcript entirely and leverages the caption track URL YouTube returns to clients.
How it works (TubeSage implementation)
-
Call
youtubei/v1/playerwith Android client context:- POST
https://www.youtube.com/youtubei/v1/player?key=<INNERTUBE_API_KEY>&prettyPrint=false context.client.clientName = "ANDROID"- Android UA +
X-Youtube-Client-Name: 3
Implemented in:
YouTubeTranscriptExtractor.fetchViaPlayerApiAndroid()(src/youtube-transcript.ts)
- POST
-
Pick a caption track (prefer matching
lang, else first track). -
Fetch captions from
captionTracks[].baseUrl:- add/replace
fmt=json3 - parse JSON3 (or XML variants) into
TranscriptSegment[]
Implemented in:
YouTubeTranscriptExtractor.fetchCaptionTrack()(src/youtube-transcript.ts)
- add/replace
Current failure (2026-03-05): FAILED_PRECONDITION on both paths
Observed behavior
Recent logs show:
youtubei/v1/player(ANDROID) →HTTP 400FAILED_PRECONDITIONyoutubei/v1/next(WEB) →HTTP 200(still succeeds)youtubei/v1/get_transcript(WEB) →HTTP 400FAILED_PRECONDITION
The error shape returned by YouTube looks like:
{
"error": {
"code": 400,
"message": "Precondition check failed.",
"status": "FAILED_PRECONDITION"
}
}
What this likely means
This isn’t an LLM/provider issue; transcript extraction is failing upstream.
FAILED_PRECONDITION from youtubei generally indicates YouTube is rejecting requests that don’t meet newer integrity, session binding, or anti-automation requirements. Because youtubei is private/unsupported, changes like this can (and do) happen without warning.
Impact on TubeSage
When all extraction paths fail, TubeSage:
- may still return metadata (title/author) if it was successfully extracted
- writes a placeholder transcript line indicating extraction failure
Note: the placeholder message currently says “ANDROID, WEB, and Supadata methods all failed” even when Supadata wasn’t configured (Supadata only runs when an API key is provided). This is just messaging accuracy, not the root cause.
Recommended next steps (more robust, less “signature chasing”)
1) Prefer watch-page captions over youtubei (recommended)
Instead of calling private youtubei endpoints, fetch the watch HTML and parse:
ytInitialPlayerResponse.captions.playerCaptionsTracklistRenderer.captionTracks[].baseUrl
TubeSage already parses ytInitialPlayerResponse for metadata in:
YouTubeTranscriptExtractor.getVideoMetadata()(src/youtube-transcript.ts)
Extending that to retrieve caption tracks and download captions via baseUrl should be more resilient than continuing to chase youtubei preconditions.
2) Add a public captions fallback (/api/timedtext)
When captionTracks aren’t present in ytInitialPlayerResponse, attempt the public caption endpoints (language list + download) as a best-effort fallback.
3) Keep youtubei paths as “experimental”
If we keep ANDROID spoof / get_transcript:
- hide behind a toggle (default off)
- fail fast on
FAILED_PRECONDITION(don’t loop/retry) - make the user-facing error explicit (“YouTube blocked internal API; try alternate extraction mode or a transcript service”)
4) Optional: third-party transcript service
TubeSage already includes a Supadata fallback when configured. This can remain a reliable alternative when YouTube blocks local extraction.
Appendix — Code map
- Main orchestrator / fallbacks:
YouTubeTranscriptExtractor.fetchTranscript()(src/youtube-transcript.ts) - Config extraction from watch HTML:
YouTubeTranscriptExtractor.getYouTubeConfig()(src/youtube-transcript.ts) - WEB two-step (ScrapeCreators-style):
youtubei/v1/next→youtubei/v1/get_transcript(withinfetchTranscript()) - ANDROID spoof via
youtubei/v1/player:YouTubeTranscriptExtractor.fetchViaPlayerApiAndroid()(src/youtube-transcript.ts) - Caption download + parsing:
YouTubeTranscriptExtractor.fetchCaptionTrack()(src/youtube-transcript.ts) - HTTP shim used everywhere:
obsidianFetch()(src/utils/fetch-shim.ts)