Skip to content

Instantly share code, notes, and snippets.

@LuckyArdhika
Created March 17, 2025 07:41
Show Gist options
  • Save LuckyArdhika/9656e596ad212338996c7f8b37802894 to your computer and use it in GitHub Desktop.
Save LuckyArdhika/9656e596ad212338996c7f8b37802894 to your computer and use it in GitHub Desktop.
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