@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
JavaScript
/**
* 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