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