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.
- package/README.md +171 -45
- package/dist/cli.js +8 -2
- package/dist/cli.js.map +1 -1
- package/dist/react/YouTubeTranscriptModal.d.ts +2 -5
- package/dist/react/YouTubeTranscriptModal.d.ts.map +1 -1
- package/dist/react/index.cjs +1 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.ts +12 -4
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.mjs +17 -3
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/player/FloatingYouTubePlayer.d.ts +64 -0
- package/dist/react/player/FloatingYouTubePlayer.d.ts.map +1 -0
- package/dist/react/player/PlayerControls.d.ts +30 -0
- package/dist/react/player/PlayerControls.d.ts.map +1 -0
- package/dist/react/player/PlayerQueue.d.ts +8 -0
- package/dist/react/player/PlayerQueue.d.ts.map +1 -0
- package/dist/react/player/PlayerResizeHandles.d.ts +12 -0
- package/dist/react/player/PlayerResizeHandles.d.ts.map +1 -0
- package/dist/react/player/PlayerSubtitles.d.ts +15 -0
- package/dist/react/player/PlayerSubtitles.d.ts.map +1 -0
- package/dist/react/player/playerPersistence.d.ts +30 -0
- package/dist/react/player/playerPersistence.d.ts.map +1 -0
- package/dist/react/player/playerStore.d.ts +116 -0
- package/dist/react/player/playerStore.d.ts.map +1 -0
- package/dist/react/player/styles.d.ts +15 -0
- package/dist/react/player/styles.d.ts.map +1 -0
- package/dist/react/player/useDocumentPictureInPicture.d.ts +26 -0
- package/dist/react/player/useDocumentPictureInPicture.d.ts.map +1 -0
- package/dist/react/player/useDragResize.d.ts +22 -0
- package/dist/react/player/useDragResize.d.ts.map +1 -0
- package/dist/react/player/youtubeEmbed.d.ts +43 -0
- package/dist/react/player/youtubeEmbed.d.ts.map +1 -0
- package/dist/react/transcript.d.ts +41 -0
- package/dist/react/transcript.d.ts.map +1 -0
- package/package.json +9 -3
- package/src/react/YouTubeTranscriptModal.tsx +8 -30
- package/src/react/index.ts +29 -4
- package/src/react/player/FloatingYouTubePlayer.tsx +498 -0
- package/src/react/player/PlayerControls.tsx +120 -0
- package/src/react/player/PlayerQueue.tsx +23 -0
- package/src/react/player/PlayerResizeHandles.tsx +39 -0
- package/src/react/player/PlayerSubtitles.tsx +112 -0
- package/src/react/player/playerPersistence.ts +114 -0
- package/src/react/player/playerStore.ts +226 -0
- package/src/react/player/styles.ts +172 -0
- package/src/react/player/useDocumentPictureInPicture.ts +102 -0
- package/src/react/player/useDragResize.ts +140 -0
- package/src/react/player/youtubeEmbed.ts +102 -0
- 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
|
|
249
|
-
|
|
250
|
-
`extract-youtube/react`
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
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.
|
|
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);
|
|
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
|
|
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
|
|
357
|
-
|
|
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
|
|
369
|
-
|
|
370
|
-
|
|
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/
|
|
382
|
-
|
|
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 = {
|