extract-youtube 1.0.421 → 1.0.423

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +849 -757
  2. package/dist/cli.js +16 -6
  3. package/dist/cli.js.map +1 -1
  4. package/dist/library/client.d.ts +77 -0
  5. package/dist/library/client.d.ts.map +1 -0
  6. package/dist/library/fields.d.ts +91 -0
  7. package/dist/library/fields.d.ts.map +1 -0
  8. package/dist/library/handler.d.ts +90 -0
  9. package/dist/library/handler.d.ts.map +1 -0
  10. package/dist/library/index.cjs +2 -0
  11. package/dist/library/index.cjs.map +1 -0
  12. package/dist/library/index.d.ts +31 -0
  13. package/dist/library/index.d.ts.map +1 -0
  14. package/dist/library/index.mjs +2 -0
  15. package/dist/library/index.mjs.map +1 -0
  16. package/dist/library/library.d.ts +154 -0
  17. package/dist/library/library.d.ts.map +1 -0
  18. package/dist/library/query.d.ts +43 -0
  19. package/dist/library/query.d.ts.map +1 -0
  20. package/dist/library/stacks.d.ts +102 -0
  21. package/dist/library/stacks.d.ts.map +1 -0
  22. package/dist/library/store.d.ts +51 -0
  23. package/dist/library/store.d.ts.map +1 -0
  24. package/dist/library/stores/d1.d.ts +57 -0
  25. package/dist/library/stores/d1.d.ts.map +1 -0
  26. package/dist/library/stores/memory.d.ts +21 -0
  27. package/dist/library/stores/memory.d.ts.map +1 -0
  28. package/dist/library/tree.d.ts +121 -0
  29. package/dist/library/tree.d.ts.map +1 -0
  30. package/dist/library/types.d.ts +188 -0
  31. package/dist/library/types.d.ts.map +1 -0
  32. package/dist/library/youtube-data.d.ts +78 -0
  33. package/dist/library/youtube-data.d.ts.map +1 -0
  34. package/dist/react/admin/VideoAvailabilityPanel.d.ts +21 -0
  35. package/dist/react/admin/VideoAvailabilityPanel.d.ts.map +1 -0
  36. package/dist/react/admin/VideoEditDialog.d.ts +55 -0
  37. package/dist/react/admin/VideoEditDialog.d.ts.map +1 -0
  38. package/dist/react/admin/VideoLibraryAdmin.d.ts +31 -0
  39. package/dist/react/admin/VideoLibraryAdmin.d.ts.map +1 -0
  40. package/dist/react/grid/StackNav.d.ts +29 -0
  41. package/dist/react/grid/StackNav.d.ts.map +1 -0
  42. package/dist/react/grid/StackedVideoCard.d.ts +20 -0
  43. package/dist/react/grid/StackedVideoCard.d.ts.map +1 -0
  44. package/dist/react/grid/VideoCard.d.ts +72 -0
  45. package/dist/react/grid/VideoCard.d.ts.map +1 -0
  46. package/dist/react/grid/VideoGrid.d.ts +53 -0
  47. package/dist/react/grid/VideoGrid.d.ts.map +1 -0
  48. package/dist/react/grid/VideoList.d.ts +57 -0
  49. package/dist/react/grid/VideoList.d.ts.map +1 -0
  50. package/dist/react/grid/format.d.ts +34 -0
  51. package/dist/react/grid/format.d.ts.map +1 -0
  52. package/dist/react/grid/styles.d.ts +27 -0
  53. package/dist/react/grid/styles.d.ts.map +1 -0
  54. package/dist/react/grid/useResizableColumns.d.ts +18 -0
  55. package/dist/react/grid/useResizableColumns.d.ts.map +1 -0
  56. package/dist/react/grid/useVideoLibrary.d.ts +22 -0
  57. package/dist/react/grid/useVideoLibrary.d.ts.map +1 -0
  58. package/dist/react/index.cjs +1 -1
  59. package/dist/react/index.cjs.map +1 -1
  60. package/dist/react/index.d.ts +25 -0
  61. package/dist/react/index.d.ts.map +1 -1
  62. package/dist/react/index.mjs +132 -17
  63. package/dist/react/index.mjs.map +1 -1
  64. package/dist/react/player/playerStore.d.ts +9 -0
  65. package/dist/react/player/playerStore.d.ts.map +1 -1
  66. package/package.json +17 -7
  67. package/src/library/client.ts +138 -0
  68. package/src/library/fields.ts +329 -0
  69. package/src/library/handler.ts +335 -0
  70. package/src/library/index.ts +100 -0
  71. package/src/library/library.ts +385 -0
  72. package/src/library/query.ts +170 -0
  73. package/src/library/stacks.ts +275 -0
  74. package/src/library/store.ts +54 -0
  75. package/src/library/stores/d1.ts +361 -0
  76. package/src/library/stores/memory.ts +99 -0
  77. package/src/library/tree.ts +299 -0
  78. package/src/library/types.ts +215 -0
  79. package/src/library/youtube-data.ts +195 -0
  80. package/src/react/admin/VideoAvailabilityPanel.tsx +158 -0
  81. package/src/react/admin/VideoEditDialog.tsx +399 -0
  82. package/src/react/admin/VideoLibraryAdmin.tsx +486 -0
  83. package/src/react/grid/StackNav.tsx +71 -0
  84. package/src/react/grid/StackedVideoCard.tsx +48 -0
  85. package/src/react/grid/VideoCard.tsx +315 -0
  86. package/src/react/grid/VideoGrid.tsx +103 -0
  87. package/src/react/grid/VideoList.tsx +419 -0
  88. package/src/react/grid/format.ts +74 -0
  89. package/src/react/grid/styles.tsx +247 -0
  90. package/src/react/grid/useResizableColumns.tsx +85 -0
  91. package/src/react/grid/useVideoLibrary.ts +72 -0
  92. package/src/react/index.ts +33 -0
  93. package/src/react/player/playerStore.ts +16 -0
package/README.md CHANGED
@@ -1,757 +1,849 @@
1
- <p align="center">
2
- <img width="300px" src="https://i.imgur.com/dwg2IYg.png" />
3
- </p>
4
-
5
- <!-- template-git-repo:badges:start -->
6
- <p align="center">
7
- <a href="https://qwksearch.com/api"><img src="https://img.shields.io/badge/Docs-blue?logo=ReadTheDocs&logoColor=white" alt="Documentation" /></a>
8
- <br />
9
- <a href="https://github.com/OpenSourceAGI/qwksearch-research-agent/stargazers"><img src="https://img.shields.io/github/stars/OpenSourceAGI/qwksearch-research-agent" alt="GitHub Stars" /></a>
10
- <a href="https://www.npmjs.com/package/extract-youtube"><img src="https://img.shields.io/npm/dm/extract-youtube.svg" alt="NPM Monthly Downloads" /></a>
11
- <a href="https://www.npmjs.com/package/extract-youtube"><img src="https://img.shields.io/npm/v/extract-youtube.svg" alt="npm version" /></a>
12
- <a href="https://www.npmjs.com/package/extract-youtube"><img src="https://img.shields.io/npm/dt/extract-youtube.svg" alt="NPM Total Downloads" /></a>
13
- <a href="https://www.npmjs.com/package/extract-youtube"><img src="https://img.shields.io/npm/types/extract-youtube" alt="TypeScript types" /></a>
14
- <a href="https://packagephobia.com/result?p=extract-youtube"><img src="https://packagephobia.com/badge?p=extract-youtube" alt="Install size" /></a>
15
- <br />
16
- <a href="https://github.com/OpenSourceAGI/qwksearch-research-agent/issues"><img src="https://img.shields.io/github/issues/OpenSourceAGI/qwksearch-research-agent?logo=github" alt="GitHub Issues" /></a>
17
- <a href="https://github.com/OpenSourceAGI/qwksearch-research-agent/pulls"><img src="https://img.shields.io/github/issues-pr/OpenSourceAGI/qwksearch-research-agent?logo=github&label=PRs" alt="Open Pull Requests" /></a>
18
- <a href="https://github.com/OpenSourceAGI/qwksearch-research-agent/pulls?q=is%3Apr+is%3Aclosed"><img src="https://img.shields.io/github/issues-pr-closed/OpenSourceAGI/qwksearch-research-agent?logo=github&label=PRs%20merged&color=8957e5" alt="Merged Pull Requests" /></a>
19
- <a href="https://github.com/OpenSourceAGI/qwksearch-research-agent/discussions"><img src="https://img.shields.io/github/discussions/OpenSourceAGI/qwksearch-research-agent" alt="GitHub Discussions" /></a>
20
- <a href="https://github.com/OpenSourceAGI/qwksearch-research-agent/commits/master/"><img src="https://img.shields.io/github/last-commit/OpenSourceAGI/qwksearch-research-agent.svg" alt="GitHub last commit" /></a>
21
- <br />
22
- <a href="https://stackblitz.com/github/OpenSourceAGI/qwksearch-research-agent/tree/master/packages/extract-youtube"><img height="20px" src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt="Open in StackBlitz" /></a>
23
- <img src="https://img.shields.io/badge/Bun-14151A?logo=bun&logoColor=white" alt="Bun" /> <img src="https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white" alt="TypeScript" /> <img src="https://img.shields.io/badge/React-20232A?logo=react&logoColor=white" alt="React" /> <img src="https://img.shields.io/badge/Vite-646CFF?logo=vite&logoColor=white" alt="Vite" /> <img src="https://img.shields.io/badge/Jest-C21325?logo=jest&logoColor=white" alt="Jest" />
24
- </p>
25
- <!-- template-git-repo:badges:end -->
26
-
27
- # Extract YouTube Transcript
28
- .
29
- ⚡ **No API keys, no headless browsers, no dependencies. Only the fastest, most optimized YouTube transcript extractor.**
30
-
31
- A production-ready TypeScript port of the popular Python [`extract-youtube`](https://github.com/dbeley/youtube_extract) (100k+ monthly PyPI downloads), optimized for serverless environments and edge computing. No API keys, no headless browsers, no dependencies bloat.
32
-
33
- ## Why This Package?
34
-
35
- ### 🚀 **Superior to Other NPM Alternatives**
36
-
37
- -
38
- - ✅ **Proven algorithm** - Direct port of Python's `extract-youtube` (100k+ monthly PyPI downloads)
39
- - ✅ **Serverless-first** - Works flawlessly in AWS Lambda, Vercel, Cloudflare Workers, Next.js Edge
40
- - ✅ **70% smaller** - ~7KB gzipped vs 20-30KB+ for alternatives
41
- - ✅ **Zero native deps** - Pure TypeScript, no puppeteer or heavy scraping libraries
42
- - ✅ **Better DX** - Full TypeScript, comprehensive error handling, tree-shakeable
43
- - ✅ **Battle-tested** - Same extraction logic trusted by 100k+ monthly users
44
-
45
- ### 📊 **Package Comparison**
46
-
47
- | Feature | **extract-youtube** (this) | youtube-transcript (npm) | extract-youtube (Python) |
48
- | ----------------------------- | -------------------------------- | ------------------------ | ------------------------------- |
49
- | **Language** | TypeScript/Node.js | JavaScript/Node.js | Python |
50
- | **Bundle Size** | **~7KB gzipped** | ~20-30KB+ | N/A |
51
- | **Serverless Ready** | ✅ Yes | ⚠️ Limited | ❌ No |
52
- | **Edge Compatible** | ✅ Yes | ❌ No | ❌ No |
53
- | **Native Deps** | ✅ None | ⚠️ Some | ✅ None |
54
- | **Type Safety** | ✅ Full TypeScript | ⚠️ Partial | ❌ No |
55
- | **Auto-generated Subs** | ✅ Yes | ✅ Yes | ✅ Yes |
56
- | **Translation** | ✅ Yes | ⚠️ Limited | ✅ Yes |
57
- | **Proxy Support** | ✅ Advanced | ⚠️ Basic | ✅ Yes |
58
- | **Error Handling** | ✅ Comprehensive | ⚠️ Basic | ✅ Good |
59
- | **CLI Tool** | ✅ Yes | ❌ No | ✅ Yes |
60
- | **Format Output** | ✅ 6 formats | ⚠️ 1-2 formats | ✅ 5 formats |
61
- | **Monthly Downloads** | Growing | ~50k | ~400k (PyPI) |
62
- | **Code Quality** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
63
-
64
- ### 🎯 **Why Choose This Over Python?**
65
-
66
- **Top packages for YouTube transcripts are `youtube-transcript` (JS/NPM) and `extract-youtube` (Python).** This package combines the best of both:
67
-
68
- 1. **Same reliability as Python** - Direct port of the proven Python implementation
69
- 2. **Better for modern stacks** - Works natively in Node.js, Next.js, React Server Components
70
- 3. **Serverless-first** - Perfect for AWS Lambda, Vercel Functions, Cloudflare Workers
71
- 4. **Faster cold starts** - No Python runtime overhead
72
- 5. **Modern tooling** - NPM ecosystem, TypeScript, tree-shaking, ESM/CJS
73
-
74
- ### ⚡ **Optimized for Production**
75
-
76
- - **Bundle optimized with Vite + Terser** - Aggressive minification and tree-shaking
77
- - **Dual ESM/CJS builds** - Works everywhere (Node.js, bundlers, edge runtimes)
78
- - **Zero external HTTP clients** - Uses native `grab-url` (polyfilled in browsers)
79
- - **Efficient parsing** - Fast XML parsing with minimal memory footprint
80
-
81
- ## 🎯 Quick Facts
82
-
83
- ```
84
- Bundle Size: 7KB gzipped (vs 20-30KB for alternatives)
85
- Cold Start: ~50ms (vs 150-500ms for alternatives)
86
- Memory: ~30MB (vs 45-80MB for alternatives)
87
- Dependencies: 4 minimal (vs 10-50+ for alternatives)
88
- Serverless: ✅ Optimized (vs ⚠️ Limited support)
89
- TypeScript: ✅ Full (vs ⚠️ Partial/None)
90
- ```
91
-
92
- ## Installation
93
-
94
- ```bash
95
- npm install extract-youtube
96
- ```
97
-
98
- **Or try it instantly with npx:**
99
-
100
- ```bash
101
- npx extract-youtube dQw4w9WgXcQ
102
- ```
103
-
104
- ## Quick Start
105
-
106
- ```typescript
107
- import { YouTubeTranscriptApi } from 'extract-youtube';
108
-
109
- const api = new YouTubeTranscriptApi();
110
- const transcript = await api.fetch('dQw4w9WgXcQ');
111
-
112
- for (const snippet of transcript) {
113
- console.log(`${snippet.start}s: ${snippet.text}`);
114
- }
115
- ```
116
-
117
- ## Basic Usage
118
-
119
- ```typescript
120
- // Fetch with language preference
121
- const transcript = await api.fetch('video_id', {
122
- languages: ['de', 'en'] // Try German first, then English
123
- });
124
-
125
- // List available transcripts
126
- const transcriptList = await api.list('video_id');
127
- for (const t of transcriptList) {
128
- console.log(`${t.language} (${t.languageCode})`);
129
- }
130
-
131
- // Find and translate
132
- const transcript = transcriptList.findTranscript(['en']);
133
- const translated = transcript.translate('de');
134
- const fetched = await translated.fetch();
135
- ```
136
-
137
- ## Formatters
138
-
139
- ```typescript
140
- import { SRTFormatter, WebVTTFormatter, JSONFormatter, ArticleFormatter } from 'extract-youtube';
141
-
142
- const transcript = await api.fetch('video_id');
143
-
144
- // SRT format
145
- const srt = new SRTFormatter().formatTranscript(transcript);
146
-
147
- // WebVTT format
148
- const webvtt = new WebVTTFormatter().formatTranscript(transcript);
149
-
150
- // JSON format
151
- const json = new JSONFormatter().formatTranscript(transcript, { indent: 2 });
152
-
153
- // Article format with character-to-timestamp mappings
154
- const article = new ArticleFormatter().formatTranscript(transcript);
155
- console.log(article);
156
- // Output:
157
- // {
158
- // "text": "Full transcript text...",
159
- // "timestamps": "10,15,20 100,200,300", // speeds and positions
160
- // "wordCount": 1234,
161
- // "charCount": 5678
162
- // }
163
- ```
164
-
165
- ## Proxy Support
166
-
167
- ```typescript
168
- import { WebshareProxyConfig } from 'extract-youtube';
169
-
170
- const api = new YouTubeTranscriptApi({
171
- proxyConfig: new WebshareProxyConfig({
172
- proxyUsername: 'your-username',
173
- proxyPassword: 'your-password'
174
- })
175
- });
176
- ```
177
-
178
- See [docs/proxy.md](./docs/proxy.md) for the full guide: generic vs. Webshare
179
- proxies, the CLI flags, why `proxyConfig` does nothing on Cloudflare
180
- Workers/edge (and what to use there instead), and caching in front of the
181
- fetcher so a proxy request is only spent on a video you don't already have.
182
-
183
- ## Error Handling
184
-
185
- ```typescript
186
- import { TranscriptsDisabled, NoTranscriptFound } from 'extract-youtube';
187
-
188
- try {
189
- const transcript = await api.fetch('video_id');
190
- } catch (error) {
191
- if (error instanceof TranscriptsDisabled) {
192
- console.error('Subtitles are disabled');
193
- } else if (error instanceof NoTranscriptFound) {
194
- console.error('No transcript found');
195
- }
196
- }
197
- ```
198
-
199
- ## CLI Usage
200
-
201
- After installing globally or using npx, you can use the `extract-youtube` command:
202
-
203
- ```bash
204
- # Install globally
205
- npm install -g extract-youtube
206
-
207
- # Or use with npx
208
- npx extract-youtube <video-id> [options]
209
- ```
210
-
211
- ### CLI Examples
212
-
213
- ```bash
214
- # Extract transcript in JSON format (default)
215
- extract-youtube jNQXAC9IVRw
216
-
217
- # Extract transcript in SRT format
218
- extract-youtube jNQXAC9IVRw -f srt
219
-
220
- # Extract transcript with specific languages
221
- extract-youtube jNQXAC9IVRw -l en,de
222
-
223
- # Extract transcript with proxy
224
- extract-youtube jNQXAC9IVRw --proxy http://proxy.example.com:8080
225
-
226
- # Extract transcript with Webshare proxy
227
- extract-youtube jNQXAC9IVRw --webshare-user myuser --webshare-pass mypass
228
-
229
- # Extract as plain text with preserved formatting
230
- extract-youtube jNQXAC9IVRw -f text -p
231
-
232
- # Show help
233
- extract-youtube --help
234
- ```
235
-
236
- ### CLI Options
237
-
238
- - `-h, --help` - Show help message
239
- - `-v, --version` - Show version number
240
- - `-l, --languages <codes>` - Comma-separated language codes (e.g., en,de,fr)
241
- - `-f, --format <type>` - Output format: json, text, srt, webvtt, pretty, article (default: json)
242
- - `-p, --preserve-formatting` - Preserve text formatting (line breaks, etc.)
243
- - `--proxy <url>` - HTTP/HTTPS proxy URL
244
- - `--webshare-user <username>` - Webshare proxy username
245
- - `--webshare-pass <password>` - Webshare proxy password
246
-
247
- ## React Components (Floating Player + Transcript Modal)
248
-
249
- `extract-youtube/react` is the package's UI half — two self-contained
250
- components, both ported from the video player used in production on
251
- [debate-ai.com](https://debate-ai.com), with everything app-specific
252
- stripped out. Neither depends on a design system, a state library, or a CSS
253
- framework: they inject their own minimal scoped styles and drop into any
254
- React app.
255
-
256
- - **`<FloatingYouTubePlayer />`** — a floating, draggable, resizable player
257
- that keeps playing while the user moves around your app. Mount it once;
258
- drive it from anywhere with `youtubePlayer.play({ videoId })`.
259
- - **`<YouTubeTranscriptModal />`** — a popout modal with the video on the
260
- left and a transcript panel on the right that scrolls and highlights in
261
- sync with playback; click any line to seek.
262
-
263
- ### Why it's a separate entry point
264
-
265
- `extract-youtube` (the main entry) fetches captions server-side with no
266
- browser dependency at all — that's the whole point of the package.
267
- `extract-youtube/react` is a second, independent entry point that only
268
- exports UI: it never imports the transcript-fetching code, and the main
269
- entry never imports React. Import only the one you need and the other never
270
- ends up in your bundle. React, ReactDOM, and `lucide-react` (used for the
271
- icons) are peer dependencies — install them yourself if you don't already
272
- have them:
273
-
274
- ```bash
275
- npm install extract-youtube react react-dom lucide-react
276
- ```
277
-
278
- ### The floating player
279
-
280
- Mount it once, near the root of your app. It renders through a portal into
281
- `document.body`, so nothing in the tree around it can clip it, hide it, or
282
- remount the playing video on a route change. It renders nothing at all until
283
- something asks it to play.
284
-
285
- ```tsx
286
- // app/layout.tsx (or wherever your app root lives)
287
- import { FloatingYouTubePlayer } from 'extract-youtube/react';
288
-
289
- export default function RootLayout({ children }) {
290
- return (
291
- <>
292
- {children}
293
- <FloatingYouTubePlayer transcriptUrl="/api/transcript" />
294
- </>
295
- );
296
- }
297
- ```
298
-
299
- Then play something from anywhere — a grid, a search result, a keyboard
300
- shortcut. No context provider, no prop drilling:
301
-
302
- ```tsx
303
- import { youtubePlayer, usePlayerState, thumbnailUrl } from 'extract-youtube/react';
304
-
305
- function VideoCard({ videoId, title }: { videoId: string; title: string }) {
306
- const { activeVideo } = usePlayerState();
307
-
308
- return (
309
- <button onClick={() => youtubePlayer.play({ videoId, title })}>
310
- <img src={thumbnailUrl(videoId)} alt="" />
311
- {title} {activeVideo?.videoId === videoId && '(playing)'}
312
- </button>
313
- );
314
- }
315
- ```
316
-
317
- What you get, without wiring any of it up yourself:
318
-
319
- | | |
320
- | --- | --- |
321
- | **Drag & resize** | Drag by the title bar, resize from either side edge or a bottom corner, clamped to the viewport. Mouse and touch. |
322
- | **Minimize** | Collapses to the title bar. The iframe is hidden with CSS, never unmounted, so playback isn't interrupted. |
323
- | **Picture-in-picture** | Pops the video into an always-on-top OS window via the Document Picture-in-Picture API, where the browser supports it. The node is *moved*, not cloned, so playback continues. |
324
- | **Queue** | `addToQueue` / `setQueue` / `playNext`, with an "Up next" strip under the video. |
325
- | **Synced captions** | Optional subtitles panel above the video — caption cues are regrouped into whole sentences (no timestamps), the spoken one highlights and auto-scrolls, and clicking a sentence seeks. Needs `transcriptUrl` or `fetchTranscript` (see below). |
326
- | **Resume** | Remembers what was playing, and how far into it, across a reload — plus a per-video position for the last 50 videos, for 24 hours. `storageKey={null}` turns it off. |
327
- | **Error recovery** | Reads the IFrame API's error codes, explains them ("this video is private", "the owner doesn't allow embedding"), and offers Retry or Watch on YouTube from the same spot. |
328
- | **Theming** | Colours are CSS custom properties on `.eytp-root` and follow `prefers-color-scheme` by default. Override them to match your app. |
329
-
330
- #### Custom controls belong to your app, not the package
331
-
332
- The built-in control strip only holds buttons that mean the same thing for
333
- any YouTube video: play/pause, skip, captions, picture-in-picture, minimize,
334
- close. Anything specific to *your* product — a bookmark, a share menu, a
335
- speed control framed for your users — is yours to render, through
336
- `extraControls`:
337
-
338
- ```tsx
339
- import { Gauge } from 'lucide-react';
340
- import { FloatingYouTubePlayer } from 'extract-youtube/react';
341
-
342
- <FloatingYouTubePlayer
343
- transcriptUrl="/api/transcript"
344
- extraControls={({ playbackRate, player }) => (
345
- <button
346
- // The player's own control classes, so custom buttons match the built-ins.
347
- className={`eytp-btn${playbackRate !== 1 ? ' eytp-btn-active' : ''}`}
348
- onClick={() => player.setPlaybackRate(playbackRate !== 1 ? 1 : 0.65)}
349
- title="Slow it down"
350
- >
351
- <Gauge size={13} />
352
- </button>
353
- )}
354
- />
355
- ```
356
-
357
- debate-ai.com uses exactly this seam for its "slow the debate spread down"
358
- button — its own label, icon and rate, sitting in the same strip. The
359
- package stays generic: it exposes `player.setPlaybackRate()` and reports the
360
- current `playbackRate`, and the host decides what the button says and does.
361
- `renderTitle` is the same idea for the title bar, if you want badges or
362
- links instead of a plain video title.
363
-
364
- #### The imperative API
365
-
366
- Everything the player can do is on `youtubePlayer`, importable anywhere:
367
-
368
- ```ts
369
- import { youtubePlayer, usePlayerState, getPlayerState } from 'extract-youtube/react';
370
-
371
- youtubePlayer.play({ videoId, title, meta }); // meta is yours; passed back untouched
372
- youtubePlayer.play({ videoId }, { startSeconds: 120 });
373
- youtubePlayer.togglePlay();
374
- youtubePlayer.seekTo(90);
375
- youtubePlayer.setPlaybackRate(1.5);
376
- youtubePlayer.addToQueue({ videoId, title });
377
- youtubePlayer.setQueue(videos); // e.g. "play all" over a grid
378
- youtubePlayer.playNext();
379
- youtubePlayer.getCurrentTime(); // seconds
380
- youtubePlayer.close();
381
-
382
- usePlayerState(); // in a component: { activeVideo, isPlaying, isMinimized, playbackRate, queue, startTime }
383
- getPlayerState(); // the same, outside React
384
- ```
385
-
386
- #### Floating player props
387
-
388
- | Prop | Type | Description |
389
- | --- | --- | --- |
390
- | `transcriptUrl` | `string` | Your captions endpoint (see below). Enables the subtitles button. |
391
- | `fetchTranscript` | `(videoId: string) => Promise<{ snippets, error? }>` | Custom caption loader, instead of `transcriptUrl`. |
392
- | `extraControls` | `ReactNode \| (ctx: PlayerControlContext) => ReactNode` | Your own buttons, rendered in the control strip. |
393
- | `renderTitle` | `(ctx: PlayerControlContext) => ReactNode` | Custom title-bar content. Defaults to the video title. |
394
- | `showSubtitles` | `boolean` | Force the captions button on or off. By default it appears only for videos that turned out to have a transcript — every video played is checked, and one without captions gets no button and no error. |
395
- | `showPictureInPicture` | `boolean` | Show the PiP button where supported. Default `true`. |
396
- | `storageKey` | `string \| null` | localStorage key for resume-after-reload. `null` disables persistence entirely. |
397
- | `className` | `string` | Extra class on the player root, for host-side positioning or theming. |
398
- | `onClose` | `() => void` | Called when the user closes the player. |
399
- | `minWidth` / `maxWidth` | `number` | Resize bounds in px. Default 256 / 800. |
400
-
401
- ### Setup: video captions still have to be fetched server-side
402
-
403
- Neither component talks to YouTube's caption endpoints directly — browsers
404
- can't (no CORS, and it would leak this package's whole fetching strategy
405
- client-side for no benefit). Instead, you expose one small backend endpoint
406
- that calls this package's `YouTubeTranscriptApi`, and point the components
407
- at it with `transcriptUrl`.
408
-
409
- **1. Add a backend endpoint** (any framework works — this is a Next.js
410
- route handler, following the same pattern as the Vercel Edge example
411
- above):
412
-
413
- ```typescript
414
- // app/api/transcript/route.ts
415
- import { NextRequest, NextResponse } from 'next/server';
416
- import { YouTubeTranscriptApi, extractVideoId } from 'extract-youtube';
417
-
418
- const api = new YouTubeTranscriptApi();
419
-
420
- export async function GET(req: NextRequest) {
421
- const raw = req.nextUrl.searchParams.get('videoId');
422
- const videoId = raw ? extractVideoId(raw) : null;
423
- if (!videoId) {
424
- return NextResponse.json({ error: 'Missing videoId' }, { status: 400 });
425
- }
426
-
427
- try {
428
- const transcript = await api.fetchTranscript(videoId, { languages: ['en'] });
429
- return NextResponse.json({ videoId, snippets: transcript.toRawData() });
430
- } catch (error) {
431
- // 200 + an `error` field, not a 4xx/5xx — "no captions for this video"
432
- // isn't a server failure, and the components check this field either way.
433
- return NextResponse.json({
434
- videoId,
435
- snippets: [],
436
- error: error instanceof Error ? error.message : 'Failed to fetch transcript',
437
- });
438
- }
439
- }
440
- ```
441
-
442
- **2. Point the components at it** with `transcriptUrl="/api/transcript"`.
443
- The `videoId` query param is appended for you, and one request is shared
444
- per video across every component asking for it.
445
-
446
- `extractVideoId` (exported from the main entry, since it runs fine in Node)
447
- accepts any of the URL shapes YouTube uses — watch, youtu.be, embed, shorts,
448
- live — so you can pass what a user pastes in straight through.
449
-
450
- ### Transcript modal
451
-
452
- ```tsx
453
- import { YouTubeTranscriptModal } from 'extract-youtube/react';
454
- import { extractVideoId } from 'extract-youtube';
455
-
456
- function VideoCard({ url, title }: { url: string; title: string }) {
457
- const videoId = extractVideoId(url);
458
- if (!videoId) return null;
459
-
460
- return <YouTubeTranscriptModal videoId={videoId} title={title} transcriptUrl="/api/transcript" />;
461
- }
462
- ```
463
-
464
- | Prop | Type | Description |
465
- | --- | --- | --- |
466
- | `videoId` | `string` | **Required.** The YouTube video ID (not a full URL — run it through `extractVideoId` first if needed). |
467
- | `title` | `string` | Shown in the modal header and as the iframe's accessible title. |
468
- | `transcriptUrl` | `string` | URL of your backend endpoint (see above). Fetched with `fetch()` when the modal opens. The `videoId` query param is appended automatically if not already present. |
469
- | `fetchTranscript` | `(videoId: string) => Promise<{ snippets, error? }>` | Use instead of `transcriptUrl` if you want to load the transcript some other way (e.g. from a React Query cache). |
470
- | `snippets` | `TranscriptSnippet[]` | Pass transcript data directly to skip fetching entirely — e.g. if you already loaded it server-side. |
471
- | `trigger` | `ReactNode` | Custom element that opens the modal on click. Defaults to a small captions-icon button. |
472
- | `onOpenChange` | `(open: boolean) => void` | Called whenever the modal opens or closes. |
473
-
474
- ### Demo: a video library, a floating player, synced subtitles
475
-
476
- The `demo/` folder is a small standalone app — an Express server for the
477
- transcript endpoint plus a Vite/React page — that puts the whole thing
478
- together: a **grid of saved favorite videos** to pick from, the floating
479
- player they open in, a queue, and per-card transcripts.
480
-
481
- ```bash
482
- cd packages/extract-youtube
483
- npm run build # builds dist/ (main lib + dist/react), which the demo depends on
484
- npm run demo # installs the demo's own deps and starts it
485
- ```
486
-
487
- `npm run demo` runs `cd demo && npm install && npm run dev`, which starts
488
- both the Express transcript API (port 8787) and the Vite dev server
489
- (port 5173, proxying `/api` to 8787) together. Open
490
- **http://localhost:5173** and click any video in the grid: it opens in the
491
- floating player, which you can drag, resize, minimize or pop out while you
492
- keep browsing. Star videos to favorite them, queue more up, hit **Play all**
493
- to run the grid as a playlist, or turn on the captions button to follow a
494
- synced transcript.
495
-
496
- Run the two halves separately if you'd rather:
497
-
498
- ```bash
499
- cd packages/extract-youtube/demo
500
- npm install
501
- npm run server # Express API on :8787
502
- npm run dev # in another terminal — Vite dev server on :5173
503
- ```
504
-
505
- See `demo/README.md` for what each file does — including
506
- `demo/src/SpeedButton.jsx`, the worked example of an app-supplied custom
507
- control.
508
-
509
- ## Features
510
-
511
- ### Core Functionality
512
-
513
- - ✅ Retrieve transcripts for any YouTube video (no API key needed)
514
- - ✅ Support for manually created and auto-generated subtitles
515
- - ✅ Translate transcripts to 100+ languages
516
- - ✅ Multiple output formats (JSON, Text, SRT, WebVTT, Pretty Print, Article)
517
- - ✅ Language preference fallback system
518
- - ✅ Timed segments with start/duration timestamps
519
-
520
- ### Developer Experience
521
-
522
- - ✅ **Full TypeScript support** with comprehensive type definitions
523
- - ✅ **Tree-shakeable** - Only bundle what you use
524
- - ✅ **ESM + CJS** - Works with all module systems
525
- - ✅ **Zero configuration** - Works out of the box
526
- - ✅ Comprehensive JSDoc documentation
527
- - ✅ Intuitive error messages with troubleshooting guidance
528
-
529
- ### Advanced Features
530
-
531
- - ✅ **Proxy support** (Generic HTTP/HTTPS & Webshare residential proxies)
532
- - ✅ **Rate limit handling** with rotating IP pools
533
- - ✅ **Serverless-optimized** - No file system dependencies
534
- - ✅ **Edge runtime compatible** - Runs on Cloudflare Workers, Vercel Edge
535
- - ✅ Command-line interface (CLI) for quick extraction
536
-
537
- ### Production Ready
538
-
539
- - ✅ Battle-tested algorithm from Python package (100k+ monthly downloads)
540
- - ✅ Comprehensive error handling with typed exceptions
541
- - ✅ Automatic retry logic for network failures
542
- - ✅ Small bundle size (~7KB gzipped)
543
- - ✅ No native dependencies or binaries
544
-
545
- ## Serverless & Edge Deployment
546
-
547
- This package is optimized for serverless and edge computing environments:
548
-
549
- ### AWS Lambda
550
-
551
- ```typescript
552
- import { YouTubeTranscriptApi } from 'extract-youtube';
553
-
554
- export const handler = async (event) => {
555
- const api = new YouTubeTranscriptApi();
556
- const transcript = await api.fetch(event.videoId);
557
-
558
- return {
559
- statusCode: 200,
560
- body: JSON.stringify(transcript.toRawData())
561
- };
562
- };
563
- ```
564
-
565
- ### Vercel Edge Functions
566
-
567
- ```typescript
568
- import { YouTubeTranscriptApi } from 'extract-youtube';
569
-
570
- export const config = { runtime: 'edge' };
571
-
572
- export default async function handler(req: Request) {
573
- const { searchParams } = new URL(req.url);
574
- const videoId = searchParams.get('videoId');
575
-
576
- const api = new YouTubeTranscriptApi();
577
- const transcript = await api.fetch(videoId);
578
-
579
- return new Response(JSON.stringify(transcript.toRawData()), {
580
- headers: { 'content-type': 'application/json' }
581
- });
582
- }
583
- ```
584
-
585
- ### Cloudflare Workers
586
-
587
- ```typescript
588
- import { YouTubeTranscriptApi } from 'extract-youtube';
589
-
590
- export default {
591
- async fetch(request: Request) {
592
- const url = new URL(request.url);
593
- const videoId = url.searchParams.get('videoId');
594
-
595
- const api = new YouTubeTranscriptApi();
596
- const transcript = await api.fetch(videoId);
597
-
598
- return new Response(JSON.stringify(transcript.toRawData()), {
599
- headers: { 'content-type': 'application/json' }
600
- });
601
- }
602
- };
603
- ```
604
-
605
- ### Next.js Server Actions / API Routes
606
-
607
- ```typescript
608
- 'use server';
609
-
610
- import { YouTubeTranscriptApi } from 'extract-youtube';
611
-
612
- export async function getTranscript(videoId: string) {
613
- const api = new YouTubeTranscriptApi();
614
- return await api.fetch(videoId);
615
- }
616
- ```
617
-
618
- ### Why It Works Great in Serverless
619
-
620
- - ✅ **Fast cold starts** - Minimal initialization overhead
621
- - ✅ **No file system** - Pure in-memory operations
622
- - ✅ **Small bundle** - Fits well within size limits
623
- - ✅ **No native deps** - No compilation needed
624
- - ✅ **Stateless** - Perfect for serverless architecture
625
-
626
- ## Testing
627
-
628
- ```bash
629
- npm test
630
- ```
631
-
632
- ## Performance Benchmarks
633
-
634
- ### Bundle Size Comparison
635
-
636
- ```
637
- extract-youtube (this): ~7KB gzipped ✅
638
- youtube-transcript: ~25KB gzipped ❌
639
- ytdl-core: ~300KB+ ❌❌
640
- puppeteer-based solutions: ~200MB+ ❌❌❌
641
- ```
642
-
643
- ### Cold Start Times (AWS Lambda)
644
-
645
- ```
646
- extract-youtube: ~50ms ✅
647
- youtube-transcript: ~150ms ⚠️
648
- Python package: ~500ms ❌
649
- ```
650
-
651
- ### Memory Usage
652
-
653
- ```
654
- extract-youtube: ~30MB ✅
655
- youtube-transcript: ~45MB ⚠️
656
- Python package: ~80MB ❌
657
- ```
658
-
659
-
660
- ## Reliability
661
-
662
- This package uses the same proven algorithm as the Python `extract-youtube`:
663
-
664
- - ✅ **100k+ monthly downloads** on PyPI (Python version)
665
- - ✅ **Battle-tested** across thousands of production deployments
666
- - ✅ **Maintained** - Regular updates to handle YouTube API changes
667
- - ✅ **Comprehensive error handling** - Clear error messages for all failure modes
668
-
669
- **Unlike scraping-based alternatives**, this package:
670
-
671
- - Fetches transcripts directly from YouTube's caption endpoints
672
- - Doesn't rely on brittle HTML parsing
673
- - Handles both manual and auto-generated captions
674
- - Works with age-restricted videos (with authentication)
675
-
676
- ## Common Use Cases
677
-
678
- ### 1. AI/ML Applications
679
-
680
- ```typescript
681
- // Extract transcripts for training data, analysis, or AI processing
682
- const transcript = await api.fetch('video_id');
683
- const text = transcript.snippets.map(s => s.text).join(' ');
684
- // Feed to GPT, LLM, or ML model
685
- ```
686
-
687
- ### 2. Accessibility Tools
688
-
689
- ```typescript
690
- // Generate subtitles in multiple formats
691
- const transcript = await api.fetch('video_id');
692
- const srt = new SRTFormatter().formatTranscript(transcript);
693
- const webvtt = new WebVTTFormatter().formatTranscript(transcript);
694
- ```
695
-
696
- ### 3. Content Analysis
697
-
698
- ```typescript
699
- // Analyze video content programmatically
700
- const transcript = await api.fetch('video_id');
701
- for (const snippet of transcript) {
702
- if (snippet.text.includes('keyword')) {
703
- console.log(`Found at ${snippet.start}s: ${snippet.text}`);
704
- }
705
- }
706
- ```
707
-
708
- ### 4. Translation Services
709
-
710
- ```typescript
711
- // Translate videos to multiple languages
712
- const list = await api.list('video_id');
713
- const transcript = list.findTranscript(['en']);
714
- const german = await transcript.translate('de').fetch();
715
- const spanish = await transcript.translate('es').fetch();
716
- ```
717
-
718
- ## Troubleshooting
719
-
720
- ### Rate Limiting / IP Blocks
721
-
722
- If you're getting blocked by YouTube:
723
-
724
- ```typescript
725
- import { YouTubeTranscriptApi, WebshareProxyConfig } from 'extract-youtube';
726
-
727
- const api = new YouTubeTranscriptApi({
728
- proxyConfig: new WebshareProxyConfig({
729
- proxyUsername: 'your-username',
730
- proxyPassword: 'your-password',
731
- // Rotate through 30M+ residential IPs
732
- })
733
- });
734
- ```
735
-
736
- ### No Transcript Found
737
-
738
- ```typescript
739
- try {
740
- const transcript = await api.fetch('video_id', {
741
- languages: ['en', 'de', 'es'] // Fallback languages
742
- });
743
- } catch (error) {
744
- if (error instanceof NoTranscriptFound) {
745
- // Handle case where no transcript exists
746
- console.log('Available:', error.availableTranscripts);
747
- }
748
- }
749
- ```
750
-
751
- ## License
752
-
753
- MIT
754
-
755
- ## Contributing
756
-
757
- Contributions are welcome! Please feel free to submit a Pull Request.
1
+ <p align="center">
2
+ <img width="300px" src="https://i.imgur.com/dwg2IYg.png" />
3
+ </p>
4
+
5
+ <!-- template-git-repo:badges:start -->
6
+ <p align="center">
7
+ <a href="https://qwksearch.com/api"><img src="https://img.shields.io/badge/Docs-blue?logo=ReadTheDocs&logoColor=white" alt="Documentation" /></a>
8
+ <br />
9
+ <a href="https://github.com/OpenSourceAGI/qwksearch-research-agent/stargazers"><img src="https://img.shields.io/github/stars/OpenSourceAGI/qwksearch-research-agent" alt="GitHub Stars" /></a>
10
+ <a href="https://www.npmjs.com/package/extract-youtube"><img src="https://img.shields.io/npm/dm/extract-youtube.svg" alt="NPM Monthly Downloads" /></a>
11
+ <a href="https://www.npmjs.com/package/extract-youtube"><img src="https://img.shields.io/npm/v/extract-youtube.svg" alt="npm version" /></a>
12
+ <a href="https://www.npmjs.com/package/extract-youtube"><img src="https://img.shields.io/npm/dt/extract-youtube.svg" alt="NPM Total Downloads" /></a>
13
+ <a href="https://www.npmjs.com/package/extract-youtube"><img src="https://img.shields.io/npm/types/extract-youtube" alt="TypeScript types" /></a>
14
+ <a href="https://packagephobia.com/result?p=extract-youtube"><img src="https://packagephobia.com/badge?p=extract-youtube" alt="Install size" /></a>
15
+ <br />
16
+ <a href="https://github.com/OpenSourceAGI/qwksearch-research-agent/issues"><img src="https://img.shields.io/github/issues/OpenSourceAGI/qwksearch-research-agent?logo=github" alt="GitHub Issues" /></a>
17
+ <a href="https://github.com/OpenSourceAGI/qwksearch-research-agent/pulls"><img src="https://img.shields.io/github/issues-pr/OpenSourceAGI/qwksearch-research-agent?logo=github&label=PRs" alt="Open Pull Requests" /></a>
18
+ <a href="https://github.com/OpenSourceAGI/qwksearch-research-agent/pulls?q=is%3Apr+is%3Aclosed"><img src="https://img.shields.io/github/issues-pr-closed/OpenSourceAGI/qwksearch-research-agent?logo=github&label=PRs%20merged&color=8957e5" alt="Merged Pull Requests" /></a>
19
+ <a href="https://github.com/OpenSourceAGI/qwksearch-research-agent/discussions"><img src="https://img.shields.io/github/discussions/OpenSourceAGI/qwksearch-research-agent" alt="GitHub Discussions" /></a>
20
+ <a href="https://github.com/OpenSourceAGI/qwksearch-research-agent/commits/master/"><img src="https://img.shields.io/github/last-commit/OpenSourceAGI/qwksearch-research-agent.svg" alt="GitHub last commit" /></a>
21
+ <br />
22
+ <a href="https://stackblitz.com/github/OpenSourceAGI/qwksearch-research-agent/tree/master/packages/extract-youtube"><img height="20px" src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt="Open in StackBlitz" /></a>
23
+ <img src="https://img.shields.io/badge/Bun-14151A?logo=bun&logoColor=white" alt="Bun" /> <img src="https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white" alt="TypeScript" /> <img src="https://img.shields.io/badge/React-20232A?logo=react&logoColor=white" alt="React" /> <img src="https://img.shields.io/badge/Vite-646CFF?logo=vite&logoColor=white" alt="Vite" /> <img src="https://img.shields.io/badge/Jest-C21325?logo=jest&logoColor=white" alt="Jest" />
24
+ </p>
25
+ <!-- template-git-repo:badges:end -->
26
+
27
+ # Extract YouTube Transcript
28
+ .
29
+
30
+ **[Live demo at youtube.js.org](https://youtube.js.org)** · [Storybook](https://youtube.js.org/storybook/)
31
+
32
+ ⚡ **No API keys, no headless browsers, no dependencies. Only the fastest, most optimized YouTube transcript extractor.**
33
+
34
+ A production-ready TypeScript port of the popular Python [`extract-youtube`](https://github.com/dbeley/youtube_extract) (100k+ monthly PyPI downloads), optimized for serverless environments and edge computing. No API keys, no headless browsers, no dependencies bloat.
35
+
36
+ ## Why This Package?
37
+
38
+ ### 🚀 **Superior to Other NPM Alternatives**
39
+
40
+ -
41
+ - ✅ **Proven algorithm** - Direct port of Python's `extract-youtube` (100k+ monthly PyPI downloads)
42
+ - ✅ **Serverless-first** - Works flawlessly in AWS Lambda, Vercel, Cloudflare Workers, Next.js Edge
43
+ - ✅ **70% smaller** - ~7KB gzipped vs 20-30KB+ for alternatives
44
+ - ✅ **Zero native deps** - Pure TypeScript, no puppeteer or heavy scraping libraries
45
+ - ✅ **Better DX** - Full TypeScript, comprehensive error handling, tree-shakeable
46
+ - ✅ **Battle-tested** - Same extraction logic trusted by 100k+ monthly users
47
+
48
+ ### 📊 **Package Comparison**
49
+
50
+ | Feature | **extract-youtube** (this) | youtube-transcript (npm) | extract-youtube (Python) |
51
+ | ----------------------------- | -------------------------------- | ------------------------ | ------------------------------- |
52
+ | **Language** | TypeScript/Node.js | JavaScript/Node.js | Python |
53
+ | **Bundle Size** | **~7KB gzipped** | ~20-30KB+ | N/A |
54
+ | **Serverless Ready** | ✅ Yes | ⚠️ Limited | ❌ No |
55
+ | **Edge Compatible** | ✅ Yes | ❌ No | ❌ No |
56
+ | **Native Deps** | ✅ None | ⚠️ Some | ✅ None |
57
+ | **Type Safety** | ✅ Full TypeScript | ⚠️ Partial | ❌ No |
58
+ | **Auto-generated Subs** | ✅ Yes | ✅ Yes | ✅ Yes |
59
+ | **Translation** | ✅ Yes | ⚠️ Limited | ✅ Yes |
60
+ | **Proxy Support** | ✅ Advanced | ⚠️ Basic | ✅ Yes |
61
+ | **Error Handling** | ✅ Comprehensive | ⚠️ Basic | ✅ Good |
62
+ | **CLI Tool** | ✅ Yes | ❌ No | ✅ Yes |
63
+ | **Format Output** | ✅ 6 formats | ⚠️ 1-2 formats | ✅ 5 formats |
64
+ | **Monthly Downloads** | Growing | ~50k | ~400k (PyPI) |
65
+ | **Code Quality** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
66
+
67
+ ### 🎯 **Why Choose This Over Python?**
68
+
69
+ **Top packages for YouTube transcripts are `youtube-transcript` (JS/NPM) and `extract-youtube` (Python).** This package combines the best of both:
70
+
71
+ 1. **Same reliability as Python** - Direct port of the proven Python implementation
72
+ 2. **Better for modern stacks** - Works natively in Node.js, Next.js, React Server Components
73
+ 3. **Serverless-first** - Perfect for AWS Lambda, Vercel Functions, Cloudflare Workers
74
+ 4. **Faster cold starts** - No Python runtime overhead
75
+ 5. **Modern tooling** - NPM ecosystem, TypeScript, tree-shaking, ESM/CJS
76
+
77
+ ### ⚡ **Optimized for Production**
78
+
79
+ - **Bundle optimized with Vite + Terser** - Aggressive minification and tree-shaking
80
+ - **Dual ESM/CJS builds** - Works everywhere (Node.js, bundlers, edge runtimes)
81
+ - **Zero external HTTP clients** - Uses native `grab-url` (polyfilled in browsers)
82
+ - **Efficient parsing** - Fast XML parsing with minimal memory footprint
83
+
84
+ ## 🎯 Quick Facts
85
+
86
+ ```
87
+ Bundle Size: 7KB gzipped (vs 20-30KB for alternatives)
88
+ Cold Start: ~50ms (vs 150-500ms for alternatives)
89
+ Memory: ~30MB (vs 45-80MB for alternatives)
90
+ Dependencies: 4 minimal (vs 10-50+ for alternatives)
91
+ Serverless: ✅ Optimized (vs ⚠️ Limited support)
92
+ TypeScript: ✅ Full (vs ⚠️ Partial/None)
93
+ ```
94
+
95
+ ## Installation
96
+
97
+ ```bash
98
+ npm install extract-youtube
99
+ ```
100
+
101
+ **Or try it instantly with npx:**
102
+
103
+ ```bash
104
+ npx extract-youtube dQw4w9WgXcQ
105
+ ```
106
+
107
+ ## Quick Start
108
+
109
+ ```typescript
110
+ import { YouTubeTranscriptApi } from 'extract-youtube';
111
+
112
+ const api = new YouTubeTranscriptApi();
113
+ const transcript = await api.fetch('dQw4w9WgXcQ');
114
+
115
+ for (const snippet of transcript) {
116
+ console.log(`${snippet.start}s: ${snippet.text}`);
117
+ }
118
+ ```
119
+
120
+ ## Basic Usage
121
+
122
+ ```typescript
123
+ // Fetch with language preference
124
+ const transcript = await api.fetch('video_id', {
125
+ languages: ['de', 'en'] // Try German first, then English
126
+ });
127
+
128
+ // List available transcripts
129
+ const transcriptList = await api.list('video_id');
130
+ for (const t of transcriptList) {
131
+ console.log(`${t.language} (${t.languageCode})`);
132
+ }
133
+
134
+ // Find and translate
135
+ const transcript = transcriptList.findTranscript(['en']);
136
+ const translated = transcript.translate('de');
137
+ const fetched = await translated.fetch();
138
+ ```
139
+
140
+ ## Formatters
141
+
142
+ ```typescript
143
+ import { SRTFormatter, WebVTTFormatter, JSONFormatter, ArticleFormatter } from 'extract-youtube';
144
+
145
+ const transcript = await api.fetch('video_id');
146
+
147
+ // SRT format
148
+ const srt = new SRTFormatter().formatTranscript(transcript);
149
+
150
+ // WebVTT format
151
+ const webvtt = new WebVTTFormatter().formatTranscript(transcript);
152
+
153
+ // JSON format
154
+ const json = new JSONFormatter().formatTranscript(transcript, { indent: 2 });
155
+
156
+ // Article format with character-to-timestamp mappings
157
+ const article = new ArticleFormatter().formatTranscript(transcript);
158
+ console.log(article);
159
+ // Output:
160
+ // {
161
+ // "text": "Full transcript text...",
162
+ // "timestamps": "10,15,20 100,200,300", // speeds and positions
163
+ // "wordCount": 1234,
164
+ // "charCount": 5678
165
+ // }
166
+ ```
167
+
168
+ ## Proxy Support
169
+
170
+ ```typescript
171
+ import { WebshareProxyConfig } from 'extract-youtube';
172
+
173
+ const api = new YouTubeTranscriptApi({
174
+ proxyConfig: new WebshareProxyConfig({
175
+ proxyUsername: 'your-username',
176
+ proxyPassword: 'your-password'
177
+ })
178
+ });
179
+ ```
180
+
181
+ See [docs/proxy.md](./docs/proxy.md) for the full guide: generic vs. Webshare
182
+ proxies, the CLI flags, why `proxyConfig` does nothing on Cloudflare
183
+ Workers/edge (and what to use there instead), and caching in front of the
184
+ fetcher so a proxy request is only spent on a video you don't already have.
185
+
186
+ ## Error Handling
187
+
188
+ ```typescript
189
+ import { TranscriptsDisabled, NoTranscriptFound } from 'extract-youtube';
190
+
191
+ try {
192
+ const transcript = await api.fetch('video_id');
193
+ } catch (error) {
194
+ if (error instanceof TranscriptsDisabled) {
195
+ console.error('Subtitles are disabled');
196
+ } else if (error instanceof NoTranscriptFound) {
197
+ console.error('No transcript found');
198
+ }
199
+ }
200
+ ```
201
+
202
+ ## CLI Usage
203
+
204
+ After installing globally or using npx, you can use the `extract-youtube` command:
205
+
206
+ ```bash
207
+ # Install globally
208
+ npm install -g extract-youtube
209
+
210
+ # Or use with npx
211
+ npx extract-youtube <video-id> [options]
212
+ ```
213
+
214
+ ### CLI Examples
215
+
216
+ ```bash
217
+ # Extract transcript in JSON format (default)
218
+ extract-youtube jNQXAC9IVRw
219
+
220
+ # Extract transcript in SRT format
221
+ extract-youtube jNQXAC9IVRw -f srt
222
+
223
+ # Extract transcript with specific languages
224
+ extract-youtube jNQXAC9IVRw -l en,de
225
+
226
+ # Extract transcript with proxy
227
+ extract-youtube jNQXAC9IVRw --proxy http://proxy.example.com:8080
228
+
229
+ # Extract transcript with Webshare proxy
230
+ extract-youtube jNQXAC9IVRw --webshare-user myuser --webshare-pass mypass
231
+
232
+ # Extract as plain text with preserved formatting
233
+ extract-youtube jNQXAC9IVRw -f text -p
234
+
235
+ # Show help
236
+ extract-youtube --help
237
+ ```
238
+
239
+ ### CLI Options
240
+
241
+ - `-h, --help` - Show help message
242
+ - `-v, --version` - Show version number
243
+ - `-l, --languages <codes>` - Comma-separated language codes (e.g., en,de,fr)
244
+ - `-f, --format <type>` - Output format: json, text, srt, webvtt, pretty, article (default: json)
245
+ - `-p, --preserve-formatting` - Preserve text formatting (line breaks, etc.)
246
+ - `--proxy <url>` - HTTP/HTTPS proxy URL
247
+ - `--webshare-user <username>` - Webshare proxy username
248
+ - `--webshare-pass <password>` - Webshare proxy password
249
+
250
+ ## React Components (Floating Player + Transcript Modal)
251
+
252
+ `extract-youtube/react` is the package's UI half — two self-contained
253
+ components, both ported from the video player used in production on
254
+ [debate-ai.com](https://debate-ai.com), with everything app-specific
255
+ stripped out. Neither depends on a design system, a state library, or a CSS
256
+ framework: they inject their own minimal scoped styles and drop into any
257
+ React app.
258
+
259
+ - **`<FloatingYouTubePlayer />`** — a floating, draggable, resizable player
260
+ that keeps playing while the user moves around your app. Mount it once;
261
+ drive it from anywhere with `youtubePlayer.play({ videoId })`.
262
+ - **`<YouTubeTranscriptModal />`** — a popout modal with the video on the
263
+ left and a transcript panel on the right that scrolls and highlights in
264
+ sync with playback; click any line to seek.
265
+
266
+ ### Why it's a separate entry point
267
+
268
+ `extract-youtube` (the main entry) fetches captions server-side with no
269
+ browser dependency at all — that's the whole point of the package.
270
+ `extract-youtube/react` is a second, independent entry point that only
271
+ exports UI: it never imports the transcript-fetching code, and the main
272
+ entry never imports React. Import only the one you need and the other never
273
+ ends up in your bundle. React, ReactDOM, and `lucide-react` (used for the
274
+ icons) are peer dependencies — install them yourself if you don't already
275
+ have them:
276
+
277
+ ```bash
278
+ npm install extract-youtube react react-dom lucide-react
279
+ ```
280
+
281
+ ### The floating player
282
+
283
+ Mount it once, near the root of your app. It renders through a portal into
284
+ `document.body`, so nothing in the tree around it can clip it, hide it, or
285
+ remount the playing video on a route change. It renders nothing at all until
286
+ something asks it to play.
287
+
288
+ ```tsx
289
+ // app/layout.tsx (or wherever your app root lives)
290
+ import { FloatingYouTubePlayer } from 'extract-youtube/react';
291
+
292
+ export default function RootLayout({ children }) {
293
+ return (
294
+ <>
295
+ {children}
296
+ <FloatingYouTubePlayer transcriptUrl="/api/transcript" />
297
+ </>
298
+ );
299
+ }
300
+ ```
301
+
302
+ Then play something from anywhere — a grid, a search result, a keyboard
303
+ shortcut. No context provider, no prop drilling:
304
+
305
+ ```tsx
306
+ import { youtubePlayer, usePlayerState, thumbnailUrl } from 'extract-youtube/react';
307
+
308
+ function VideoCard({ videoId, title }: { videoId: string; title: string }) {
309
+ const { activeVideo } = usePlayerState();
310
+
311
+ return (
312
+ <button onClick={() => youtubePlayer.play({ videoId, title })}>
313
+ <img src={thumbnailUrl(videoId)} alt="" />
314
+ {title} {activeVideo?.videoId === videoId && '(playing)'}
315
+ </button>
316
+ );
317
+ }
318
+ ```
319
+
320
+ What you get, without wiring any of it up yourself:
321
+
322
+ | | |
323
+ | --- | --- |
324
+ | **Drag & resize** | Drag by the title bar, resize from either side edge or a bottom corner, clamped to the viewport. Mouse and touch. |
325
+ | **Minimize** | Collapses to the title bar. The iframe is hidden with CSS, never unmounted, so playback isn't interrupted. |
326
+ | **Picture-in-picture** | Pops the video into an always-on-top OS window via the Document Picture-in-Picture API, where the browser supports it. The node is *moved*, not cloned, so playback continues. |
327
+ | **Queue** | `addToQueue` / `setQueue` / `playNext`, with an "Up next" strip under the video. |
328
+ | **Synced captions** | Optional subtitles panel above the video — caption cues are regrouped into whole sentences (no timestamps), the spoken one highlights and auto-scrolls, and clicking a sentence seeks. Needs `transcriptUrl` or `fetchTranscript` (see below). |
329
+ | **Resume** | Remembers what was playing, and how far into it, across a reload — plus a per-video position for the last 50 videos, for 24 hours. `storageKey={null}` turns it off. |
330
+ | **Error recovery** | Reads the IFrame API's error codes, explains them ("this video is private", "the owner doesn't allow embedding"), and offers Retry or Watch on YouTube from the same spot. |
331
+ | **Theming** | Colours are CSS custom properties on `.eytp-root` and follow `prefers-color-scheme` by default. Override them to match your app. |
332
+
333
+ #### Custom controls belong to your app, not the package
334
+
335
+ The built-in control strip only holds buttons that mean the same thing for
336
+ any YouTube video: play/pause, skip, captions, picture-in-picture, minimize,
337
+ close. Anything specific to *your* product — a bookmark, a share menu, a
338
+ speed control framed for your users — is yours to render, through
339
+ `extraControls`:
340
+
341
+ ```tsx
342
+ import { Gauge } from 'lucide-react';
343
+ import { FloatingYouTubePlayer } from 'extract-youtube/react';
344
+
345
+ <FloatingYouTubePlayer
346
+ transcriptUrl="/api/transcript"
347
+ extraControls={({ playbackRate, player }) => (
348
+ <button
349
+ // The player's own control classes, so custom buttons match the built-ins.
350
+ className={`eytp-btn${playbackRate !== 1 ? ' eytp-btn-active' : ''}`}
351
+ onClick={() => player.setPlaybackRate(playbackRate !== 1 ? 1 : 0.65)}
352
+ title="Slow it down"
353
+ >
354
+ <Gauge size={13} />
355
+ </button>
356
+ )}
357
+ />
358
+ ```
359
+
360
+ debate-ai.com uses exactly this seam for its "slow the debate spread down"
361
+ button — its own label, icon and rate, sitting in the same strip. The
362
+ package stays generic: it exposes `player.setPlaybackRate()` and reports the
363
+ current `playbackRate`, and the host decides what the button says and does.
364
+ `renderTitle` is the same idea for the title bar, if you want badges or
365
+ links instead of a plain video title.
366
+
367
+ #### The imperative API
368
+
369
+ Everything the player can do is on `youtubePlayer`, importable anywhere:
370
+
371
+ ```ts
372
+ import { youtubePlayer, usePlayerState, getPlayerState } from 'extract-youtube/react';
373
+
374
+ youtubePlayer.play({ videoId, title, meta }); // meta is yours; passed back untouched
375
+ youtubePlayer.play({ videoId }, { startSeconds: 120 });
376
+ youtubePlayer.togglePlay();
377
+ youtubePlayer.seekTo(90);
378
+ youtubePlayer.setPlaybackRate(1.5);
379
+ youtubePlayer.addToQueue({ videoId, title });
380
+ youtubePlayer.setQueue(videos); // e.g. "play all" over a grid
381
+ youtubePlayer.playNext();
382
+ youtubePlayer.getCurrentTime(); // seconds
383
+ youtubePlayer.close();
384
+
385
+ usePlayerState(); // in a component: { activeVideo, isPlaying, isMinimized, playbackRate, queue, startTime }
386
+ getPlayerState(); // the same, outside React
387
+ ```
388
+
389
+ #### Floating player props
390
+
391
+ | Prop | Type | Description |
392
+ | --- | --- | --- |
393
+ | `transcriptUrl` | `string` | Your captions endpoint (see below). Enables the subtitles button. |
394
+ | `fetchTranscript` | `(videoId: string) => Promise<{ snippets, error? }>` | Custom caption loader, instead of `transcriptUrl`. |
395
+ | `extraControls` | `ReactNode \| (ctx: PlayerControlContext) => ReactNode` | Your own buttons, rendered in the control strip. |
396
+ | `renderTitle` | `(ctx: PlayerControlContext) => ReactNode` | Custom title-bar content. Defaults to the video title. |
397
+ | `showSubtitles` | `boolean` | Force the captions button on or off. By default it appears only for videos that turned out to have a transcript — every video played is checked, and one without captions gets no button and no error. |
398
+ | `showPictureInPicture` | `boolean` | Show the PiP button where supported. Default `true`. |
399
+ | `storageKey` | `string \| null` | localStorage key for resume-after-reload. `null` disables persistence entirely. |
400
+ | `className` | `string` | Extra class on the player root, for host-side positioning or theming. |
401
+ | `onClose` | `() => void` | Called when the user closes the player. |
402
+ | `minWidth` / `maxWidth` | `number` | Resize bounds in px. Default 256 / 800. |
403
+
404
+ ### Setup: video captions still have to be fetched server-side
405
+
406
+ Neither component talks to YouTube's caption endpoints directly — browsers
407
+ can't (no CORS, and it would leak this package's whole fetching strategy
408
+ client-side for no benefit). Instead, you expose one small backend endpoint
409
+ that calls this package's `YouTubeTranscriptApi`, and point the components
410
+ at it with `transcriptUrl`.
411
+
412
+ **1. Add a backend endpoint** (any framework works — this is a Next.js
413
+ route handler, following the same pattern as the Vercel Edge example
414
+ above):
415
+
416
+ ```typescript
417
+ // app/api/transcript/route.ts
418
+ import { NextRequest, NextResponse } from 'next/server';
419
+ import { YouTubeTranscriptApi, extractVideoId } from 'extract-youtube';
420
+
421
+ const api = new YouTubeTranscriptApi();
422
+
423
+ export async function GET(req: NextRequest) {
424
+ const raw = req.nextUrl.searchParams.get('videoId');
425
+ const videoId = raw ? extractVideoId(raw) : null;
426
+ if (!videoId) {
427
+ return NextResponse.json({ error: 'Missing videoId' }, { status: 400 });
428
+ }
429
+
430
+ try {
431
+ const transcript = await api.fetchTranscript(videoId, { languages: ['en'] });
432
+ return NextResponse.json({ videoId, snippets: transcript.toRawData() });
433
+ } catch (error) {
434
+ // 200 + an `error` field, not a 4xx/5xx — "no captions for this video"
435
+ // isn't a server failure, and the components check this field either way.
436
+ return NextResponse.json({
437
+ videoId,
438
+ snippets: [],
439
+ error: error instanceof Error ? error.message : 'Failed to fetch transcript',
440
+ });
441
+ }
442
+ }
443
+ ```
444
+
445
+ **2. Point the components at it** with `transcriptUrl="/api/transcript"`.
446
+ The `videoId` query param is appended for you, and one request is shared
447
+ per video across every component asking for it.
448
+
449
+ `extractVideoId` (exported from the main entry, since it runs fine in Node)
450
+ accepts any of the URL shapes YouTube uses — watch, youtu.be, embed, shorts,
451
+ live — so you can pass what a user pastes in straight through.
452
+
453
+ ### Transcript modal
454
+
455
+ ```tsx
456
+ import { YouTubeTranscriptModal } from 'extract-youtube/react';
457
+ import { extractVideoId } from 'extract-youtube';
458
+
459
+ function VideoCard({ url, title }: { url: string; title: string }) {
460
+ const videoId = extractVideoId(url);
461
+ if (!videoId) return null;
462
+
463
+ return <YouTubeTranscriptModal videoId={videoId} title={title} transcriptUrl="/api/transcript" />;
464
+ }
465
+ ```
466
+
467
+ | Prop | Type | Description |
468
+ | --- | --- | --- |
469
+ | `videoId` | `string` | **Required.** The YouTube video ID (not a full URL — run it through `extractVideoId` first if needed). |
470
+ | `title` | `string` | Shown in the modal header and as the iframe's accessible title. |
471
+ | `transcriptUrl` | `string` | URL of your backend endpoint (see above). Fetched with `fetch()` when the modal opens. The `videoId` query param is appended automatically if not already present. |
472
+ | `fetchTranscript` | `(videoId: string) => Promise<{ snippets, error? }>` | Use instead of `transcriptUrl` if you want to load the transcript some other way (e.g. from a React Query cache). |
473
+ | `snippets` | `TranscriptSnippet[]` | Pass transcript data directly to skip fetching entirely — e.g. if you already loaded it server-side. |
474
+ | `trigger` | `ReactNode` | Custom element that opens the modal on click. Defaults to a small captions-icon button. |
475
+ | `onOpenChange` | `(open: boolean) => void` | Called whenever the modal opens or closes. |
476
+
477
+ ## Video Library: Grid, List and Admin
478
+
479
+ Ported from the video library on [debate-ai.com](https://debate-ai.com) (its
480
+ `debate-videos` grid package and its admin API routes), with everything
481
+ debate-specific generalised into **custom fields** you declare yourself.
482
+
483
+ | Entry point | What you get |
484
+ | --- | --- |
485
+ | `extract-youtube/library` | Storage (in-memory or Cloudflare D1), one framework-agnostic HTTP handler with public and admin routes, a typed client, stacked playlists, grouping, YouTube Data API resync and auto-fill. No React, no Node built-ins: it runs on Workers. |
486
+ | `extract-youtube/react` | `<VideoGrid />`, `<VideoList />`, `<VideoCard />`, `<StackedVideoCard />`, `<StackNav />`, `useVideoLibrary()`, and the admin screens `<VideoLibraryAdmin />`, `<VideoEditDialog />`, `<CustomFieldInput />` and `<VideoAvailabilityPanel />`. |
487
+
488
+ ### Serve the library (Cloudflare Worker + D1)
489
+
490
+ ```ts
491
+ import { bearerTokenAuth, createD1LibraryStore, createVideoLibraryHandler, type CustomFieldDef } from 'extract-youtube/library';
492
+
493
+ const fields: CustomFieldDef[] = [
494
+ { key: 'speaker', label: 'Speaker', type: 'text', searchable: true, showOnCard: true, showInList: true },
495
+ { key: 'level', label: 'Level', type: 'select', options: ['Beginner', 'Advanced'] },
496
+ ];
497
+
498
+ export default {
499
+ async fetch(request: Request, env: { DB: D1Database; ADMIN_TOKEN: string; YOUTUBE_API_KEY?: string }) {
500
+ const store = createD1LibraryStore(env.DB);
501
+ await store.ensureSchema();
502
+ const library = createVideoLibraryHandler({
503
+ store,
504
+ authorize: bearerTokenAuth(env.ADMIN_TOKEN), // omit it and every admin route answers 403
505
+ customFields: fields,
506
+ youtubeApiKey: env.YOUTUBE_API_KEY, // optional: resync + richer auto-fill
507
+ });
508
+ return (await library.handle(request)) ?? new Response('Not found', { status: 404 });
509
+ },
510
+ };
511
+ ```
512
+
513
+ Routes live under `/api/library`: public `GET /videos` (search, filter,
514
+ sort, page), `/videos/:id`, `/stacks`, `/categories`, `/fields`, `/session`;
515
+ admin `POST/PATCH/DELETE /videos`, `/autofill`, `/resync`, `/availability`,
516
+ `/import`, `/stacks/recompute` and `/exclusions`. The same handler mounts in
517
+ Next.js, vinext, Hono, Bun or Deno, since it only takes a `Request`. Use
518
+ `createMemoryLibraryStore()` for tests and demos, or implement
519
+ `VideoLibraryStore` for any other database.
520
+
521
+ ### Show it
522
+
523
+ ```tsx
524
+ import { createLibraryClient } from 'extract-youtube/library';
525
+ import { FloatingYouTubePlayer, VideoGrid, VideoList, useVideoLibrary } from 'extract-youtube/react';
526
+
527
+ const client = createLibraryClient({ baseUrl: '/api/library' });
528
+
529
+ function Library() {
530
+ const { videos, stacks } = useVideoLibrary(client, { sort: 'views', availability: 'available' });
531
+ return (
532
+ <>
533
+ <VideoGrid videos={videos} stacks={stacks} customFields={fields} transcriptUrl="/api/transcript" />
534
+ <VideoList videos={videos} stacks={stacks} customFields={fields} groupBy={['year', 'channel']} />
535
+ <FloatingYouTubePlayer transcriptUrl="/api/transcript" />
536
+ </>
537
+ );
538
+ }
539
+ ```
540
+
541
+ - **Cards** show title, channel, date, views, category, "Top pick" and your
542
+ custom fields as badges, with opt-in favorite, hide, queue, transcript and
543
+ YouTube actions.
544
+ - **Stacked playlists**: videos whose descriptions link each other ("Part 2:
545
+ https://youtu.be/…") fold into one card with `<` / `>` arrows.
546
+ - **The list** is a table with sortable, resizable columns, one column per
547
+ custom field, and `groupBy` for a tree by year, channel, category or any
548
+ custom field, collapsible level by level.
549
+ - Styles are scoped (`eytg-` classes) and injected once, follow dark mode,
550
+ and theme through `--eytg-*` CSS variables.
551
+
552
+ ### Curate it
553
+
554
+ ```tsx
555
+ import { VideoAvailabilityPanel, VideoLibraryAdmin } from 'extract-youtube/react';
556
+
557
+ const admin = createLibraryClient({ baseUrl: '/api/library', headers: () => ({ authorization: `Bearer ${token}` }) });
558
+
559
+ <VideoLibraryAdmin client={admin} /> // search, sort, page, add, auto-fill, edit, feature, delete, resync
560
+ <VideoAvailabilityPanel client={admin} /> // videos YouTube no longer plays
561
+ ```
562
+
563
+ Edits send only the fields that changed and mark the row `adminEdited`, so
564
+ bulk imports leave it alone. Deletes record an exclusion, so imports don't
565
+ bring the video back. Auto-fill reads the YouTube Data API (or oEmbed with no
566
+ key) and then your own `suggest` hook, and never overwrites what the admin
567
+ typed.
568
+
569
+ Full guides: [video grid and list](../../apps/extract-youtube-docs/content/docs/react/video-grid.mdx),
570
+ [library admin API](../../apps/extract-youtube-docs/content/docs/admin/library-api.mdx),
571
+ [custom fields](../../apps/extract-youtube-docs/content/docs/admin/custom-fields.mdx) and
572
+ [admin components](../../apps/extract-youtube-docs/content/docs/admin/admin-components.mdx).
573
+
574
+ ## Live Demo and Storybook
575
+
576
+ **Live demo: [youtube.js.org](https://youtube.js.org)** · Storybook: [youtube.js.org/storybook](https://youtube.js.org/storybook/)
577
+
578
+ [`apps/extract-youtube-demo`](../../apps/extract-youtube-demo) runs all of
579
+ this on one Cloudflare Worker: the library grid and list, the admin screens,
580
+ the floating player with captions from `/api/transcript`, the library API at
581
+ `/api/library`, and a Storybook with a story for every component at
582
+ `/storybook/`.
583
+
584
+ ```bash
585
+ bun install # from the monorepo root
586
+ cd apps/extract-youtube-demo
587
+ bun run dev # app + Worker on one port
588
+ bun run storybook # Storybook on :6006
589
+ bun run build && bunx wrangler deploy # app, Worker and Storybook in one deploy
590
+ ```
591
+
592
+ With no secrets set it runs as a sandbox (an in-memory library anyone can
593
+ edit, reset per Worker isolate). Set `ADMIN_TOKEN` to require a token, bind a
594
+ D1 database as `DB` to persist, and set `YOUTUBE_API_KEY` for resync. The
595
+ docs site is [`apps/extract-youtube-docs`](../../apps/extract-youtube-docs),
596
+ built from template-fumadocs and also deployed to Workers.
597
+
598
+ The demo's `src/SpeedButton.tsx` is the worked example of an app-supplied
599
+ custom control, passed in through `<FloatingYouTubePlayer extraControls />`.
600
+
601
+ ## Features
602
+
603
+ ### Core Functionality
604
+
605
+ - ✅ Retrieve transcripts for any YouTube video (no API key needed)
606
+ - ✅ Support for manually created and auto-generated subtitles
607
+ - ✅ Translate transcripts to 100+ languages
608
+ - ✅ Multiple output formats (JSON, Text, SRT, WebVTT, Pretty Print, Article)
609
+ - ✅ Language preference fallback system
610
+ - ✅ Timed segments with start/duration timestamps
611
+
612
+ ### Developer Experience
613
+
614
+ - ✅ **Full TypeScript support** with comprehensive type definitions
615
+ - ✅ **Tree-shakeable** - Only bundle what you use
616
+ - ✅ **ESM + CJS** - Works with all module systems
617
+ - ✅ **Zero configuration** - Works out of the box
618
+ - ✅ Comprehensive JSDoc documentation
619
+ - ✅ Intuitive error messages with troubleshooting guidance
620
+
621
+ ### Advanced Features
622
+
623
+ - ✅ **Proxy support** (Generic HTTP/HTTPS & Webshare residential proxies)
624
+ - ✅ **Rate limit handling** with rotating IP pools
625
+ - ✅ **Serverless-optimized** - No file system dependencies
626
+ - ✅ **Edge runtime compatible** - Runs on Cloudflare Workers, Vercel Edge
627
+ - ✅ Command-line interface (CLI) for quick extraction
628
+
629
+ ### Production Ready
630
+
631
+ - ✅ Battle-tested algorithm from Python package (100k+ monthly downloads)
632
+ - ✅ Comprehensive error handling with typed exceptions
633
+ - ✅ Automatic retry logic for network failures
634
+ - ✅ Small bundle size (~7KB gzipped)
635
+ - ✅ No native dependencies or binaries
636
+
637
+ ## Serverless & Edge Deployment
638
+
639
+ This package is optimized for serverless and edge computing environments:
640
+
641
+ ### AWS Lambda
642
+
643
+ ```typescript
644
+ import { YouTubeTranscriptApi } from 'extract-youtube';
645
+
646
+ export const handler = async (event) => {
647
+ const api = new YouTubeTranscriptApi();
648
+ const transcript = await api.fetch(event.videoId);
649
+
650
+ return {
651
+ statusCode: 200,
652
+ body: JSON.stringify(transcript.toRawData())
653
+ };
654
+ };
655
+ ```
656
+
657
+ ### Vercel Edge Functions
658
+
659
+ ```typescript
660
+ import { YouTubeTranscriptApi } from 'extract-youtube';
661
+
662
+ export const config = { runtime: 'edge' };
663
+
664
+ export default async function handler(req: Request) {
665
+ const { searchParams } = new URL(req.url);
666
+ const videoId = searchParams.get('videoId');
667
+
668
+ const api = new YouTubeTranscriptApi();
669
+ const transcript = await api.fetch(videoId);
670
+
671
+ return new Response(JSON.stringify(transcript.toRawData()), {
672
+ headers: { 'content-type': 'application/json' }
673
+ });
674
+ }
675
+ ```
676
+
677
+ ### Cloudflare Workers
678
+
679
+ ```typescript
680
+ import { YouTubeTranscriptApi } from 'extract-youtube';
681
+
682
+ export default {
683
+ async fetch(request: Request) {
684
+ const url = new URL(request.url);
685
+ const videoId = url.searchParams.get('videoId');
686
+
687
+ const api = new YouTubeTranscriptApi();
688
+ const transcript = await api.fetch(videoId);
689
+
690
+ return new Response(JSON.stringify(transcript.toRawData()), {
691
+ headers: { 'content-type': 'application/json' }
692
+ });
693
+ }
694
+ };
695
+ ```
696
+
697
+ ### Next.js Server Actions / API Routes
698
+
699
+ ```typescript
700
+ 'use server';
701
+
702
+ import { YouTubeTranscriptApi } from 'extract-youtube';
703
+
704
+ export async function getTranscript(videoId: string) {
705
+ const api = new YouTubeTranscriptApi();
706
+ return await api.fetch(videoId);
707
+ }
708
+ ```
709
+
710
+ ### Why It Works Great in Serverless
711
+
712
+ - ✅ **Fast cold starts** - Minimal initialization overhead
713
+ - ✅ **No file system** - Pure in-memory operations
714
+ - ✅ **Small bundle** - Fits well within size limits
715
+ - ✅ **No native deps** - No compilation needed
716
+ - ✅ **Stateless** - Perfect for serverless architecture
717
+
718
+ ## Testing
719
+
720
+ ```bash
721
+ npm test
722
+ ```
723
+
724
+ ## Performance Benchmarks
725
+
726
+ ### Bundle Size Comparison
727
+
728
+ ```
729
+ extract-youtube (this): ~7KB gzipped ✅
730
+ youtube-transcript: ~25KB gzipped ❌
731
+ ytdl-core: ~300KB+ ❌❌
732
+ puppeteer-based solutions: ~200MB+ ❌❌❌
733
+ ```
734
+
735
+ ### Cold Start Times (AWS Lambda)
736
+
737
+ ```
738
+ extract-youtube: ~50ms ✅
739
+ youtube-transcript: ~150ms ⚠️
740
+ Python package: ~500ms ❌
741
+ ```
742
+
743
+ ### Memory Usage
744
+
745
+ ```
746
+ extract-youtube: ~30MB ✅
747
+ youtube-transcript: ~45MB ⚠️
748
+ Python package: ~80MB ❌
749
+ ```
750
+
751
+
752
+ ## Reliability
753
+
754
+ This package uses the same proven algorithm as the Python `extract-youtube`:
755
+
756
+ - ✅ **100k+ monthly downloads** on PyPI (Python version)
757
+ - ✅ **Battle-tested** across thousands of production deployments
758
+ - ✅ **Maintained** - Regular updates to handle YouTube API changes
759
+ - ✅ **Comprehensive error handling** - Clear error messages for all failure modes
760
+
761
+ **Unlike scraping-based alternatives**, this package:
762
+
763
+ - Fetches transcripts directly from YouTube's caption endpoints
764
+ - Doesn't rely on brittle HTML parsing
765
+ - Handles both manual and auto-generated captions
766
+ - Works with age-restricted videos (with authentication)
767
+
768
+ ## Common Use Cases
769
+
770
+ ### 1. AI/ML Applications
771
+
772
+ ```typescript
773
+ // Extract transcripts for training data, analysis, or AI processing
774
+ const transcript = await api.fetch('video_id');
775
+ const text = transcript.snippets.map(s => s.text).join(' ');
776
+ // Feed to GPT, LLM, or ML model
777
+ ```
778
+
779
+ ### 2. Accessibility Tools
780
+
781
+ ```typescript
782
+ // Generate subtitles in multiple formats
783
+ const transcript = await api.fetch('video_id');
784
+ const srt = new SRTFormatter().formatTranscript(transcript);
785
+ const webvtt = new WebVTTFormatter().formatTranscript(transcript);
786
+ ```
787
+
788
+ ### 3. Content Analysis
789
+
790
+ ```typescript
791
+ // Analyze video content programmatically
792
+ const transcript = await api.fetch('video_id');
793
+ for (const snippet of transcript) {
794
+ if (snippet.text.includes('keyword')) {
795
+ console.log(`Found at ${snippet.start}s: ${snippet.text}`);
796
+ }
797
+ }
798
+ ```
799
+
800
+ ### 4. Translation Services
801
+
802
+ ```typescript
803
+ // Translate videos to multiple languages
804
+ const list = await api.list('video_id');
805
+ const transcript = list.findTranscript(['en']);
806
+ const german = await transcript.translate('de').fetch();
807
+ const spanish = await transcript.translate('es').fetch();
808
+ ```
809
+
810
+ ## Troubleshooting
811
+
812
+ ### Rate Limiting / IP Blocks
813
+
814
+ If you're getting blocked by YouTube:
815
+
816
+ ```typescript
817
+ import { YouTubeTranscriptApi, WebshareProxyConfig } from 'extract-youtube';
818
+
819
+ const api = new YouTubeTranscriptApi({
820
+ proxyConfig: new WebshareProxyConfig({
821
+ proxyUsername: 'your-username',
822
+ proxyPassword: 'your-password',
823
+ // Rotate through 30M+ residential IPs
824
+ })
825
+ });
826
+ ```
827
+
828
+ ### No Transcript Found
829
+
830
+ ```typescript
831
+ try {
832
+ const transcript = await api.fetch('video_id', {
833
+ languages: ['en', 'de', 'es'] // Fallback languages
834
+ });
835
+ } catch (error) {
836
+ if (error instanceof NoTranscriptFound) {
837
+ // Handle case where no transcript exists
838
+ console.log('Available:', error.availableTranscripts);
839
+ }
840
+ }
841
+ ```
842
+
843
+ ## License
844
+
845
+ MIT
846
+
847
+ ## Contributing
848
+
849
+ Contributions are welcome! Please feel free to submit a Pull Request.