youtube-dl-og
Version:
A powerful YouTube video downloader package with support for multiple quality levels
389 lines (329 loc) • 14.7 kB
JavaScript
const fs = require('fs');
const path = require('path');
const ytdl = require('ytdl-core');
const youtubeDl = require('youtube-dl-exec');
const ffmpegPath = require('ffmpeg-static');
async function downloadVideo(url, options = {}) {
if (!ytdl.validateURL(url)) {
throw new Error('Invalid YouTube URL');
}
const defaultOptions = {
quality: 'highest',
format: 'mp4',
output: './video.mp4',
audioOnly: false,
formatOption: null
};
const finalOptions = { ...defaultOptions, ...options };
try {
console.log('Fetching video information...');
const info = await ytdl.getBasicInfo(url).catch(err => {
console.error('Error fetching video info with ytdl:', err.message);
});
const videoTitle = info ? info.videoDetails.title : 'YouTube Video';
console.log(`Downloading: ${videoTitle}`);
let outputPath = finalOptions.output;
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
console.log(`Created directory: ${outputDir}`);
}
const youtubeDlOptions = {
output: outputPath,
noCheckCertificate: true,
noWarnings: true,
preferFreeFormats: true,
addHeader: [
'referer:youtube.com',
'user-agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
]
};
if (ffmpegPath) {
youtubeDlOptions.ffmpegLocation = ffmpegPath;
console.log(`Using ffmpeg from: ${ffmpegPath}`);
}
if (finalOptions.audioOnly) {
console.log('Downloading audio only...');
youtubeDlOptions.format = 'bestaudio/best';
} else {
console.log('Downloading video...');
if (finalOptions.formatOption) {
youtubeDlOptions.format = finalOptions.formatOption;
console.log(`Using custom format: ${finalOptions.formatOption}`);
}
else if (finalOptions.format === 'mp4') {
youtubeDlOptions.format = 'best[ext=mp4]/best';
} else {
youtubeDlOptions.format = `best[ext=${finalOptions.format}]/best`;
}
}
console.log('Starting download with youtube-dl-exec...');
try {
await youtubeDl(url, youtubeDlOptions);
console.log(`Download completed: ${outputPath}`);
return {
title: videoTitle,
filePath: outputPath
};
} catch (err) {
console.error('Error in youtube-dl download:', err.message);
console.log('Trying alternative approach...');
if (finalOptions.audioOnly) {
try {
const altOptions = {
output: outputPath,
format: 'bestaudio',
ffmpegLocation: ffmpegPath,
noCheckCertificate: true,
noWarnings: true
};
await youtubeDl(url, altOptions);
console.log(`Download completed with alternative method: ${outputPath}`);
return {
title: videoTitle,
filePath: outputPath
};
} catch (altErr) {
console.error('Error in alternative download:', altErr.message);
console.log('Trying last resort method with ytdl-core...');
return new Promise((resolve, reject) => {
try {
const stream = ytdl(url, {
quality: 'highestaudio',
filter: 'audioonly'
});
const fileStream = fs.createWriteStream(outputPath);
stream.pipe(fileStream);
fileStream.on('finish', () => {
console.log(`Download completed with ytdl-core: ${outputPath}`);
resolve({
title: videoTitle,
filePath: outputPath
});
});
fileStream.on('error', (err) => {
console.error('Error writing file:', err);
reject(err);
});
} catch (e) {
console.error('Error in ytdl-core method:', e.message);
reject(new Error(`All download methods failed: ${e.message}`));
}
});
}
} else {
throw new Error(`Download failed: ${err.message}`);
}
}
} catch (err) {
console.error('Error:', err.message);
throw err;
}
}
async function downloadAllQualities(url, options = {}) {
if (!ytdl.validateURL(url)) {
throw new Error('Invalid YouTube URL');
}
const defaultOptions = {
outputDir: './downloads',
format: 'mp4',
qualities: ['720p'],
includeAudioOnly: true
};
const finalOptions = { ...defaultOptions, ...options };
try {
console.log('Fetching video information for multi-quality download...');
const videoInfo = await getVideoInfo(url);
const videoTitle = videoInfo.title;
console.log(`Preparing to download "${videoTitle}" in multiple qualities...`);
if (!fs.existsSync(finalOptions.outputDir)) {
fs.mkdirSync(finalOptions.outputDir, { recursive: true });
console.log(`Created output directory: ${finalOptions.outputDir}`);
}
const formats = videoInfo.formats;
console.log(`Found ${formats.length} available formats`);
const formatsByResolution = {};
formats.forEach(format => {
if (format.hasVideo) {
const resolution = format.resolution.split('x')[1] || 'unknown';
if (resolution && resolution !== 'unknown') {
const qualityKey = `${resolution}p`;
if (!formatsByResolution[qualityKey]) {
formatsByResolution[qualityKey] = [];
}
formatsByResolution[qualityKey].push(format);
}
}
});
const availableQualities = Object.keys(formatsByResolution).sort((a, b) => {
return parseInt(a.replace('p', '')) - parseInt(b.replace('p', ''));
});
console.log('Available quality levels:', availableQualities.join(', '));
let requestedQualities = [...finalOptions.qualities];
if (requestedQualities.includes('highest') && availableQualities.length > 0) {
const highestQuality = availableQualities[availableQualities.length - 1];
requestedQualities = requestedQualities.filter(q => q !== 'highest');
requestedQualities.push(highestQuality);
console.log(`Replaced 'highest' with ${highestQuality}`);
}
if (requestedQualities.includes('lowest') && availableQualities.length > 0) {
const lowestQuality = availableQualities[0];
requestedQualities = requestedQualities.filter(q => q !== 'lowest');
requestedQualities.push(lowestQuality);
console.log(`Replaced 'lowest' with ${lowestQuality}`);
}
requestedQualities = [...new Set(requestedQualities)];
console.log('Downloading the following qualities:', requestedQualities.join(', '));
const results = [];
for (const quality of requestedQualities) {
try {
const sanitizedTitle = videoTitle.replace(/[^\w\s]/g, '_');
const outputPath = path.join(
finalOptions.outputDir,
`${sanitizedTitle}_${quality}.${finalOptions.format}`
);
console.log(`\nDownloading ${quality} quality...`);
const height = parseInt(quality.replace('p', ''));
let formatOption;
if (!isNaN(height)) {
formatOption = `bestvideo[height=${height}]+bestaudio/best[height<=${height}]/best`;
} else {
formatOption = 'bestvideo+bestaudio/best';
}
const result = await downloadVideo(url, {
quality: quality,
format: finalOptions.format,
output: outputPath,
formatOption: formatOption
});
results.push({
quality: quality,
...result
});
} catch (err) {
console.error(`Error downloading ${quality} quality:`, err.message);
results.push({
quality: quality,
error: err.message
});
}
}
if (finalOptions.includeAudioOnly) {
try {
const sanitizedTitle = videoTitle.replace(/[^\w\s]/g, '_');
const audioOutputPath = path.join(
finalOptions.outputDir,
`${sanitizedTitle}_audio-only.mp3`
);
console.log('\nDownloading audio-only version...');
const result = await downloadVideo(url, {
audioOnly: true,
output: audioOutputPath
});
results.push({
quality: 'audio-only',
...result
});
} catch (err) {
console.error('Error downloading audio-only version:', err.message);
results.push({
quality: 'audio-only',
error: err.message
});
}
}
console.log(`\nCompleted multi-quality download for "${videoTitle}"`);
return results;
} catch (err) {
console.error('Error in multi-quality download:', err.message);
throw err;
}
}
async function downloadWithYoutubeDL(url, videoTitle, outputPath, options) {
try {
await youtubeDl(url, options);
console.log(`Download completed: ${outputPath}`);
return {
title: videoTitle,
filePath: outputPath
};
} catch (err) {
console.error('Error in youtube-dl download:', err.message);
throw new Error(`Download failed: ${err.message}`);
}
}
async function getVideoInfo(url) {
if (!ytdl.validateURL(url)) {
throw new Error('Invalid YouTube URL');
}
try {
console.log('Fetching video information...');
try {
const info = await ytdl.getBasicInfo(url, {
requestOptions: {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
},
}
});
return {
title: info.videoDetails.title || 'Unknown Title',
description: info.videoDetails.description || 'No description',
lengthSeconds: info.videoDetails.lengthSeconds || '0',
viewCount: info.videoDetails.viewCount || '0',
author: info.videoDetails.author || { name: 'Unknown' },
formats: info.formats.map(format => ({
itag: format.itag || '0',
container: format.container || 'unknown',
qualityLabel: format.qualityLabel || 'N/A',
resolution: format.height ? `${format.width}x${format.height}` : 'N/A',
fps: format.fps || 0,
hasAudio: format.hasAudio || false,
hasVideo: format.hasVideo || false,
}))
};
} catch (err) {
console.log('Falling back to youtube-dl-exec for video info...');
const result = await youtubeDl(url, {
dumpSingleJson: true,
noWarnings: true,
noCheckCertificate: true,
ffmpegLocation: ffmpegPath,
preferFreeFormats: true,
addHeader: [
'referer:youtube.com',
'user-agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
]
});
return {
title: result.title || 'Unknown Title',
description: result.description || 'No description',
lengthSeconds: result.duration || '0',
viewCount: result.view_count?.toString() || '0',
author: { name: result.uploader || 'Unknown' },
formats: result.formats?.map(format => ({
itag: format.format_id || '0',
container: format.ext || 'unknown',
qualityLabel: format.format_note || 'N/A',
resolution: format.width && format.height ? `${format.width}x${format.height}` : 'N/A',
fps: format.fps || 0,
hasAudio: Boolean(format.acodec !== 'none'),
hasVideo: Boolean(format.vcodec !== 'none'),
})) || []
};
}
} catch (err) {
console.error('Error getting video info:', err.message);
throw new Error(`Failed to get video info: ${err.message}`);
}
}
function isValidYoutubeUrl(url) {
return ytdl.validateURL(url);
}
module.exports = {
downloadVideo,
downloadAllQualities,
getVideoInfo,
isValidYoutubeUrl,
version: require('./package.json').version
};