UNPKG

@opensubtitles/video-metadata-extractor

Version:

A comprehensive NPM package for video metadata extraction and subtitle processing using FFmpeg WASM. Supports metadata extraction, individual subtitle extraction, batch subtitle extraction with ZIP downloads, and memory-safe processing of files of any siz

241 lines 7.08 kB
/** * Common utility functions used across the application * Centralizes reusable functionality to eliminate duplication */ import { PROCESSING_CONSTANTS, LANGUAGE_CODES, SUPPORTED_FORMATS, ERROR_MESSAGES } from '../constants/index.js'; /** * Async retry utility with exponential backoff */ export async function withRetry(operation, maxAttempts = PROCESSING_CONSTANTS.RETRY.MAX_ATTEMPTS, baseDelay = PROCESSING_CONSTANTS.RETRY.BASE_DELAY) { let lastError; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await operation(); } catch (error) { lastError = error; if (attempt === maxAttempts) { throw lastError; } // Exponential backoff const delay = baseDelay * Math.pow(2, attempt - 1); await sleep(delay); } } throw lastError; } /** * Safe async operation wrapper that returns Result type */ export async function safeAsync(operation) { try { const data = await operation(); return { success: true, data }; } catch (error) { return { success: false, error: error }; } } /** * Sleep utility for delays */ export const sleep = (ms) => { return new Promise(resolve => setTimeout(resolve, ms)); }; /** * Format file size in human-readable format * Eliminates duplication across multiple files */ export function formatFileSize(bytes) { if (bytes === 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; const k = 1024; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${units[i]}`; } /** * Format duration from seconds to human-readable format */ export function formatDuration(seconds) { const totalSeconds = typeof seconds === 'string' ? parseFloat(seconds) : seconds; if (isNaN(totalSeconds)) return 'Unknown'; const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const secs = Math.floor(totalSeconds % 60); if (hours > 0) { return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } return `${minutes}:${secs.toString().padStart(2, '0')}`; } /** * Get file format from filename */ export function getFormatFromFileName(filename) { const extension = filename.split('.').pop()?.toLowerCase(); return extension || 'unknown'; } /** * Validate file extension and properties */ export function validateFile(file) { const errors = []; const extension = getFormatFromFileName(file.name); // Check file size if (file.size === 0) { errors.push(ERROR_MESSAGES.FILE.EMPTY); } // Check file format if (!SUPPORTED_FORMATS.ALL.includes(extension)) { errors.push(`${ERROR_MESSAGES.FILE.UNSUPPORTED_FORMAT}: ${extension}`); } return { isValid: errors.length === 0, extension, size: file.size, errors }; } /** * Generate subtitle filename with language code and metadata */ export function generateSubtitleFilename(movieFilename, language, isForced, codecName) { // Remove extension from movie filename const nameWithoutExt = movieFilename.replace(/\.[^/.]+$/, ''); // Get standardized language code const langCode = language ? (LANGUAGE_CODES[language.toLowerCase()] || language.toLowerCase()) : 'unknown'; // Determine file extension based on codec let extension = 'srt'; // Default to SRT if (codecName) { const codec = codecName.toLowerCase(); if (codec.includes('ass') || codec.includes('ssa')) { extension = 'ass'; } else if (codec.includes('vtt') || codec.includes('webvtt')) { extension = 'vtt'; } else if (codec.includes('srt') || codec.includes('subrip')) { extension = 'srt'; } else if (codec.includes('dvd') || codec.includes('vobsub')) { extension = 'srt'; // Convert DVD subtitles to SRT } } // Build filename: MovieName.lang[.forced].ext let filename = `${nameWithoutExt}.${langCode}`; if (isForced) { filename += '.forced'; } filename += `.${extension}`; return { filename, extension }; } /** * Safely decode data for preview with proper error handling */ export function safeDecodePreview(data, maxLength = PROCESSING_CONSTANTS.LIMITS.MAX_PROGRESSIVE_CHUNKS) { try { if (typeof data === 'string') { return data.slice(0, maxLength); } const previewData = data.slice(0, Math.min(maxLength, data.length)); return new TextDecoder('utf-8', { fatal: false }).decode(previewData); } catch (error) { return '[Preview unavailable - encoding error]'; } } /** * Create processing statistics tracker */ export function createProcessingStats(fileSize) { return { startTime: Date.now(), fileSize, chunksProcessed: 0 }; } /** * Update processing statistics */ export function updateProcessingStats(stats, chunksProcessed, memoryUsed) { return { ...stats, chunksProcessed, memoryUsed, endTime: Date.now() }; } /** * Calculate processing duration */ export function getProcessingDuration(stats) { if (!stats.endTime) return 0; return stats.endTime - stats.startTime; } /** * Debounce function for performance optimization */ export function debounce(func, wait) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; } /** * Throttle function for performance optimization */ export function throttle(func, limit) { let inThrottle; return (...args) => { if (!inThrottle) { func(...args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } /** * Deep clone utility for objects */ export function deepClone(obj) { if (obj === null || typeof obj !== 'object') return obj; if (obj instanceof Date) return new Date(obj.getTime()); if (obj instanceof Array) return obj.map(item => deepClone(item)); if (typeof obj === 'object') { const clonedObj = {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { clonedObj[key] = deepClone(obj[key]); } } return clonedObj; } return obj; } /** * Check if running in browser environment */ export const isBrowser = () => { return typeof window !== 'undefined' && typeof document !== 'undefined'; }; /** * Check if WebAssembly is supported */ export const isWebAssemblySupported = () => { return typeof WebAssembly !== 'undefined'; }; /** * Generate unique ID for operations */ export function generateUniqueId() { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } //# sourceMappingURL=common.js.map