MovieMapper/utils/fileUtils.js

223 lines
7.0 KiB
JavaScript

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>} - 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.quality;
fileData.fps = quality.fps;
} catch (error) {
console.warn(`Failed to extract quality for ${filePath}:`, error.message);
fileData.quality = 'unknown';
fileData.fps = 'unknown';
}
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<string>} - 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<Object>} - 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<Object>} - 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
};