UNPKG

youtube-dl-og

Version:

A powerful YouTube video downloader and audio converter package with support for multiple quality levels

389 lines (329 loc) 14.7 kB
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 };