@lunarity/nebula-fetch-cli 0.0.4 → 0.1.1

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/package.json CHANGED
@@ -1,39 +1,56 @@
1
1
  {
2
2
  "name": "@lunarity/nebula-fetch-cli",
3
- "version": "0.0.4",
4
- "module": "index.ts",
3
+ "version": "0.1.1",
5
4
  "description": "CLI tool for downloading media from different platforms",
6
5
  "type": "module",
7
- "main": "dist/index.js",
6
+ "main": "dist/index.cjs",
8
7
  "devDependencies": {
9
- "@types/bun": "latest"
10
- },
11
- "peerDependencies": {
8
+ "@types/node": "^22.0.0",
9
+ "@types/turndown": "^5.0.6",
10
+ "tsup": "^8.0.0",
11
+ "tsx": "^4.0.0",
12
12
  "typescript": "^5.0.0"
13
13
  },
14
14
  "dependencies": {
15
- "@ybd-project/ytdl-core": "6.0.8",
16
15
  "chalk": "^5.3.0",
17
- "commander": "^12.1.0"
16
+ "cheerio": "^1.2.0",
17
+ "commander": "^12.1.0",
18
+ "debug": "^4.4.3",
19
+ "turndown": "^7.2.2",
20
+ "youtube-dl-exec": "^3.0.30"
18
21
  },
22
+ "files": [
23
+ "dist"
24
+ ],
19
25
  "bin": {
20
- "nebula-fetch": "./dist/index.js"
26
+ "nebula-fetch": "./dist/index.cjs"
21
27
  },
22
28
  "scripts": {
23
- "build": "bun build ./index.ts --outdir ./dist --target node && chmod +x ./dist/index.js",
24
- "dev": "bun index.ts",
25
- "prepublish": "bun run build"
29
+ "build": "tsup && chmod +x ./dist/index.cjs",
30
+ "dev": "tsx index.ts",
31
+ "prepublishOnly": "pnpm run build"
32
+ },
33
+ "engines": {
34
+ "node": ">=18"
26
35
  },
27
36
  "keywords": [
28
37
  "cli",
29
38
  "media",
30
39
  "download",
31
- "youtube"
40
+ "youtube",
41
+ "scrape",
42
+ "yt-dlp"
32
43
  ],
33
44
  "author": "Krzysztof Oszczapiński",
34
45
  "license": "MIT",
35
46
  "repository": {
36
47
  "type": "git",
37
48
  "url": "git+https://github.com/LUNARITYai/nebula-fetch-cli.git"
49
+ },
50
+ "pnpm": {
51
+ "onlyBuiltDependencies": [
52
+ "esbuild",
53
+ "youtube-dl-exec"
54
+ ]
38
55
  }
39
- }
56
+ }
package/bun.lockb DELETED
Binary file
@@ -1,156 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { YtdlCore } from "@ybd-project/ytdl-core";
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;
16
-
17
- try {
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));
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..."));
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
- }
124
- });
125
-
126
- if (!response.ok || !response.body) {
127
- throw new Error(`Failed to download video stream: ${response.status} ${response.statusText}`);
128
- }
129
-
130
- const 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
- });
146
-
147
- console.log(chalk.green.bold(`✅ Download finished: ${outputFilePath}`));
148
- } catch (error) {
149
- if (error instanceof Error) {
150
- console.error(chalk.red.bold("❌ Error:", error.message));
151
- if (verbose && error.stack) {
152
- console.error(error.stack);
153
- }
154
- }
155
- }
156
- }