@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 CHANGED
Binary file
@@ -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
- const ytdl = new YtdlCore({});
20
- let videoTitle = "";
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 stream = await ytdl.download(url, {
36
- filter: audioOnly ? "audioonly" : "videoandaudio",
37
- quality: "highest",
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
- toPipeableStream(stream).pipe(fs.createWriteStream(outputFilePath));
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
+ }