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 * @param {string} directoryPath - Path to the directory to scan * @returns {Promise} - Array of media file paths */ async function scanDirectory(directoryPath) { try { const files = fs.readdirSync(directoryPath); const mediaFiles = []; for (const file of files) { const filePath = path.join(directoryPath, file); try { const stat = fs.statSync(filePath); if (stat.isDirectory()) { // Recursively scan subdirectories const subDirFiles = await scanDirectory(filePath); mediaFiles.push(...subDirFiles); } else if (isMediaFile(filePath)) { // Extract duration and quality for media files let fileData = { path: filePath, name: file, size: stat.size, modified: stat.mtime, duration: '00:00', isProblematic: false, quality: 'unknown' }; 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; } catch (error) { console.warn(`Failed to extract quality for ${filePath}:`, error.message); } mediaFiles.push(fileData); } } catch (fileError) { // Skip files/directories that cause permission errors console.warn(`Skipping file/directory due to permission error: ${filePath}`); continue; } } return mediaFiles; } 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 fs = require('fs'); const stats = fs.statSync(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 information (e.g., "1080p30fps") */ 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('unknown'); return; } try { // Find the video stream const videoStream = metadata.streams.find(stream => stream.codec_type === 'video'); if (!videoStream) { resolve('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 = ''; 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`; } } // Format the quality string if (quality !== 'unknown' && fps) { resolve(`${quality}${fps}`); } else if (quality !== 'unknown') { resolve(quality); } else if (fps) { resolve(fps); } else { resolve('unknown'); } } catch (error) { console.warn(`Error extracting quality for ${filePath}:`, error.message); resolve('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 };