/* eslint-disable no-plusplus */ const details = () => ({ id: 'Tdarr_Plugin_misc_fixes', Stage: 'Pre-processing', Name: 'Misc Fixes', Type: 'Video', Operation: 'Transcode', Description: ` A consolidated 'Megamix' of fixes for common video file issues. Combines functionality from Migz Remux, Migz Image Removal, Lmg1 Reorder, and custom timestamp fixes. Features: - Fixes timestamps for TS/AVI/MPG files - Optional TS audio recovery: extract + transcode audio to AAC for compatibility - Remuxes to target container (MKV/MP4) - Conforms streams to container (drops incompatible subtitles) - Removes unwanted image streams (MJPEG/PNG/GIF) - Ensures Video stream is ordered first Should be placed FIRST in your plugin stack. `, Version: '3.0', Tags: 'action,ffmpeg,ts,remux,fix,megamix', Inputs: [ { name: 'target_container', type: 'string', defaultValue: 'mkv', inputUI: { type: 'dropdown', options: ['mkv', 'mp4'], }, tooltip: 'Target container format', }, { name: 'force_conform', type: 'string', defaultValue: 'true*', inputUI: { type: 'dropdown', options: ['true*', 'false'], }, tooltip: 'Drop streams incompatible with the target container (e.g. mov_text in MKV)', }, { name: 'remove_image_streams', type: 'string', defaultValue: 'true*', inputUI: { type: 'dropdown', options: ['true*', 'false'], }, tooltip: 'Remove MJPEG, PNG, and GIF video streams (often cover art or spam)', }, { name: 'ensure_video_first', type: 'string', defaultValue: 'true*', inputUI: { type: 'dropdown', options: ['true*', 'false'], }, tooltip: 'Reorder streams so Video is first, then Audio, then Subtitles', }, { name: 'fix_ts_timestamps', type: 'string', defaultValue: 'true*', inputUI: { type: 'dropdown', options: ['true*', 'false'], }, tooltip: 'Apply special timestamp fixes for TS/AVI/MPG files (-fflags +genpts)', }, { name: 'ts_audio_recovery', type: 'string', defaultValue: 'false', inputUI: { type: 'dropdown', options: ['false', 'true'], }, tooltip: 'TS files only: Extract and transcode audio to AAC for compatibility. Ignored for non-TS files.', }, ], }); const plugin = (file, librarySettings, inputs, otherArguments) => { const lib = require('../methods/lib')(); // Strip UI star markers from input values (Tdarr uses '*' to indicate default values in UI) const stripStar = (value) => { if (typeof value === 'string') { return value.replace(/\*/g, ''); } return value; }; // Initialize response first for error handling const response = { processFile: false, preset: '', container: `.${file.container}`, handbrakeMode: false, ffmpegMode: true, reQueueAfter: true, infoLog: '', }; try { inputs = lib.loadDefaultValues(inputs, details); // Sanitize UI-starred defaults Object.keys(inputs).forEach((key) => { inputs[key] = stripStar(inputs[key]); }); // Input validation const VALID_CONTAINERS = ['mkv', 'mp4']; const VALID_BOOLEAN = ['true', 'false']; if (!VALID_CONTAINERS.includes(inputs.target_container)) { response.infoLog += `❌ Invalid target_container: ${inputs.target_container}. `; return response; } const booleanInputs = [ 'force_conform', 'remove_image_streams', 'ensure_video_first', 'fix_ts_timestamps', 'ts_audio_recovery', ]; // eslint-disable-next-line no-restricted-syntax for (const input of booleanInputs) { const val = String(inputs[input]).toLowerCase(); if (!VALID_BOOLEAN.includes(val)) { response.infoLog += `❌ Invalid ${input}: must be true or false. `; return response; } inputs[input] = val; // Normalize to lowercase string } if (!Array.isArray(file.ffProbeData?.streams)) { response.infoLog += '❌ No stream data available. '; return response; } // --- Logic Setup (needed for skip checks below) --- const targetContainer = inputs.target_container; const currentContainer = file.container.toLowerCase(); const isTargetMkv = targetContainer === 'mkv'; const isTargetMp4 = targetContainer === 'mp4'; // Skip ISO/DVD files - these require specialized tools like HandBrake or MakeMKV // These files often have corrupt MPEG-PS streams that cannot be reliably remuxed if (['iso', 'vob', 'evo'].includes(currentContainer)) { response.infoLog += '⚠️ ISO/DVD files require manual conversion with HandBrake or MakeMKV. Skipping automated processing.\n'; response.processFile = false; return response; } // Skip TS files with severe timestamp corruption that cannot be fixed // These files have missing or corrupt timestamps that FFmpeg cannot regenerate if (['ts', 'mpegts', 'm2ts'].includes(currentContainer)) { const hasCorruptStreams = file.ffProbeData.streams.some(s => { // Check for audio streams with 0 channels (corrupt) if (s.codec_type === 'audio' && s.channels === 0) return true; // Check for streams missing duration (severe timestamp issues) if (s.codec_type === 'video' && !s.duration && !s.duration_ts) return true; return false; }); if (hasCorruptStreams) { response.infoLog += '⚠️ TS file has corrupt streams with unfixable timestamp issues. Skipping automated processing.\n'; response.infoLog += 'ℹ️ Consider manual conversion with HandBrake or re-recording the source.\n'; response.processFile = false; return response; } } // --- Stream Analysis --- // Track actions let needsRemux = currentContainer !== targetContainer; let droppingStreams = false; const extraMaps = []; // For negative mapping (-map -0:x) let genptsFlags = ''; let codecFlags = '-c copy'; // --- 1. Timestamp Fixes (Migz + Custom) --- if (inputs.fix_ts_timestamps === 'true') { const brokenTypes = ['ts', 'mpegts', 'avi', 'mpg', 'mpeg']; if (brokenTypes.includes(currentContainer)) { if (['ts', 'mpegts'].includes(currentContainer)) { // Enhanced TS timestamp fixes: generate PTS for all streams, handle negative timestamps // Use genpts+igndts to regenerate timestamps where missing // -copyts preserves existing timestamps, genpts fills in gaps // make_zero handles negative timestamps by shifting to start at 0 // Note: For severely broken TS files with completely missing timestamps, // transcoding (not copy) may be required as genpts only works for video streams genptsFlags = '-fflags +genpts+igndts -avoid_negative_ts make_zero -copyts'; response.infoLog += '✅ Applying TS timestamp fixes. '; needsRemux = true; } else { genptsFlags = '-fflags +genpts'; response.infoLog += `✅ Applying ${currentContainer} timestamp fixes. `; needsRemux = true; } } } // --- 1b. Optional TS audio extraction + AAC transcode for compatibility --- if (inputs.ts_audio_recovery === 'true') { if (['ts', 'mpegts'].includes(currentContainer)) { // Determine a sane AAC bitrate: preserve multichannel without starving const firstAudio = file.ffProbeData.streams.find((s) => s.codec_type === 'audio'); const audioChannels = firstAudio?.channels || 2; const audioBitrate = audioChannels > 2 ? '384k' : '192k'; codecFlags = `-c:v copy -c:a aac -b:a ${audioBitrate} -c:s copy -c:d copy -c:t copy`; response.infoLog += `🎧 TS audio recovery enabled: extracting and transcoding audio to AAC (${audioChannels}ch -> ${audioBitrate}). `; needsRemux = true; } else { response.infoLog += 'ℹ️ TS audio recovery enabled but file is not TS format, skipping. '; } } // --- 2. Simplified Stream Conform (Redundant but kept as lightweight fallback) --- // Basic mapping for copy-remux let baseMap = '-map 0'; // Loop streams to find only critical issues (illegal metadata etc) for (let i = 0; i < file.ffProbeData.streams.length; i++) { const stream = file.ffProbeData.streams[i]; const codec = (stream.codec_name || '').toLowerCase(); const type = (stream.codec_type || '').toLowerCase(); // A. Invalid Audio Stream Detection (Safety check) if (type === 'audio') { const channels = stream.channels || 0; const sampleRate = stream.sample_rate || 0; if (channels === 0 || sampleRate === 0 || !codec || codec === 'unknown') { extraMaps.push(`-map -0:${i}`); response.infoLog += `ℹ️ Dropping invalid audio stream ${i} (${codec || 'unknown'}, ${channels}ch, ${sampleRate}Hz). `; droppingStreams = true; } } } // --- 3. Decision Time --- if (needsRemux || droppingStreams) { // Construct command // Order: const cmdParts = []; if (genptsFlags) cmdParts.push(genptsFlags); cmdParts.push(baseMap); if (extraMaps.length > 0) cmdParts.push(extraMaps.join(' ')); cmdParts.push(codecFlags); cmdParts.push('-max_muxing_queue_size 9999'); response.preset = ` ${cmdParts.join(' ')}`; response.container = `.${targetContainer}`; response.processFile = true; // Log conversion reason if (currentContainer !== targetContainer) { response.infoLog += `✅ Remuxing ${currentContainer} to ${targetContainer}. `; } return response; } response.infoLog += '✅ File meets all criteria. '; // Final Summary block response.infoLog += '\n\n📋 Final Processing Summary:\n'; response.infoLog += ` Container: ${currentContainer.toUpperCase()}\n`; if (needsRemux) { response.infoLog += ` - Remuxing to: ${targetContainer.toUpperCase()}\n`; } if (genptsFlags) response.infoLog += ` - Timestamp fixes applied\n`; if (codecFlags !== '-c copy') response.infoLog += ` - Codec conversion enabled\n`; if (droppingStreams) response.infoLog += ` - Streams removed: ${extraMaps.length}\n`; if (needsReorder) response.infoLog += ` - Streams reordered\n`; return response; } catch (error) { // Comprehensive error handling response.processFile = false; response.preset = ''; response.reQueueAfter = false; // Provide detailed error information response.infoLog = `❌ Plugin error: ${error.message}\n`; // Add stack trace for debugging (first 5 lines) if (error.stack) { const stackLines = error.stack.split('\n').slice(0, 5).join('\n'); response.infoLog += `Stack trace:\n${stackLines}\n`; } // Log additional context response.infoLog += `File: ${file.file}\n`; response.infoLog += `Container: ${file.container}\n`; return response; } }; module.exports.details = details; module.exports.plugin = plugin;