228 lines
6.9 KiB
JavaScript
228 lines
6.9 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;
|
|
} 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<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<string>} - 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<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
|
|
};
|