@lunarity/nebula-fetch-cli 0.0.3 → 0.0.4
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/bun.lockb +0 -0
- package/commands/youtube.ts +126 -21
- package/dist/index.js +3409 -2758
- package/index.ts +1 -1
- package/package.json +2 -2
package/bun.lockb
CHANGED
|
Binary file
|
package/commands/youtube.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
|
|
4
|
-
import { YtdlCore, toPipeableStream } from "@ybd-project/ytdl-core";
|
|
3
|
+
import { YtdlCore } from "@ybd-project/ytdl-core";
|
|
5
4
|
import chalk from "chalk";
|
|
5
|
+
import { Readable } from "stream";
|
|
6
6
|
|
|
7
7
|
interface DownloadOptions {
|
|
8
8
|
url: string;
|
|
@@ -16,36 +16,141 @@ export async function downloadYoutube(options: DownloadOptions): Promise<void> {
|
|
|
16
16
|
|
|
17
17
|
try {
|
|
18
18
|
console.log(chalk.cyan("🔍 Fetching video information..."));
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
await ytdl.getBasicInfo(url).then((info) => {
|
|
23
|
-
console.log(chalk.cyan(`🎬 Video title: ${info.videoDetails.title}`));
|
|
24
|
-
console.log(
|
|
25
|
-
chalk.cyan(
|
|
26
|
-
`🎬 Video author: ${info.videoDetails.author?.name || "Unknown"}`
|
|
27
|
-
)
|
|
28
|
-
);
|
|
29
|
-
if (verbose) {
|
|
30
|
-
console.log(JSON.stringify(info.videoDetails, null, 2));
|
|
31
|
-
}
|
|
32
|
-
videoTitle = info.videoDetails.title.replace(/[^\w\s]/gi, "_");
|
|
19
|
+
// Use all clients to maximize chances of finding a working one
|
|
20
|
+
const ytdl = new YtdlCore({
|
|
21
|
+
clients: ["web", "android", "ios", "tv", "mweb"],
|
|
33
22
|
});
|
|
23
|
+
|
|
24
|
+
const info = await ytdl.getFullInfo(url);
|
|
25
|
+
|
|
26
|
+
console.log(chalk.cyan(`🎬 Video title: ${info.videoDetails.title}`));
|
|
27
|
+
console.log(
|
|
28
|
+
chalk.cyan(
|
|
29
|
+
`🎬 Video author: ${info.videoDetails.author?.name || "Unknown"}`
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (verbose) {
|
|
34
|
+
console.log(JSON.stringify(info.videoDetails, null, 2));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const videoTitle = info.videoDetails.title.replace(/[^\w\s]/gi, "_");
|
|
38
|
+
|
|
39
|
+
// Filter to only valid (deciphered) formats
|
|
40
|
+
let validFormats = info.formats.filter(f => f.url);
|
|
41
|
+
|
|
42
|
+
if (validFormats.length === 0) {
|
|
43
|
+
throw new Error("No downloadable formats found (signatures could not be deciphered).");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (verbose) {
|
|
47
|
+
console.log(`Found ${info.formats.length} total formats, ${validFormats.length} valid (deciphered).`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Sort based on preference to try best ones first
|
|
51
|
+
validFormats.sort((a, b) => {
|
|
52
|
+
if (audioOnly) {
|
|
53
|
+
const aAudio = a.hasAudio && !a.hasVideo;
|
|
54
|
+
const bAudio = b.hasAudio && !b.hasVideo;
|
|
55
|
+
if (aAudio && !bAudio) return -1;
|
|
56
|
+
if (!aAudio && bAudio) return 1;
|
|
57
|
+
return (b.audioBitrate || 0) - (a.audioBitrate || 0);
|
|
58
|
+
} else {
|
|
59
|
+
const aMuxed = a.hasAudio && a.hasVideo;
|
|
60
|
+
const bMuxed = b.hasAudio && b.hasVideo;
|
|
61
|
+
if (aMuxed && !bMuxed) return -1;
|
|
62
|
+
if (!aMuxed && bMuxed) return 1;
|
|
63
|
+
return (b.bitrate || 0) - (a.bitrate || 0);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let workingFormat = null;
|
|
68
|
+
let workingUrl = "";
|
|
69
|
+
let finalUserAgent = "";
|
|
70
|
+
|
|
71
|
+
console.log(chalk.yellow("🔄 Testing formats to find a valid one..."));
|
|
34
72
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
73
|
+
for (const format of validFormats) {
|
|
74
|
+
let downloadUrl = format.url;
|
|
75
|
+
if (info.poToken && !downloadUrl.includes('pot=')) {
|
|
76
|
+
downloadUrl += `&pot=${encodeURIComponent(info.poToken)}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Determine User-Agent
|
|
80
|
+
let userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36';
|
|
81
|
+
if (format.sourceClientName === 'ios') {
|
|
82
|
+
userAgent = 'com.google.ios.youtube/19.29.1 (iPhone16,2; U; CPU iOS 17_5_1 like Mac OS X; en_US)';
|
|
83
|
+
} else if (format.sourceClientName === 'android') {
|
|
84
|
+
userAgent = 'com.google.android.youtube/19.29.35 (Linux; U; Android 14; en_US) gzip';
|
|
85
|
+
} else if (format.sourceClientName === 'tv') {
|
|
86
|
+
userAgent = 'Mozilla/5.0 (ChromiumNet/53.0.2785.124) Cobalt/53.0.2785.124-devel-000000-000 Star/1.0';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const response = await fetch(downloadUrl, {
|
|
91
|
+
method: 'HEAD',
|
|
92
|
+
headers: {
|
|
93
|
+
'User-Agent': userAgent,
|
|
94
|
+
'Referer': 'https://www.youtube.com/',
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (response.ok) {
|
|
99
|
+
workingFormat = format;
|
|
100
|
+
workingUrl = downloadUrl;
|
|
101
|
+
finalUserAgent = userAgent;
|
|
102
|
+
if (verbose) console.log(chalk.green(` ✅ Found working format: ${format.itag} (${format.sourceClientName})`));
|
|
103
|
+
break;
|
|
104
|
+
} else {
|
|
105
|
+
if (verbose) console.log(chalk.red(` ❌ Format ${format.itag} (${format.sourceClientName}) failed: ${response.status}`));
|
|
106
|
+
}
|
|
107
|
+
} catch (e) {
|
|
108
|
+
if (verbose) console.log(chalk.red(` ❌ Format ${format.itag} error: ${e}`));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!workingFormat || !workingUrl) {
|
|
113
|
+
throw new Error("Could not find any valid downloadable format (all returned 403/Error).");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(chalk.blue(`⬇️ Downloading format: ${workingFormat.container} | ${workingFormat.qualityLabel || 'Audio'} | ${workingFormat.audioBitrate || '?'}kbps | Client: ${workingFormat.sourceClientName}`));
|
|
117
|
+
|
|
118
|
+
// Direct download bypass
|
|
119
|
+
const response = await fetch(workingUrl, {
|
|
120
|
+
headers: {
|
|
121
|
+
'User-Agent': finalUserAgent,
|
|
122
|
+
'Referer': 'https://www.youtube.com/',
|
|
123
|
+
}
|
|
38
124
|
});
|
|
39
125
|
|
|
126
|
+
if (!response.ok || !response.body) {
|
|
127
|
+
throw new Error(`Failed to download video stream: ${response.status} ${response.statusText}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
40
130
|
const outputFilePath =
|
|
41
131
|
outputPath ||
|
|
42
132
|
path.join(process.cwd(), `${videoTitle}.${audioOnly ? "mp3" : "mp4"}`);
|
|
43
|
-
|
|
133
|
+
|
|
134
|
+
const fileStream = fs.createWriteStream(outputFilePath);
|
|
135
|
+
|
|
136
|
+
// Convert Web ReadableStream to Node Readable Stream
|
|
137
|
+
// @ts-ignore
|
|
138
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
139
|
+
|
|
140
|
+
await new Promise<void>((resolve, reject) => {
|
|
141
|
+
nodeStream.pipe(fileStream);
|
|
142
|
+
nodeStream.on('error', reject);
|
|
143
|
+
fileStream.on('finish', resolve);
|
|
144
|
+
fileStream.on('error', reject);
|
|
145
|
+
});
|
|
44
146
|
|
|
45
147
|
console.log(chalk.green.bold(`✅ Download finished: ${outputFilePath}`));
|
|
46
148
|
} catch (error) {
|
|
47
149
|
if (error instanceof Error) {
|
|
48
150
|
console.error(chalk.red.bold("❌ Error:", error.message));
|
|
151
|
+
if (verbose && error.stack) {
|
|
152
|
+
console.error(error.stack);
|
|
153
|
+
}
|
|
49
154
|
}
|
|
50
155
|
}
|
|
51
|
-
}
|
|
156
|
+
}
|