568 lines
23 KiB
JavaScript
568 lines
23 KiB
JavaScript
/* eslint-disable no-plusplus */
|
||
/**
|
||
* Tdarr Plugin 00 - File Audit
|
||
*
|
||
* Read-only audit plugin that runs first in the pipeline.
|
||
* Logs file information and flags potential issues for downstream plugins.
|
||
* Makes NO changes to files - pure analysis and reporting.
|
||
*/
|
||
|
||
const details = () => ({
|
||
id: 'Tdarr_Plugin_00_file_audit',
|
||
Stage: 'Pre-processing',
|
||
Name: '00 - File Audit',
|
||
Type: 'Video',
|
||
Operation: 'Filter',
|
||
Description: `
|
||
**READ-ONLY** file auditor that logs comprehensive file information and flags potential issues.
|
||
Runs FIRST in the pipeline to provide early warning of problems.
|
||
|
||
**Reports**:
|
||
- Container format and compatibility notes for BOTH MKV and MP4
|
||
- All streams with codec details
|
||
- Potential issues (broken timestamps, incompatible codecs, corrupt streams)
|
||
- Standards compliance (HDR, color space, etc.)
|
||
|
||
**Never modifies files** - Filter type plugin that always passes files through.
|
||
`,
|
||
Version: '4.0.0',
|
||
Tags: 'filter,audit,analysis,diagnostic,pre-check',
|
||
Inputs: [
|
||
{
|
||
name: 'log_level',
|
||
type: 'string',
|
||
defaultValue: 'detailed*',
|
||
inputUI: {
|
||
type: 'dropdown',
|
||
options: ['minimal', 'detailed*', 'verbose'],
|
||
},
|
||
tooltip: 'minimal=issues only, detailed=streams+issues, verbose=everything including metadata',
|
||
},
|
||
],
|
||
});
|
||
|
||
// ============================================================================
|
||
// COMPATIBILITY DEFINITIONS
|
||
// ============================================================================
|
||
|
||
// Codecs incompatible with containers
|
||
const MKV_INCOMPATIBLE_CODECS = new Set(['mov_text', 'eia_608', 'timed_id3', 'bmp', 'tx3g']);
|
||
const MP4_INCOMPATIBLE_CODECS = new Set(['hdmv_pgs_subtitle', 'eia_608', 'subrip', 'timed_id3', 'ass', 'ssa', 'webvtt']);
|
||
|
||
// Containers with known timestamp issues
|
||
const TIMESTAMP_PROBLEM_CONTAINERS = new Set(['ts', 'mpegts', 'avi', 'mpg', 'mpeg', 'm2ts', 'vob']);
|
||
|
||
// Legacy codecs that often have timestamp/remux issues
|
||
const LEGACY_VIDEO_CODECS = {
|
||
'mpeg4': { risk: 'high', note: 'MPEG-4 Part 2 - often has timestamp issues' },
|
||
'msmpeg4v1': { risk: 'high', note: 'MS-MPEG4v1 - severe timestamp issues' },
|
||
'msmpeg4v2': { risk: 'high', note: 'MS-MPEG4v2 - severe timestamp issues' },
|
||
'msmpeg4v3': { risk: 'high', note: 'MS-MPEG4v3/DivX3 - severe timestamp issues' },
|
||
'mpeg1video': { risk: 'medium', note: 'MPEG-1 - may need re-encoding' },
|
||
'mpeg2video': { risk: 'medium', note: 'MPEG-2 - may have GOP issues' },
|
||
'wmv1': { risk: 'high', note: 'WMV7 - poor container compatibility' },
|
||
'wmv2': { risk: 'high', note: 'WMV8 - poor container compatibility' },
|
||
'wmv3': { risk: 'medium', note: 'WMV9 - may have issues in MKV/MP4' },
|
||
'rv10': { risk: 'high', note: 'RealVideo 1.0 - very limited support' },
|
||
'rv20': { risk: 'high', note: 'RealVideo 2.0 - very limited support' },
|
||
'rv30': { risk: 'high', note: 'RealVideo 3.0 - very limited support' },
|
||
'rv40': { risk: 'high', note: 'RealVideo 4.0 - very limited support' },
|
||
'vp6': { risk: 'medium', note: 'VP6 - legacy Flash codec' },
|
||
'vp6f': { risk: 'medium', note: 'VP6 Flash - legacy Flash codec' },
|
||
'flv1': { risk: 'medium', note: 'FLV/Sorenson Spark - legacy codec' },
|
||
};
|
||
|
||
// XviD/DivX codec tags that indicate packed bitstream issues
|
||
const XVID_DIVX_TAGS = new Set(['XVID', 'DIVX', 'DX50', 'DIV3', 'DIV4', 'DIV5', 'FMP4', 'MP4V', 'MP42', 'MP43']);
|
||
|
||
// Image codecs (cover art) that should be removed
|
||
const IMAGE_CODECS = new Set(['mjpeg', 'png', 'gif', 'bmp', 'webp', 'tiff']);
|
||
|
||
// Data stream codecs that cause issues
|
||
const PROBLEMATIC_DATA_CODECS = new Set(['bin_data', 'timed_id3', 'dvd_nav_packet']);
|
||
|
||
// ============================================================================
|
||
// UTILITY FUNCTIONS
|
||
// ============================================================================
|
||
|
||
const stripStar = (v) => (typeof v === 'string' ? v.replace(/\*/g, '') : v);
|
||
|
||
const sanitizeInputs = (inputs) => {
|
||
Object.keys(inputs).forEach((k) => { inputs[k] = stripStar(inputs[k]); });
|
||
return inputs;
|
||
};
|
||
|
||
const formatBitrate = (bps) => {
|
||
if (!bps || bps === 0) return 'unknown';
|
||
const kbps = Math.round(bps / 1000);
|
||
if (kbps >= 1000) return `${(kbps / 1000).toFixed(1)} Mbps`;
|
||
return `${kbps} kbps`;
|
||
};
|
||
|
||
const formatDuration = (seconds) => {
|
||
if (!seconds) return 'unknown';
|
||
const hrs = Math.floor(seconds / 3600);
|
||
const mins = Math.floor((seconds % 3600) / 60);
|
||
const secs = Math.floor(seconds % 60);
|
||
if (hrs > 0) return `${hrs}h ${mins}m ${secs}s`;
|
||
if (mins > 0) return `${mins}m ${secs}s`;
|
||
return `${secs}s`;
|
||
};
|
||
|
||
const formatSize = (bytes) => {
|
||
if (!bytes) return 'unknown';
|
||
const gb = bytes / (1024 * 1024 * 1024);
|
||
if (gb >= 1) return `${gb.toFixed(2)} GB`;
|
||
const mb = bytes / (1024 * 1024);
|
||
return `${mb.toFixed(1)} MB`;
|
||
};
|
||
|
||
// ============================================================================
|
||
// AUDIT CHECKS
|
||
// ============================================================================
|
||
|
||
/**
|
||
* Analyze container format and flag issues
|
||
*/
|
||
const auditContainer = (file) => {
|
||
const issues = [];
|
||
const info = [];
|
||
|
||
const container = (file.container || '').toLowerCase();
|
||
const formatName = file.ffProbeData?.format?.format_name || '';
|
||
|
||
info.push(`Container: ${container.toUpperCase()} (format: ${formatName})`);
|
||
|
||
// Check for timestamp-problematic containers
|
||
if (TIMESTAMP_PROBLEM_CONTAINERS.has(container)) {
|
||
issues.push(`⚠️ TIMESTAMP: ${container.toUpperCase()} containers often have timestamp issues requiring -fflags +genpts`);
|
||
}
|
||
|
||
// Check for containers that need special handling
|
||
if (['iso', 'vob', 'evo'].includes(container)) {
|
||
issues.push(`❌ UNSUPPORTED: ${container.toUpperCase()} requires manual conversion (HandBrake/MakeMKV)`);
|
||
}
|
||
|
||
// Note current container for user reference
|
||
if (!['mkv', 'mp4'].includes(container) && !['iso', 'vob', 'evo'].includes(container)) {
|
||
info.push(`📦 Current container will need remuxing to MKV or MP4`);
|
||
}
|
||
|
||
return { issues, info };
|
||
};
|
||
|
||
/**
|
||
* Analyze video streams
|
||
*/
|
||
const auditVideoStreams = (streams) => {
|
||
const issues = [];
|
||
const info = [];
|
||
|
||
const videoStreams = streams.filter(s => s.codec_type === 'video' && !IMAGE_CODECS.has((s.codec_name || '').toLowerCase()));
|
||
|
||
if (videoStreams.length === 0) {
|
||
issues.push('❌ NO VIDEO: No valid video stream found');
|
||
return { issues, info };
|
||
}
|
||
|
||
if (videoStreams.length > 1) {
|
||
issues.push(`⚠️ MULTI-VIDEO: ${videoStreams.length} video streams detected (unusual)`);
|
||
}
|
||
|
||
videoStreams.forEach((stream, idx) => {
|
||
const codec = (stream.codec_name || 'unknown').toLowerCase();
|
||
const codecTag = (stream.codec_tag_string || '').toUpperCase();
|
||
const width = stream.width || '?';
|
||
const height = stream.height || '?';
|
||
const fps = stream.r_frame_rate || stream.avg_frame_rate || '?';
|
||
const bitrate = stream.bit_rate || 0;
|
||
const pixFmt = stream.pix_fmt || 'unknown';
|
||
|
||
// Basic info
|
||
let streamInfo = `🎬 Video ${idx}: ${codec.toUpperCase()} ${width}x${height}`;
|
||
if (fps && fps !== '?') {
|
||
const [num, den] = fps.split('/').map(Number);
|
||
if (den && den > 0) streamInfo += ` @ ${(num / den).toFixed(2)}fps`;
|
||
}
|
||
if (bitrate) streamInfo += ` (${formatBitrate(bitrate)})`;
|
||
streamInfo += ` [${pixFmt}]`;
|
||
info.push(streamInfo);
|
||
|
||
// Check for legacy codec issues
|
||
if (LEGACY_VIDEO_CODECS[codec]) {
|
||
const legacy = LEGACY_VIDEO_CODECS[codec];
|
||
issues.push(`⚠️ LEGACY (${legacy.risk}): ${legacy.note}`);
|
||
}
|
||
|
||
// Check for XviD/DivX packed bitstream
|
||
if (codec === 'mpeg4' && XVID_DIVX_TAGS.has(codecTag)) {
|
||
issues.push(`⚠️ XVID/DIVX: ${codecTag} may have packed bitstream timestamp issues`);
|
||
}
|
||
|
||
// Check for divx_packed flag
|
||
if (stream.divx_packed === 'true' || stream.divx_packed === true) {
|
||
issues.push('❌ PACKED BITSTREAM: DivX packed bitstream detected - will need re-encoding');
|
||
}
|
||
|
||
// HDR detection
|
||
const colorTransfer = stream.color_transfer || '';
|
||
const colorPrimaries = stream.color_primaries || '';
|
||
const colorSpace = stream.color_space || '';
|
||
|
||
if (colorTransfer === 'smpte2084') {
|
||
info.push(' 🌈 HDR10 (PQ) detected - metadata preservation needed');
|
||
} else if (colorTransfer === 'arib-std-b67') {
|
||
info.push(' 🌈 HLG detected - metadata preservation needed');
|
||
}
|
||
|
||
if (colorPrimaries === 'bt2020' || colorSpace === 'bt2020nc') {
|
||
info.push(' 📺 BT.2020 color space detected');
|
||
}
|
||
|
||
// Check for unusual pixel formats
|
||
if (pixFmt.includes('12le') || pixFmt.includes('12be')) {
|
||
info.push(' ⚠️ 12-bit depth - may have limited player support');
|
||
}
|
||
|
||
// Check for interlaced content
|
||
if (stream.field_order && !['progressive', 'unknown'].includes(stream.field_order)) {
|
||
issues.push(`⚠️ INTERLACED: Field order "${stream.field_order}" - may need deinterlacing`);
|
||
}
|
||
});
|
||
|
||
return { issues, info };
|
||
};
|
||
|
||
/**
|
||
* Analyze audio streams - checks both MKV and MP4 compatibility
|
||
*/
|
||
const auditAudioStreams = (streams) => {
|
||
const issues = [];
|
||
const info = [];
|
||
|
||
const audioStreams = streams.filter(s => s.codec_type === 'audio');
|
||
|
||
if (audioStreams.length === 0) {
|
||
issues.push('⚠️ NO AUDIO: No audio streams found');
|
||
return { issues, info };
|
||
}
|
||
|
||
audioStreams.forEach((stream, idx) => {
|
||
const codec = (stream.codec_name || 'unknown').toLowerCase();
|
||
const channels = stream.channels || 0;
|
||
const sampleRate = stream.sample_rate || 0;
|
||
const bitrate = stream.bit_rate || 0;
|
||
const lang = stream.tags?.language || 'und';
|
||
const title = stream.tags?.title || '';
|
||
|
||
// Check for corrupt audio
|
||
if (channels === 0) {
|
||
issues.push(`❌ CORRUPT AUDIO ${idx}: 0 channels detected - stream will be removed`);
|
||
return;
|
||
}
|
||
|
||
if (sampleRate === 0) {
|
||
issues.push(`⚠️ CORRUPT AUDIO ${idx}: No sample rate detected`);
|
||
}
|
||
|
||
// Channel layout description
|
||
let channelDesc = `${channels}ch`;
|
||
if (channels === 1) channelDesc = 'Mono';
|
||
else if (channels === 2) channelDesc = 'Stereo';
|
||
else if (channels === 6) channelDesc = '5.1';
|
||
else if (channels === 8) channelDesc = '7.1';
|
||
|
||
let streamInfo = `🔊 Audio ${idx}: ${codec.toUpperCase()} ${channelDesc}`;
|
||
if (sampleRate) streamInfo += ` @ ${sampleRate}Hz`;
|
||
if (bitrate) streamInfo += ` (${formatBitrate(bitrate)})`;
|
||
streamInfo += ` [${lang}]`;
|
||
if (title) streamInfo += ` "${title}"`;
|
||
info.push(streamInfo);
|
||
|
||
// Check MP4-specific audio compatibility issues
|
||
if (['vorbis', 'opus'].includes(codec)) {
|
||
issues.push(`⚠️ [MP4 only] ${codec.toUpperCase()} has limited MP4 support (OK in MKV)`);
|
||
}
|
||
if (['dts', 'truehd'].includes(codec)) {
|
||
issues.push(`⚠️ [MP4 only] ${codec.toUpperCase()} not standard in MP4 (OK in MKV)`);
|
||
}
|
||
|
||
// Check for unusual audio codecs (both containers)
|
||
if (['cook', 'ra_144', 'ra_288', 'sipr', 'atrac3', 'atrac3p'].includes(codec)) {
|
||
issues.push(`⚠️ [BOTH] RARE CODEC: ${codec.toUpperCase()} - very limited support`);
|
||
}
|
||
});
|
||
|
||
return { issues, info };
|
||
};
|
||
|
||
/**
|
||
* Analyze subtitle streams - checks both MKV and MP4 compatibility
|
||
*/
|
||
const auditSubtitleStreams = (streams, file) => {
|
||
const issues = [];
|
||
const info = [];
|
||
|
||
const subStreams = streams.filter(s => s.codec_type === 'subtitle');
|
||
|
||
if (subStreams.length === 0) {
|
||
info.push('📝 Subtitles: None');
|
||
return { issues, info };
|
||
}
|
||
|
||
subStreams.forEach((stream, idx) => {
|
||
// Robust codec identification
|
||
let codec = (stream.codec_name || '').toLowerCase();
|
||
if (codec === 'none' || codec === 'unknown' || !codec) {
|
||
// Try metadata fallback
|
||
const codecTag = (stream.codec_tag_string || '').toUpperCase();
|
||
if (codecTag.includes('WEBVTT')) codec = 'webvtt';
|
||
else if (codecTag.includes('ASS')) codec = 'ass';
|
||
else if (codecTag.includes('SSA')) codec = 'ssa';
|
||
else {
|
||
const tagCodecId = (stream.tags?.CodecID || stream.tags?.codec_id || '').toUpperCase();
|
||
if (tagCodecId.includes('WEBVTT') || tagCodecId.includes('S_TEXT/WEBVTT')) codec = 'webvtt';
|
||
else if (tagCodecId.includes('ASS') || tagCodecId.includes('S_TEXT/ASS')) codec = 'ass';
|
||
else if (tagCodecId.includes('SSA') || tagCodecId.includes('S_TEXT/SSA')) codec = 'ssa';
|
||
}
|
||
}
|
||
|
||
// If still unknown, check MediaInfo/ExifTool if available
|
||
if (codec === 'none' || codec === 'unknown' || !codec) {
|
||
const mediaInfoCodec = (file.mediaInfo?.track?.find(t => t['@type'] === 'Text' && t.StreamOrder == stream.index)?.CodecID || '').toLowerCase();
|
||
if (mediaInfoCodec.includes('webvtt')) codec = 'webvtt';
|
||
else if (mediaInfoCodec.includes('ass')) codec = 'ass';
|
||
else if (mediaInfoCodec.includes('ssa')) codec = 'ssa';
|
||
}
|
||
|
||
codec = codec || 'unknown';
|
||
|
||
const lang = stream.tags?.language || 'und';
|
||
const title = stream.tags?.title || '';
|
||
const forced = stream.disposition?.forced === 1 ? ' [FORCED]' : '';
|
||
|
||
let streamInfo = `📝 Sub ${idx}: ${codec.toUpperCase()} [${lang}]${forced}`;
|
||
if (title) streamInfo += ` "${title}"`;
|
||
info.push(streamInfo);
|
||
|
||
// Check for specific problematic states
|
||
if (codec === 'unknown') {
|
||
issues.push(`⚠️ [BOTH] Subtitle stream ${idx} codec could not be identified - may cause transcode failure`);
|
||
}
|
||
|
||
// Check container-specific compatibility
|
||
const mkvIncompat = MKV_INCOMPATIBLE_CODECS.has(codec);
|
||
const mp4Incompat = MP4_INCOMPATIBLE_CODECS.has(codec);
|
||
|
||
if (mkvIncompat && mp4Incompat) {
|
||
issues.push(`⚠️ [BOTH] ${codec.toUpperCase()} incompatible with MKV and MP4`);
|
||
} else if (mkvIncompat) {
|
||
issues.push(`⚠️ [MKV only] ${codec.toUpperCase()} not compatible with MKV (OK in MP4)`);
|
||
} else if (mp4Incompat) {
|
||
issues.push(`⚠️ [MP4 only] ${codec.toUpperCase()} not compatible with MP4 (OK in MKV)`);
|
||
}
|
||
|
||
// Check for image-based subs that can't be converted to SRT
|
||
if (['hdmv_pgs_subtitle', 'dvd_subtitle', 'dvdsub'].includes(codec)) {
|
||
info.push(` ℹ️ Image-based subtitle - cannot convert to SRT`);
|
||
}
|
||
|
||
// Check for formats that will be converted
|
||
if (['ass', 'ssa', 'webvtt', 'mov_text'].includes(codec)) {
|
||
info.push(` ℹ️ Will convert to SRT for compatibility`);
|
||
}
|
||
});
|
||
|
||
return { issues, info };
|
||
};
|
||
|
||
/**
|
||
* Analyze other streams (data, attachments, images)
|
||
*/
|
||
const auditOtherStreams = (streams) => {
|
||
const issues = [];
|
||
const info = [];
|
||
|
||
// Image streams (cover art)
|
||
const imageStreams = streams.filter(s =>
|
||
(s.codec_type === 'video' && IMAGE_CODECS.has((s.codec_name || '').toLowerCase())) ||
|
||
s.disposition?.attached_pic === 1
|
||
);
|
||
|
||
if (imageStreams.length > 0) {
|
||
info.push(`🖼️ Cover Art: ${imageStreams.length} image stream(s) - will be removed`);
|
||
}
|
||
|
||
// Data streams
|
||
const dataStreams = streams.filter(s => s.codec_type === 'data');
|
||
dataStreams.forEach((stream, idx) => {
|
||
const codec = (stream.codec_name || 'unknown').toLowerCase();
|
||
|
||
if (PROBLEMATIC_DATA_CODECS.has(codec)) {
|
||
issues.push(`⚠️ DATA STREAM: ${codec} will cause muxing issues - will be removed`);
|
||
} else {
|
||
info.push(`📊 Data ${idx}: ${codec.toUpperCase()}`);
|
||
}
|
||
});
|
||
|
||
// Attachments (fonts, etc.)
|
||
const attachments = streams.filter(s => s.codec_type === 'attachment');
|
||
if (attachments.length > 0) {
|
||
info.push(`📎 Attachments: ${attachments.length} (fonts, etc.)`);
|
||
}
|
||
|
||
return { issues, info };
|
||
};
|
||
|
||
/**
|
||
* Analyze file-level metadata
|
||
*/
|
||
const auditFileMetadata = (file, logLevel) => {
|
||
const issues = [];
|
||
const info = [];
|
||
|
||
const format = file.ffProbeData?.format || {};
|
||
const duration = parseFloat(format.duration) || 0;
|
||
const size = file.statSync?.size || parseInt(format.size) || 0;
|
||
const bitrate = parseInt(format.bit_rate) || 0;
|
||
|
||
// Basic file info
|
||
info.push(`📁 Size: ${formatSize(size)} | Duration: ${formatDuration(duration)} | Bitrate: ${formatBitrate(bitrate)}`);
|
||
|
||
// Check for very short files
|
||
if (duration > 0 && duration < 10) {
|
||
issues.push('⚠️ SHORT FILE: Duration under 10 seconds');
|
||
}
|
||
|
||
// Check for suspiciously low bitrate
|
||
if (bitrate > 0 && bitrate < 100000) { // Under 100kbps
|
||
issues.push('⚠️ LOW BITRATE: File bitrate is very low - possible quality issues');
|
||
}
|
||
|
||
// Check for missing duration (common in broken files)
|
||
if (!duration || duration === 0) {
|
||
issues.push('⚠️ NO DURATION: Could not determine file duration - may be corrupt');
|
||
}
|
||
|
||
// Verbose: show all format tags
|
||
if (logLevel === 'verbose' && format.tags) {
|
||
const importantTags = ['title', 'encoder', 'creation_time', 'copyright'];
|
||
importantTags.forEach(tag => {
|
||
if (format.tags[tag]) {
|
||
info.push(` 📋 ${tag}: ${format.tags[tag]}`);
|
||
}
|
||
});
|
||
}
|
||
|
||
return { issues, info };
|
||
};
|
||
|
||
// ============================================================================
|
||
// MAIN PLUGIN
|
||
// ============================================================================
|
||
|
||
const plugin = (file, librarySettings, inputs, otherArguments) => {
|
||
const lib = require('../methods/lib')();
|
||
|
||
const response = {
|
||
processFile: true, // MUST be true for Filter plugins to pass files through!
|
||
preset: '',
|
||
container: `.${file.container}`,
|
||
handbrakeMode: false,
|
||
ffmpegMode: false,
|
||
reQueueAfter: false,
|
||
infoLog: '',
|
||
};
|
||
|
||
try {
|
||
inputs = sanitizeInputs(lib.loadDefaultValues(inputs, details));
|
||
const logLevel = inputs.log_level;
|
||
|
||
// Header
|
||
response.infoLog += '═══════════════════════════════════════════════════════════════\n';
|
||
response.infoLog += ' 📋 FILE AUDIT REPORT\n';
|
||
response.infoLog += '═══════════════════════════════════════════════════════════════\n\n';
|
||
|
||
if (!file.ffProbeData?.streams || !Array.isArray(file.ffProbeData.streams)) {
|
||
response.infoLog += '❌ CRITICAL: No stream data available - file may be corrupt\n';
|
||
return response;
|
||
}
|
||
|
||
const streams = file.ffProbeData.streams;
|
||
const allIssues = [];
|
||
const allInfo = [];
|
||
|
||
// Run all audits (no target container - checks both MKV and MP4)
|
||
const containerAudit = auditContainer(file);
|
||
const videoAudit = auditVideoStreams(streams);
|
||
const audioAudit = auditAudioStreams(streams);
|
||
const subtitleAudit = auditSubtitleStreams(streams, file);
|
||
const otherAudit = auditOtherStreams(streams);
|
||
const metadataAudit = auditFileMetadata(file, logLevel);
|
||
|
||
// Collect all results
|
||
allIssues.push(...containerAudit.issues, ...videoAudit.issues, ...audioAudit.issues,
|
||
...subtitleAudit.issues, ...otherAudit.issues, ...metadataAudit.issues);
|
||
allInfo.push(...metadataAudit.info, ...containerAudit.info, ...videoAudit.info,
|
||
...audioAudit.info, ...subtitleAudit.info, ...otherAudit.info);
|
||
|
||
// Output based on log level
|
||
if (logLevel === 'minimal') {
|
||
// Minimal: issues only
|
||
if (allIssues.length > 0) {
|
||
response.infoLog += `🔍 Found ${allIssues.length} potential issue(s):\n`;
|
||
allIssues.forEach(issue => {
|
||
response.infoLog += ` ${issue}\n`;
|
||
});
|
||
} else {
|
||
response.infoLog += '✅ No issues detected\n';
|
||
}
|
||
} else {
|
||
// Detailed/Verbose: show info and issues
|
||
allInfo.forEach(info => {
|
||
response.infoLog += `${info}\n`;
|
||
});
|
||
|
||
response.infoLog += '\n───────────────────────────────────────────────────────────────\n';
|
||
|
||
if (allIssues.length > 0) {
|
||
response.infoLog += `\n🔍 POTENTIAL ISSUES (${allIssues.length}):\n`;
|
||
response.infoLog += ' [MKV only] = Issue only affects MKV container\n';
|
||
response.infoLog += ' [MP4 only] = Issue only affects MP4 container\n';
|
||
response.infoLog += ' [BOTH] = Issue affects both containers\n\n';
|
||
allIssues.forEach(issue => {
|
||
response.infoLog += ` ${issue}\n`;
|
||
});
|
||
} else {
|
||
response.infoLog += '\n✅ No issues detected - file ready for processing\n';
|
||
}
|
||
}
|
||
|
||
// Stream count summary
|
||
const videoCount = streams.filter(s => s.codec_type === 'video' && !IMAGE_CODECS.has((s.codec_name || '').toLowerCase())).length;
|
||
const audioCount = streams.filter(s => s.codec_type === 'audio').length;
|
||
const subCount = streams.filter(s => s.codec_type === 'subtitle').length;
|
||
|
||
response.infoLog += '\n───────────────────────────────────────────────────────────────\n';
|
||
response.infoLog += `📊 Summary: ${videoCount}V ${audioCount}A ${subCount}S | Checked: MKV+MP4 | Issues: ${allIssues.length}\n`;
|
||
response.infoLog += '═══════════════════════════════════════════════════════════════\n';
|
||
|
||
// Final Summary block (for consistency with other plugins)
|
||
if (logLevel !== 'minimal') {
|
||
response.infoLog += '\n📋 Final Processing Summary:\n';
|
||
response.infoLog += ` Streams: ${videoCount} video, ${audioCount} audio, ${subCount} subtitle\n`;
|
||
response.infoLog += ` Issues detected: ${allIssues.length}\n`;
|
||
response.infoLog += ` Container compatibility: MKV + MP4 checked\n`;
|
||
}
|
||
|
||
return response;
|
||
|
||
} catch (error) {
|
||
response.infoLog = `❌ Audit plugin error: ${error.message}\n`;
|
||
return response;
|
||
}
|
||
};
|
||
|
||
module.exports.details = details;
|
||
module.exports.plugin = plugin;
|