npmytd 1.0.7 → 1.0.9

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,6 +1,6 @@
1
1
  {
2
2
  "name": "npmytd",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "A CLI tool to download YouTube videos or audio.",
5
5
  "main": "src/cli.js",
6
6
  "type": "module",
@@ -22,8 +22,6 @@
22
22
  "dependencies": {
23
23
  "chalk": "^5.6.2",
24
24
  "cli-progress": "^3.12.0",
25
- "fluent-ffmpeg": "^2.1.3",
26
- "inquirer": "^13.2.2",
27
- "ytdl-core": "^4.11.5"
25
+ "inquirer": "^13.2.2"
28
26
  }
29
27
  }
package/src/cli.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { getYouTubeUrl, getOutputFormat, getQuality, getOutputPath } from './prompts.js';
2
2
  import { downloadYouTubeMedia } from './downloader.js';
3
- import ytdl from 'ytdl-core';
4
3
  import chalk from 'chalk';
5
4
  import path from 'path';
6
5
  import { sanitizeFilename } from './utils.js';
@@ -33,25 +32,10 @@ async function main() {
33
32
  const format = await getOutputFormat();
34
33
  const quality = await getQuality(format);
35
34
 
36
- // Get video info to suggest a default filename
37
- let videoInfo;
38
- try {
39
- videoInfo = await ytdl.getInfo(url);
40
- } catch (error) {
41
- if (error.message.includes('Could not extract functions')) {
42
- console.error(chalk.red(`Error fetching video information: ${error.message}`));
43
- console.error(chalk.yellow(`This often happens when YouTube updates its website, temporarily breaking the downloader.
44
- Please try again later, or check the 'ytdl-core' GitHub page for updates.`));
45
- } else {
46
- console.error(chalk.red('Error fetching video information:', error.message));
47
- }
48
- process.exit(1);
49
- }
50
- const suggestedFilename = sanitizeFilename(videoInfo.videoDetails.title);
35
+ // outputPath will now be the directory
36
+ const outputDirectory = await getOutputPath();
51
37
 
52
- const outputPath = await getOutputPath(suggestedFilename);
53
-
54
- await downloadYouTubeMedia(url, format, quality, outputPath);
38
+ await downloadYouTubeMedia(url, format, quality, outputDirectory);
55
39
 
56
40
  console.log(chalk.bold.green(`
57
41
  Process completed successfully!`));
package/src/downloader.js CHANGED
@@ -1,26 +1,17 @@
1
- import ytdl from 'ytdl-core';
1
+ import { spawn } from 'child_process';
2
2
  import cliProgress from 'cli-progress';
3
3
  import path from 'path';
4
- import fs from 'fs';
5
- import ffmpeg from 'fluent-ffmpeg';
4
+ import { promises as fs } from 'fs'; // Use promises API for fs
6
5
  import { sanitizeFilename, ensureDirectoryExists } from './utils.js';
7
6
  import chalk from 'chalk';
8
7
 
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
8
  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);
9
+ let videoTitle = 'unknown_video'; // Default title
10
+ const targetDirectory = path.dirname(outputPath);
11
+ let finalFileName = path.basename(outputPath); // Will be updated after getting info
12
+ let fullOutputPath;
23
13
 
14
+ try {
24
15
  // Ensure the output directory exists
25
16
  const dirExists = await ensureDirectoryExists(targetDirectory);
26
17
  if (!dirExists) {
@@ -28,158 +19,140 @@ export async function downloadYouTubeMedia(url, format, quality, outputPath) {
28
19
  return;
29
20
  }
30
21
 
31
- let ytdlOptions = {
32
- quality: 'highest', // Default to highest, then adjust based on user input
33
- };
22
+ // --- Step 1: Get video information using yt-dlp --print-json ---
23
+ console.log(chalk.blue('Fetching video information...'));
24
+ const infoProcess = spawn('yt-dlp', ['--print-json', url]);
25
+ let rawInfo = '';
26
+ infoProcess.stdout.on('data', (data) => {
27
+ rawInfo += data.toString();
28
+ });
29
+
30
+ await new Promise((resolve, reject) => {
31
+ infoProcess.on('close', (code) => {
32
+ if (code === 0) {
33
+ try {
34
+ const videoInfo = JSON.parse(rawInfo);
35
+ videoTitle = sanitizeFilename(videoInfo.title);
36
+ resolve();
37
+ } catch (e) {
38
+ reject(new Error('Failed to parse video info from yt-dlp.'));
39
+ }
40
+ } else {
41
+ reject(new Error(`yt-dlp info command failed with code ${code}`));
42
+ }
43
+ });
44
+ infoProcess.on('error', (err) => {
45
+ reject(new Error(`Failed to run yt-dlp. Is yt-dlp installed and in your PATH? (${err.message})`));
46
+ });
47
+ });
48
+
49
+ // Update finalFileName with sanitized video title and desired extension
50
+ const outputExtension = format === 'mp4' ? '.mp4' : '.mp3';
51
+ finalFileName = `${videoTitle}${outputExtension}`;
52
+ fullOutputPath = path.join(targetDirectory, finalFileName);
34
53
 
35
- let outputExtension = '';
54
+ console.log(chalk.blue(`\nDownloading "${videoTitle}" as ${format.toUpperCase()}...`));
55
+
56
+ // --- Step 2: Construct yt-dlp arguments for download ---
57
+ const ytDlpArgs = [url, '-o', fullOutputPath]; // -o for output path
36
58
 
37
59
  if (format === 'mp4') {
38
- outputExtension = '.mp4';
39
60
  switch (quality) {
40
61
  case 'highest':
41
- ytdlOptions.filter = 'videoandaudio'; // ytdl-core typically combines them by default for highest quality
42
- ytdlOptions.quality = 'highestvideo';
62
+ ytDlpArgs.push('-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]');
63
+ ytDlpArgs.push('--merge-output-format', 'mp4');
43
64
  break;
44
65
  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
- }
66
+ ytDlpArgs.push('-f', 'bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/best[height<=720][ext=mp4]');
67
+ ytDlpArgs.push('--merge-output-format', 'mp4');
54
68
  break;
55
69
  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
- }
70
+ ytDlpArgs.push('-f', 'bestvideo[height<=360][ext=mp4]+bestaudio[ext=m4a]/best[height<=360][ext=mp4]');
71
+ ytDlpArgs.push('--merge-output-format', 'mp4');
60
72
  break;
61
73
  }
62
74
  } else if (format === 'mp3') {
63
- outputExtension = '.mp3';
64
- ytdlOptions.filter = 'audioonly';
75
+ ytDlpArgs.push('--extract-audio', '--audio-format', 'mp3');
65
76
  switch (quality) {
66
77
  case 'highest':
67
- ytdlOptions.quality = 'highestaudio';
78
+ ytDlpArgs.push('--audio-quality', '0'); // best quality
68
79
  break;
69
80
  case 'medium':
70
- ytdlOptions.quality = 'lowestaudio'; // ytdl-core often only has highest and lowest for audio, 'lowestaudio' is typically still good quality.
81
+ ytDlpArgs.push('--audio-quality', '5'); // VBR ~130-160 kbps
71
82
  break;
72
83
  case 'lowest':
73
- ytdlOptions.quality = 'lowestaudio';
84
+ ytDlpArgs.push('--audio-quality', '9'); // VBR ~45-85 kbps
74
85
  break;
75
86
  }
76
87
  }
77
88
 
89
+ // --- Step 3: Execute yt-dlp for download and show progress ---
90
+ const downloadProcess = spawn('yt-dlp', ytDlpArgs);
78
91
  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}`));
92
+ let totalDownloadSize = 0; // in bytes
93
+ let currentProgress = 0; // in percentage or bytes
94
+
95
+ downloadProcess.stderr.on('data', (data) => {
96
+ const output = data.toString();
97
+ // yt-dlp progress format example: [download] 10.0% of 100.00MiB at 1.23MiB/s ETA 00:05
98
+ const progressMatch = output.match(/\[download\]\s+(\d+\.\d+)% of (~?\d+\.\d+\w+)/);
99
+ const sizeMatch = output.match(/\[download\] Destination: (.+)/); // Match the final output file to determine size if no % progress
100
+
101
+ if (progressMatch) {
102
+ const percentage = parseFloat(progressMatch[1]);
103
+ const totalSizeStr = progressMatch[2];
104
+
105
+ // yt-dlp might report 'of ~SIZE' when total size is not known yet.
106
+ // For simplicity, we'll start the bar when we have a good estimate.
107
+ if (totalDownloadSize === 0 && !totalSizeStr.startsWith('~')) {
108
+ // Convert string size (e.g., 100.00MiB) to bytes. Simple approximation.
109
+ const sizeValue = parseFloat(totalSizeStr.match(/(\d+\.\d+)/)[1]);
110
+ const sizeUnit = totalSizeStr.match(/(\w+)$/)[1];
111
+ let multiplier = 1;
112
+ if (sizeUnit === 'KiB') multiplier = 1024;
113
+ else if (sizeUnit === 'MiB') multiplier = 1024 * 1024;
114
+ else if (sizeUnit === 'GiB') multiplier = 1024 * 1024 * 1024;
115
+ totalDownloadSize = sizeValue * multiplier;
116
+ bar.start(100, percentage); // Start with 100 as total and update with percentage
117
+ }
118
+ bar.update(percentage);
119
+ } else if (output.includes('[download] 100%')) {
120
+ // Ensure bar reaches 100% at the end
121
+ bar.update(100);
122
+ }
123
+ // For debugging yt-dlp output
124
+ // console.error(output.trim());
125
+ });
126
+
127
+ await new Promise((resolve, reject) => {
128
+ downloadProcess.on('close', (code) => {
129
+ bar.stop();
130
+ if (code === 0) {
131
+ console.log(chalk.green(`\nDownload completed: ${fullOutputPath}`));
105
132
  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);
133
+ } else {
134
+ reject(new Error(`yt-dlp download failed with code ${code}`));
135
+ }
122
136
  });
123
-
124
- audioStream.on('data', (chunk) => {
125
- receivedBytes += chunk.length;
126
- bar.update(receivedBytes);
137
+ downloadProcess.on('error', (err) => {
138
+ bar.stop();
139
+ reject(new Error(`Failed to run yt-dlp. Is yt-dlp installed and in your PATH? (${err.message})`));
127
140
  });
141
+ });
128
142
 
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
143
  } 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')) {
144
+ if (error.message.includes('Failed to run yt-dlp')) {
145
+ console.error(chalk.red(error.message));
146
+ console.error(chalk.yellow('Please ensure yt-dlp is installed and available in your system\'s PATH.'));
147
+ console.error(chalk.yellow('You can install it with `pip install yt-dlp` or from https://github.com/yt-dlp/yt-dlp/releases.'));
148
+ } else if (error.message.includes('Failed to parse video info')) {
149
+ console.error(chalk.red(error.message));
150
+ } else if (error.message.includes('This video is unavailable')) {
179
151
  console.error(chalk.red('This video is unavailable. It may have been removed or is private.'));
180
- } else {
152
+ }
153
+ else {
181
154
  console.error(chalk.red('An unexpected error occurred during the download process:'), error.message);
182
155
  }
183
156
  process.exit(1);
184
157
  }
185
- }
158
+ }
package/src/prompts.js CHANGED
@@ -71,8 +71,8 @@ export async function getQuality(format) {
71
71
  return answers.quality;
72
72
  }
73
73
 
74
- export async function getOutputPath(defaultFilename = 'download') {
75
- const defaultPath = path.join(process.cwd(), sanitizeFilename(defaultFilename));
74
+ export async function getOutputPath() { // Removed defaultFilename parameter
75
+ const defaultPath = process.cwd(); // Default to current working directory
76
76
 
77
77
  const questions = [
78
78
  {
@@ -90,5 +90,5 @@ export async function getOutputPath(defaultFilename = 'download') {
90
90
  },
91
91
  ];
92
92
  const answers = await inquirer.prompt(questions);
93
- return answers.outputPath;
93
+ return answers.outputPath; // Returns just the directory path
94
94
  }