TubeScript
Read, search and copy the transcript of any YouTube video, with clickable timestamps. Works as a Chrome extension on desktop and as a bookmarklet on iPhone and iPad.
TypeScript · React 19 · Chrome Manifest V3 · esbuild · Node test runner · Vercel · no backend, no API keys, $0 to run
The Chrome popup: pick a caption track, search, click any timestamp to jump there, or copy/download the text.
Why I built it
A lot of what I learn comes from YouTube: lectures, tutorials, long talks. Often I don't want to rewatch a 40-minute video to find one explanation. I want to skim it, search it for a word, or paste a section into my notes. YouTube has a "Show transcript" panel, but it's buried, it isn't searchable, it's awkward to copy from, and it's even harder to use on a phone.
This is the follow-up to my other YouTube extension, YTBookmarks, which marks moments in a video. This one makes a video's words searchable. I set strict rules: use only YouTube's own captions, no server, no paid APIs, and never alter or invent text.
The interesting problem
The first obstacle: every caption request came back empty. YouTube now requires a proof-of-origin token (pot) on caption URLs, minted inside its own player. That change in 2026 broke most of the tutorials and open-source libraries I'd normally have leaned on.
Instead of trying to forge tokens, I changed approach: let YouTube's player do the work and observe it.
- A small script runs in the page from the very start and watches the caption requests the player itself makes, remembering each one (with its token) per video.
- To fetch a track, it takes that track's signed URL from the player and adds the parameters from the player's own request.
- If the player hasn't loaded captions yet, it switches CC on through the player's API, waits for the request, then restores your CC setting.
- If YouTube still returns nothing, it falls back to opening YouTube's transcript panel and reading that.
Observed on live YouTube during development (not a formal benchmark): a 23-minute auto-generated transcript (817 lines) loads in about 90 ms, and a cached 47-minute video in about 41 ms.
What it does
- Transcript with clickable timestamps. Click any line to jump the video there.
- Search with highlighted matches and keyboard navigation ("3 / 17").
- Caption track picker: creator captions vs. auto-generated, with a sensible default for your language.
- Copy or download as TXT, with or without timestamps.
- Local cache (8 MB, least-recently-used eviction, 30-day expiry) so reopening is instant.
- iPhone and iPad: a bookmarklet, installed from a static page on Vercel, that opens the same transcript panel on the YouTube site in Safari.
- Shortcut: Alt+T on desktop.
Engineering highlights
- All YouTube-specific code is isolated in
src/extractor/. When YouTube changes something, there's one place to fix. The UI only sees a small interface: getVideoInfo, getTranscript, seekTo.
- One extractor, two products. The Chrome extension and the iPhone bookmarklet share the same page-side code; esbuild builds both from one
build.mjs.
- Pure parsers, tested against real responses. 69 unit tests run on Node's built-in test runner, with saved real YouTube responses as fixtures (JSON3 and XML captions, manual and auto-generated tracks).
- Errors over wrong answers. If a response is empty, cut off or unreadable, the user gets a specific error explaining which part failed, never a partial transcript.
- Privacy by design. Every request goes to
www.youtube.com, made the same way the page makes it. Nothing is collected or sent anywhere else.
- Honest documentation. Known limitations and unverified paths are written down below, along with a manual test checklist for live testing.
What I learned
- Don't fight the platform; observe it. Reverse-engineering the token would have been fragile and against the spirit of the project. Watching the player's own network requests was simpler, more robust, and needed no secrets.
- Isolate what will break. Building on undocumented internals means breakage is a matter of when, not if. Putting all of it behind one module boundary, with fixture tests, turns a future outage into a small, local fix.
- Real data beats assumptions. Saving real YouTube responses as test fixtures kept the parsers honest about what YouTube actually sends, including the differences between manual and auto-generated captions.
- Mobile is a different world. Getting the bookmarklet working in iPhone Safari meant handling the mobile site, Shorts,
youtu.be links and other URL formats, plus Safari quirks like bookmarklets only running from the Bookmarks list.
Tech stack
- TypeScript throughout, React 19 for the popup UI, plain DOM for the bookmarklet
- Chrome Extension Manifest V3 (content script, page-world script, popup)
- esbuild for both builds, Vercel for the static bookmarklet site
- Node's built-in test runner with real-response fixtures
chrome.storage.local for the cache
Run it locally
Requires Chrome 111+ and Node 24+ (the tests use Node's built-in TypeScript support).
npm install
npm run build
Then in Chrome:
- Open
chrome://extensions and turn on Developer mode (top right).
- Click Load unpacked and select the
dist folder.
- Reload any YouTube tabs that were already open. The extension only attaches to pages loaded after it's installed.
- Pin the extension from the puzzle-piece menu so its icon sits in the toolbar.
Updating after a code change: npm run build, click the reload arrow on the extension's card in
chrome://extensions, then reload the YouTube tab. npm run watch rebuilds on every save.
여기까지가 README 앞부분입니다. 나머지는 원문에서 읽을 수 있습니다. README 전문 보기 ↗