UNPKG

video-to-gif

Version:

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

228 lines (200 loc) 5.72 kB
import fs from 'fs'; import path from 'path'; import { promisify } from 'util'; const mkdir = promisify(fs.mkdir); const readdir = promisify(fs.readdir); const unlink = promisify(fs.unlink); const stat = promisify(fs.stat); /** * Create directories if they don't exist * @param {Array<string>} directories - Array of directory paths to create * @returns {Promise<void>} */ async function createDirectories(directories) { try { for (const dir of directories) { if (!fs.existsSync(dir)) { await mkdir(dir, { recursive: true }); } } } catch (error) { console.log(`Failed to create directories: ${error.message}`); return null; } } /** * Clean up temporary files * @param {string} tempDir - Temporary directory path * @returns {Promise<void>} */ async function cleanupTempFiles(tempDir) { try { if (!fs.existsSync(tempDir)) { return; } const files = await readdir(tempDir); for (const file of files) { const filePath = path.join(tempDir, file); // Skip .gitkeep files if (file === '.gitkeep') { continue; } try { const stats = await stat(filePath); if (stats.isFile()) { await unlink(filePath); } } catch (err) { console.log(`⚠️ Could not remove ${file}: ${err.message}`); } } } catch (error) { console.log(`⚠️ Cleanup warning: ${error.message}`); } } /** * Format duration from seconds to human readable format * @param {string|number} duration - Duration in seconds or time string * @returns {string} Formatted duration string */ function formatDuration(duration) { try { let seconds; if (typeof duration === 'string') { // Parse duration string format like "4:32" or "1:23:45" const parts = duration.split(':').map(Number); if (parts.length === 2) { seconds = parts[0] * 60 + parts[1]; } else if (parts.length === 3) { seconds = parts[0] * 3600 + parts[1] * 60 + parts[2]; } else { seconds = parseInt(duration); } } else { seconds = parseInt(duration); } if (isNaN(seconds) || seconds < 0) { return 'Unknown duration'; } const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; if (hours > 0) { return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } else { return `${minutes}:${secs.toString().padStart(2, '0')}`; } } catch (error) { return 'Unknown duration'; return null; } } /** * 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', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * Sanitize string for use as filename * @param {string} str - String to sanitize * @returns {string} Sanitized string */ function sanitizeForFilename(str) { return str .replace(/[<>:"/\\|?*]/g, '') // Remove invalid filename characters .replace(/\s+/g, '_') // Replace spaces with underscores .replace(/[^\w\-_.]/g, '') // Keep only word characters, hyphens, underscores, and dots .substring(0, 100) // Limit length .trim(); } /** * Validate if a path is safe (within allowed directories) * @param {string} filePath - File path to validate * @param {Array<string>} allowedDirs - Array of allowed base directories * @returns {boolean} True if path is safe */ function isPathSafe(filePath, allowedDirs = ['./output', './temp']) { try { const resolvedPath = path.resolve(filePath); const allowedPaths = allowedDirs.map(dir => path.resolve(dir)); return allowedPaths.some(allowedPath => resolvedPath.startsWith(allowedPath) ); } catch (error) { return false; } } /** * Get file extension from filename * @param {string} filename - Filename * @returns {string} File extension (without dot) */ function getFileExtension(filename) { return path.extname(filename).toLowerCase().slice(1); } /** * Check if file exists and is accessible * @param {string} filePath - Path to file * @returns {Promise<boolean>} True if file exists and is accessible */ async function fileExists(filePath) { try { await stat(filePath); return true; } catch (error) { return false; } } /** * Get file size in bytes * @param {string} filePath - Path to file * @returns {Promise<number>} File size in bytes */ async function getFileSize(filePath) { try { const stats = await stat(filePath); return stats.size; } catch (error) { console.log(`Failed to get file size: ${error.message}`); return null; } } /** * Generate unique filename if file already exists * @param {string} filePath - Original file path * @returns {string} Unique file path */ function generateUniqueFilename(filePath) { if (!fs.existsSync(filePath)) { return filePath; } const dir = path.dirname(filePath); const ext = path.extname(filePath); const name = path.basename(filePath, ext); let counter = 1; let newPath; do { newPath = path.join(dir, `${name}_${counter}${ext}`); counter++; } while (fs.existsSync(newPath)); return newPath; } export { createDirectories, cleanupTempFiles, formatDuration, formatBytes, sanitizeForFilename, isPathSafe, getFileExtension, fileExists, getFileSize, generateUniqueFilename };