extract-youtube 1.0.459 → 1.0.461

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 CHANGED
@@ -571,6 +571,71 @@ Full guides: [video grid and list](./site/content/docs/react/video-grid.mdx),
571
571
  [custom fields](./site/content/docs/admin/custom-fields.mdx) and
572
572
  [admin components](./site/content/docs/admin/admin-components.mdx).
573
573
 
574
+ ## Media Download: Audio, Video, Subtitles (`extract-youtube/download`)
575
+
576
+ For what captions can't give you — a video with **no transcript at all** — the
577
+ `extract-youtube/download` entry downloads the media itself, built on
578
+ [`cloud-ytdl`](https://github.com/cloudkuimages/cloud-ytdl) (YouTube's Android
579
+ InnerTube client, with signature/cipher decoding handled internally). Pull the
580
+ audio, then hand it to a speech-to-text model.
581
+
582
+ It is **Node-only** (undici streams, and `ffmpeg` for MP3), so it is its own
583
+ entry point and `cloud-ytdl` is an *optional* peer — a transcript-only install
584
+ never pulls it in, and the other entries stay Worker-safe.
585
+
586
+ ```bash
587
+ npm install extract-youtube cloud-ytdl # plus ffmpeg on PATH for MP3
588
+ ```
589
+
590
+ ```ts
591
+ import { createMediaExtractor } from 'extract-youtube/download';
592
+ import { createWriteStream } from 'node:fs';
593
+
594
+ const media = createMediaExtractor({ cookies: process.env.YOUTUBE_COOKIES });
595
+
596
+ const info = await media.getInfo('dQw4w9WgXcQ'); // title, length, every stream, caption languages
597
+ const audio = await media.downloadAudio('dQw4w9WgXcQ', {
598
+ container: 'mp3', // or 'm4a' / 'webm' — YouTube's own stream, untouched, no ffmpeg
599
+ bitrateKbps: 48, // speech needs no more; ~21 MB an hour
600
+ mono: true,
601
+ sampleRate: 16000,
602
+ });
603
+ audio.stream.pipe(createWriteStream(audio.filename));
604
+
605
+ await media.downloadVideo(id, { quality: 'highest' }); // or 'highestvideo', or an itag
606
+ await media.getSubtitles(id, { lang: 'en', format: 'srt' });
607
+ await media.getPlaylist('https://www.youtube.com/playlist?list=PL…');
608
+ await media.getPost('https://www.youtube.com/post/Ugkx…');
609
+ ```
610
+
611
+ ### Run it as a cloud service
612
+
613
+ A Cloudflare Worker can't run `cloud-ytdl` or ffmpeg, so deploy the media API on
614
+ any Node host and call it over HTTP. `createMediaHandler` is a fetch-style
615
+ `(Request) => Response`; `serveMediaApi` wraps it in `node:http`:
616
+
617
+ ```bash
618
+ MEDIA_API_KEY=… YOUTUBE_COOKIES=… npx extract-youtube serve-media --port 8787
619
+ curl -H "Authorization: Bearer $MEDIA_API_KEY" \
620
+ "http://localhost:8787/audio?v=dQw4w9WgXcQ&format=mp3&bitrate=48&mono=1&rate=16000" -o round.mp3
621
+ ```
622
+
623
+ | Route | Query | Answers |
624
+ | --- | --- | --- |
625
+ | `GET …/info` | `v` | video info and stream list (JSON) |
626
+ | `GET …/audio` | `v`, `format` (`mp3`/`m4a`/`webm`), `bitrate`, `mono`, `rate` | the audio file |
627
+ | `GET …/video` | `v`, `quality` | the video file |
628
+ | `GET …/subtitles` | `v`, `lang`, `format` (`srt`/`xml`) | the captions |
629
+ | `GET …/playlist` | `url` | playlist JSON |
630
+ | `GET …/post` | `url` | community post JSON |
631
+
632
+ Routes match on the last path segment, so the handler mounts under any prefix.
633
+ Always set `MEDIA_API_KEY` (or `apiKey`) on a public host — an open download
634
+ proxy is bandwidth anyone can spend. Treat cookie strings as passwords. Use it
635
+ within YouTube's Terms of Service.
636
+
637
+ From the CLI: `extract-youtube audio <id> [--format mp3|m4a|webm] [--bitrate 64] [--mono] [-o file]`.
638
+
574
639
  ## Live Demo and Storybook
575
640
 
576
641
  **Docs: [youtube.js.org](https://youtube.js.org)** · Live demo: [youtube.js.org/demo](https://youtube.js.org/demo) · Storybook: [youtube.js.org/storybook](https://youtube.js.org/storybook/)
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.414";
2013
- description = "Fast, no-browser serverless-optimized YouTube transcript extractor, with a video library admin API (D1 or in-memory) and React UI: a floating YouTube player, a synced-transcript modal, video grid/list components and library admin screens";
2013
+ description = "Fast, no-browser serverless-optimized YouTube transcript extractor, with optional cloud-ytdl audio/video download (MP3 via ffmpeg) and a media HTTP API, with a video library admin API (D1 or in-memory) and React UI: a floating YouTube player, a synced-transcript modal, video grid/list components and library admin screens";
2014
2014
  main$1 = "dist/index.cjs";
2015
2015
  module$1 = "dist/index.mjs";
2016
2016
  types = "dist/index.d.ts";
@@ -2029,19 +2029,25 @@ var init_package = require_rolldown_runtime.__esmMin((() => {
2029
2029
  "types": "./dist/library/index.d.ts",
2030
2030
  "import": "./dist/library/index.mjs",
2031
2031
  "require": "./dist/library/index.cjs"
2032
+ },
2033
+ "./download": {
2034
+ "types": "./dist/download/index.d.ts",
2035
+ "import": "./dist/download/index.mjs",
2036
+ "require": "./dist/download/index.cjs"
2032
2037
  }
2033
2038
  };
2034
2039
  bin = { "extract-youtube": "dist/cli.js" };
2035
2040
  files = ["dist", "src"];
2036
2041
  scripts = {
2037
2042
  "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
2038
- "build": "npm run clean && npm run build:lib && npm run build:cli && npm run build:react && npm run build:library && npm run build:types",
2043
+ "build": "npm run clean && npm run build:lib && npm run build:cli && npm run build:react && npm run build:library && npm run build:download && npm run build:types",
2039
2044
  "build:types": "tsc --emitDeclarationOnly --declaration --declarationMap",
2040
2045
  "build:lib": "vite build",
2041
2046
  "build:cli": "BUILD_CLI=true vite build",
2042
2047
  "build:react": "BUILD_REACT=true vite build",
2043
2048
  "build:library": "BUILD_LIBRARY=true vite build",
2044
- "build:bundle": "npm run build:lib && npm run build:cli && npm run build:react && npm run build:library",
2049
+ "build:download": "BUILD_DOWNLOAD=true vite build",
2050
+ "build:bundle": "npm run build:lib && npm run build:cli && npm run build:react && npm run build:library && npm run build:download",
2045
2051
  "build:legacy": "tsc",
2046
2052
  "ship": "npm run build && npx standard-version --release-as patch; npm publish",
2047
2053
  "test": "jest",
@@ -2079,6 +2085,9 @@ var init_package = require_rolldown_runtime.__esmMin((() => {
2079
2085
  "video-grid",
2080
2086
  "admin",
2081
2087
  "cloudflare-d1",
2088
+ "youtube-downloader",
2089
+ "mp3",
2090
+ "audio-extraction",
2082
2091
  "storybook"
2083
2092
  ];
2084
2093
  license = "rights.institute/PROSPER";
@@ -2095,11 +2104,13 @@ var init_package = require_rolldown_runtime.__esmMin((() => {
2095
2104
  "youtube-transcript-api": "^3.0.6"
2096
2105
  };
2097
2106
  peerDependencies = {
2107
+ "cloud-ytdl": ">=1.0.5-next",
2098
2108
  "react": ">=18",
2099
2109
  "react-dom": ">=18",
2100
2110
  "lucide-react": "^0.300.0 || ^1.0.0"
2101
2111
  };
2102
2112
  peerDependenciesMeta = {
2113
+ "cloud-ytdl": { "optional": true },
2103
2114
  "react": { "optional": true },
2104
2115
  "react-dom": { "optional": true },
2105
2116
  "lucide-react": { "optional": true }
@@ -2109,6 +2120,7 @@ var init_package = require_rolldown_runtime.__esmMin((() => {
2109
2120
  "@types/node": "^26.5.0",
2110
2121
  "@types/react": "^19.2.18",
2111
2122
  "@types/react-dom": "^19.2.7",
2123
+ "cloud-ytdl": "^1.0.5-next",
2112
2124
  "jest": "^30.5.1",
2113
2125
  "lucide-react": "^1.43.0",
2114
2126
  "react": "^19.2.8",
@@ -2137,6 +2149,7 @@ var init_package = require_rolldown_runtime.__esmMin((() => {
2137
2149
  license,
2138
2150
  repository,
2139
2151
  dependencies,
2152
+ "//peerDependencies": "cloud-ytdl backs only the Node-only extract-youtube/download entry (audio/video download, MP3 via ffmpeg); optional so a transcript-only install stays dependency-light and Worker-safe.",
2140
2153
  peerDependencies,
2141
2154
  peerDependenciesMeta,
2142
2155
  devDependencies,
@@ -2226,13 +2239,57 @@ EXAMPLES:
2226
2239
 
2227
2240
  # Extract speech speed encoding with joined text and timestamps
2228
2241
  extract-youtube jNQXAC9IVRw -f speeds
2242
+
2243
+ MEDIA (needs the optional peer: npm install cloud-ytdl; MP3 also needs ffmpeg):
2244
+ extract-youtube audio <video-id> [--format mp3|m4a|webm] [--bitrate 64] [--mono] [-o file]
2245
+ extract-youtube serve-media [--port 8787] # HTTP API; set MEDIA_API_KEY to require a key
2246
+ Cookies for restricted videos: YOUTUBE_COOKIES env var. Proxy: --proxy <url>.
2229
2247
  `);
2230
2248
  }
2231
2249
  function showVersion() {
2232
2250
  const pkg = (init_package(), require_rolldown_runtime.__toCommonJS(package_exports).default);
2233
2251
  console.log(`extract-youtube v${pkg.version}`);
2234
2252
  }
2253
+ /** `audio` and `serve-media`: the cloud-ytdl backed `extract-youtube/download` entry. */
2254
+ async function runMediaCommand(command, args) {
2255
+ const { createMediaExtractor, serveMediaApi } = await Promise.resolve().then(() => require("./download-B1w1a7Bx.js"));
2256
+ const flag = (name) => {
2257
+ const i = args.indexOf(name);
2258
+ return i >= 0 ? args[i + 1] : void 0;
2259
+ };
2260
+ const shared = {
2261
+ cookies: process.env.YOUTUBE_COOKIES,
2262
+ proxy: flag("--proxy")
2263
+ };
2264
+ if (command === "serve-media") {
2265
+ const port = Number(flag("--port") ?? process.env.PORT ?? 8787);
2266
+ await serveMediaApi({
2267
+ ...shared,
2268
+ port,
2269
+ apiKey: process.env.MEDIA_API_KEY
2270
+ });
2271
+ console.error(`extract-youtube media API listening on :${port}${process.env.MEDIA_API_KEY ? "" : " (no MEDIA_API_KEY: open to anyone)"}`);
2272
+ return;
2273
+ }
2274
+ const videoId = args.find((a, i) => !a.startsWith("-") && !args[i - 1]?.startsWith("-"));
2275
+ if (!videoId) throw new Error("Usage: extract-youtube audio <video-id> [--format mp3|m4a|webm] [-o file]");
2276
+ const download = await createMediaExtractor(shared).downloadAudio(videoId, {
2277
+ container: flag("--format") ?? "mp3",
2278
+ bitrateKbps: flag("--bitrate") ? Number(flag("--bitrate")) : void 0,
2279
+ mono: args.includes("--mono")
2280
+ });
2281
+ const file = flag("-o") ?? flag("--output") ?? download.filename;
2282
+ const fs = await import("node:fs");
2283
+ const { pipeline } = await import("node:stream/promises");
2284
+ await pipeline(download.stream, fs.createWriteStream(file));
2285
+ console.error(`Saved ${download.info.title} to ${file}`);
2286
+ }
2235
2287
  async function main() {
2288
+ const [command, ...rest] = process.argv.slice(2);
2289
+ if (command === "audio" || command === "serve-media") {
2290
+ await runMediaCommand(command, rest);
2291
+ return;
2292
+ }
2236
2293
  const options = parseArgs();
2237
2294
  if (options.help) {
2238
2295
  showHelp();
@@ -2295,7 +2352,10 @@ async function main() {
2295
2352
  process.exit(1);
2296
2353
  }
2297
2354
  }
2298
- main();
2355
+ main().catch((error) => {
2356
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
2357
+ process.exit(1);
2358
+ });
2299
2359
  //#endregion
2300
2360
 
2301
2361
  //# sourceMappingURL=cli.js.map