Created
March 17, 2025 07:41
-
-
Save LuckyArdhika/9656e596ad212338996c7f8b37802894 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import fs from 'fs'; | |
import path from 'path'; | |
import fetch from 'node-fetch'; | |
import ytdl from 'ytdl-core'; | |
import { pipeline } from 'stream/promises'; | |
/** | |
* Downloads a video from a direct URL | |
* @param {string} url - The URL of the video to download | |
* @param {string} outputPath - The path where the video will be saved | |
* @param {Function} progressCallback - Optional callback for progress updates | |
* @returns {Promise<string>} - A promise that resolves to the path of the downloaded video | |
*/ | |
async function downloadVideoFromDirectUrl(url, outputPath, progressCallback) { | |
try { | |
// Create directory if it doesn't exist | |
const dir = path.dirname(outputPath); | |
if (!fs.existsSync(dir)) { | |
fs.mkdirSync(dir, { recursive: true }); | |
} | |
// Fetch the video | |
const response = await fetch(url); | |
if (!response.ok) { | |
throw new Error(`Failed to fetch video: ${response.status} ${response.statusText}`); | |
} | |
// Get the total size of the video | |
const totalSize = parseInt(response.headers.get('content-length') || '0', 10); | |
let downloadedSize = 0; | |
// Create a writable stream to save the video | |
const fileStream = fs.createWriteStream(outputPath); | |
// Create a transform stream to track progress | |
const progressStream = new (require('stream').Transform)({ | |
transform(chunk, encoding, callback) { | |
downloadedSize += chunk.length; | |
if (progressCallback && totalSize > 0) { | |
const progress = (downloadedSize / totalSize) * 100; | |
progressCallback(progress.toFixed(2), downloadedSize, totalSize); | |
} | |
callback(null, chunk); | |
} | |
}); | |
// Use pipeline to handle the streams properly | |
await pipeline( | |
response.body, | |
progressStream, | |
fileStream | |
); | |
console.log(`Video downloaded successfully to ${outputPath}`); | |
return outputPath; | |
} catch (error) { | |
console.error('Error downloading video:', error); | |
throw error; | |
} | |
} | |
/** | |
* Downloads a video from YouTube | |
* @param {string} url - The YouTube URL | |
* @param {string} outputPath - The path where the video will be saved | |
* @param {Object} options - Options for the download (quality, format, etc.) | |
* @param {Function} progressCallback - Optional callback for progress updates | |
* @returns {Promise<string>} - A promise that resolves to the path of the downloaded video | |
*/ | |
async function downloadYouTubeVideo(url, outputPath, options = {}, progressCallback) { | |
try { | |
// Verify that the URL is a valid YouTube URL | |
if (!ytdl.validateURL(url)) { | |
throw new Error('Invalid YouTube URL'); | |
} | |
// Create directory if it doesn't exist | |
const dir = path.dirname(outputPath); | |
if (!fs.existsSync(dir)) { | |
fs.mkdirSync(dir, { recursive: true }); | |
} | |
// Get video info | |
const info = await ytdl.getInfo(url); | |
console.log(`Downloading: ${info.videoDetails.title}`); | |
// Default options | |
const defaultOptions = { | |
quality: 'highest', | |
filter: 'videoandaudio', | |
...options | |
}; | |
// Create the download stream | |
const videoStream = ytdl(url, defaultOptions); | |
// Create a writable stream to save the video | |
const fileStream = fs.createWriteStream(outputPath); | |
// Track download progress | |
let downloadedSize = 0; | |
const totalSize = parseInt(info.videoDetails.lengthSeconds, 10) * 1000000; // Rough estimate | |
videoStream.on('progress', (_, downloaded, total) => { | |
if (progressCallback) { | |
const progress = (downloaded / total) * 100; | |
progressCallback(progress.toFixed(2), downloaded, total); | |
} | |
}); | |
// Use pipeline to handle the streams properly | |
await pipeline( | |
videoStream, | |
fileStream | |
); | |
console.log(`YouTube video downloaded successfully to ${outputPath}`); | |
return outputPath; | |
} catch (error) { | |
console.error('Error downloading YouTube video:', error); | |
throw error; | |
} | |
} | |
/** | |
* Downloads a video from any URL (detects YouTube URLs automatically) | |
* @param {string} url - The URL of the video to download | |
* @param {string} outputPath - The path where the video will be saved | |
* @param {Object} options - Options for YouTube downloads | |
* @param {Function} progressCallback - Optional callback for progress updates | |
* @returns {Promise<string>} - A promise that resolves to the path of the downloaded video | |
*/ | |
async function downloadVideo(url, outputPath, options = {}, progressCallback) { | |
// Check if it's a YouTube URL | |
if (url.includes('youtube.com') || url.includes('youtu.be')) { | |
return downloadYouTubeVideo(url, outputPath, options, progressCallback); | |
} else { | |
return downloadVideoFromDirectUrl(url, outputPath, progressCallback); | |
} | |
} | |
// Example usage | |
async function main() { | |
try { | |
// Example progress callback | |
const showProgress = (percent, downloaded, total) => { | |
process.stdout.write(`Downloading: ${percent}% (${formatBytes(downloaded)}/${formatBytes(total)})\r`); | |
}; | |
// Example 1: Download from a direct URL | |
const directUrl = 'https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4'; | |
await downloadVideo( | |
directUrl, | |
'./downloads/sample-video.mp4', | |
{}, | |
showProgress | |
); | |
// Example 2: Download from YouTube | |
// Note: Replace with a valid YouTube URL | |
const youtubeUrl = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'; | |
await downloadVideo( | |
youtubeUrl, | |
'./downloads/youtube-video.mp4', | |
{ quality: '18' }, // 360p | |
showProgress | |
); | |
console.log('\nAll downloads completed!'); | |
} catch (error) { | |
console.error('\nError in main function:', error); | |
} | |
} | |
// Helper function to format bytes | |
function formatBytes(bytes, decimals = 2) { | |
if (bytes === 0) return '0 Bytes'; | |
const k = 1024; | |
const dm = decimals < 0 ? 0 : decimals; | |
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; | |
const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; | |
} | |
// Uncomment to run the example | |
// main(); | |
// Export the functions | |
export { downloadVideo, downloadVideoFromDirectUrl, downloadYouTubeVideo }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment