const fs = require('fs'); const path = require('path'); const ffmpeg = require('fluent-ffmpeg'); // Supported media file extensions const MEDIA_EXTENSIONS = ['.mp4', '.mkv', '.avi', '.mov', '.flv', '.webm']; /** * Check if a file has a media extension * @param {string} filePath - Path to the file * @returns {boolean} - True if file is a media file */ function isMediaFile(filePath) { const ext = path.extname(filePath).toLowerCase(); return MEDIA_EXTENSIONS.includes(ext); } /** * Scan a directory for media files and folders (non-recursive) * @param {string} directoryPath - Path to the directory to scan * @param {Function} progressCallback - Optional callback for progress updates (current, total, fileName) * @returns {Promise} - Array of media files and folders */ async function scanDirectory(directoryPath, progressCallback = null) { try { const files = await fs.promises.readdir(directoryPath); const items = []; // First pass: collect folders and media files to process const folders = []; const mediaFilesToProcess = []; for (const file of files) { // Skip hidden files if (file.startsWith('.')) continue; const filePath = path.join(directoryPath, file); try { const stat = await fs.promises.stat(filePath); if (stat.isDirectory()) { folders.push({ path: filePath, name: file, isFolder: true, duration: '', quality: '', fps: '' }); } else if (isMediaFile(filePath)) { mediaFilesToProcess.push({ file, filePath, stat }); } } catch (fileError) { console.warn(`Skipping file/directory due to permission error: ${filePath}`); continue; } } // Add folders first (no processing needed) items.push(...folders); // Helper to yield to event loop (allows IPC messages to be sent) const yieldToEventLoop = () => new Promise(resolve => setImmediate(resolve)); // Process media files with progress updates const totalMedia = mediaFilesToProcess.length; // Send initial progress if (progressCallback && totalMedia > 0) { progressCallback(0, totalMedia, 'Starting...'); await yieldToEventLoop(); } for (let i = 0; i < mediaFilesToProcess.length; i++) { const { file, filePath, stat } = mediaFilesToProcess[i]; // Report progress before processing this file if (progressCallback) { progressCallback(i, totalMedia, file); await yieldToEventLoop(); } let fileData = { path: filePath, name: file, size: stat.size, modified: stat.mtime, duration: '00:00', isProblematic: false, quality: 'unknown', isFolder: false }; try { const durationResult = await extractFileDuration(filePath); fileData.duration = durationResult.duration; fileData.isProblematic = durationResult.isProblematic; } catch (error) { console.warn(`Failed to extract duration for ${filePath}:`, error.message); } try { const quality = await extractVideoQuality(filePath); fileData.quality = quality.quality; fileData.fps = quality.fps; } catch (error) { console.warn(`Failed to extract quality for ${filePath}:`, error.message); fileData.quality = 'unknown'; fileData.fps = 'unknown'; } items.push(fileData); // Report completion of this file if (progressCallback) { progressCallback(i + 1, totalMedia, file); } } // Sort: folders first, then files items.sort((a, b) => { if (a.isFolder && !b.isFolder) return -1; if (!a.isFolder && b.isFolder) return 1; return a.name.localeCompare(b.name); }); return items; } catch (error) { throw new Error(`Failed to scan directory: ${error.message}`); } } /** * Extract file duration using ffmpeg * @param {string} filePath - Path to the media file * @returns {Promise} - Duration in mm:ss format */ async function extractFileDuration(filePath) { return new Promise((resolve, reject) => { ffmpeg.ffprobe(filePath, (err, metadata) => { if (err) { console.warn(`ffprobe error for ${filePath}:`, err.message); // Try alternative approach - attempt to get duration from file stats or use fallback try { const stats = await fs.promises.stat(filePath); console.log(`File size for ${filePath}: ${stats.size} bytes`); // For now, return 00:00 as fallback for problematic files resolve({ duration: '00:00', isProblematic: true }); } catch (statsError) { console.warn(`Could not get file stats for ${filePath}:`, statsError.message); resolve({ duration: '00:00', isProblematic: true }); } return; } const duration = metadata.format.duration; if (!duration) { resolve({ duration: '00:00', isProblematic: false }); return; } // Convert seconds to mm:ss format const minutes = Math.floor(duration / 60); const seconds = Math.floor(duration % 60); const formattedDuration = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; resolve({ duration: formattedDuration, isProblematic: false }); }); }); } /** * Extract video quality information using ffmpeg * @param {string} filePath - Path to the media file * @returns {Promise} - Quality and frame rate information */ async function extractVideoQuality(filePath) { return new Promise((resolve, reject) => { ffmpeg.ffprobe(filePath, (err, metadata) => { if (err) { console.warn(`ffprobe error for quality extraction ${filePath}:`, err.message); resolve({ quality: 'unknown', fps: 'unknown' }); return; } try { // Find the video stream const videoStream = metadata.streams.find(stream => stream.codec_type === 'video'); if (!videoStream) { resolve({ quality: 'unknown', fps: 'unknown' }); return; } // Get resolution const width = videoStream.width || 0; const height = videoStream.height || 0; // Determine quality based on height let quality = 'unknown'; if (height >= 2160) { quality = '4K'; } else if (height >= 1440) { quality = '1440p'; } else if (height >= 1080) { quality = '1080p'; } else if (height >= 720) { quality = '720p'; } else if (height >= 480) { quality = '480p'; } else { quality = 'unknown'; } // Get frame rate let fps = 'unknown'; if (videoStream.r_frame_rate) { // Parse frame rate like "30/1" or "25/1" const fpsParts = videoStream.r_frame_rate.split('/'); if (fpsParts.length === 2) { const fpsValue = Math.round(parseInt(fpsParts[0]) / parseInt(fpsParts[1])); fps = `${fpsValue}fps`; } else { const fpsValue = Math.round(parseFloat(videoStream.r_frame_rate)); fps = `${fpsValue}fps`; } } // Return both quality and fps resolve({ quality: quality, fps: fps }); } catch (error) { console.warn(`Error extracting quality for ${filePath}:`, error.message); resolve({ quality: 'unknown', fps: 'unknown' }); } }); }); } /** * Extract file metadata (duration and quality) * @param {string} filePath - Path to the media file * @returns {Promise} - File metadata */ async function extractFileMetadata(filePath) { // Extract duration and quality using ffmpeg let duration = '00:00'; let quality = 'unknown'; try { duration = await extractFileDuration(filePath); } catch (error) { console.warn(`Failed to extract duration for ${filePath}:`, error.message); } try { quality = await extractVideoQuality(filePath); } catch (error) { console.warn(`Failed to extract quality for ${filePath}:`, error.message); } return { duration: duration, quality: quality }; } module.exports = { scanDirectory, extractFileMetadata, isMediaFile, extractFileDuration, extractVideoQuality };