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 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
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import("../src/cli.js");
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
+ }