extract-youtube 1.0.276 → 1.0.278

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 (50) hide show
  1. package/README.md +171 -45
  2. package/dist/cli.js +8 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/react/YouTubeTranscriptModal.d.ts +2 -5
  5. package/dist/react/YouTubeTranscriptModal.d.ts.map +1 -1
  6. package/dist/react/index.cjs +1 -1
  7. package/dist/react/index.cjs.map +1 -1
  8. package/dist/react/index.d.ts +12 -4
  9. package/dist/react/index.d.ts.map +1 -1
  10. package/dist/react/index.mjs +17 -3
  11. package/dist/react/index.mjs.map +1 -1
  12. package/dist/react/player/FloatingYouTubePlayer.d.ts +64 -0
  13. package/dist/react/player/FloatingYouTubePlayer.d.ts.map +1 -0
  14. package/dist/react/player/PlayerControls.d.ts +30 -0
  15. package/dist/react/player/PlayerControls.d.ts.map +1 -0
  16. package/dist/react/player/PlayerQueue.d.ts +8 -0
  17. package/dist/react/player/PlayerQueue.d.ts.map +1 -0
  18. package/dist/react/player/PlayerResizeHandles.d.ts +12 -0
  19. package/dist/react/player/PlayerResizeHandles.d.ts.map +1 -0
  20. package/dist/react/player/PlayerSubtitles.d.ts +15 -0
  21. package/dist/react/player/PlayerSubtitles.d.ts.map +1 -0
  22. package/dist/react/player/playerPersistence.d.ts +30 -0
  23. package/dist/react/player/playerPersistence.d.ts.map +1 -0
  24. package/dist/react/player/playerStore.d.ts +116 -0
  25. package/dist/react/player/playerStore.d.ts.map +1 -0
  26. package/dist/react/player/styles.d.ts +15 -0
  27. package/dist/react/player/styles.d.ts.map +1 -0
  28. package/dist/react/player/useDocumentPictureInPicture.d.ts +26 -0
  29. package/dist/react/player/useDocumentPictureInPicture.d.ts.map +1 -0
  30. package/dist/react/player/useDragResize.d.ts +22 -0
  31. package/dist/react/player/useDragResize.d.ts.map +1 -0
  32. package/dist/react/player/youtubeEmbed.d.ts +43 -0
  33. package/dist/react/player/youtubeEmbed.d.ts.map +1 -0
  34. package/dist/react/transcript.d.ts +41 -0
  35. package/dist/react/transcript.d.ts.map +1 -0
  36. package/package.json +9 -3
  37. package/src/react/YouTubeTranscriptModal.tsx +8 -30
  38. package/src/react/index.ts +29 -4
  39. package/src/react/player/FloatingYouTubePlayer.tsx +498 -0
  40. package/src/react/player/PlayerControls.tsx +120 -0
  41. package/src/react/player/PlayerQueue.tsx +23 -0
  42. package/src/react/player/PlayerResizeHandles.tsx +39 -0
  43. package/src/react/player/PlayerSubtitles.tsx +112 -0
  44. package/src/react/player/playerPersistence.ts +114 -0
  45. package/src/react/player/playerStore.ts +226 -0
  46. package/src/react/player/styles.ts +172 -0
  47. package/src/react/player/useDocumentPictureInPicture.ts +102 -0
  48. package/src/react/player/useDragResize.ts +140 -0
  49. package/src/react/player/youtubeEmbed.ts +102 -0
  50. package/src/react/transcript.ts +143 -0
package/README.md CHANGED
@@ -245,17 +245,21 @@ extract-youtube --help
245
245
  - `--webshare-user <username>` - Webshare proxy username
246
246
  - `--webshare-pass <password>` - Webshare proxy password
247
247
 
248
- ## React Popout Modal (Video + Synced Subtitles)
249
-
250
- `extract-youtube/react` ships a self-contained popout modal component:
251
- click a trigger, and it opens a YouTube player next to a transcript panel
252
- that scrolls and highlights in sync with playback — click any line to seek
253
- the video there. It has no dependency on any particular design system (it
254
- injects its own minimal, scoped styles), so it drops into any React app.
255
- It was ported from the video transcript modal used in production on
256
- [debate-ai.com](https://debate-ai.com). The layout: player on the left,
257
- a "Transcript" panel on the right (stacks below the player on narrow
258
- screens) — see the `demo/` app below to try it live.
248
+ ## React Components (Floating Player + Transcript Modal)
249
+
250
+ `extract-youtube/react` is the package's UI half — two self-contained
251
+ components, both ported from the video player used in production on
252
+ [debate-ai.com](https://debate-ai.com), with everything app-specific
253
+ stripped out. Neither depends on a design system, a state library, or a CSS
254
+ framework: they inject their own minimal scoped styles and drop into any
255
+ React app.
256
+
257
+ - **`<FloatingYouTubePlayer />`** — a floating, draggable, resizable player
258
+ that keeps playing while the user moves around your app. Mount it once;
259
+ drive it from anywhere with `youtubePlayer.play({ videoId })`.
260
+ - **`<YouTubeTranscriptModal />`** — a popout modal with the video on the
261
+ left and a transcript panel on the right that scrolls and highlights in
262
+ sync with playback; click any line to seek.
259
263
 
260
264
  ### Why it's a separate entry point
261
265
 
@@ -263,22 +267,145 @@ screens) — see the `demo/` app below to try it live.
263
267
  browser dependency at all — that's the whole point of the package.
264
268
  `extract-youtube/react` is a second, independent entry point that only
265
269
  exports UI: it never imports the transcript-fetching code, and the main
266
- entry never imports React. Import only the one you need and the other
267
- never ends up in your bundle. React, ReactDOM, and `lucide-react` (used
268
- for the modal's icons) are peer dependencies — install them yourself if
269
- you don't already have them:
270
+ entry never imports React. Import only the one you need and the other never
271
+ ends up in your bundle. React, ReactDOM, and `lucide-react` (used for the
272
+ icons) are peer dependencies — install them yourself if you don't already
273
+ have them:
270
274
 
271
275
  ```bash
272
276
  npm install extract-youtube react react-dom lucide-react
273
277
  ```
274
278
 
279
+ ### The floating player
280
+
281
+ Mount it once, near the root of your app. It renders through a portal into
282
+ `document.body`, so nothing in the tree around it can clip it, hide it, or
283
+ remount the playing video on a route change. It renders nothing at all until
284
+ something asks it to play.
285
+
286
+ ```tsx
287
+ // app/layout.tsx (or wherever your app root lives)
288
+ import { FloatingYouTubePlayer } from 'extract-youtube/react';
289
+
290
+ export default function RootLayout({ children }) {
291
+ return (
292
+ <>
293
+ {children}
294
+ <FloatingYouTubePlayer transcriptUrl="/api/transcript" />
295
+ </>
296
+ );
297
+ }
298
+ ```
299
+
300
+ Then play something from anywhere — a grid, a search result, a keyboard
301
+ shortcut. No context provider, no prop drilling:
302
+
303
+ ```tsx
304
+ import { youtubePlayer, usePlayerState, thumbnailUrl } from 'extract-youtube/react';
305
+
306
+ function VideoCard({ videoId, title }: { videoId: string; title: string }) {
307
+ const { activeVideo } = usePlayerState();
308
+
309
+ return (
310
+ <button onClick={() => youtubePlayer.play({ videoId, title })}>
311
+ <img src={thumbnailUrl(videoId)} alt="" />
312
+ {title} {activeVideo?.videoId === videoId && '(playing)'}
313
+ </button>
314
+ );
315
+ }
316
+ ```
317
+
318
+ What you get, without wiring any of it up yourself:
319
+
320
+ | | |
321
+ | --- | --- |
322
+ | **Drag & resize** | Drag by the title bar, resize from either side edge or a bottom corner, clamped to the viewport. Mouse and touch. |
323
+ | **Minimize** | Collapses to the title bar. The iframe is hidden with CSS, never unmounted, so playback isn't interrupted. |
324
+ | **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. |
325
+ | **Queue** | `addToQueue` / `setQueue` / `playNext`, with an "Up next" strip under the video. |
326
+ | **Synced captions** | Optional subtitles panel above the video — the spoken line highlights and auto-scrolls, and clicking a line seeks. Needs `transcriptUrl` or `fetchTranscript` (see below). |
327
+ | **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. |
328
+ | **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. |
329
+ | **Theming** | Colours are CSS custom properties on `.eytp-root` and follow `prefers-color-scheme` by default. Override them to match your app. |
330
+
331
+ #### Custom controls belong to your app, not the package
332
+
333
+ The built-in control strip only holds buttons that mean the same thing for
334
+ any YouTube video: play/pause, skip, captions, picture-in-picture, minimize,
335
+ close. Anything specific to *your* product — a bookmark, a share menu, a
336
+ speed control framed for your users — is yours to render, through
337
+ `extraControls`:
338
+
339
+ ```tsx
340
+ import { Gauge } from 'lucide-react';
341
+ import { FloatingYouTubePlayer } from 'extract-youtube/react';
342
+
343
+ <FloatingYouTubePlayer
344
+ transcriptUrl="/api/transcript"
345
+ extraControls={({ playbackRate, player }) => (
346
+ <button
347
+ // The player's own control classes, so custom buttons match the built-ins.
348
+ className={`eytp-btn${playbackRate !== 1 ? ' eytp-btn-active' : ''}`}
349
+ onClick={() => player.setPlaybackRate(playbackRate !== 1 ? 1 : 0.65)}
350
+ title="Slow it down"
351
+ >
352
+ <Gauge size={13} />
353
+ </button>
354
+ )}
355
+ />
356
+ ```
357
+
358
+ debate-ai.com uses exactly this seam for its "slow the debate spread down"
359
+ button — its own label, icon and rate, sitting in the same strip. The
360
+ package stays generic: it exposes `player.setPlaybackRate()` and reports the
361
+ current `playbackRate`, and the host decides what the button says and does.
362
+ `renderTitle` is the same idea for the title bar, if you want badges or
363
+ links instead of a plain video title.
364
+
365
+ #### The imperative API
366
+
367
+ Everything the player can do is on `youtubePlayer`, importable anywhere:
368
+
369
+ ```ts
370
+ import { youtubePlayer, usePlayerState, getPlayerState } from 'extract-youtube/react';
371
+
372
+ youtubePlayer.play({ videoId, title, meta }); // meta is yours; passed back untouched
373
+ youtubePlayer.play({ videoId }, { startSeconds: 120 });
374
+ youtubePlayer.togglePlay();
375
+ youtubePlayer.seekTo(90);
376
+ youtubePlayer.setPlaybackRate(1.5);
377
+ youtubePlayer.addToQueue({ videoId, title });
378
+ youtubePlayer.setQueue(videos); // e.g. "play all" over a grid
379
+ youtubePlayer.playNext();
380
+ youtubePlayer.getCurrentTime(); // seconds
381
+ youtubePlayer.close();
382
+
383
+ usePlayerState(); // in a component: { activeVideo, isPlaying, isMinimized, playbackRate, queue, startTime }
384
+ getPlayerState(); // the same, outside React
385
+ ```
386
+
387
+ #### Floating player props
388
+
389
+ | Prop | Type | Description |
390
+ | --- | --- | --- |
391
+ | `transcriptUrl` | `string` | Your captions endpoint (see below). Enables the subtitles button. |
392
+ | `fetchTranscript` | `(videoId: string) => Promise<{ snippets, error? }>` | Custom caption loader, instead of `transcriptUrl`. |
393
+ | `extraControls` | `ReactNode \| (ctx: PlayerControlContext) => ReactNode` | Your own buttons, rendered in the control strip. |
394
+ | `renderTitle` | `(ctx: PlayerControlContext) => ReactNode` | Custom title-bar content. Defaults to the video title. |
395
+ | `showSubtitles` | `boolean` | Force the captions button on or off. Defaults to on when a transcript source is given. |
396
+ | `showPictureInPicture` | `boolean` | Show the PiP button where supported. Default `true`. |
397
+ | `storageKey` | `string \| null` | localStorage key for resume-after-reload. `null` disables persistence entirely. |
398
+ | `className` | `string` | Extra class on the player root, for host-side positioning or theming. |
399
+ | `onClose` | `() => void` | Called when the user closes the player. |
400
+ | `minWidth` / `maxWidth` | `number` | Resize bounds in px. Default 256 / 800. |
401
+
275
402
  ### Setup: video captions still have to be fetched server-side
276
403
 
277
- The modal itself never talks to YouTube's caption endpoints directly —
278
- browsers can't (no CORS, and it would leak this package's whole fetching
279
- strategy client-side for no benefit). Instead, you expose one small
280
- backend endpoint that calls this package's `YouTubeTranscriptApi`, and
281
- point the modal at it with `transcriptUrl`.
404
+ Neither component talks to YouTube's caption endpoints directly — browsers
405
+ can't (no CORS, and it would leak this package's whole fetching strategy
406
+ client-side for no benefit). Instead, you expose one small backend endpoint
407
+ that calls this package's `YouTubeTranscriptApi`, and point the components
408
+ at it with `transcriptUrl`.
282
409
 
283
410
  **1. Add a backend endpoint** (any framework works — this is a Next.js
284
411
  route handler, following the same pattern as the Vercel Edge example
@@ -303,7 +430,7 @@ export async function GET(req: NextRequest) {
303
430
  return NextResponse.json({ videoId, snippets: transcript.toRawData() });
304
431
  } catch (error) {
305
432
  // 200 + an `error` field, not a 4xx/5xx — "no captions for this video"
306
- // isn't a server failure, and the modal checks this field either way.
433
+ // isn't a server failure, and the components check this field either way.
307
434
  return NextResponse.json({
308
435
  videoId,
309
436
  snippets: [],
@@ -313,33 +440,28 @@ export async function GET(req: NextRequest) {
313
440
  }
314
441
  ```
315
442
 
316
- **2. Render the modal**, pointing it at that endpoint:
443
+ **2. Point the components at it** with `transcriptUrl="/api/transcript"`.
444
+ The `videoId` query param is appended for you, and one request is shared
445
+ per video across every component asking for it.
446
+
447
+ `extractVideoId` (exported from the main entry, since it runs fine in Node)
448
+ accepts any of the URL shapes YouTube uses — watch, youtu.be, embed, shorts,
449
+ live — so you can pass what a user pastes in straight through.
450
+
451
+ ### Transcript modal
317
452
 
318
453
  ```tsx
319
454
  import { YouTubeTranscriptModal } from 'extract-youtube/react';
320
455
  import { extractVideoId } from 'extract-youtube';
321
456
 
322
457
  function VideoCard({ url, title }: { url: string; title: string }) {
323
- const videoId = extractVideoId(url); // handles watch/youtu.be/embed/shorts/live URLs, or a bare ID
458
+ const videoId = extractVideoId(url);
324
459
  if (!videoId) return null;
325
460
 
326
- return (
327
- <YouTubeTranscriptModal
328
- videoId={videoId}
329
- title={title}
330
- transcriptUrl="/api/transcript"
331
- />
332
- );
461
+ return <YouTubeTranscriptModal videoId={videoId} title={title} transcriptUrl="/api/transcript" />;
333
462
  }
334
463
  ```
335
464
 
336
- That's the whole setup — one endpoint, one component. `extractVideoId`
337
- (exported from the main entry, since it runs fine in Node) also accepts
338
- any of the URL shapes YouTube uses, so you can pass what a user pastes in
339
- straight through.
340
-
341
- ### Props
342
-
343
465
  | Prop | Type | Description |
344
466
  | --- | --- | --- |
345
467
  | `videoId` | `string` | **Required.** The YouTube video ID (not a full URL — run it through `extractVideoId` first if needed). |
@@ -350,11 +472,12 @@ straight through.
350
472
  | `trigger` | `ReactNode` | Custom element that opens the modal on click. Defaults to a small captions-icon button. |
351
473
  | `onOpenChange` | `(open: boolean) => void` | Called whenever the modal opens or closes. |
352
474
 
353
- ### Demo: video player + subtitle sidebar, for any video
475
+ ### Demo: a video library, a floating player, synced subtitles
354
476
 
355
477
  The `demo/` folder is a small standalone app — an Express server for the
356
- transcript endpoint plus a Vite/React page with a "paste any YouTube URL"
357
- input — that puts the whole flow together end to end.
478
+ transcript endpoint plus a Vite/React page — that puts the whole thing
479
+ together: a **grid of saved favorite videos** to pick from, the floating
480
+ player they open in, a queue, and per-card transcripts.
358
481
 
359
482
  ```bash
360
483
  cd packages/extract-youtube
@@ -365,9 +488,11 @@ npm run demo # installs the demo's own deps and starts it
365
488
  `npm run demo` runs `cd demo && npm install && npm run dev`, which starts
366
489
  both the Express transcript API (port 8787) and the Vite dev server
367
490
  (port 5173, proxying `/api` to 8787) together. Open
368
- **http://localhost:5173**, paste any YouTube URL (or leave the default),
369
- click **Load**, then **▶ Open video + subtitles** — the player opens with
370
- the transcript loading in the side panel, synced to playback.
491
+ **http://localhost:5173** and click any video in the grid: it opens in the
492
+ floating player, which you can drag, resize, minimize or pop out while you
493
+ keep browsing. Star videos to favorite them, queue more up, hit **Play all**
494
+ to run the grid as a playlist, or turn on the captions button to follow a
495
+ synced transcript.
371
496
 
372
497
  Run the two halves separately if you'd rather:
373
498
 
@@ -378,8 +503,9 @@ npm run server # Express API on :8787
378
503
  npm run dev # in another terminal — Vite dev server on :5173
379
504
  ```
380
505
 
381
- See `demo/server.js` for the Express endpoint and `demo/src/App.jsx` for
382
- the page that wires the input box up to the modal.
506
+ See `demo/README.md` for what each file does — including
507
+ `demo/src/SpeedButton.jsx`, the worked example of an app-supplied custom
508
+ control.
383
509
 
384
510
  ## Features
385
511
 
package/dist/cli.js CHANGED
@@ -2010,7 +2010,7 @@ var package_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
2010
2010
  var init_package = require_rolldown_runtime.__esmMin((() => {
2011
2011
  name = "extract-youtube";
2012
2012
  version = "1.0.275";
2013
- description = "Fast, no-browser serverless-optimized YouTube transcript extractor";
2013
+ description = "Fast, no-browser serverless-optimized YouTube transcript extractor, with optional React UI: a floating YouTube player and a synced-transcript modal";
2014
2014
  main$1 = "dist/index.cjs";
2015
2015
  module$1 = "dist/index.mjs";
2016
2016
  types = "dist/index.d.ts";
@@ -2063,7 +2063,13 @@ var init_package = require_rolldown_runtime.__esmMin((() => {
2063
2063
  "video",
2064
2064
  "parsing",
2065
2065
  "extraction",
2066
- "optimized"
2066
+ "optimized",
2067
+ "react",
2068
+ "floating-player",
2069
+ "picture-in-picture",
2070
+ "video-player",
2071
+ "youtube-player",
2072
+ "playlist"
2067
2073
  ];
2068
2074
  license = "rights.institute/PROSPER";
2069
2075
  repository = {