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 +65 -0
- package/dist/cli.js +64 -4
- package/dist/cli.js.map +1 -1
- package/dist/download/handler.d.ts +47 -0
- package/dist/download/handler.d.ts.map +1 -0
- package/dist/download/index.cjs +2 -0
- package/dist/download/index.cjs.map +1 -0
- package/dist/download/index.d.ts +10 -0
- package/dist/download/index.d.ts.map +1 -0
- package/dist/download/index.mjs +2 -0
- package/dist/download/index.mjs.map +1 -0
- package/dist/download/media.d.ts +228 -0
- package/dist/download/media.d.ts.map +1 -0
- package/dist/download-B1w1a7Bx.js +443 -0
- package/dist/download-B1w1a7Bx.js.map +1 -0
- package/package.json +19 -4
- package/src/cli.ts +250 -206
- package/src/download/handler.ts +184 -0
- package/src/download/index.ts +29 -0
- package/src/download/media.ts +523 -0
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:
|
|
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
|