npmytd 1.0.0
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 +86 -0
- package/bin/npmytd.js +3 -0
- package/package.json +29 -0
- package/src/cli.js +40 -0
- package/src/downloader.js +185 -0
- package/src/prompts.js +94 -0
- package/src/utils.js +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
███╗ ██╗██████╗ ███╗ ███╗██╗ ██╗████████╗██████╗
|
|
2
|
+
████╗ ██║██╔══██╗████╗ ████║╚██╗ ██╔╝╚══██╔══╝██╔══██╗
|
|
3
|
+
██╔██╗ ██║██████╔╝██╔████╔██║ ╚████╔╝ ██║ ██║ ██║
|
|
4
|
+
██║╚██╗██║██╔═══╝ ██║╚██╔╝██║ ╚██╔╝ ██║ ██║ ██║
|
|
5
|
+
██║ ╚████║██║ ██║ ╚═╝ ██║ ██║ ██║ ██████╔╝
|
|
6
|
+
╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
A simple and interactive command-line interface (CLI) tool for downloading YouTube videos and audio (MP3 or MP4) in various qualities.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
* **Interactive Prompts**: Guides you through the download process.
|
|
14
|
+
* **MP4 Video Download**: Download videos in your desired quality.
|
|
15
|
+
* **MP3 Audio Download**: Extract and convert audio to MP3 format.
|
|
16
|
+
* **Quality Selection**: Choose between highest, medium, or lowest quality.
|
|
17
|
+
* **Progress Indicator**: See the download progress in real-time.
|
|
18
|
+
* **Output Path Customization**: Specify where to save your downloads.
|
|
19
|
+
|
|
20
|
+
## Prerequisites
|
|
21
|
+
|
|
22
|
+
Before installing and using `npmytd`, ensure you have the following installed on your system:
|
|
23
|
+
|
|
24
|
+
* **Node.js**: Version 18 or higher. You can download it from [nodejs.org](https://nodejs.org/).
|
|
25
|
+
* **ffmpeg**: Required for **MP3 audio conversion**. If you only plan to download MP4 videos, `ffmpeg` is not strictly necessary.
|
|
26
|
+
* You can download `ffmpeg` from [ffmpeg.org/download.html](https://ffmpeg.org/download.html).
|
|
27
|
+
* After downloading, make sure `ffmpeg` is added to your system's `PATH` environment variable so that `npmytd` can find it.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
To install `npmytd` globally, open your terminal or command prompt and run:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install -g npmytd
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This will make the `npmytd` command available system-wide.
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
To start a download, simply run the `npmytd` command in your terminal:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npmytd
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The CLI will then guide you through a series of prompts:
|
|
48
|
+
|
|
49
|
+
1. **YouTube video URL or ID**: Enter the full URL of the YouTube video or just its video ID (e.g., `dQw4w9WgXcQ`).
|
|
50
|
+
2. **Output format**: Choose between `MP4 (Video)` or `MP3 (Audio)`.
|
|
51
|
+
3. **Desired quality**: Select `Highest`, `Medium`, or `Lowest` quality for your chosen format.
|
|
52
|
+
4. **Output directory**: Specify the path where you want to save the downloaded file. A default path (current working directory) with a sanitized video title will be suggested.
|
|
53
|
+
|
|
54
|
+
### Example Walkthrough
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
$ npmytd
|
|
58
|
+
|
|
59
|
+
Welcome to NPMYTD - YouTube Downloader CLI!
|
|
60
|
+
|
|
61
|
+
? Enter YouTube video URL or ID: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
62
|
+
? Select output format: MP4 (Video)
|
|
63
|
+
? Select desired quality for MP4: Highest (Best available video quality)
|
|
64
|
+
? Enter output directory (absolute or relative path): C:\Users\GraphStats\Desktop\MyDownloads\Never-Gonna-Give-You-Up-Official-Music-Video.mp4
|
|
65
|
+
|
|
66
|
+
Downloading "Rick Astley - Never Gonna Give You Up (Official Music Video)" as MP4...
|
|
67
|
+
[========================================] 100% | ETA: 0s | total: 4.8MB
|
|
68
|
+
|
|
69
|
+
Download completed: C:\Users\GraphStats\Desktop\MyDownloads\Never-Gonna-Give-You-Up-Official-Music-Video.mp4
|
|
70
|
+
|
|
71
|
+
Process completed successfully!
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Troubleshooting
|
|
75
|
+
|
|
76
|
+
* **`ffmpeg` not found for MP3 conversion**: If you selected MP3 format and encounter an error about `ffmpeg` not being found, please ensure `ffmpeg` is installed and properly added to your system's `PATH`.
|
|
77
|
+
* **Video Unavailable**: Some YouTube videos may be age-restricted, private, or have been removed. `npmytd` might not be able to download these.
|
|
78
|
+
* **`fluent-ffmpeg` deprecation warning**: You might see a warning during installation about `fluent-ffmpeg` being deprecated. While it currently functions, this might indicate future compatibility issues. We will monitor for stable alternatives if this becomes problematic.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
This project is licensed under the MIT License.
|
|
83
|
+
|
|
84
|
+
## Author
|
|
85
|
+
|
|
86
|
+
GraphStats
|
package/bin/npmytd.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "npmytd",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A CLI tool to download YouTube videos or audio.",
|
|
5
|
+
"main": "src/cli.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"npmytd": "bin/npmytd.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/npmytd.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"youtube",
|
|
15
|
+
"downloader",
|
|
16
|
+
"cli",
|
|
17
|
+
"mp3",
|
|
18
|
+
"mp4"
|
|
19
|
+
],
|
|
20
|
+
"author": "GraphStats",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"chalk": "^5.6.2",
|
|
24
|
+
"cli-progress": "^3.12.0",
|
|
25
|
+
"fluent-ffmpeg": "^2.1.3",
|
|
26
|
+
"inquirer": "^13.2.2",
|
|
27
|
+
"ytdl-core": "^4.11.5"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getYouTubeUrl, getOutputFormat, getQuality, getOutputPath } from './prompts.js';
|
|
2
|
+
import { downloadYouTubeMedia } from './downloader.js';
|
|
3
|
+
import ytdl from 'ytdl-core';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { sanitizeFilename } from './utils.js';
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
console.log(chalk.bold.hex('#FF0000')('
|
|
10
|
+
Welcome to NPMYTD - YouTube Downloader CLI!'));
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const url = await getYouTubeUrl();
|
|
14
|
+
const format = await getOutputFormat();
|
|
15
|
+
const quality = await getQuality(format);
|
|
16
|
+
|
|
17
|
+
// Get video info to suggest a default filename
|
|
18
|
+
let videoInfo;
|
|
19
|
+
try {
|
|
20
|
+
videoInfo = await ytdl.getInfo(url);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error(chalk.red('Error fetching video information:', error.message));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const suggestedFilename = sanitizeFilename(videoInfo.videoDetails.title);
|
|
26
|
+
|
|
27
|
+
const outputPath = await getOutputPath(suggestedFilename);
|
|
28
|
+
|
|
29
|
+
await downloadYouTubeMedia(url, format, quality, outputPath);
|
|
30
|
+
|
|
31
|
+
console.log(chalk.bold.green('
|
|
32
|
+
Process completed successfully!'));
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(chalk.red('
|
|
35
|
+
An error occurred:'), error.message);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
main();
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import ytdl from 'ytdl-core';
|
|
2
|
+
import cliProgress from 'cli-progress';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import ffmpeg from 'fluent-ffmpeg';
|
|
6
|
+
import { sanitizeFilename, ensureDirectoryExists } from './utils.js';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
|
|
9
|
+
// Ensure ffmpeg path is set if it's not in the system's PATH
|
|
10
|
+
// This might be necessary on some systems. For broader compatibility,
|
|
11
|
+
// it's good practice to ensure ffmpeg is discoverable.
|
|
12
|
+
// ffmpeg.setFfmpegPath('/path/to/your/ffmpeg'); // Uncomment and set if needed
|
|
13
|
+
|
|
14
|
+
export async function downloadYouTubeMedia(url, format, quality, outputPath) {
|
|
15
|
+
try {
|
|
16
|
+
const videoId = ytdl.getVideoID(url);
|
|
17
|
+
const info = await ytdl.getInfo(videoId);
|
|
18
|
+
const videoTitle = sanitizeFilename(info.videoDetails.title);
|
|
19
|
+
|
|
20
|
+
const targetDirectory = path.dirname(outputPath);
|
|
21
|
+
const targetFilename = path.basename(outputPath);
|
|
22
|
+
const finalPath = path.join(targetDirectory, targetFilename);
|
|
23
|
+
|
|
24
|
+
// Ensure the output directory exists
|
|
25
|
+
const dirExists = await ensureDirectoryExists(targetDirectory);
|
|
26
|
+
if (!dirExists) {
|
|
27
|
+
console.error(chalk.red('Failed to create output directory.'));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let ytdlOptions = {
|
|
32
|
+
quality: 'highest', // Default to highest, then adjust based on user input
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let outputExtension = '';
|
|
36
|
+
|
|
37
|
+
if (format === 'mp4') {
|
|
38
|
+
outputExtension = '.mp4';
|
|
39
|
+
switch (quality) {
|
|
40
|
+
case 'highest':
|
|
41
|
+
ytdlOptions.filter = 'videoandaudio'; // ytdl-core typically combines them by default for highest quality
|
|
42
|
+
ytdlOptions.quality = 'highestvideo';
|
|
43
|
+
break;
|
|
44
|
+
case 'medium':
|
|
45
|
+
// Find a 720p or 480p format with audio
|
|
46
|
+
ytdlOptions.filter = (format) => format.qualityLabel === '720p' && format.hasAudio && format.hasVideo;
|
|
47
|
+
// Fallback if 720p with audio is not found
|
|
48
|
+
if (!info.formats.find(f => f.qualityLabel === '720p' && f.hasAudio && f.hasVideo)) {
|
|
49
|
+
ytdlOptions.filter = (format) => format.qualityLabel === '480p' && format.hasAudio && format.hasVideo;
|
|
50
|
+
}
|
|
51
|
+
if (!info.formats.find(f => f.qualityLabel === '480p' && f.hasAudio && f.hasVideo)) {
|
|
52
|
+
ytdlOptions.quality = 'lowestvideo'; // Fallback to lowest if medium not found
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
case 'lowest':
|
|
56
|
+
ytdlOptions.filter = (format) => format.qualityLabel === '360p' && format.hasAudio && format.hasVideo;
|
|
57
|
+
if (!info.formats.find(f => f.qualityLabel === '360p' && f.hasAudio && f.hasVideo)) {
|
|
58
|
+
ytdlOptions.quality = 'lowestvideo'; // Fallback to lowest available video with audio
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
} else if (format === 'mp3') {
|
|
63
|
+
outputExtension = '.mp3';
|
|
64
|
+
ytdlOptions.filter = 'audioonly';
|
|
65
|
+
switch (quality) {
|
|
66
|
+
case 'highest':
|
|
67
|
+
ytdlOptions.quality = 'highestaudio';
|
|
68
|
+
break;
|
|
69
|
+
case 'medium':
|
|
70
|
+
ytdlOptions.quality = 'lowestaudio'; // ytdl-core often only has highest and lowest for audio, 'lowestaudio' is typically still good quality.
|
|
71
|
+
break;
|
|
72
|
+
case 'lowest':
|
|
73
|
+
ytdlOptions.quality = 'lowestaudio';
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
|
|
79
|
+
let receivedBytes = 0;
|
|
80
|
+
let totalBytes = 0;
|
|
81
|
+
|
|
82
|
+
console.log(chalk.blue(`
|
|
83
|
+
Downloading "${videoTitle}" as ${format.toUpperCase()}...`));
|
|
84
|
+
|
|
85
|
+
if (format === 'mp4') {
|
|
86
|
+
const video = ytdl(url, ytdlOptions);
|
|
87
|
+
|
|
88
|
+
video.on('response', (res) => {
|
|
89
|
+
totalBytes = parseInt(res.headers['content-length'], 10);
|
|
90
|
+
bar.start(totalBytes, 0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
video.on('data', (chunk) => {
|
|
94
|
+
receivedBytes += chunk.length;
|
|
95
|
+
bar.update(receivedBytes);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
video.pipe(fs.createWriteStream(finalPath + outputExtension));
|
|
99
|
+
|
|
100
|
+
await new Promise((resolve, reject) => {
|
|
101
|
+
video.on('end', () => {
|
|
102
|
+
bar.stop();
|
|
103
|
+
console.log(chalk.green(`
|
|
104
|
+
Download completed: ${finalPath}${outputExtension}`));
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
video.on('error', (err) => {
|
|
108
|
+
bar.stop();
|
|
109
|
+
console.error(chalk.red('
|
|
110
|
+
Error during download:', err.message));
|
|
111
|
+
reject(err);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
} else if (format === 'mp3') {
|
|
116
|
+
const audioStream = ytdl(url, ytdlOptions);
|
|
117
|
+
const tempFilePath = finalPath + '.tmp'; // Use a temporary file for download
|
|
118
|
+
|
|
119
|
+
audioStream.on('response', (res) => {
|
|
120
|
+
totalBytes = parseInt(res.headers['content-length'], 10);
|
|
121
|
+
bar.start(totalBytes, 0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
audioStream.on('data', (chunk) => {
|
|
125
|
+
receivedBytes += chunk.length;
|
|
126
|
+
bar.update(receivedBytes);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await new Promise((resolve, reject) => {
|
|
130
|
+
audioStream.pipe(fs.createWriteStream(tempFilePath))
|
|
131
|
+
.on('finish', () => {
|
|
132
|
+
bar.stop();
|
|
133
|
+
console.log(chalk.blue('
|
|
134
|
+
Download of audio stream completed. Starting conversion to MP3...'));
|
|
135
|
+
|
|
136
|
+
// Check if ffmpeg is available
|
|
137
|
+
ffmpeg.getFfmpegVersion((err) => {
|
|
138
|
+
if (err) {
|
|
139
|
+
console.error(chalk.red('ffmpeg is not installed or not found in your system's PATH.'));
|
|
140
|
+
console.error(chalk.red('Please install ffmpeg to convert to MP3.'));
|
|
141
|
+
console.error(chalk.red('You can download it from https://ffmpeg.org/download.html and ensure it's accessible in your PATH.'));
|
|
142
|
+
reject(new Error('ffmpeg not found.'));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
ffmpeg(tempFilePath)
|
|
147
|
+
.audioBitrate(quality === 'highest' ? 320 : (quality === 'medium' ? 192 : 128)) // Set bitrate based on quality
|
|
148
|
+
.save(finalPath + outputExtension)
|
|
149
|
+
.on('end', () => {
|
|
150
|
+
fs.unlink(tempFilePath, (err) => { // Clean up temporary file
|
|
151
|
+
if (err) console.error(chalk.yellow('Warning: Could not remove temporary file:', err.message));
|
|
152
|
+
});
|
|
153
|
+
console.log(chalk.green(`
|
|
154
|
+
Conversion and download completed: ${finalPath}${outputExtension}`));
|
|
155
|
+
resolve();
|
|
156
|
+
})
|
|
157
|
+
.on('error', (err) => {
|
|
158
|
+
fs.unlink(tempFilePath, (err) => { // Clean up temporary file on error
|
|
159
|
+
if (err) console.error(chalk.yellow('Warning: Could not remove temporary file:', err.message));
|
|
160
|
+
});
|
|
161
|
+
console.error(chalk.red('
|
|
162
|
+
Error during MP3 conversion:', err.message));
|
|
163
|
+
reject(err);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
})
|
|
167
|
+
.on('error', (err) => {
|
|
168
|
+
bar.stop();
|
|
169
|
+
console.error(chalk.red('
|
|
170
|
+
Error during audio stream download:', err.message));
|
|
171
|
+
reject(err);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error.message.includes('No video id found')) {
|
|
177
|
+
console.error(chalk.red('Invalid YouTube URL or video ID. Please check and try again.'));
|
|
178
|
+
} else if (error.message.includes('Code: 410')) {
|
|
179
|
+
console.error(chalk.red('This video is unavailable. It may have been removed or is private.'));
|
|
180
|
+
} else {
|
|
181
|
+
console.error(chalk.red('An unexpected error occurred during the download process:'), error.message);
|
|
182
|
+
}
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/prompts.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { isValidYoutubeUrl, sanitizeFilename } from './utils.js';
|
|
4
|
+
|
|
5
|
+
export async function getYouTubeUrl() {
|
|
6
|
+
const questions = [
|
|
7
|
+
{
|
|
8
|
+
type: 'input',
|
|
9
|
+
name: 'url',
|
|
10
|
+
message: 'Enter YouTube video URL or ID:',
|
|
11
|
+
validate: (input) => {
|
|
12
|
+
if (!input) {
|
|
13
|
+
return 'YouTube URL or ID cannot be empty.';
|
|
14
|
+
}
|
|
15
|
+
// Basic validation, more robust validation can be added if needed
|
|
16
|
+
// For now, check if it contains "youtube.com" or "youtu.be" or is a potential ID
|
|
17
|
+
if (isValidYoutubeUrl(input) || input.length >= 5 && input.length <= 15) { // Assuming video IDs are usually 11 characters
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return 'Please enter a valid YouTube URL or video ID.';
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
const answers = await inquirer.prompt(questions);
|
|
25
|
+
return answers.url;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function getOutputFormat() {
|
|
29
|
+
const questions = [
|
|
30
|
+
{
|
|
31
|
+
type: 'list',
|
|
32
|
+
name: 'format',
|
|
33
|
+
message: 'Select output format:',
|
|
34
|
+
choices: [
|
|
35
|
+
{ name: 'MP4 (Video)', value: 'mp4' },
|
|
36
|
+
{ name: 'MP3 (Audio)', value: 'mp3' },
|
|
37
|
+
],
|
|
38
|
+
default: 'mp4',
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
const answers = await inquirer.prompt(questions);
|
|
42
|
+
return answers.format;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function getQuality(format) {
|
|
46
|
+
let choices = [];
|
|
47
|
+
if (format === 'mp4') {
|
|
48
|
+
choices = [
|
|
49
|
+
{ name: 'Highest (Best available video quality)', value: 'highest' },
|
|
50
|
+
{ name: 'Medium (720p if available, fallback to 480p)', value: 'medium' },
|
|
51
|
+
{ name: 'Lowest (Smallest video quality)', value: 'lowest' },
|
|
52
|
+
];
|
|
53
|
+
} else { // mp3
|
|
54
|
+
choices = [
|
|
55
|
+
{ name: 'Highest (Best available audio quality)', value: 'highest' },
|
|
56
|
+
{ name: 'Medium (Standard audio quality)', value: 'medium' },
|
|
57
|
+
{ name: 'Lowest (Smallest audio quality)', value: 'lowest' },
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const questions = [
|
|
62
|
+
{
|
|
63
|
+
type: 'list',
|
|
64
|
+
name: 'quality',
|
|
65
|
+
message: `Select desired quality for ${format.toUpperCase()}:`,
|
|
66
|
+
choices: choices,
|
|
67
|
+
default: 'highest',
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
const answers = await inquirer.prompt(questions);
|
|
71
|
+
return answers.quality;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function getOutputPath(defaultFilename = 'download') {
|
|
75
|
+
const defaultPath = path.join(process.cwd(), sanitizeFilename(defaultFilename));
|
|
76
|
+
|
|
77
|
+
const questions = [
|
|
78
|
+
{
|
|
79
|
+
type: 'input',
|
|
80
|
+
name: 'outputPath',
|
|
81
|
+
message: 'Enter output directory (absolute or relative path):',
|
|
82
|
+
default: defaultPath,
|
|
83
|
+
validate: (input) => {
|
|
84
|
+
// Basic path validation, more robust checks (e.g., write permissions) will be done in utils
|
|
85
|
+
if (!input) {
|
|
86
|
+
return 'Output path cannot be empty.';
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
const answers = await inquirer.prompt(questions);
|
|
93
|
+
return answers.outputPath;
|
|
94
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export function isValidYoutubeUrl(url) {
|
|
5
|
+
// Regex for YouTube URLs and IDs
|
|
6
|
+
// Matches youtube.com/watch?v=VIDEO_ID, youtu.be/VIDEO_ID, and plain VIDEO_ID
|
|
7
|
+
const youtubeRegex = /^(?:https?:\/\/)?(?:www\.)?(?:m\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=|embed\/|v\/|)([\w-]{11})(?:\S+)?$/;
|
|
8
|
+
const match = url.match(youtubeRegex);
|
|
9
|
+
if (match) {
|
|
10
|
+
return match[1]; // Return the video ID
|
|
11
|
+
}
|
|
12
|
+
// If it's not a full URL, check if it's a valid YouTube video ID (11 characters, alphanumeric, hyphen, underscore)
|
|
13
|
+
const videoIdRegex = /^[\w-]{11}$/;
|
|
14
|
+
if (url.match(videoIdRegex)) {
|
|
15
|
+
return url; // It's a video ID
|
|
16
|
+
}
|
|
17
|
+
return null; // Not a valid URL or ID
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function sanitizeFilename(filename) {
|
|
21
|
+
// Remove invalid characters for filenames and replace spaces with hyphens
|
|
22
|
+
// Windows invalid characters: < > : " / \ | ? *
|
|
23
|
+
// Unix invalid characters: /
|
|
24
|
+
return filename
|
|
25
|
+
.replace(/[<>:"/\|?*\x00-\x1f]/g, '_') // Replace invalid characters with underscore
|
|
26
|
+
.replace(/\s/g, '-') // Replace spaces with hyphens
|
|
27
|
+
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
|
|
28
|
+
.substring(0, 250); // Truncate to a reasonable length
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function ensureDirectoryExists(dirPath) {
|
|
32
|
+
try {
|
|
33
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
34
|
+
return true;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(`Error creating directory ${dirPath}:`, error.message);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|