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.
- package/README.md +849 -757
- package/dist/cli.js +16 -6
- package/dist/cli.js.map +1 -1
- package/dist/library/client.d.ts +77 -0
- package/dist/library/client.d.ts.map +1 -0
- package/dist/library/fields.d.ts +91 -0
- package/dist/library/fields.d.ts.map +1 -0
- package/dist/library/handler.d.ts +90 -0
- package/dist/library/handler.d.ts.map +1 -0
- package/dist/library/index.cjs +2 -0
- package/dist/library/index.cjs.map +1 -0
- package/dist/library/index.d.ts +31 -0
- package/dist/library/index.d.ts.map +1 -0
- package/dist/library/index.mjs +2 -0
- package/dist/library/index.mjs.map +1 -0
- package/dist/library/library.d.ts +154 -0
- package/dist/library/library.d.ts.map +1 -0
- package/dist/library/query.d.ts +43 -0
- package/dist/library/query.d.ts.map +1 -0
- package/dist/library/stacks.d.ts +102 -0
- package/dist/library/stacks.d.ts.map +1 -0
- package/dist/library/store.d.ts +51 -0
- package/dist/library/store.d.ts.map +1 -0
- package/dist/library/stores/d1.d.ts +57 -0
- package/dist/library/stores/d1.d.ts.map +1 -0
- package/dist/library/stores/memory.d.ts +21 -0
- package/dist/library/stores/memory.d.ts.map +1 -0
- package/dist/library/tree.d.ts +121 -0
- package/dist/library/tree.d.ts.map +1 -0
- package/dist/library/types.d.ts +188 -0
- package/dist/library/types.d.ts.map +1 -0
- package/dist/library/youtube-data.d.ts +78 -0
- package/dist/library/youtube-data.d.ts.map +1 -0
- package/dist/react/admin/VideoAvailabilityPanel.d.ts +21 -0
- package/dist/react/admin/VideoAvailabilityPanel.d.ts.map +1 -0
- package/dist/react/admin/VideoEditDialog.d.ts +55 -0
- package/dist/react/admin/VideoEditDialog.d.ts.map +1 -0
- package/dist/react/admin/VideoLibraryAdmin.d.ts +31 -0
- package/dist/react/admin/VideoLibraryAdmin.d.ts.map +1 -0
- package/dist/react/grid/StackNav.d.ts +29 -0
- package/dist/react/grid/StackNav.d.ts.map +1 -0
- package/dist/react/grid/StackedVideoCard.d.ts +20 -0
- package/dist/react/grid/StackedVideoCard.d.ts.map +1 -0
- package/dist/react/grid/VideoCard.d.ts +72 -0
- package/dist/react/grid/VideoCard.d.ts.map +1 -0
- package/dist/react/grid/VideoGrid.d.ts +53 -0
- package/dist/react/grid/VideoGrid.d.ts.map +1 -0
- package/dist/react/grid/VideoList.d.ts +57 -0
- package/dist/react/grid/VideoList.d.ts.map +1 -0
- package/dist/react/grid/format.d.ts +34 -0
- package/dist/react/grid/format.d.ts.map +1 -0
- package/dist/react/grid/styles.d.ts +27 -0
- package/dist/react/grid/styles.d.ts.map +1 -0
- package/dist/react/grid/useResizableColumns.d.ts +18 -0
- package/dist/react/grid/useResizableColumns.d.ts.map +1 -0
- package/dist/react/grid/useVideoLibrary.d.ts +22 -0
- package/dist/react/grid/useVideoLibrary.d.ts.map +1 -0
- package/dist/react/index.cjs +1 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.ts +25 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.mjs +132 -17
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/player/playerStore.d.ts +9 -0
- package/dist/react/player/playerStore.d.ts.map +1 -1
- package/package.json +17 -7
- package/src/library/client.ts +138 -0
- package/src/library/fields.ts +329 -0
- package/src/library/handler.ts +335 -0
- package/src/library/index.ts +100 -0
- package/src/library/library.ts +385 -0
- package/src/library/query.ts +170 -0
- package/src/library/stacks.ts +275 -0
- package/src/library/store.ts +54 -0
- package/src/library/stores/d1.ts +361 -0
- package/src/library/stores/memory.ts +99 -0
- package/src/library/tree.ts +299 -0
- package/src/library/types.ts +215 -0
- package/src/library/youtube-data.ts +195 -0
- package/src/react/admin/VideoAvailabilityPanel.tsx +158 -0
- package/src/react/admin/VideoEditDialog.tsx +399 -0
- package/src/react/admin/VideoLibraryAdmin.tsx +486 -0
- package/src/react/grid/StackNav.tsx +71 -0
- package/src/react/grid/StackedVideoCard.tsx +48 -0
- package/src/react/grid/VideoCard.tsx +315 -0
- package/src/react/grid/VideoGrid.tsx +103 -0
- package/src/react/grid/VideoList.tsx +419 -0
- package/src/react/grid/format.ts +74 -0
- package/src/react/grid/styles.tsx +247 -0
- package/src/react/grid/useResizableColumns.tsx +85 -0
- package/src/react/grid/useVideoLibrary.ts +72 -0
- package/src/react/index.ts +33 -0
- 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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
- ✅ **
|
|
42
|
-
- ✅ **
|
|
43
|
-
- ✅ **
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
| **
|
|
53
|
-
| **
|
|
54
|
-
| **
|
|
55
|
-
| **
|
|
56
|
-
| **
|
|
57
|
-
| **
|
|
58
|
-
| **
|
|
59
|
-
| **
|
|
60
|
-
| **
|
|
61
|
-
| **
|
|
62
|
-
| **
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
- **
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
//
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
//
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// "
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
#
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
# Extract transcript in
|
|
218
|
-
extract-youtube jNQXAC9IVRw
|
|
219
|
-
|
|
220
|
-
# Extract transcript
|
|
221
|
-
extract-youtube jNQXAC9IVRw -
|
|
222
|
-
|
|
223
|
-
# Extract transcript with
|
|
224
|
-
extract-youtube jNQXAC9IVRw
|
|
225
|
-
|
|
226
|
-
# Extract transcript with
|
|
227
|
-
extract-youtube jNQXAC9IVRw --
|
|
228
|
-
|
|
229
|
-
# Extract
|
|
230
|
-
extract-youtube jNQXAC9IVRw -
|
|
231
|
-
|
|
232
|
-
#
|
|
233
|
-
extract-youtube
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
- `-
|
|
242
|
-
- `-
|
|
243
|
-
-
|
|
244
|
-
-
|
|
245
|
-
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
- **`<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
|
323
|
-
|
|
|
324
|
-
| **
|
|
325
|
-
| **
|
|
326
|
-
| **
|
|
327
|
-
| **
|
|
328
|
-
| **
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
/>
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
youtubePlayer.
|
|
375
|
-
youtubePlayer.
|
|
376
|
-
youtubePlayer.
|
|
377
|
-
youtubePlayer.
|
|
378
|
-
youtubePlayer.
|
|
379
|
-
youtubePlayer.
|
|
380
|
-
youtubePlayer.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
|
392
|
-
|
|
|
393
|
-
| `
|
|
394
|
-
| `
|
|
395
|
-
| `
|
|
396
|
-
| `
|
|
397
|
-
| `
|
|
398
|
-
| `
|
|
399
|
-
| `
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
`
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
|
468
|
-
|
|
|
469
|
-
| `
|
|
470
|
-
| `
|
|
471
|
-
| `
|
|
472
|
-
| `
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
-
|
|
542
|
-
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
- ✅ **
|
|
624
|
-
- ✅ **
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
##
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
```
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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.
|