UNPKG

video-to-gif

Version:

A Node.js module that searches YouTube videos by song name and converts clips to animated GIFs

145 lines (125 loc) 4.29 kB
import ytdl from '@distube/ytdl-core'; import fs from 'fs'; import path from 'path'; import { promisify } from 'util'; import { pipeline } from 'stream'; const pipelineAsync = promisify(pipeline); /** * Download a YouTube video * @param {string} videoId - YouTube video ID * @param {string} outputDir - Directory to save the video * @param {string} quality - Video quality preference * @returns {Promise<string>} Path to the downloaded video file */ async function downloadVideo(videoId, outputDir, quality = 'high') { try { const videoUrl = `https://www.youtube.com/watch?v=${videoId}`; // Verify video exists and get info const info = await ytdl.getInfo(videoUrl); if (!info) { console.log('Failed to get video information'); return null; } // Get video title for filename (sanitized) const title = sanitizeFilename(info.videoDetails.title); const outputPath = path.join(outputDir, `${title}_${videoId}.mp4`); // Check if file already exists if (fs.existsSync(outputPath)) { return outputPath; } // Get available formats const formats = ytdl.filterFormats(info.formats, 'videoandaudio'); if (formats.length === 0) { console.log('No suitable video formats found'); } // Select format based on quality preference let selectedFormat; switch (quality.toLowerCase()) { case 'highest': selectedFormat = ytdl.chooseFormat(formats, { quality: 'highest' }); break; case 'high': selectedFormat = ytdl.chooseFormat(formats, { quality: 'highestvideo' }); break; case 'medium': selectedFormat = ytdl.chooseFormat(formats, { quality: 'medium' }); break; case 'low': selectedFormat = ytdl.chooseFormat(formats, { quality: 'lowest' }); break; default: selectedFormat = ytdl.chooseFormat(formats, { quality: 'highestvideo' }); } if (!selectedFormat) { console.log('No suitable format found for the specified quality'); } // Create download stream const videoStream = ytdl(videoUrl, { format: selectedFormat }); const writeStream = fs.createWriteStream(outputPath); // Download the video await pipelineAsync(videoStream, writeStream); // Verify file was created and has content const stats = fs.statSync(outputPath); if (stats.size === 0) { console.log('Downloaded file is empty'); return null; } return outputPath; } catch (error) { console.log('❌ Download failed:', error.message); console.log(`Failed to download video: ${error.message}`); return null; } } /** * Get video information without downloading * @param {string} videoId - YouTube video ID * @returns {Promise<Object>} Video information */ async function getVideoInfo(videoId) { try { const videoUrl = `https://www.youtube.com/watch?v=${videoId}`; const info = await ytdl.getInfo(videoUrl); return { title: info.videoDetails.title, duration: parseInt(info.videoDetails.lengthSeconds), author: info.videoDetails.author.name, viewCount: parseInt(info.videoDetails.viewCount), uploadDate: info.videoDetails.uploadDate, description: info.videoDetails.description }; } catch (error) { console.log(`Failed to get video info: ${error.message}`); return null; } } /** * Sanitize filename by removing invalid characters * @param {string} filename - Original filename * @returns {string} Sanitized filename */ function sanitizeFilename(filename) { return filename .replace(/[<>:"/\\|?*]/g, '') // Remove invalid characters .replace(/\s+/g, '_') // Replace spaces with underscores .substring(0, 100) // Limit length .trim(); } /** * Format bytes to human readable format * @param {number} bytes - Number of bytes * @returns {string} Formatted string */ function formatBytes(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } export { downloadVideo, getVideoInfo, sanitizeFilename, formatBytes };