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
JavaScript
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
};