@lunarity/nebula-fetch-cli 0.0.2 → 0.0.3
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 +56 -1
- package/commands/youtube.ts +25 -13
- package/dist/index.js +92 -45
- package/index.ts +33 -5
- package/package.json +10 -3
- package/tsconfig.json +6 -1
- package/utils/validators.ts +45 -0
package/README.md
CHANGED
|
@@ -1 +1,56 @@
|
|
|
1
|
-
#
|
|
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/commands/youtube.ts
CHANGED
|
@@ -4,36 +4,48 @@ import path from "path";
|
|
|
4
4
|
import { YtdlCore, toPipeableStream } from "@ybd-project/ytdl-core";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
url: string
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
throw new Error("Please provide a YouTube URL");
|
|
14
|
-
}
|
|
7
|
+
interface DownloadOptions {
|
|
8
|
+
url: string;
|
|
9
|
+
audioOnly?: boolean;
|
|
10
|
+
verbose?: boolean;
|
|
11
|
+
outputPath?: string;
|
|
12
|
+
}
|
|
15
13
|
|
|
14
|
+
export async function downloadYoutube(options: DownloadOptions): Promise<void> {
|
|
15
|
+
const { url, audioOnly = false, verbose = false, outputPath } = options;
|
|
16
|
+
|
|
17
|
+
try {
|
|
16
18
|
console.log(chalk.cyan("🔍 Fetching video information..."));
|
|
17
19
|
const ytdl = new YtdlCore({});
|
|
18
20
|
let videoTitle = "";
|
|
19
21
|
|
|
20
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
|
+
}
|
|
21
32
|
videoTitle = info.videoDetails.title.replace(/[^\w\s]/gi, "_");
|
|
22
33
|
});
|
|
23
34
|
|
|
24
35
|
const stream = await ytdl.download(url, {
|
|
25
|
-
filter: "audioonly",
|
|
36
|
+
filter: audioOnly ? "audioonly" : "videoandaudio",
|
|
37
|
+
quality: "highest",
|
|
26
38
|
});
|
|
27
39
|
|
|
28
40
|
const outputFilePath =
|
|
29
|
-
outputPath ||
|
|
41
|
+
outputPath ||
|
|
42
|
+
path.join(process.cwd(), `${videoTitle}.${audioOnly ? "mp3" : "mp4"}`);
|
|
30
43
|
toPipeableStream(stream).pipe(fs.createWriteStream(outputFilePath));
|
|
31
44
|
|
|
32
|
-
console.log(chalk.green(`✅ Download finished: ${outputFilePath}`));
|
|
45
|
+
console.log(chalk.green.bold(`✅ Download finished: ${outputFilePath}`));
|
|
33
46
|
} catch (error) {
|
|
34
47
|
if (error instanceof Error) {
|
|
35
|
-
console.error(chalk.red("Error:", error.message));
|
|
48
|
+
console.error(chalk.red.bold("❌ Error:", error.message));
|
|
36
49
|
}
|
|
37
|
-
throw error;
|
|
38
50
|
}
|
|
39
51
|
}
|
package/dist/index.js
CHANGED
|
@@ -2928,22 +2928,22 @@ var require_diagnostics = __commonJS((exports, module) => {
|
|
|
2928
2928
|
const debuglog = fetchDebuglog.enabled ? fetchDebuglog : undiciDebugLog;
|
|
2929
2929
|
diagnosticsChannel.channel("undici:client:beforeConnect").subscribe((evt) => {
|
|
2930
2930
|
const {
|
|
2931
|
-
connectParams: { version, protocol, port, host }
|
|
2931
|
+
connectParams: { version: version2, protocol, port, host }
|
|
2932
2932
|
} = evt;
|
|
2933
|
-
debuglog("connecting to %s using %s%s", `${host}${port ? `:${port}` : ""}`, protocol,
|
|
2933
|
+
debuglog("connecting to %s using %s%s", `${host}${port ? `:${port}` : ""}`, protocol, version2);
|
|
2934
2934
|
});
|
|
2935
2935
|
diagnosticsChannel.channel("undici:client:connected").subscribe((evt) => {
|
|
2936
2936
|
const {
|
|
2937
|
-
connectParams: { version, protocol, port, host }
|
|
2937
|
+
connectParams: { version: version2, protocol, port, host }
|
|
2938
2938
|
} = evt;
|
|
2939
|
-
debuglog("connected to %s using %s%s", `${host}${port ? `:${port}` : ""}`, protocol,
|
|
2939
|
+
debuglog("connected to %s using %s%s", `${host}${port ? `:${port}` : ""}`, protocol, version2);
|
|
2940
2940
|
});
|
|
2941
2941
|
diagnosticsChannel.channel("undici:client:connectError").subscribe((evt) => {
|
|
2942
2942
|
const {
|
|
2943
|
-
connectParams: { version, protocol, port, host },
|
|
2943
|
+
connectParams: { version: version2, protocol, port, host },
|
|
2944
2944
|
error
|
|
2945
2945
|
} = evt;
|
|
2946
|
-
debuglog("connection to %s using %s%s errored - %s", `${host}${port ? `:${port}` : ""}`, protocol,
|
|
2946
|
+
debuglog("connection to %s using %s%s errored - %s", `${host}${port ? `:${port}` : ""}`, protocol, version2, error.message);
|
|
2947
2947
|
});
|
|
2948
2948
|
diagnosticsChannel.channel("undici:client:sendHeaders").subscribe((evt) => {
|
|
2949
2949
|
const {
|
|
@@ -2978,22 +2978,22 @@ var require_diagnostics = __commonJS((exports, module) => {
|
|
|
2978
2978
|
const debuglog = undiciDebugLog.enabled ? undiciDebugLog : websocketDebuglog;
|
|
2979
2979
|
diagnosticsChannel.channel("undici:client:beforeConnect").subscribe((evt) => {
|
|
2980
2980
|
const {
|
|
2981
|
-
connectParams: { version, protocol, port, host }
|
|
2981
|
+
connectParams: { version: version2, protocol, port, host }
|
|
2982
2982
|
} = evt;
|
|
2983
|
-
debuglog("connecting to %s%s using %s%s", host, port ? `:${port}` : "", protocol,
|
|
2983
|
+
debuglog("connecting to %s%s using %s%s", host, port ? `:${port}` : "", protocol, version2);
|
|
2984
2984
|
});
|
|
2985
2985
|
diagnosticsChannel.channel("undici:client:connected").subscribe((evt) => {
|
|
2986
2986
|
const {
|
|
2987
|
-
connectParams: { version, protocol, port, host }
|
|
2987
|
+
connectParams: { version: version2, protocol, port, host }
|
|
2988
2988
|
} = evt;
|
|
2989
|
-
debuglog("connected to %s%s using %s%s", host, port ? `:${port}` : "", protocol,
|
|
2989
|
+
debuglog("connected to %s%s using %s%s", host, port ? `:${port}` : "", protocol, version2);
|
|
2990
2990
|
});
|
|
2991
2991
|
diagnosticsChannel.channel("undici:client:connectError").subscribe((evt) => {
|
|
2992
2992
|
const {
|
|
2993
|
-
connectParams: { version, protocol, port, host },
|
|
2993
|
+
connectParams: { version: version2, protocol, port, host },
|
|
2994
2994
|
error
|
|
2995
2995
|
} = evt;
|
|
2996
|
-
debuglog("connection to %s%s using %s%s errored - %s", host, port ? `:${port}` : "", protocol,
|
|
2996
|
+
debuglog("connection to %s%s using %s%s errored - %s", host, port ? `:${port}` : "", protocol, version2, error.message);
|
|
2997
2997
|
});
|
|
2998
2998
|
diagnosticsChannel.channel("undici:client:sendHeaders").subscribe((evt) => {
|
|
2999
2999
|
const {
|
|
@@ -25208,10 +25208,10 @@ var require_acorn = __commonJS((exports, module) => {
|
|
|
25208
25208
|
}
|
|
25209
25209
|
return this.finishToken(type, word);
|
|
25210
25210
|
};
|
|
25211
|
-
var
|
|
25211
|
+
var version2 = "8.14.0";
|
|
25212
25212
|
Parser.acorn = {
|
|
25213
25213
|
Parser,
|
|
25214
|
-
version,
|
|
25214
|
+
version: version2,
|
|
25215
25215
|
defaultOptions,
|
|
25216
25216
|
Position,
|
|
25217
25217
|
SourceLocation,
|
|
@@ -25260,7 +25260,7 @@ var require_acorn = __commonJS((exports, module) => {
|
|
|
25260
25260
|
exports2.tokContexts = types;
|
|
25261
25261
|
exports2.tokTypes = types$1;
|
|
25262
25262
|
exports2.tokenizer = tokenizer;
|
|
25263
|
-
exports2.version =
|
|
25263
|
+
exports2.version = version2;
|
|
25264
25264
|
});
|
|
25265
25265
|
});
|
|
25266
25266
|
|
|
@@ -50243,7 +50243,7 @@ var require_nwsapi = __commonJS((exports, module) => {
|
|
|
50243
50243
|
global2.NW.Dom = factory(global2, Export);
|
|
50244
50244
|
}
|
|
50245
50245
|
})(exports, function Factory(global2, Export) {
|
|
50246
|
-
var
|
|
50246
|
+
var version2 = "nwsapi-2.2.16", doc = global2.document, root = doc.documentElement, slice = Array.prototype.slice, HSP = "[\\x20\\t]", VSP = "[\\r\\n\\f]", WSP = "[\\x20\\t\\r\\n\\f]", CFG = {
|
|
50247
50247
|
operators: "[~*^$|]=|=",
|
|
50248
50248
|
combinators: "[\\x20\\t>+~](?=[^>+~])"
|
|
50249
50249
|
}, NOT = {
|
|
@@ -51490,7 +51490,7 @@ var require_nwsapi = __commonJS((exports, module) => {
|
|
|
51490
51490
|
emit,
|
|
51491
51491
|
Config,
|
|
51492
51492
|
Snapshot,
|
|
51493
|
-
Version:
|
|
51493
|
+
Version: version2,
|
|
51494
51494
|
install,
|
|
51495
51495
|
uninstall,
|
|
51496
51496
|
Operators,
|
|
@@ -55018,9 +55018,9 @@ var require_saxes = __commonJS((exports) => {
|
|
|
55018
55018
|
}
|
|
55019
55019
|
}
|
|
55020
55020
|
}
|
|
55021
|
-
setXMLVersion(
|
|
55022
|
-
this.currentXMLVersion =
|
|
55023
|
-
if (
|
|
55021
|
+
setXMLVersion(version2) {
|
|
55022
|
+
this.currentXMLVersion = version2;
|
|
55023
|
+
if (version2 === "1.0") {
|
|
55024
55024
|
this.isChar = isChar10;
|
|
55025
55025
|
this.getCode = this.getCode10;
|
|
55026
55026
|
} else {
|
|
@@ -55491,12 +55491,12 @@ var require_saxes = __commonJS((exports) => {
|
|
|
55491
55491
|
switch (this.name) {
|
|
55492
55492
|
case "version": {
|
|
55493
55493
|
this.xmlDeclExpects = ["encoding", "standalone"];
|
|
55494
|
-
const
|
|
55495
|
-
this.xmlDecl.version =
|
|
55496
|
-
if (!/^1\.[0-9]+$/.test(
|
|
55494
|
+
const version2 = value;
|
|
55495
|
+
this.xmlDecl.version = version2;
|
|
55496
|
+
if (!/^1\.[0-9]+$/.test(version2)) {
|
|
55497
55497
|
this.fail("version number must match /^1\\.[0-9]+$/.");
|
|
55498
55498
|
} else if (!this.opt.forceXMLVersion) {
|
|
55499
|
-
this.setXMLVersion(
|
|
55499
|
+
this.setXMLVersion(version2);
|
|
55500
55500
|
}
|
|
55501
55501
|
break;
|
|
55502
55502
|
}
|
|
@@ -125844,10 +125844,10 @@ var require_supports_color = __commonJS((exports, module) => {
|
|
|
125844
125844
|
return 3;
|
|
125845
125845
|
}
|
|
125846
125846
|
if ("TERM_PROGRAM" in env2) {
|
|
125847
|
-
const
|
|
125847
|
+
const version2 = parseInt((env2.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
|
|
125848
125848
|
switch (env2.TERM_PROGRAM) {
|
|
125849
125849
|
case "iTerm.app":
|
|
125850
|
-
return
|
|
125850
|
+
return version2 >= 3 ? 3 : 2;
|
|
125851
125851
|
case "Apple_Terminal":
|
|
125852
125852
|
return 2;
|
|
125853
125853
|
}
|
|
@@ -140758,7 +140758,7 @@ var require_websocket_server = __commonJS((exports, module) => {
|
|
|
140758
140758
|
socket.on("error", socketOnError);
|
|
140759
140759
|
const key = req.headers["sec-websocket-key"];
|
|
140760
140760
|
const upgrade = req.headers.upgrade;
|
|
140761
|
-
const
|
|
140761
|
+
const version2 = +req.headers["sec-websocket-version"];
|
|
140762
140762
|
if (req.method !== "GET") {
|
|
140763
140763
|
const message = "Invalid HTTP method";
|
|
140764
140764
|
abortHandshakeOrEmitwsClientError(this, req, socket, 405, message);
|
|
@@ -140774,7 +140774,7 @@ var require_websocket_server = __commonJS((exports, module) => {
|
|
|
140774
140774
|
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
|
|
140775
140775
|
return;
|
|
140776
140776
|
}
|
|
140777
|
-
if (
|
|
140777
|
+
if (version2 !== 8 && version2 !== 13) {
|
|
140778
140778
|
const message = "Missing or invalid Sec-WebSocket-Version header";
|
|
140779
140779
|
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
|
|
140780
140780
|
return;
|
|
@@ -140812,7 +140812,7 @@ var require_websocket_server = __commonJS((exports, module) => {
|
|
|
140812
140812
|
}
|
|
140813
140813
|
if (this.options.verifyClient) {
|
|
140814
140814
|
const info = {
|
|
140815
|
-
origin: req.headers[`${
|
|
140815
|
+
origin: req.headers[`${version2 === 8 ? "sec-websocket-origin" : "origin"}`],
|
|
140816
140816
|
secure: !!(req.socket.authorized || req.socket.encrypted),
|
|
140817
140817
|
req
|
|
140818
140818
|
};
|
|
@@ -151183,6 +151183,8 @@ var {
|
|
|
151183
151183
|
Option,
|
|
151184
151184
|
Help
|
|
151185
151185
|
} = import__.default;
|
|
151186
|
+
// package.json
|
|
151187
|
+
var version = "0.0.3";
|
|
151186
151188
|
|
|
151187
151189
|
// node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
151188
151190
|
var ANSI_BACKGROUND_OFFSET = 10;
|
|
@@ -151453,10 +151455,10 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
|
|
|
151453
151455
|
return 3;
|
|
151454
151456
|
}
|
|
151455
151457
|
if ("TERM_PROGRAM" in env) {
|
|
151456
|
-
const
|
|
151458
|
+
const version2 = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
|
|
151457
151459
|
switch (env.TERM_PROGRAM) {
|
|
151458
151460
|
case "iTerm.app": {
|
|
151459
|
-
return
|
|
151461
|
+
return version2 >= 3 ? 3 : 2;
|
|
151460
151462
|
}
|
|
151461
151463
|
case "Apple_Terminal": {
|
|
151462
151464
|
return 2;
|
|
@@ -151668,41 +151670,86 @@ var source_default = chalk;
|
|
|
151668
151670
|
var import_ytdl_core = __toESM(require_Default(), 1);
|
|
151669
151671
|
import fs from "fs";
|
|
151670
151672
|
import path from "path";
|
|
151671
|
-
async function
|
|
151673
|
+
async function downloadYoutube(options) {
|
|
151674
|
+
const { url, audioOnly = false, verbose = false, outputPath } = options;
|
|
151672
151675
|
try {
|
|
151673
|
-
if (!url) {
|
|
151674
|
-
throw new Error("Please provide a YouTube URL");
|
|
151675
|
-
}
|
|
151676
151676
|
console.log(source_default.cyan("\uD83D\uDD0D Fetching video information..."));
|
|
151677
151677
|
const ytdl = new import_ytdl_core.YtdlCore({});
|
|
151678
151678
|
let videoTitle = "";
|
|
151679
151679
|
await ytdl.getBasicInfo(url).then((info) => {
|
|
151680
|
+
console.log(source_default.cyan(`\uD83C\uDFAC Video title: ${info.videoDetails.title}`));
|
|
151681
|
+
console.log(source_default.cyan(`\uD83C\uDFAC Video author: ${info.videoDetails.author?.name || "Unknown"}`));
|
|
151682
|
+
if (verbose) {
|
|
151683
|
+
console.log(JSON.stringify(info.videoDetails, null, 2));
|
|
151684
|
+
}
|
|
151680
151685
|
videoTitle = info.videoDetails.title.replace(/[^\w\s]/gi, "_");
|
|
151681
151686
|
});
|
|
151682
151687
|
const stream2 = await ytdl.download(url, {
|
|
151683
|
-
filter: "audioonly"
|
|
151688
|
+
filter: audioOnly ? "audioonly" : "videoandaudio",
|
|
151689
|
+
quality: "highest"
|
|
151684
151690
|
});
|
|
151685
|
-
const outputFilePath = outputPath || path.join(process.cwd(), `${videoTitle}
|
|
151691
|
+
const outputFilePath = outputPath || path.join(process.cwd(), `${videoTitle}.${audioOnly ? "mp3" : "mp4"}`);
|
|
151686
151692
|
import_ytdl_core.toPipeableStream(stream2).pipe(fs.createWriteStream(outputFilePath));
|
|
151687
|
-
console.log(source_default.green(`\u2705 Download finished: ${outputFilePath}`));
|
|
151693
|
+
console.log(source_default.green.bold(`\u2705 Download finished: ${outputFilePath}`));
|
|
151688
151694
|
} catch (error) {
|
|
151689
151695
|
if (error instanceof Error) {
|
|
151690
|
-
console.error(source_default.red("Error:", error.message));
|
|
151696
|
+
console.error(source_default.red.bold("\u274C Error:", error.message));
|
|
151697
|
+
}
|
|
151698
|
+
}
|
|
151699
|
+
}
|
|
151700
|
+
|
|
151701
|
+
// utils/validators.ts
|
|
151702
|
+
function isValidYoutubeUrl(url) {
|
|
151703
|
+
try {
|
|
151704
|
+
const urlObj = new URL(url);
|
|
151705
|
+
const validDomains = [
|
|
151706
|
+
"youtube.com",
|
|
151707
|
+
"www.youtube.com",
|
|
151708
|
+
"m.youtube.com",
|
|
151709
|
+
"youtu.be"
|
|
151710
|
+
];
|
|
151711
|
+
if (!validDomains.includes(urlObj.hostname)) {
|
|
151712
|
+
return false;
|
|
151691
151713
|
}
|
|
151692
|
-
|
|
151714
|
+
if (urlObj.hostname === "youtu.be") {
|
|
151715
|
+
return urlObj.pathname.length > 1;
|
|
151716
|
+
}
|
|
151717
|
+
if (urlObj.pathname === "/watch") {
|
|
151718
|
+
const videoId = urlObj.searchParams.get("v");
|
|
151719
|
+
return !!videoId && videoId.length === 11;
|
|
151720
|
+
}
|
|
151721
|
+
if (urlObj.pathname.startsWith("/embed/")) {
|
|
151722
|
+
const videoId = urlObj.pathname.split("/")[2];
|
|
151723
|
+
return !!videoId && videoId.length === 11;
|
|
151724
|
+
}
|
|
151725
|
+
return false;
|
|
151726
|
+
} catch {
|
|
151727
|
+
return false;
|
|
151693
151728
|
}
|
|
151694
151729
|
}
|
|
151695
151730
|
|
|
151696
151731
|
// index.ts
|
|
151697
151732
|
var program2 = new Command;
|
|
151698
|
-
program2.name("nebula-fetch").description("CLI tool for downloading media from different platforms").version(
|
|
151699
|
-
program2.command("youtube").description("Download a video from YouTube").argument("<url>", "URL of the video").option("-o, --output <path>", "Output path for the video").action(async (url, options) => {
|
|
151733
|
+
program2.name("nebula-fetch").description("CLI tool for downloading media from different platforms").version(version);
|
|
151734
|
+
program2.command("youtube").alias("yt").description("Download a video from YouTube").argument("<url>", "URL of the video").option("-o, --output <path>", "Output path for the video").option("-a, --audio", "Download only the audio", false).option("-v, --verbose", "Show verbose output", false).action(async (url, options) => {
|
|
151700
151735
|
try {
|
|
151701
|
-
|
|
151702
|
-
|
|
151736
|
+
if (!isValidYoutubeUrl(url)) {
|
|
151737
|
+
console.error(source_default.red("Error: Invalid YouTube URL. Please provide a valid YouTube video URL"));
|
|
151738
|
+
process.exit(1);
|
|
151739
|
+
}
|
|
151740
|
+
console.log(source_default.blue(`\uD83D\uDE80 Downloading ${options.audio ? "audio" : "video"} from: ${url}`));
|
|
151741
|
+
await downloadYoutube({
|
|
151742
|
+
url,
|
|
151743
|
+
audioOnly: options.audio,
|
|
151744
|
+
outputPath: options.output,
|
|
151745
|
+
verbose: options.verbose
|
|
151746
|
+
});
|
|
151703
151747
|
} catch (error) {
|
|
151704
151748
|
console.error(source_default.red("Error:"), error);
|
|
151705
151749
|
process.exit(1);
|
|
151706
151750
|
}
|
|
151707
151751
|
});
|
|
151708
|
-
program2.
|
|
151752
|
+
program2.parseAsync().catch((error) => {
|
|
151753
|
+
console.error(source_default.red("Fatal error:"), error);
|
|
151754
|
+
process.exit(1);
|
|
151755
|
+
});
|
package/index.ts
CHANGED
|
@@ -1,29 +1,57 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
|
+
import { version } from "@/package.json";
|
|
5
|
+
|
|
4
6
|
import chalk from "chalk";
|
|
5
|
-
|
|
7
|
+
|
|
8
|
+
import { downloadYoutube } from "@/commands/youtube";
|
|
9
|
+
import { isValidYoutubeUrl } from "./utils/validators";
|
|
6
10
|
|
|
7
11
|
const program = new Command();
|
|
8
12
|
|
|
9
13
|
program
|
|
10
14
|
.name("nebula-fetch")
|
|
11
15
|
.description("CLI tool for downloading media from different platforms")
|
|
12
|
-
.version(
|
|
16
|
+
.version(version);
|
|
13
17
|
|
|
14
18
|
program
|
|
15
19
|
.command("youtube")
|
|
20
|
+
.alias("yt")
|
|
16
21
|
.description("Download a video from YouTube")
|
|
17
22
|
.argument("<url>", "URL of the video")
|
|
18
23
|
.option("-o, --output <path>", "Output path for the video")
|
|
24
|
+
.option("-a, --audio", "Download only the audio", false)
|
|
25
|
+
.option("-v, --verbose", "Show verbose output", false)
|
|
19
26
|
.action(async (url, options) => {
|
|
20
27
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
if (!isValidYoutubeUrl(url)) {
|
|
29
|
+
console.error(
|
|
30
|
+
chalk.red(
|
|
31
|
+
"Error: Invalid YouTube URL. Please provide a valid YouTube video URL"
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(
|
|
38
|
+
chalk.blue(
|
|
39
|
+
`🚀 Downloading ${options.audio ? "audio" : "video"} from: ${url}`
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
await downloadYoutube({
|
|
43
|
+
url,
|
|
44
|
+
audioOnly: options.audio,
|
|
45
|
+
outputPath: options.output,
|
|
46
|
+
verbose: options.verbose,
|
|
47
|
+
});
|
|
23
48
|
} catch (error) {
|
|
24
49
|
console.error(chalk.red("Error:"), error);
|
|
25
50
|
process.exit(1);
|
|
26
51
|
}
|
|
27
52
|
});
|
|
28
53
|
|
|
29
|
-
program.
|
|
54
|
+
program.parseAsync().catch((error) => {
|
|
55
|
+
console.error(chalk.red("Fatal error:"), error);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunarity/nebula-fetch-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"description": "CLI tool for downloading media from different platforms",
|
|
6
6
|
"type": "module",
|
|
@@ -27,6 +27,13 @@
|
|
|
27
27
|
"keywords": [
|
|
28
28
|
"cli",
|
|
29
29
|
"media",
|
|
30
|
-
"download"
|
|
31
|
-
|
|
30
|
+
"download",
|
|
31
|
+
"youtube"
|
|
32
|
+
],
|
|
33
|
+
"author": "Krzysztof Oszczapiński",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/LUNARITYai/nebula-fetch-cli.git"
|
|
38
|
+
}
|
|
32
39
|
}
|
package/tsconfig.json
CHANGED
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
// Some stricter flags (disabled by default)
|
|
23
23
|
"noUnusedLocals": false,
|
|
24
24
|
"noUnusedParameters": false,
|
|
25
|
-
"noPropertyAccessFromIndexSignature": false
|
|
25
|
+
"noPropertyAccessFromIndexSignature": false,
|
|
26
|
+
|
|
27
|
+
"baseUrl": ".",
|
|
28
|
+
"paths": {
|
|
29
|
+
"@/*": ["./*"]
|
|
30
|
+
}
|
|
26
31
|
}
|
|
27
32
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates YouTube URLs in various formats:
|
|
3
|
+
* - Standard: https://www.youtube.com/watch?v=VIDEO_ID
|
|
4
|
+
* - Short: https://youtu.be/VIDEO_ID
|
|
5
|
+
* - Embedded: https://www.youtube.com/embed/VIDEO_ID
|
|
6
|
+
* - Mobile: https://m.youtube.com/watch?v=VIDEO_ID
|
|
7
|
+
* - With timestamps: https://www.youtube.com/watch?v=VIDEO_ID&t=123s
|
|
8
|
+
*/
|
|
9
|
+
export function isValidYoutubeUrl(url: string): boolean {
|
|
10
|
+
try {
|
|
11
|
+
const urlObj = new URL(url);
|
|
12
|
+
|
|
13
|
+
// Check for valid YouTube domains
|
|
14
|
+
const validDomains = [
|
|
15
|
+
"youtube.com",
|
|
16
|
+
"www.youtube.com",
|
|
17
|
+
"m.youtube.com",
|
|
18
|
+
"youtu.be",
|
|
19
|
+
];
|
|
20
|
+
if (!validDomains.includes(urlObj.hostname)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Handle youtu.be format
|
|
25
|
+
if (urlObj.hostname === "youtu.be") {
|
|
26
|
+
return urlObj.pathname.length > 1; // Must have video ID after /
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Handle standard YouTube URLs
|
|
30
|
+
if (urlObj.pathname === "/watch") {
|
|
31
|
+
const videoId = urlObj.searchParams.get("v");
|
|
32
|
+
return !!videoId && videoId.length === 11;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Handle embedded format
|
|
36
|
+
if (urlObj.pathname.startsWith("/embed/")) {
|
|
37
|
+
const videoId = urlObj.pathname.split("/")[2];
|
|
38
|
+
return !!videoId && videoId.length === 11;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return false;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|