social-dl 0.0.2 → 0.9.0

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
@@ -1,9 +1,166 @@
1
- # social-dl ⚉
1
+ ## SOCIAL MEDIA SCRAPPER ⚉
2
+ > brought to you by CRYSNOVA_AI
2
3
 
3
- Direct social media video downloader - **no prexzy, no hitube, no 3rd party API**.
4
+ # social-dl — The last downloader you'll ever need
4
5
 
5
- Downloads straight from `fbcdn.net`, `tiktokcdn`, etc. Won't go down when those APIs go down.
6
+ Direct CDN social downloader for YouTube, TikTok, Facebook, Instagram, Twitter/X, Pinterest, and CapCut. **No API. No keys. No third-party downloader service.**
7
+
8
+ [![npm](https://img.shields.io/npm/v/social-dl)](https://www.npmjs.com/package/social-dl) [![License](https://img.shields.io/badge/license-proprietary-red)](./LICENSE) [![Node](https://img.shields.io/badge/node-%3E%3D18-green)](https://nodejs.org/)
9
+
10
+ ![social-dl direct media downloader](https://cdn.crysnova.qzz.io/files/1790612783695-28bb146a-6d45-4d09-8255-def439edb896.jpeg)
11
+
12
+ ## Why social-dl?
13
+
14
+ | Capability | social-dl | Proxy downloader API |
15
+ |---|---:|---:|
16
+ | Direct CDN extraction | Yes | No |
17
+ | Depends on a third-party API staying online | No | Yes |
18
+ | API key required | No | Often |
19
+ | Open source | No — proprietary | Varies |
20
+
21
+ ## Install
6
22
 
7
- ### Install
8
23
  ```bash
9
24
  npm install social-dl
25
+ ```
26
+
27
+ ## CLI
28
+
29
+ ```bash
30
+ npx social-dl https://youtu.be/BJs3DBrMTSc
31
+ npx social-dl https://youtu.be/BJs3DBrMTSc --mp3 -o audio.m4a
32
+ npx social-dl https://facebook.com/share/r/xxx/ --json
33
+ npx social-dl https://www.tiktok.com/@user/video/123 --no-watermark
34
+ npx social-dl doctor
35
+ ```
36
+
37
+ Use `--quality 720p`, `--json`, `--no-watermark`, and `-o output.mp4` as needed. YouTube may reject datacenter IPs; `doctor` reports this as `BOT_DETECTED`.
38
+
39
+ ## Library
40
+
41
+ ```js
42
+ import { universalDl, universalSave, universalStream } from 'social-dl';
43
+
44
+ const data = await universalDl('https://youtu.be/BJs3DBrMTSc');
45
+ await universalSave('https://youtu.be/BJs3DBrMTSc', './video.mp4');
46
+ ```
47
+
48
+ ### Platform-specific library examples
49
+
50
+ Every platform also exposes its own `xxxDl()` and `xxxSave()` functions. Each `Dl()` call returns normalized metadata and direct CDN formats; each `Save()` call downloads the selected format.
51
+
52
+ #### Facebook
53
+
54
+ ```js
55
+ import { facebookDl, facebookSave } from 'social-dl/facebook';
56
+
57
+ const facebook = await facebookDl('https://www.facebook.com/reel/1234567890/');
58
+ await facebookSave('https://www.facebook.com/reel/1234567890/', './facebook.mp4', 'HD');
59
+ ```
60
+
61
+ #### YouTube video
62
+
63
+ ```js
64
+ import { youtubeDl, youtubeSave } from 'social-dl/youtube';
65
+
66
+ const youtube = await youtubeDl('https://youtu.be/BJs3DBrMTSc');
67
+ await youtubeSave('https://youtu.be/BJs3DBrMTSc', './youtube.mp4', '720p');
68
+ ```
69
+
70
+ #### YouTube audio
71
+
72
+ ```js
73
+ import { youtubeMp3Dl, youtubeMp3Save } from 'social-dl/ytmp3';
74
+
75
+ const audio = await youtubeMp3Dl('https://youtu.be/BJs3DBrMTSc');
76
+ await youtubeMp3Save('https://youtu.be/BJs3DBrMTSc', './audio.m4a');
77
+ ```
78
+
79
+ #### Instagram
80
+
81
+ ```js
82
+ import { instagramDl, instagramSave } from 'social-dl/instagram';
83
+
84
+ const instagram = await instagramDl('https://www.instagram.com/reel/CODE/');
85
+ await instagramSave('https://www.instagram.com/reel/CODE/', './instagram.mp4');
86
+ ```
87
+
88
+ #### TikTok
89
+
90
+ ```js
91
+ import { tiktokDl, tiktokSave } from 'social-dl/tiktok';
92
+
93
+ const tiktok = await tiktokDl('https://www.tiktok.com/@user/video/1234567890123456789');
94
+ await tiktokSave('https://www.tiktok.com/@user/video/1234567890123456789', './tiktok.mp4', 'NOWM');
95
+ ```
96
+
97
+ #### Twitter/X
98
+
99
+ ```js
100
+ import { twitterDl, twitterSave } from 'social-dl/twitter';
101
+
102
+ const tweet = await twitterDl('https://x.com/user/status/1851234567890');
103
+ await twitterSave('https://x.com/user/status/1851234567890', './twitter.mp4');
104
+ ```
105
+
106
+ #### Pinterest
107
+
108
+ ```js
109
+ import { pinterestDl, pinterestSave } from 'social-dl/pinterest';
110
+
111
+ const pin = await pinterestDl('https://www.pinterest.com/pin/1234567890/');
112
+ await pinterestSave('https://www.pinterest.com/pin/1234567890/', './pinterest.mp4');
113
+ ```
114
+
115
+ #### CapCut
116
+
117
+ ```js
118
+ import { capcutDl, capcutSave } from 'social-dl/capcut';
119
+
120
+ const template = await capcutDl('https://www.capcut.com/template-detail/1234567890');
121
+ await capcutSave('https://www.capcut.com/template-detail/1234567890', './capcut.mp4');
122
+ ```
123
+
124
+ ### Express/Next.js streaming
125
+
126
+ ```js
127
+ const { stream, contentType, filename } = await universalStream(url);
128
+ res.setHeader('Content-Type', contentType);
129
+ res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
130
+ Readable.fromWeb(stream).pipe(res);
131
+ ```
132
+
133
+ ## Platforms
134
+
135
+ | Platform | Example |
136
+ |---|---|
137
+ | Facebook | `https://www.facebook.com/reel/1234567890/` |
138
+ | YouTube | `https://youtu.be/BJs3DBrMTSc` |
139
+ | Instagram | `https://www.instagram.com/reel/CODE/` |
140
+ | TikTok | `https://www.tiktok.com/@user/video/123` |
141
+ | Twitter/X | `https://x.com/user/status/123` |
142
+ | Pinterest | `https://www.pinterest.com/pin/1234567890/` |
143
+ | CapCut | `https://www.capcut.com/template-detail/1234567890` |
144
+ | YouTube audio | Same YouTube URLs with `{ audioOnly: true }` or `--mp3` |
145
+
146
+ ## API reference
147
+
148
+ - `universalDl(url, { audioOnly, quality })` — return normalized direct-CDN formats.
149
+ - `universalSave(url, destination, { audioOnly, quality, noWatermark })` — save a selected format.
150
+ - `universalStream(url, options)` — return a Web `ReadableStream` for piping.
151
+ - `doctor({ log })` — test provider reachability, Node, and ffmpeg.
152
+ - Every platform exports `xxxDl()` and `xxxSave()`.
153
+
154
+ ## Error handling
155
+
156
+ Handle `BotDetectedError` for provider anti-automation blocks, `PrivateError` for login/private content, `NotFoundError` for missing media, and `UnsupportedError` for unsupported URLs.
157
+
158
+ ## Demo
159
+
160
+ <!-- DEMO TODO FOR 1.0.0: After publishing 0.9.0, record terminal and replace with ![demo](./demo.gif) -->
161
+
162
+ Demo video coming in 1.0.0 — npx becomes available after this 0.9.0 pre-launch.
163
+
164
+ ## License
165
+
166
+ Proprietary license. See [LICENSE](./LICENSE).
package/bin/cli.js ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ import { createWriteStream } from 'node:fs';
3
+ import { pipeline } from 'node:stream/promises';
4
+ import { universalDl, universalSave } from '../src/index.js';
5
+ import { doctor } from '../src/doctor.js';
6
+ import { randomUserAgent } from '../src/utils/fetcher.js';
7
+
8
+ const VERSION = '0.9.0';
9
+ const HELP = `social-dl ${VERSION}\n\nUsage:\n social-dl <url> [-o file] [--quality 720p]\n social-dl <url> --mp3 -o audio.m4a\n social-dl <url> --json\n social-dl <url> --no-watermark\n social-dl doctor [--json]\n\nOptions: --quality <label> --mp3 --json --no-watermark -o, --output <file> --help`;
10
+ const color = (code, text) => process.stdout.isTTY ? `\x1b[${code}m${text}\x1b[0m` : text;
11
+
12
+ function parse(argv) {
13
+ const opts = { _: [] };
14
+ for (let i = 0; i < argv.length; i += 1) {
15
+ const arg = argv[i];
16
+ if (arg === '--help' || arg === '-h') opts.help = true;
17
+ else if (arg === '--mp3') opts.audioOnly = true;
18
+ else if (arg === '--json') opts.json = true;
19
+ else if (arg === '--no-watermark') opts.noWatermark = true;
20
+ else if (arg === '--quality') opts.quality = argv[++i];
21
+ else if (arg === '-o' || arg === '--output') opts.output = argv[++i];
22
+ else opts._.push(arg);
23
+ }
24
+ return opts;
25
+ }
26
+
27
+ async function streamSave(info, output, quality, noWatermark) {
28
+ let formats = info.formats;
29
+ if (noWatermark) formats = formats.filter(item => item.isNoWatermark !== false);
30
+ const selected = quality ? formats.find(item => item.tag === quality || item.quality === quality) : formats[0];
31
+ const format = selected || formats[0];
32
+ if (!format) throw new Error('NO_FORMATS');
33
+ const response = await fetch(format.url, { headers: { 'User-Agent': randomUserAgent(), Referer: info.originalUrl } });
34
+ if (!response.ok || !response.body) throw new Error(`CDN_FETCH_FAILED_${response.status}`);
35
+ const total = Number(response.headers.get('content-length')) || 0;
36
+ let received = 0;
37
+ const reader = response.body.getReader();
38
+ const outputStream = createWriteStream(output);
39
+ const readable = new ReadableStream({ async pull(controller) { const { done, value } = await reader.read(); if (done) return controller.close(); received += value.byteLength; controller.enqueue(value); if (!process.stdout.isTTY) return; const mb = (received / 1048576).toFixed(1); const suffix = total ? ` / ${(total / 1048576).toFixed(1)} MB` : ' MB'; process.stdout.write(`\r${color(36, `Downloading ${mb}${suffix}`)}`); }, cancel() { reader.cancel(); } });
40
+ await pipeline(readable, outputStream);
41
+ if (process.stdout.isTTY) process.stdout.write('\n');
42
+ return output;
43
+ }
44
+
45
+ async function main() {
46
+ const opts = parse(process.argv.slice(2));
47
+ if (opts.help || opts._[0] === undefined) return console.log(HELP);
48
+ if (opts._[0] === 'doctor') return opts.json ? console.log(JSON.stringify(await doctor({ log: false }), null, 2)) : await doctor();
49
+ const url = opts._[0];
50
+ const info = await universalDl(url, { audioOnly: opts.audioOnly });
51
+ if (opts.json) return console.log(JSON.stringify(info, null, 2));
52
+ const output = opts.output || `${info.id}.${opts.audioOnly ? 'm4a' : (info.formats[0]?.type || 'mp4')}`;
53
+ await streamSave(info, output, opts.quality, opts.noWatermark);
54
+ console.log(`${color(32, 'Saved')} ${output}`);
55
+ }
56
+
57
+ main().catch(error => { console.error(color(31, `${error.code || error.name || 'ERROR'}: ${error.message}`)); process.exitCode = 1; });
package/index.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ export interface Format { tag: string; quality: string; type: string; url: string; hasAudio?: boolean; isAudioOnly?: boolean; isNoWatermark?: boolean; }
2
+ export interface DlResult { creator?: string; platform: string; id: string; title?: string; author?: string; thumbnail?: string; originalUrl: string; isAudioOnly?: boolean; formats: Format[]; needsMux?: boolean; videoUrl?: string; audioUrl?: string; }
3
+ export interface UniversalOptions { audioOnly?: boolean; quality?: string; noWatermark?: boolean; }
4
+ export interface StreamResult { stream: ReadableStream<Uint8Array>; filename: string; info: DlResult; contentType: string; contentLength?: string | null; }
5
+ export function detectPlatform(url: string): string;
6
+ export function universalDl(url: string, opts?: UniversalOptions): Promise<DlResult>;
7
+ export function universalSave(url: string, dest?: string, opts?: UniversalOptions): Promise<string>;
8
+ export function universalStream(url: string, opts?: UniversalOptions): Promise<StreamResult>;
9
+ export function doctor(options?: { log?: boolean }): Promise<any>;
10
+ export function facebookDl(url: string): Promise<DlResult>;
11
+ export function facebookSave(url: string, dest?: string, quality?: string): Promise<string>;
12
+ export function youtubeDl(url: string): Promise<DlResult>;
13
+ export function youtubeSave(url: string, dest?: string, quality?: string): Promise<string>;
14
+ export function youtubeMp3Dl(url: string): Promise<DlResult>;
15
+ export function youtubeMp3Save(url: string, dest?: string, quality?: string): Promise<string>;
16
+ export function instagramDl(url: string): Promise<DlResult>;
17
+ export function instagramSave(url: string, dest?: string, quality?: string): Promise<string>;
18
+ export function tiktokDl(url: string): Promise<DlResult>;
19
+ export function tiktokSave(url: string, dest?: string, quality?: string): Promise<string>;
20
+ export function twitterDl(url: string): Promise<DlResult>;
21
+ export function twitterSave(url: string, dest?: string, quality?: string): Promise<string>;
22
+ export function pinterestDl(url: string): Promise<DlResult>;
23
+ export function pinterestSave(url: string, dest?: string, quality?: string): Promise<string>;
24
+ export function capcutDl(url: string): Promise<DlResult>;
25
+ export function capcutSave(url: string, dest?: string, quality?: string): Promise<string>;
26
+ export class SocialDlError extends Error { code: string; }
27
+ export class BotDetectedError extends SocialDlError {}
28
+ export class PrivateError extends SocialDlError {}
29
+ export class NotFoundError extends SocialDlError {}
30
+ export class UnsupportedError extends SocialDlError {}
31
+ export const CREATOR: string;
package/package.json CHANGED
@@ -1,35 +1,30 @@
1
1
  {
2
2
  "name": "social-dl",
3
- "version": "0.0.2",
3
+ "version": "0.9.0",
4
4
  "license": "SEE LICENSE IN LICENSE ⚉",
5
- "description": "Universal social media downloader - ⚉ direct from Facebook, TikTok, Instagram, Twitter/X without any 3rd party API like prexzy",
5
+ "description": "Direct CDN social downloader - YouTube, TikTok, Facebook, Instagram, Twitter, Pinterest, CapCut - No API, No keys, Never dies",
6
6
  "main": "src/index.js",
7
7
  "type": "module",
8
+ "bin": { "social-dl": "./bin/cli.js" },
8
9
  "exports": {
9
10
  ".": "./src/index.js",
10
- "./facebook": "./src/platforms/facebook.js"
11
+ "./facebook": "./src/platforms/facebook.js",
12
+ "./youtube": "./src/platforms/youtube.js",
13
+ "./ytmp3": "./src/platforms/ytmp3.js",
14
+ "./instagram": "./src/platforms/instagram.js",
15
+ "./tiktok": "./src/platforms/tiktok.js",
16
+ "./twitter": "./src/platforms/twitter.js",
17
+ "./pinterest": "./src/platforms/pinterest.js",
18
+ "./capcut": "./src/platforms/capcut.js",
19
+ "./stream": "./src/stream.js"
11
20
  },
12
21
  "scripts": {
13
- "test": "node test.js",
22
+ "test": "node tests/test.js",
14
23
  "dev": "node src/index.js"
15
24
  },
16
- "keywords": [
17
- "facebook",
18
- "tiktok",
19
- "instagram",
20
- "twitter",
21
- "downloader",
22
- "social-media",
23
- "video-downloader",
24
- "fbdown",
25
- "reels",
26
- "scrapper",
27
- "plogme",
28
- "crysnovax",
29
- "no-api"
30
- ],
31
- "author": "crysnovax",
32
- "license": "MIT",
25
+ "keywords": ["youtube downloader", "tiktok downloader", "facebook downloader", "instagram downloader", "twitter downloader", "pinterest downloader", "capcut downloader", "youtube mp3", "youtube to mp3", "reels downloader", "no api downloader", "direct cdn", "social-dl", "ytmp4", "ytmp3"],
26
+ "author": "@plogme",
27
+ "license": "SEE LICENSE IN LICENSE",
33
28
  "repository": {
34
29
  "type": "git",
35
30
  "url": "git+https://github.com/crysnovax/social-dl.git"
@@ -43,6 +38,9 @@
43
38
  },
44
39
  "files": [
45
40
  "src/",
46
- "README.md"
41
+ "bin/",
42
+ "index.d.ts",
43
+ "README.md",
44
+ "LICENSE"
47
45
  ]
48
46
  }
@@ -0,0 +1,2 @@
1
+ /** Constant attribution included in social-dl metadata responses. */
2
+ export const CREATOR = '@plogme';
package/src/doctor.js ADDED
@@ -0,0 +1,41 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { fetchWithRetry } from './utils/fetcher.js';
3
+ import { BotDetectedError } from './errors.js';
4
+ import { player } from './platforms/youtube.js';
5
+
6
+ const CHECKS = {
7
+ facebook: 'https://www.facebook.com/',
8
+ youtube: 'https://www.youtube.com/',
9
+ instagram: 'https://www.instagram.com/',
10
+ tiktok: 'https://www.tiktok.com/',
11
+ twitter: 'https://video.twimg.com/',
12
+ pinterest: 'https://v.pinimg.com/',
13
+ capcut: 'https://www.capcut.com/'
14
+ };
15
+
16
+ /** Check provider reachability and local runtime prerequisites. */
17
+ export async function doctor({ log = true } = {}) {
18
+ const result = { node: { ok: Number(process.versions.node.split('.')[0]) >= 18, version: process.versions.node }, ffmpeg: { ok: spawnSync('ffmpeg', ['-version'], { stdio: 'ignore' }).status === 0 }, providers: {} };
19
+ for (const [name, url] of Object.entries(CHECKS)) {
20
+ const started = Date.now();
21
+ try {
22
+ const response = await fetchWithRetry(url, { signal: AbortSignal.timeout(5000), headers: { Accept: 'text/html' } }, 0);
23
+ result.providers[name] = { ok: response.ok || response.status < 500, status: response.status, ms: Date.now() - started };
24
+ if (name === 'youtube') {
25
+ try {
26
+ await player('BJs3DBrMTSc');
27
+ } catch (error) {
28
+ if (error instanceof BotDetectedError) result.providers[name] = { ...result.providers[name], ok: false, code: error.code, warning: 'YouTube is blocking Innertube requests from this network.' };
29
+ }
30
+ }
31
+ } catch (error) {
32
+ result.providers[name] = { ok: false, code: error instanceof BotDetectedError ? error.code : 'FETCH_FAILED', message: error.message, ms: Date.now() - started, warning: name === 'youtube' && error instanceof BotDetectedError ? 'YouTube may be blocking this datacenter IP.' : undefined };
33
+ }
34
+ }
35
+ if (log) {
36
+ console.log(`Node ${result.node.ok ? '✓' : '✗'} ${result.node.version}`);
37
+ console.log(`ffmpeg ${result.ffmpeg.ok ? '✓ available' : '✗ not found'}`);
38
+ for (const [name, item] of Object.entries(result.providers)) console.log(`${name.padEnd(10)} ${item.ok ? '✓' : '✗'}${item.status ? ` HTTP ${item.status}` : ` ${item.code}`}${item.warning ? ` — ${item.warning}` : ''}`);
39
+ }
40
+ return result;
41
+ }
package/src/errors.js ADDED
@@ -0,0 +1,40 @@
1
+ /** Base error for social-dl failures. */
2
+ export class SocialDlError extends Error {
3
+ constructor(message, code = 'SOCIAL_DL_ERROR') {
4
+ super(message);
5
+ this.name = 'SocialDlError';
6
+ this.code = code;
7
+ }
8
+ }
9
+
10
+ /** Indicates that the source is private or requires authentication. */
11
+ export class PrivateError extends SocialDlError {
12
+ constructor(message = 'The media is private or requires login.') {
13
+ super(message, 'PRIVATE_OR_LOGIN_REQUIRED');
14
+ this.name = 'PrivateError';
15
+ }
16
+ }
17
+
18
+ /** Indicates that no supported media was found. */
19
+ export class NotFoundError extends SocialDlError {
20
+ constructor(message = 'No supported media was found.') {
21
+ super(message, 'NOT_FOUND');
22
+ this.name = 'NotFoundError';
23
+ }
24
+ }
25
+
26
+ /** Indicates that the URL is not supported. */
27
+ export class UnsupportedError extends SocialDlError {
28
+ constructor(message = 'Unsupported URL.') {
29
+ super(message, 'UNSUPPORTED_URL');
30
+ this.name = 'UnsupportedError';
31
+ }
32
+ }
33
+
34
+ /** Indicates that a provider blocked an automated request. */
35
+ export class BotDetectedError extends SocialDlError {
36
+ constructor(message = 'The provider blocked this automated request.') {
37
+ super(message, 'BOT_DETECTED');
38
+ this.name = 'BotDetectedError';
39
+ }
40
+ }
package/src/index.js ADDED
@@ -0,0 +1,57 @@
1
+ import { UnsupportedError } from './errors.js';
2
+ import { facebookDl, facebookSave } from './platforms/facebook.js';
3
+ import { youtubeDl, youtubeSave } from './platforms/youtube.js';
4
+ import { youtubeMp3Dl, youtubeMp3Save } from './platforms/ytmp3.js';
5
+ import { instagramDl, instagramSave } from './platforms/instagram.js';
6
+ import { tiktokDl, tiktokSave } from './platforms/tiktok.js';
7
+ import { twitterDl, twitterSave } from './platforms/twitter.js';
8
+ import { pinterestDl, pinterestSave } from './platforms/pinterest.js';
9
+ import { capcutDl, capcutSave } from './platforms/capcut.js';
10
+ import { saveResult } from './utils/media.js';
11
+
12
+ /** Identify the supported platform for a URL. */
13
+ export function detectPlatform(value) {
14
+ const host = new URL(value).hostname.toLowerCase();
15
+ if (host.includes('facebook.com') || host === 'fb.watch') return 'facebook';
16
+ if (host.includes('youtube.com') || host === 'youtu.be') return 'youtube';
17
+ if (host.includes('instagram.com')) return 'instagram';
18
+ if (host.includes('tiktok.com')) return 'tiktok';
19
+ if (host === 'x.com' || host.endsWith('twitter.com') || host === 't.co') return 'twitter';
20
+ if (host.includes('pinterest.com') || host === 'pin.it') return 'pinterest';
21
+ if (host.includes('capcut.com')) return 'capcut';
22
+ throw new UnsupportedError();
23
+ }
24
+
25
+ /** Download metadata using the correct platform extractor. */
26
+ export async function universalDl(url, opts = {}) {
27
+ switch (detectPlatform(url)) {
28
+ case 'facebook': return facebookDl(url);
29
+ case 'youtube': return opts.audioOnly ? youtubeMp3Dl(url) : youtubeDl(url);
30
+ case 'instagram': return instagramDl(url);
31
+ case 'tiktok': return tiktokDl(url);
32
+ case 'twitter': return twitterDl(url);
33
+ case 'pinterest': return pinterestDl(url);
34
+ case 'capcut': return capcutDl(url);
35
+ default: throw new UnsupportedError();
36
+ }
37
+ }
38
+
39
+ /** Save a selected format from the correct platform. */
40
+ export async function universalSave(url, dest = './video.mp4', opts = {}) {
41
+ const quality = opts.quality || 'best';
42
+ const result = await universalDl(url, opts);
43
+ return saveResult(result, dest, quality, {}, opts.noWatermark === true);
44
+ }
45
+
46
+ export * from './errors.js';
47
+ export * from './constants.js';
48
+ export { universalStream } from './stream.js';
49
+ export { doctor } from './doctor.js';
50
+ export * from './platforms/facebook.js';
51
+ export * from './platforms/youtube.js';
52
+ export * from './platforms/ytmp3.js';
53
+ export * from './platforms/instagram.js';
54
+ export * from './platforms/tiktok.js';
55
+ export * from './platforms/twitter.js';
56
+ export * from './platforms/pinterest.js';
57
+ export * from './platforms/capcut.js';
@@ -0,0 +1,28 @@
1
+ import { fetchWithRetry } from '../utils/fetcher.js';
2
+ import { resolveShortUrl } from '../utils/resolver.js';
3
+ import { saveResult, sortFormats } from '../utils/media.js';
4
+ import { unescapeJson } from '../utils/parser.js';
5
+ import { NotFoundError, UnsupportedError } from '../errors.js';
6
+ import { CREATOR } from '../constants.js';
7
+
8
+ const findUrls = value => { const urls = []; const walk = item => { if (!item) return; if (typeof item === 'string' && /\.(?:mp4)(?:[?&]|$)/i.test(item)) urls.push(item); else if (Array.isArray(item)) item.forEach(walk); else if (typeof item === 'object') Object.values(item).forEach(walk); }; walk(value); return urls; };
9
+
10
+ /** Download metadata and direct CapCut CDN formats. */
11
+ export async function capcutDl(url) {
12
+ const resolved = await resolveShortUrl(url);
13
+ const id = resolved.match(/template-detail\/(\d+)/)?.[1] || resolved.match(/\/(\d{10,19})(?:\D|$)/)?.[1];
14
+ if (!id) throw new UnsupportedError('Invalid CapCut URL.');
15
+ let urls = [];
16
+ const api = await fetchWithRetry(`https://www.capcut.com/api/v1/query/template/detail?template_id=${id}&language=en&aid=0`, { headers: { Referer: 'https://www.capcut.com/' } });
17
+ if (api.ok) { try { urls = findUrls(await api.json()); } catch {} }
18
+ if (!urls.length) {
19
+ const html = await (await fetchWithRetry(`https://www.capcut.com/template-detail/${id}`, { headers: { Referer: 'https://www.capcut.com/' } })).text();
20
+ urls = ['video_url', 'download_url', 'play_url'].map(key => html.match(new RegExp(`"${key}":"([^"]+)"`))?.[1]).filter(Boolean).map(unescapeJson);
21
+ }
22
+ const formats = sortFormats([...new Set(urls)].map((item, index) => ({ tag: index ? 'SD' : 'HD', quality: index ? '720p' : '1080p', type: 'mp4', isNoWatermark: true, url: item })));
23
+ if (!formats.length) throw new NotFoundError();
24
+ return { creator: CREATOR, platform: 'capcut', id, title: `CapCut template ${id}`, originalUrl: resolved, formats };
25
+ }
26
+
27
+ /** Save a CapCut video to disk. */
28
+ export async function capcutSave(url, dest = './video.mp4', quality = 'best') { return saveResult(await capcutDl(url), dest, quality); }
@@ -0,0 +1,42 @@
1
+ import { fetchWithRetry, downloadTo } from '../utils/fetcher.js';
2
+ import { sortFormats, saveResult } from '../utils/media.js';
3
+ import { NotFoundError, PrivateError } from '../errors.js';
4
+ import { CREATOR } from '../constants.js';
5
+
6
+ const DESKTOP = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36';
7
+ const unescape = value => { try { return JSON.parse(`"${value}"`); } catch { return value.replace(/\\u0025/g, '%').replace(/\\u0026/g, '&').replace(/\\\//g, '/'); } };
8
+
9
+ function extract(html) {
10
+ const find = name => { const match = html.match(new RegExp(`"${name}":"([^"]+)"`)); return match && unescape(match[1]); };
11
+ const hd = find('browser_native_hd_url') || find('playable_url_quality_hd');
12
+ const sd = find('browser_native_sd_url') || find('playable_url');
13
+ return sortFormats([
14
+ hd && { tag: 'HD', quality: '720p', type: 'mp4', url: hd },
15
+ sd && { tag: 'SD', quality: '360p', type: 'mp4', url: sd }
16
+ ]);
17
+ }
18
+
19
+ /** Download metadata for a Facebook video or reel. */
20
+ export async function facebookDl(url) {
21
+ const resolved = await fetchWithRetry(url, { headers: { 'User-Agent': DESKTOP } });
22
+ const originalUrl = resolved.url || url;
23
+ const id = originalUrl.match(/reel\/(\d+)|videos\/(\d+)|[?&]v=(\d+)|\/(\d+)\/?$/)?.slice(1).find(Boolean) || originalUrl;
24
+ let html = await (await fetchWithRetry(originalUrl, { headers: { 'User-Agent': DESKTOP } })).text();
25
+ let formats = extract(html);
26
+ if (html.length < 5000 || formats.length === 0) {
27
+ await new Promise(resolve => setTimeout(resolve, 1000));
28
+ const basicUrl = originalUrl.replace(/www\.facebook\.com/i, 'mbasic.facebook.com');
29
+ html = await (await fetchWithRetry(basicUrl, { headers: { 'User-Agent': DESKTOP } })).text();
30
+ formats = extract(html);
31
+ }
32
+ if (!formats.length) {
33
+ if (/login|checkpoint|not found/i.test(html)) throw new PrivateError();
34
+ throw new NotFoundError('PRIVATE_OR_NOT_FOUND');
35
+ }
36
+ return { creator: CREATOR, platform: 'facebook', id, originalUrl, formats };
37
+ }
38
+
39
+ /** Save a Facebook video to disk. */
40
+ export async function facebookSave(url, dest = './video.mp4', quality = 'best') {
41
+ return saveResult(await facebookDl(url), dest, quality, { 'User-Agent': DESKTOP, Referer: 'https://www.facebook.com/' });
42
+ }
@@ -0,0 +1,48 @@
1
+ import { fetchWithRetry } from '../utils/fetcher.js';
2
+ import { saveResult, sortFormats } from '../utils/media.js';
3
+ import { unescapeJson } from '../utils/parser.js';
4
+ import { NotFoundError, PrivateError, UnsupportedError } from '../errors.js';
5
+ import { CREATOR } from '../constants.js';
6
+
7
+ const UAS = ['Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15', 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 Chrome/120 Mobile Safari/537.36'];
8
+ const headers = () => ({ 'User-Agent': UAS[Math.floor(Math.random() * UAS.length)], 'X-IG-App-ID': '936619743392459', Accept: 'application/json' });
9
+ const codeOf = url => new URL(url).pathname.match(/\/(?:p|reel|tv)\/([A-Za-z0-9_-]+)/)?.[1] || new URL(url).pathname.match(/share\/([A-Za-z0-9_-]+)/)?.[1];
10
+
11
+ function fromHtml(html) {
12
+ const find = key => { const m = html.match(new RegExp(`"${key}":"([^"]+)"`)); return m && unescapeJson(m[1]); };
13
+ const urls = [...html.matchAll(/"url":"(https:[^\"]+)"/g)].map(m => unescapeJson(m[1])).filter(url => /(?:scontent|cdninstagram)/i.test(url) && /\.mp4(?:[?&]|$)/i.test(url));
14
+ return sortFormats([find('video_url') || urls[0], ...urls].filter(Boolean).map((url, i) => ({ tag: i ? 'SD' : 'HD', quality: i ? '480p' : '720p', type: 'mp4', url })));
15
+ }
16
+
17
+ /** Download metadata and direct formats for an Instagram post or reel. */
18
+ export async function instagramDl(url) {
19
+ const id = codeOf(url);
20
+ if (!id) throw new UnsupportedError('Invalid Instagram URL.');
21
+ const base = `https://www.instagram.com/p/${id}/`;
22
+ let response = await fetchWithRetry(`${base}?__a=1&__d=dis`, { headers: headers() });
23
+ let formats = [];
24
+ let item;
25
+ if (response.ok) {
26
+ try {
27
+ const json = await response.json();
28
+ item = json.items?.[0] || json.data?.xdt_shortcode_media;
29
+ const media = item?.carousel_media || [item];
30
+ formats = sortFormats(media.flatMap((entry, index) => [
31
+ ...(entry?.video_versions || []).map(v => ({ tag: index ? `${index + 1}` : 'HD', quality: `${v.height || 720}p`, type: 'mp4', url: v.url })),
32
+ entry?.image_versions2?.candidates?.[0] && { tag: 'IMG', quality: `${entry.image_versions2.candidates[0].width || 1080}x${entry.image_versions2.candidates[0].height || 1080}`, type: 'jpg', url: entry.image_versions2.candidates[0].url }
33
+ ]));
34
+ } catch { /* use embed fallback */ }
35
+ }
36
+ if (!formats.length) {
37
+ response = await fetchWithRetry(`https://www.instagram.com/reel/${id}/embed/`, { headers: headers() });
38
+ const html = await response.text();
39
+ if (response.url.includes('/accounts/login')) throw new PrivateError();
40
+ formats = fromHtml(html);
41
+ if (!formats.length) { const image = html.match(/"display_url":"([^"]+)"/); if (image) formats.push({ tag: 'IMG', quality: '1080x1080', type: 'jpg', url: unescapeJson(image[1]) }); }
42
+ }
43
+ if (!formats.length) throw new NotFoundError('NOT_FOUND');
44
+ return { creator: CREATOR, platform: 'instagram', id, username: item?.user?.username, author: item?.user?.username, caption: item?.caption?.text, originalUrl: url, formats };
45
+ }
46
+
47
+ /** Save an Instagram video or image. */
48
+ export async function instagramSave(url, dest = './ig.mp4', quality = 'best') { return saveResult(await instagramDl(url), dest, quality, { Referer: 'https://www.instagram.com/' }); }
@@ -0,0 +1,30 @@
1
+ import { fetchWithRetry } from '../utils/fetcher.js';
2
+ import { resolveShortUrl } from '../utils/resolver.js';
3
+ import { saveResult, sortFormats } from '../utils/media.js';
4
+ import { extractJson } from '../utils/parser.js';
5
+ import { NotFoundError, UnsupportedError } from '../errors.js';
6
+ import { CREATOR } from '../constants.js';
7
+
8
+ /** Download metadata and direct Pinterest video/image formats. */
9
+ export async function pinterestDl(url) {
10
+ const resolved = await resolveShortUrl(url);
11
+ const id = resolved.match(/pin\/(\d+)/)?.[1];
12
+ if (!id) throw new UnsupportedError('Invalid Pinterest URL.');
13
+ const response = await fetchWithRetry(`https://www.pinterest.com/pin/${id}/`, { headers: { 'User-Agent': 'Mozilla/5.0 iPhone', 'X-Pinterest-PWS-Handler': 'www/[pin]' } });
14
+ const html = await response.text();
15
+ const json = extractJson(html, '__PWS_DATA__');
16
+ const pin = json?.props?.initialReduxState?.pins?.[id];
17
+ const list = pin?.videos?.video_list || {};
18
+ const formats = sortFormats(Object.values(list).filter(v => v?.url && /\.mp4(?:[?&]|$)/i.test(v.url)).map(v => ({ tag: v.width ? `${v.width}p` : 'HD', quality: v.width ? `${v.width}p` : 'HD', type: 'mp4', url: v.url, height: v.height })));
19
+ const image = pin?.images?.orig?.url;
20
+ if (image) formats.push({ tag: 'IMG', quality: 'orig', type: 'jpg', url: image });
21
+ if (!formats.length) {
22
+ const video = html.match(/"video_url":"([^"]+)"/)?.[1];
23
+ if (video) formats.push({ tag: 'HD', quality: '720p', type: 'mp4', url: JSON.parse(`"${video}"`) });
24
+ }
25
+ if (!formats.length) throw new NotFoundError();
26
+ return { creator: CREATOR, platform: 'pinterest', id, title: pin?.grid_title || pin?.title, originalUrl: resolved, formats };
27
+ }
28
+
29
+ /** Save a Pinterest video or image to disk. */
30
+ export async function pinterestSave(url, dest = './video.mp4', quality = 'best') { return saveResult(await pinterestDl(url), dest, quality); }
@@ -0,0 +1,33 @@
1
+ import { fetchWithRetry } from '../utils/fetcher.js';
2
+ import { resolveShortUrl } from '../utils/resolver.js';
3
+ import { saveResult, sortFormats } from '../utils/media.js';
4
+ import { extractJson, getPath, unescapeJson } from '../utils/parser.js';
5
+ import { NotFoundError, PrivateError, UnsupportedError } from '../errors.js';
6
+ import { CREATOR } from '../constants.js';
7
+
8
+ const UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 Version/16.6 Mobile/15E148 Safari/604.1';
9
+ const clean = value => value && unescapeJson(value);
10
+
11
+ /** Download metadata and direct CDN formats for a TikTok video. */
12
+ export async function tiktokDl(url) {
13
+ const resolved = await resolveShortUrl(url);
14
+ const id = resolved.match(/\/video\/(\d+)/)?.[1] || resolved.match(/\/v\/(\d+)/)?.[1];
15
+ if (!id) throw new UnsupportedError('Invalid TikTok URL.');
16
+ const pageUrl = resolved.match(/\/video\//) ? resolved : `https://www.tiktok.com/@a/video/${id}`;
17
+ const response = await fetchWithRetry(pageUrl, { headers: { 'User-Agent': UA, Accept: 'text/html' } });
18
+ const html = await response.text();
19
+ const json = extractJson(html, '__UNIVERSAL_DATA_FOR_REHYDRATION__');
20
+ const video = getPath(json, '__DEFAULT_SCOPE__.webapp.video-detail.videoData.itemInfo.itemStruct.video');
21
+ const item = getPath(json, '__DEFAULT_SCOPE__.webapp.video-detail.videoData.itemInfo.itemStruct');
22
+ const play = clean(video?.playAddr) || clean(html.match(/"playAddr":"([^"]+)"/)?.[1]);
23
+ const download = clean(video?.downloadAddr) || clean(html.match(/"downloadAddr":"([^"]+)"/)?.[1]);
24
+ const formats = sortFormats([
25
+ download && { tag: 'NOWM', quality: '720p', type: 'mp4', isNoWatermark: true, url: download },
26
+ play && { tag: 'WM', quality: '720p', type: 'mp4', isNoWatermark: false, url: play }
27
+ ]);
28
+ if (!formats.length) { if (/user-posts|login/i.test(html)) throw new PrivateError('PRIVATE_OR_NOT_FOUND'); throw new NotFoundError('PRIVATE_OR_NOT_FOUND'); }
29
+ return { creator: CREATOR, platform: 'tiktok', id, username: item?.author?.uniqueId ? `@${item.author.uniqueId}` : undefined, author: item?.author?.nickname, title: item?.desc, originalUrl: resolved, formats };
30
+ }
31
+
32
+ /** Save a TikTok video to disk. */
33
+ export async function tiktokSave(url, dest = './tt.mp4', quality = 'best') { return saveResult(await tiktokDl(url), dest, quality, { Referer: 'https://www.tiktok.com/' }); }
@@ -0,0 +1,27 @@
1
+ import { fetchWithRetry } from '../utils/fetcher.js';
2
+ import { saveResult, sortFormats } from '../utils/media.js';
3
+ import { unescapeJson } from '../utils/parser.js';
4
+ import { NotFoundError, UnsupportedError } from '../errors.js';
5
+ import { CREATOR } from '../constants.js';
6
+
7
+ const idOf = url => new URL(url).pathname.match(/status\/(\d+)/)?.[1];
8
+
9
+ /** Download metadata and direct MP4 variants for an X/Twitter post. */
10
+ export async function twitterDl(url) {
11
+ const id = idOf(url);
12
+ if (!id) throw new UnsupportedError('Invalid Twitter URL.');
13
+ const response = await fetchWithRetry(`https://cdn.syndication.twimg.com/tweet-result?id=${id}&lang=en&features=tfw_timeline_list%3D%7B%7D`, { headers: { 'User-Agent': 'Mozilla/5.0' } });
14
+ const json = response.ok ? await response.json() : {};
15
+ const media = json.mediaDetails?.flatMap(item => item.video_info?.variants || []) || [];
16
+ let formats = sortFormats(media.filter(v => v.content_type === 'video/mp4' && v.url).map(v => ({ tag: `${Math.round((v.bitrate || 0) / 1000) || 0}kbps`, quality: `${Math.round((v.bitrate || 0) / 1000) || 0}kbps`, type: 'mp4', url: v.url, bitrate: v.bitrate })));
17
+ if (!formats.length) {
18
+ const html = await (await fetchWithRetry(`https://x.com/i/status/${id}`, { headers: { 'User-Agent': 'Mozilla/5.0' } })).text();
19
+ const urls = [...html.matchAll(/https:[^" ]*video\.twimg\.com[^" ]+\.mp4[^" ]*/g)].map(m => unescapeJson(m[0]));
20
+ formats = urls.map((item, index) => ({ tag: index ? '480p' : '720p', quality: index ? '480p' : '720p', type: 'mp4', url: item }));
21
+ }
22
+ if (!formats.length) throw new NotFoundError();
23
+ return { creator: CREATOR, platform: 'twitter', id, author: json.user?.screen_name ? `@${json.user.screen_name}` : undefined, title: json.text, originalUrl: url, formats };
24
+ }
25
+
26
+ /** Save a Twitter/X video to disk. */
27
+ export async function twitterSave(url, dest = './video.mp4', quality = 'best') { return saveResult(await twitterDl(url), dest, quality); }
@@ -0,0 +1,62 @@
1
+ import { fetchWithRetry, getVisitorData } from '../utils/fetcher.js';
2
+ import { saveResult, sortFormats, qualityOf } from '../utils/media.js';
3
+ import { NotFoundError, UnsupportedError } from '../errors.js';
4
+ import { CREATOR } from '../constants.js';
5
+
6
+ const KEY = 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w';
7
+ const ENDPOINT = `https://www.youtube.com/youtubei/v1/player?key=${KEY}`;
8
+ const CLIENTS = [
9
+ { name: 'ANDROID', version: '20.10.38', ua: 'com.google.android.youtube/20.10.38 (Linux; U; Android 14) gzip' },
10
+ { name: 'IOS', version: '19.45.4', deviceModel: 'iPhone15,2', ua: 'com.google.ios.youtube/19.45.4 (iPhone15,2; U; CPU iOS 17_5_1 like Mac OS X)' },
11
+ { name: 'ANDROID_MUSIC', version: '6.51', ua: 'com.google.android.apps.youtube.music/6.51 (Linux; U; Android 14) gzip' }
12
+ ];
13
+
14
+ /** Extract an 11-character YouTube video id. */
15
+ export function videoId(url) { return new URL(url).toString().match(/(?:youtu\.be\/|[?&]v=|shorts\/)([A-Za-z0-9_-]{11})/)?.[1]; }
16
+
17
+ /** Send one direct Innertube player request for a specific client profile. */
18
+ export async function innertubeRequest(id, client, visitorData) {
19
+ const context = { client: { clientName: client.name, clientVersion: client.version, ...(client.deviceModel ? { deviceModel: client.deviceModel } : {}), hl: 'en', gl: 'US' }, ...(visitorData ? { visitorData } : {}) };
20
+ const response = await fetchWithRetry(ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': client.ua, ...(visitorData ? { 'X-Goog-Visitor-Id': visitorData } : {}), Origin: 'https://www.youtube.com', Referer: 'https://www.youtube.com/' }, body: JSON.stringify({ context, videoId: id, contentCheckOk: true, racyCheckOk: true }) });
21
+ if (!response.ok) throw new Error(`YOUTUBE_HTTP_${response.status}`);
22
+ return response.json();
23
+ }
24
+
25
+ /** Try each supported YouTube client until streaming data is returned. */
26
+ export async function player(id) {
27
+ const visitorData = await getVisitorData();
28
+ let data = null;
29
+ let lastError;
30
+ for (const client of CLIENTS) {
31
+ try {
32
+ data = await innertubeRequest(id, client, visitorData);
33
+ if (data?.streamingData?.formats || data?.streamingData?.adaptiveFormats) return data;
34
+ if (data?.playabilityStatus?.status === 'ERROR' && JSON.stringify(data).includes('403')) continue;
35
+ lastError = new Error(data?.playabilityStatus?.reason || 'UNAVAILABLE');
36
+ } catch (error) {
37
+ lastError = error;
38
+ }
39
+ }
40
+ if (!data?.streamingData) throw lastError || new Error(data?.playabilityStatus?.reason || 'UNAVAILABLE');
41
+ return data;
42
+ }
43
+
44
+ /** Download metadata and direct formats for a YouTube video. */
45
+ export async function youtubeDl(url) {
46
+ const id = videoId(url);
47
+ if (!id) throw new UnsupportedError('Invalid YouTube URL.');
48
+ const data = await player(id);
49
+ if (!data.streamingData) throw new NotFoundError('PRIVATE_OR_AGE_RESTRICTED');
50
+ const all = [...(data.streamingData.formats || []), ...(data.streamingData.adaptiveFormats || [])];
51
+ const formats = sortFormats(all.filter(item => item.url && /^video\/mp4/.test(item.mimeType || '')).map(item => ({ tag: qualityOf(item, 'video'), quality: qualityOf(item, 'video'), type: 'mp4', hasAudio: !item.mimeType?.includes('video-only'), url: item.url, bitrate: item.bitrate, height: item.height })));
52
+ if (!formats.length) throw new NotFoundError('NO_PLAYABLE_MP4');
53
+ const best = formats[0];
54
+ const audio = all.find(item => item.url && String(item.itag) === '140' && item.mimeType?.startsWith('audio/')) || all.find(item => item.url && item.mimeType?.startsWith('audio/'));
55
+ const mux = best.height >= 1080 && best.hasAudio === false && audio;
56
+ return { creator: CREATOR, platform: 'youtube', id, title: data.videoDetails?.title, thumbnail: `https://i.ytimg.com/vi/${id}/maxresdefault.jpg`, originalUrl: url, formats, ...(mux ? { needsMux: true, videoUrl: best.url, audioUrl: audio.url } : {}) };
57
+ }
58
+
59
+ /** Save a YouTube video to disk. */
60
+ export async function youtubeSave(url, dest = './yt.mp4', quality = 'best') { return saveResult(await youtubeDl(url), dest, quality); }
61
+
62
+ export { CLIENTS };
@@ -0,0 +1,22 @@
1
+ import { player, videoId } from './youtube.js';
2
+ import { saveResult, sortFormats } from '../utils/media.js';
3
+ import { NotFoundError, UnsupportedError } from '../errors.js';
4
+ import { CREATOR } from '../constants.js';
5
+
6
+ /** Download audio-only metadata from YouTube (m4a/opus, not transcoded MP3). */
7
+ export async function youtubeMp3Dl(url) {
8
+ const id = videoId(url);
9
+ if (!id) throw new UnsupportedError('Invalid YouTube URL.');
10
+ const data = await player(id);
11
+ const source = data.streamingData?.adaptiveFormats || [];
12
+ const formats = sortFormats(source.filter(item => item.url && item.mimeType?.startsWith('audio/')).map(item => {
13
+ const type = item.mimeType.includes('webm') ? 'opus' : 'm4a';
14
+ const kbps = Math.round((item.bitrate || 0) / 1000);
15
+ return { tag: type === 'opus' ? `OPUS_${kbps}` : `MP3_${kbps}`, quality: `${kbps}kbps`, type, hasAudio: true, isAudioOnly: true, url: item.url, bitrate: item.bitrate };
16
+ }));
17
+ if (!formats.length) throw new NotFoundError('PRIVATE_OR_AGE_RESTRICTED');
18
+ return { creator: CREATOR, platform: 'youtube', id, title: data.videoDetails?.title, thumbnail: `https://i.ytimg.com/vi/${id}/maxresdefault.jpg`, isAudioOnly: true, originalUrl: url, formats };
19
+ }
20
+
21
+ /** Save the best YouTube audio stream to disk. */
22
+ export async function youtubeMp3Save(url, dest = './audio.m4a', quality = 'best') { return saveResult(await youtubeMp3Dl(url), dest, quality); }
package/src/stream.js ADDED
@@ -0,0 +1,13 @@
1
+ import { randomUserAgent } from './utils/fetcher.js';
2
+ import { universalDl } from './index.js';
3
+
4
+ /** Resolve a direct CDN stream and its normalized metadata. */
5
+ export async function universalStream(url, opts = {}) {
6
+ const info = await universalDl(url, opts);
7
+ const best = opts.quality ? info.formats.find(item => item.tag === opts.quality || item.quality === opts.quality) || info.formats[0] : info.formats[0];
8
+ if (!best) throw new Error('NO_FORMATS');
9
+ const response = await fetch(best.url, { headers: { 'User-Agent': randomUserAgent(), Referer: info.originalUrl } });
10
+ if (!response.ok || !response.body) throw new Error('CDN_FETCH_FAILED');
11
+ const filename = `${info.id}.${best.type}`.replace(/[^a-z0-9._-]/gi, '_');
12
+ return { stream: response.body, filename, info, contentType: best.type === 'mp4' ? 'video/mp4' : best.type === 'jpg' ? 'image/jpeg' : 'audio/mp4', contentLength: response.headers.get('content-length') };
13
+ }
@@ -0,0 +1,65 @@
1
+ import { BotDetectedError } from '../errors.js';
2
+
3
+ const USER_AGENTS = [
4
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36',
5
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 Version/16.6 Mobile/15E148 Safari/604.1',
6
+ 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 Chrome/120.0 Mobile Safari/537.36'
7
+ ];
8
+
9
+ /** Pick a browser-like user agent. */
10
+ export function randomUserAgent() {
11
+ return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
12
+ }
13
+
14
+ /** Fetch a URL with redirect following and one retry for transient failures. */
15
+ export async function fetchWithRetry(input, options = {}, retries = 1) {
16
+ let lastError;
17
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
18
+ try {
19
+ const headers = { 'User-Agent': randomUserAgent(), ...(options.headers || {}) };
20
+ const response = await fetch(input, { ...options, headers, redirect: options.redirect || 'follow' });
21
+ if (response.status === 403) {
22
+ const body = await response.clone().text();
23
+ if (/automated|unusual traffic|sorry/i.test(body)) {
24
+ throw new BotDetectedError('The provider rejected this request as automated traffic.');
25
+ }
26
+ }
27
+ if (!response.ok && attempt < retries && (response.status >= 500 || response.status === 429)) continue;
28
+ return response;
29
+ } catch (error) {
30
+ lastError = error;
31
+ if (attempt < retries) await new Promise(resolve => setTimeout(resolve, 250));
32
+ }
33
+ }
34
+ throw lastError;
35
+ }
36
+
37
+ /** Read YouTube's visitor-data token for Innertube requests. */
38
+ export async function getVisitorData() {
39
+ try {
40
+ const response = await fetch('https://www.youtube.com/', { headers: { 'User-Agent': 'Mozilla/5.0' } });
41
+ const html = await response.text();
42
+ return html.match(/"VISITOR_DATA":"([^"]+)"/)?.[1]
43
+ || html.match(/ytcfg\.set.*?"VISITOR_DATA":"([^"]+)"/)?.[1]
44
+ || null;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ /** Fetch and return text, rejecting non-success responses. */
51
+ export async function fetchText(input, options = {}) {
52
+ const response = await fetchWithRetry(input, options);
53
+ if (!response.ok) throw new Error(`HTTP_${response.status}`);
54
+ return response.text();
55
+ }
56
+
57
+ /** Fetch a direct media URL and write it to disk. */
58
+ export async function downloadTo(url, dest, options = {}) {
59
+ const response = await fetchWithRetry(url, options);
60
+ if (!response.ok) throw new Error(`DOWNLOAD_HTTP_${response.status}`);
61
+ const { writeFile } = await import('node:fs/promises');
62
+ const data = Buffer.from(await response.arrayBuffer());
63
+ await writeFile(dest, data);
64
+ return dest;
65
+ }
@@ -0,0 +1,29 @@
1
+ import { downloadTo } from './fetcher.js';
2
+ import { saveMuxed } from './mux.js';
3
+
4
+ /** Sort formats by resolution, bitrate, then audio preference. */
5
+ export function sortFormats(formats) {
6
+ return formats.filter(format => format?.url).sort((a, b) => {
7
+ const score = item => (parseInt(item.quality, 10) || 0) * 100000 + (item.bitrate || 0) + (item.hasAudio ? 100 : 0);
8
+ return score(b) - score(a);
9
+ });
10
+ }
11
+
12
+ /** Select a format by requested quality and save it. */
13
+ export async function saveResult(result, dest, quality = 'best', headers = {}, noWatermark = false) {
14
+ if (result.needsMux && quality === 'best') {
15
+ const muxed = await saveMuxed(result, dest, headers);
16
+ if (muxed) return muxed;
17
+ console.warn('Install ffmpeg to auto-mux 1080p; saving the best direct video stream instead.');
18
+ }
19
+ let formats = result.formats || [];
20
+ if (noWatermark) formats = formats.filter(item => item.isNoWatermark !== false);
21
+ const wanted = quality === 'best' ? formats[0] : formats.find(item => item.tag === quality || item.quality === quality) || formats[0];
22
+ if (!wanted) throw new Error('NO_FORMATS');
23
+ return downloadTo(wanted.url, dest, { headers });
24
+ }
25
+
26
+ /** Parse a quality label from a media object. */
27
+ export function qualityOf(item, fallback = 'unknown') {
28
+ return item.qualityLabel || (item.height ? `${item.height}p` : fallback);
29
+ }
@@ -0,0 +1,31 @@
1
+ import { spawnSync, execFile } from 'node:child_process';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { unlink } from 'node:fs/promises';
5
+ import { downloadTo } from './fetcher.js';
6
+
7
+ /** Return whether ffmpeg is available on PATH. */
8
+ export function hasFfmpeg() {
9
+ return spawnSync('ffmpeg', ['-version'], { stdio: 'ignore' }).status === 0;
10
+ }
11
+
12
+ function runFfmpeg(args) {
13
+ return new Promise((resolve, reject) => execFile('ffmpeg', args, { windowsHide: true }, (error, _stdout, stderr) => error ? reject(new Error(stderr || error.message)) : resolve()));
14
+ }
15
+
16
+ /** Download separate audio/video streams and mux them into one MP4 when possible. */
17
+ export async function saveMuxed(result, dest, headers = {}) {
18
+ if (!result.needsMux || !result.videoUrl || !result.audioUrl) return null;
19
+ if (!hasFfmpeg()) return false;
20
+ const base = join(tmpdir(), `social-dl-${Date.now()}-${Math.random().toString(36).slice(2)}`);
21
+ const videoPath = `${base}.video.mp4`;
22
+ const audioPath = `${base}.audio.m4a`;
23
+ try {
24
+ await downloadTo(result.videoUrl, videoPath, { headers });
25
+ await downloadTo(result.audioUrl, audioPath, { headers });
26
+ await runFfmpeg(['-y', '-i', videoPath, '-i', audioPath, '-c', 'copy', dest]);
27
+ return dest;
28
+ } finally {
29
+ await Promise.allSettled([unlink(videoPath), unlink(audioPath)]);
30
+ }
31
+ }
@@ -0,0 +1,18 @@
1
+ /** Decode a JSON-escaped string found inside HTML. */
2
+ export function unescapeJson(value) {
3
+ if (value == null) return value;
4
+ try { return JSON.parse(`"${value}"`); } catch { return value.replace(/\\u002F/g, '/').replace(/\\\\/g, '\\').replace(/\\"/g, '"'); }
5
+ }
6
+
7
+ /** Extract a JSON script element by id. */
8
+ export function extractJson(html, id) {
9
+ const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10
+ const match = html.match(new RegExp(`<script[^>]+id=["']${escapedId}["'][^>]*>([\\s\\S]*?)</script>`, 'i'));
11
+ if (!match) return null;
12
+ try { return JSON.parse(match[1]); } catch { return null; }
13
+ }
14
+
15
+ /** Return a JSON object nested at a dotted path. */
16
+ export function getPath(value, path) {
17
+ return path.split('.').reduce((current, key) => current?.[key], value);
18
+ }
@@ -0,0 +1,12 @@
1
+ import { fetchWithRetry } from './fetcher.js';
2
+
3
+ /** Resolve a short social URL and return its final URL. */
4
+ export async function resolveShortUrl(url) {
5
+ const input = new URL(url);
6
+ const host = input.hostname.toLowerCase();
7
+ const shortHosts = ['vm.tiktok.com', 'pin.it', 't.co', 'fb.watch'];
8
+ const isCapcutShort = host.endsWith('capcut.com') && input.pathname.startsWith('/t/');
9
+ if (!shortHosts.includes(host) && !isCapcutShort) return url;
10
+ const response = await fetchWithRetry(url, { method: 'GET', redirect: 'follow' });
11
+ return response.url || url;
12
+ }
File without changes