@lunarity/nebula-fetch-cli 0.0.2 → 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/README.md CHANGED
@@ -1 +1,56 @@
1
- # nebula-fetch-cli
1
+ # Nebula Fetch CLI
2
+
3
+ A command-line interface tool for downloading media from different platforms.
4
+
5
+ ## 🚀 Installation
6
+
7
+ ```bash
8
+ npm install -g @lunarity/nebula-fetch-cli
9
+ ```
10
+
11
+ ## 📖 Usage
12
+
13
+ ### 📺 YouTube Downloads
14
+
15
+ Download videos or audio from YouTube using the following commands:
16
+
17
+ ```bash
18
+ # Download video (default)
19
+ nebula-fetch youtube <url>
20
+ # or using alias
21
+ nebula-fetch yt <url>
22
+
23
+ # Download audio only
24
+ nebula-fetch youtube <url> --audio
25
+ # or
26
+ nebula-fetch yt <url> -a
27
+
28
+ # Specify output path
29
+ nebula-fetch youtube <url> --output path/to/file
30
+ # or
31
+ nebula-fetch yt <url> -o path/to/file
32
+
33
+ # Show verbose output
34
+ nebula-fetch youtube <url> --verbose
35
+ # or
36
+ nebula-fetch yt <url> -v
37
+ ```
38
+
39
+ ### ⚙️ Options
40
+
41
+ - `-o, --output <path>` - Specify the output path for the downloaded file
42
+ - `-a, --audio` - Download audio only (saves as .mp3)
43
+ - `-v, --verbose` - Show detailed information during download
44
+ - `--version` - Show version number
45
+ - `--help` - Show help information
46
+
47
+ ## ✨ Features
48
+
49
+ - YouTube video downloads
50
+ - Audio-only extraction option
51
+ - Custom output path
52
+ - Verbose mode for detailed information
53
+
54
+ ## 📄 License
55
+
56
+ MIT
package/bun.lockb CHANGED
Binary file
@@ -1,39 +1,156 @@
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
+
7
+ interface DownloadOptions {
8
+ url: string;
9
+ audioOnly?: boolean;
10
+ verbose?: boolean;
11
+ outputPath?: string;
12
+ }
13
+
14
+ export async function downloadYoutube(options: DownloadOptions): Promise<void> {
15
+ const { url, audioOnly = false, verbose = false, outputPath } = options;
6
16
 
7
- export async function downloadYoutubeAudio(
8
- url: string,
9
- outputPath?: string
10
- ): Promise<void> {
11
17
  try {
12
- if (!url) {
13
- throw new Error("Please provide a YouTube URL");
18
+ console.log(chalk.cyan("🔍 Fetching video information..."));
19
+ // Use all clients to maximize chances of finding a working one
20
+ const ytdl = new YtdlCore({
21
+ clients: ["web", "android", "ios", "tv", "mweb"],
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));
14
35
  }
36
+
37
+ const videoTitle = info.videoDetails.title.replace(/[^\w\s]/gi, "_");
15
38
 
16
- console.log(chalk.cyan("🔍 Fetching video information..."));
17
- const ytdl = new YtdlCore({});
18
- let videoTitle = "";
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
+ }
19
45
 
20
- await ytdl.getBasicInfo(url).then((info) => {
21
- videoTitle = info.videoDetails.title.replace(/[^\w\s]/gi, "_");
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
+ }
22
65
  });
23
66
 
24
- const stream = await ytdl.download(url, {
25
- filter: "audioonly",
67
+ let workingFormat = null;
68
+ let workingUrl = "";
69
+ let finalUserAgent = "";
70
+
71
+ console.log(chalk.yellow("🔄 Testing formats to find a valid one..."));
72
+
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
+ }
26
124
  });
27
125
 
126
+ if (!response.ok || !response.body) {
127
+ throw new Error(`Failed to download video stream: ${response.status} ${response.statusText}`);
128
+ }
129
+
28
130
  const outputFilePath =
29
- outputPath || path.join(process.cwd(), `${videoTitle}.mp3`);
30
- toPipeableStream(stream).pipe(fs.createWriteStream(outputFilePath));
131
+ outputPath ||
132
+ path.join(process.cwd(), `${videoTitle}.${audioOnly ? "mp3" : "mp4"}`);
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
+ });
31
146
 
32
- console.log(chalk.green(`✅ Download finished: ${outputFilePath}`));
147
+ console.log(chalk.green.bold(`✅ Download finished: ${outputFilePath}`));
33
148
  } catch (error) {
34
149
  if (error instanceof Error) {
35
- console.error(chalk.red("Error:", error.message));
150
+ console.error(chalk.red.bold("Error:", error.message));
151
+ if (verbose && error.stack) {
152
+ console.error(error.stack);
153
+ }
36
154
  }
37
- throw error;
38
155
  }
39
- }
156
+ }