const details = () => ({ id: 'Tdarr_Plugin_combined_audio_standardizer', Stage: 'Pre-processing', Name: 'Combined Audio Standardizer', Type: 'Audio', Operation: 'Transcode', Description: ` Converts audio streams to specified codec (AAC/Opus) with configurable bitrate and channel options. Can preserve existing channels or downmix from multichannel to stereo/mono. Also creates missing downmixed tracks (8ch->6ch, 6ch/8ch->2ch) when they don't exist. v1.15: Fixed duplicate description, added default markers, improved tooltips. v1.14: Fixed crash when input file has no subtitles (conditional mapping). `, Version: '1.15', Tags: 'audio,aac,opus,channels,stereo,downmix,quality', Inputs: [ { name: 'codec', type: 'string', defaultValue: 'opus*', inputUI: { type: 'dropdown', options: [ 'aac', 'opus*' ], }, tooltip: 'Target audio codec: AAC (best compatibility, larger files) or Opus (best efficiency, smaller files).', }, { name: 'skip_if_compatible', type: 'string', defaultValue: 'true*', inputUI: { type: 'dropdown', options: [ 'true*', 'false' ], }, tooltip: 'Skip conversion if audio is already AAC or Opus (either format acceptable). When false, converts to target codec.', }, { name: 'bitrate_per_channel', type: 'string', defaultValue: 'auto*', inputUI: { type: 'dropdown', options: [ 'auto*', '64', '80', '96', '128', '160', '192', 'original' ], }, tooltip: 'Bitrate per channel in kbps for multichannel audio. "auto" uses min(64kbps/ch, source bitrate) for optimal quality/size. Total bitrate = channels Ɨ this value. Use "original" to keep exact source bitrate.', }, { name: 'channel_mode', type: 'string', defaultValue: 'preserve*', inputUI: { type: 'dropdown', options: [ 'preserve*', 'stereo', 'mono' ], }, tooltip: 'Channel handling for existing tracks: preserve=keep original channels, stereo=downmix to 2.0, mono=downmix to 1.0.', }, { name: 'create_downmix', type: 'string', defaultValue: 'true*', inputUI: { type: 'dropdown', options: [ 'false', 'true*' ], }, tooltip: 'Create stereo (2ch) downmix from multichannel audio (5.1/7.1). Only creates if no stereo track exists AND multichannel source is present.', }, { name: 'downmix_single_track', type: 'string', defaultValue: 'false', inputUI: { type: 'dropdown', options: [ 'false', 'true' ], }, tooltip: 'Only downmix one track per channel count instead of all tracks.', }, { name: 'force_transcode', type: 'string', defaultValue: 'false', inputUI: { type: 'dropdown', options: [ 'false', 'true' ], }, tooltip: 'Force transcoding even if audio is already in target codec. Useful for changing bitrate or channel layout.', }, { name: 'opus_application', type: 'string', defaultValue: 'audio*', inputUI: { type: 'dropdown', options: [ 'audio*', 'voip', 'lowdelay' ], }, tooltip: 'Opus application (ignored for AAC): audio=music/general, voip=speech optimized, lowdelay=real-time apps.', }, { name: 'opus_vbr', type: 'string', defaultValue: 'on*', inputUI: { type: 'dropdown', options: [ 'on*', 'off', 'constrained' ], }, tooltip: 'Opus VBR mode (ignored for AAC): on=VBR (best quality/size), off=CBR, constrained=CVBR.', }, { name: 'opus_compression', type: 'string', defaultValue: '10*', inputUI: { type: 'dropdown', options: [ '0', '5', '8', '10*' ], }, tooltip: 'Opus compression level (ignored for AAC): 0=fastest/lower quality, 10=slowest/best quality. Default 10 recommended for archival.', }, { name: 'aac_profile', type: 'string', defaultValue: 'aac_low*', inputUI: { type: 'dropdown', options: [ 'aac_low*', 'aac_he', 'aac_he_v2' ], }, tooltip: 'AAC profile (ignored for Opus): aac_low=AAC-LC (best quality/compatibility), aac_he=HE-AAC (better for low bitrate), aac_he_v2=HE-AACv2 (best for very low bitrate stereo).', }, { name: 'target_sample_rate', type: 'string', defaultValue: 'original*', inputUI: { type: 'dropdown', options: [ 'original*', '48000', '44100', '32000' ], }, tooltip: 'Target sample rate in Hz. "original" keeps source sample rate. 48000 recommended for streaming, 44100 for music.', }, { name: 'create_6ch_downmix', type: 'string', defaultValue: 'false', inputUI: { type: 'dropdown', options: [ 'false', 'true' ], }, tooltip: 'Create additional 5.1 (6ch) downmix tracks from 7.1 (8ch) audio.', }, { name: 'preserve_metadata', type: 'string', defaultValue: 'true*', inputUI: { type: 'dropdown', options: [ 'false', 'true*' ], }, tooltip: 'Preserve audio metadata (title, language tags) from source streams.', }, { name: 'quality_preset', type: 'string', defaultValue: 'custom*', inputUI: { type: 'dropdown', options: [ 'custom*', 'high_quality', 'balanced', 'small_size' ], }, tooltip: 'Quality presets automatically configure bitrate settings. Use "custom" to manually set bitrate per channel and other encoder options.', } ], }); const CODECS = { AAC: 'aac', OPUS: 'opus', LIBOPUS: 'libopus' }; const CHANNEL_MODES = { PRESERVE: 'preserve', STEREO: 'stereo', MONO: 'mono' }; const COMPATIBLE_CODECS = [CODECS.AAC, CODECS.OPUS, CODECS.LIBOPUS]; const VALID_BITRATES = ['auto', '64', '80', '96', '128', '160', '192', 'original']; const VALID_BOOLEAN_VALUES = ['true', 'false']; const VALID_OPUS_APPLICATIONS = ['audio', 'voip', 'lowdelay']; const VALID_OPUS_VBR_MODES = ['on', 'off', 'constrained']; const VALID_OPUS_COMPRESSION = ['0', '5', '8', '10']; const VALID_AAC_PROFILES = ['aac_low', 'aac_he', 'aac_he_v2']; const VALID_SAMPLE_RATES = ['original', '48000', '44100', '32000']; const VALID_QUALITY_PRESETS = ['custom', 'high_quality', 'balanced', 'small_size']; // Opus compatible layouts (whitelist approach is more reliable) const OPUS_COMPATIBLE_LAYOUTS = new Set([ 'mono', 'stereo', '2.1', '3.0', '4.0', '5.0', '5.1', '5.1(side)', '7.1' ]); const isOpusIncompatibleLayout = (layout) => { if (!layout) return false; // If not in compatible list, it's incompatible return !OPUS_COMPATIBLE_LAYOUTS.has(layout); }; const QUALITY_PRESETS = { high_quality: { aac_bitrate_per_channel: '128', opus_bitrate_per_channel: '96', opus_vbr: 'on', opus_application: 'audio', aac_profile: 'aac_low', description: 'Maximum quality, larger files' }, balanced: { aac_bitrate_per_channel: '80', opus_bitrate_per_channel: '64', opus_vbr: 'on', opus_application: 'audio', aac_profile: 'aac_low', description: 'Good quality, reasonable file sizes' }, small_size: { aac_bitrate_per_channel: '64', opus_bitrate_per_channel: '64', opus_vbr: 'constrained', opus_application: 'audio', aac_profile: 'aac_he', description: 'Smaller files, acceptable quality' } }; const needsTranscoding = (stream, inputs, targetCodec) => { // Force transcode if explicitly requested if (inputs.force_transcode === 'true') return true; // Check if channel layout needs changing if (inputs.channel_mode === 'stereo' && stream.channels > 2) return true; if (inputs.channel_mode === 'mono' && stream.channels > 1) return true; // If skip_if_compatible is true, accept any compatible codec (AAC or Opus) // This means: if codec is AAC or Opus, don't transcode (even if target is different) if (inputs.skip_if_compatible === 'true') { return !COMPATIBLE_CODECS.includes(stream.codec_name); } // Otherwise, only accept exact target codec match // This means: if codec doesn't match target, transcode return !targetCodec.includes(stream.codec_name); }; const calculateBitrate = (inputs, channels, streamBitrate = null) => { let targetBitrate; if (inputs.bitrate_per_channel === 'auto') { // Smart bitrate: min(64kbps per channel, source bitrate) targetBitrate = 64 * channels; if (streamBitrate && streamBitrate > 0) { const sourceBitrateKbps = Math.round(streamBitrate / 1000); targetBitrate = Math.min(targetBitrate, sourceBitrateKbps); } } else if (inputs.bitrate_per_channel === 'original') { // Use original bitrate if available, otherwise calculate a reasonable default if (streamBitrate && streamBitrate > 0) { targetBitrate = Math.round(streamBitrate / 1000); // Convert to kbps } else { // Fallback: estimate based on channel count if original bitrate unavailable targetBitrate = channels * 96; // 96kbps per channel as fallback } } else { targetBitrate = parseInt(inputs.bitrate_per_channel) * channels; } // Enforce minimum bitrate threshold to prevent unusable audio const MIN_BITRATE_KBPS = 32; if (targetBitrate < MIN_BITRATE_KBPS) { return MIN_BITRATE_KBPS; } return targetBitrate; }; const applyQualityPreset = (inputs) => { if (inputs.quality_preset === 'custom') { return inputs; } const preset = QUALITY_PRESETS[inputs.quality_preset]; if (!preset) { // Log warning if preset not found, fallback to custom console.warn(`Warning: Quality preset '${inputs.quality_preset}' not found, using custom settings.`); return inputs; } const modifiedInputs = { ...inputs }; if (inputs.codec === CODECS.AAC) { modifiedInputs.bitrate_per_channel = preset.aac_bitrate_per_channel; if (preset.aac_profile) { modifiedInputs.aac_profile = preset.aac_profile; } } else if (inputs.codec === CODECS.OPUS) { modifiedInputs.bitrate_per_channel = preset.opus_bitrate_per_channel; modifiedInputs.opus_vbr = preset.opus_vbr; modifiedInputs.opus_application = preset.opus_application; } return modifiedInputs; }; const buildCodecArgs = (audioIdx, inputs, targetBitrate) => { if (inputs.codec === CODECS.OPUS) { // Note: -vbr, -application, -compression_level are encoder-global options // They are added once at the end of the command via getOpusGlobalArgs() return [ `-c:a:${audioIdx} libopus`, targetBitrate ? `-b:a:${audioIdx} ${targetBitrate}k` : '' ].filter(Boolean).join(' '); } // AAC with profile selection const aacProfile = inputs.aac_profile === 'aac_low' ? 'aac' : inputs.aac_profile; return [ `-c:a:${audioIdx} ${aacProfile}`, targetBitrate ? `-b:a:${audioIdx} ${targetBitrate}k` : '', '-strict -2' ].filter(Boolean).join(' '); }; // Returns global Opus encoder options (applied once per output) const getOpusGlobalArgs = (inputs) => { if (inputs.codec === CODECS.OPUS) { return ` -vbr ${inputs.opus_vbr} -application ${inputs.opus_application} -compression_level ${inputs.opus_compression}`; } return ''; }; // Returns sample rate argument if resampling is needed const getSampleRateArgs = (audioIdx, inputs) => { if (inputs.target_sample_rate === 'original') { return ''; } return ` -ar:a:${audioIdx} ${inputs.target_sample_rate}`; }; // Returns metadata preservation arguments const getMetadataArgs = (audioIdx, stream, inputs, customTitle = null) => { if (customTitle) { return ` -metadata:s:a:${audioIdx} title="${customTitle}"`; } if (inputs.preserve_metadata !== 'true') { return ''; } const args = []; if (stream.tags?.title) { args.push(`-metadata:s:a:${audioIdx} title="${stream.tags.title}"`); } if (stream.tags?.language) { args.push(`-metadata:s:a:${audioIdx} language="${stream.tags.language}"`); } return args.length > 0 ? ' ' + args.join(' ') : ''; }; const buildChannelArgs = (audioIdx, inputs) => { switch (inputs.channel_mode) { case CHANNEL_MODES.STEREO: return ` -ac:a:${audioIdx} 2`; case CHANNEL_MODES.MONO: return ` -ac:a:${audioIdx} 1`; default: return ''; } }; const buildDownmixArgs = (audioIdx, streamIndex, stream, inputs, channels) => { const baseArgs = ` -map 0:${streamIndex} -c:a:${audioIdx} `; // Calculate downmix bitrate const downmixBitrate = calculateBitrate(inputs, channels, null); if (inputs.codec === CODECS.OPUS) { // Note: global Opus options (-vbr, -application, -compression_level) are added // once at the end of the command via getOpusGlobalArgs() return baseArgs + [ 'libopus', `-b:a:${audioIdx} ${downmixBitrate}k`, `-ac ${channels}`, getSampleRateArgs(audioIdx, inputs), getMetadataArgs(audioIdx, stream, inputs, `${channels}.0 Downmix`) ].filter(Boolean).join(' '); } const aacProfile = inputs.aac_profile === 'aac_low' ? 'aac' : inputs.aac_profile; return baseArgs + [ aacProfile, `-b:a:${audioIdx} ${downmixBitrate}k`, '-strict -2', `-ac ${channels}`, getSampleRateArgs(audioIdx, inputs), getMetadataArgs(audioIdx, stream, inputs, `${channels}.0 Downmix`) ].filter(Boolean).join(' '); }; const validateStream = (stream, index) => { const warnings = []; if (!stream.channels || stream.channels < 1 || stream.channels > 16) { warnings.push(`Stream ${index}: Unusual channel count (${stream.channels})`); } if (stream.bit_rate && (stream.bit_rate < 16000 || stream.bit_rate > 5000000)) { warnings.push(`Stream ${index}: Unusual bitrate (${stream.bit_rate})`); } return warnings; }; const logStreamInfo = (stream, index) => { const info = [ `Stream ${index}:`, ` Codec: ${stream.codec_name}`, ` Channels: ${stream.channels}`, ` Bitrate: ${stream.bit_rate ? Math.round(stream.bit_rate / 1000) + 'kbps' : 'unknown'}`, ` Sample Rate: ${stream.sample_rate ? stream.sample_rate + 'Hz' : 'unknown'}`, ` Language: ${stream.tags?.language || 'unknown'}` ].join('\n'); return info; }; // eslint-disable-next-line @typescript-eslint/no-unused-vars const plugin = (file, librarySettings, inputs, otherArguments) => { const lib = require('../methods/lib')(); // Initialize response first for error handling const response = { processFile: false, preset: '', container: `.${file.container}`, handbrakeMode: false, ffmpegMode: false, reQueueAfter: false, infoLog: '', }; try { // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-param-reassign inputs = lib.loadDefaultValues(inputs, details); // 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; }; // Sanitize UI-starred defaults Object.keys(inputs).forEach(key => { inputs[key] = stripStar(inputs[key]); }); const validateInputs = (inputs) => { const errors = []; if (![CODECS.AAC, CODECS.OPUS].includes(inputs.codec)) { errors.push(`Invalid codec selection - must be "${CODECS.AAC}" or "${CODECS.OPUS}"`); } const booleanInputs = [ 'skip_if_compatible', 'create_downmix', 'create_6ch_downmix', 'downmix_single_track', 'force_transcode', 'preserve_metadata' ]; for (const input of booleanInputs) { if (!VALID_BOOLEAN_VALUES.includes(inputs[input])) { errors.push(`Invalid ${input} value - must be "true" or "false"`); } } if (!VALID_BITRATES.includes(inputs.bitrate_per_channel)) { errors.push(`Invalid bitrate_per_channel - must be one of: ${VALID_BITRATES.join(', ')}`); } if (!Object.values(CHANNEL_MODES).includes(inputs.channel_mode)) { errors.push(`Invalid channel_mode - must be "${CHANNEL_MODES.PRESERVE}", "${CHANNEL_MODES.STEREO}", or "${CHANNEL_MODES.MONO}"`); } if (inputs.codec === CODECS.OPUS) { if (!VALID_OPUS_APPLICATIONS.includes(inputs.opus_application)) { errors.push(`Invalid opus_application - must be one of: ${VALID_OPUS_APPLICATIONS.join(', ')}`); } if (!VALID_OPUS_VBR_MODES.includes(inputs.opus_vbr)) { errors.push(`Invalid opus_vbr - must be one of: ${VALID_OPUS_VBR_MODES.join(', ')}`); } if (!VALID_OPUS_COMPRESSION.includes(inputs.opus_compression)) { errors.push(`Invalid opus_compression - must be one of: ${VALID_OPUS_COMPRESSION.join(', ')}`); } } if (inputs.codec === CODECS.AAC) { if (!VALID_AAC_PROFILES.includes(inputs.aac_profile)) { errors.push(`Invalid aac_profile - must be one of: ${VALID_AAC_PROFILES.join(', ')}`); } } if (!VALID_SAMPLE_RATES.includes(inputs.target_sample_rate)) { errors.push(`Invalid target_sample_rate - must be one of: ${VALID_SAMPLE_RATES.join(', ')}`); } if (!VALID_QUALITY_PRESETS.includes(inputs.quality_preset)) { errors.push(`Invalid quality_preset - must be one of: ${VALID_QUALITY_PRESETS.join(', ')}`); } return errors; }; const validationErrors = validateInputs(inputs); if (validationErrors.length > 0) { response.infoLog += 'āŒ Input validation errors:\n'; validationErrors.forEach(error => { response.infoLog += ` - ${error}\n`; }); response.processFile = false; return response; } const originalInputs = { ...inputs }; inputs = applyQualityPreset(inputs); if (originalInputs.quality_preset !== 'custom' && originalInputs.quality_preset === inputs.quality_preset) { const preset = QUALITY_PRESETS[inputs.quality_preset]; if (preset) { response.infoLog += `šŸŽÆ Applied quality preset: ${inputs.quality_preset}\n`; response.infoLog += ` Description: ${preset.description}\n`; if (inputs.codec === CODECS.AAC) { response.infoLog += ` AAC bitrate per channel: ${preset.aac_bitrate_per_channel}kbps\n`; } else { response.infoLog += ` Opus bitrate per channel: ${preset.opus_bitrate_per_channel}kbps\n`; } } } if (file.fileMedium !== 'video') { response.infoLog += 'ā„¹ļø File is not video.\n'; response.processFile = false; return response; } let audioStreams = []; let needsTranscode = false; let streamWarnings = []; const targetCodec = inputs.codec === CODECS.OPUS ? [CODECS.OPUS, CODECS.LIBOPUS] : [CODECS.AAC]; try { for (let i = 0; i < file.ffProbeData.streams.length; i++) { const stream = file.ffProbeData.streams[i]; if (stream.codec_type === 'audio') { audioStreams.push({ index: i, ...stream }); const warnings = validateStream(stream, i); streamWarnings.push(...warnings); if (needsTranscoding(stream, inputs, targetCodec)) { needsTranscode = true; } } } } catch (error) { response.infoLog += `āŒ Error analyzing audio streams: ${error.message}\n`; response.processFile = false; return response; } if (audioStreams.length === 0) { response.infoLog += 'ā„¹ļø No audio streams found.\n'; response.processFile = false; return response; } response.infoLog += 'šŸ” Audio Stream Analysis:\n'; audioStreams.forEach(stream => { response.infoLog += logStreamInfo(stream, stream.index) + '\n'; }); if (streamWarnings.length > 0) { response.infoLog += 'āš ļø Stream warnings:\n'; streamWarnings.forEach(warning => { response.infoLog += ` - ${warning}\n`; }); } if (!needsTranscode && inputs.create_downmix !== 'true') { response.infoLog += 'āœ… File already meets all requirements.\n'; return response; } // Check if file has attachment streams (fonts, cover art, etc.) const hasAttachments = file.ffProbeData.streams.some(s => s.codec_type === 'attachment'); // Build stream mapping explicitly by type to prevent attachment processing errors // Using -map 0 would map ALL streams including attachments, which causes muxing errors // when combined with additional -map commands for downmix tracks let streamMap = '-map 0:v -map 0:a'; // Check if file has subtitle streams before mapping them const hasSubtitles = file.ffProbeData.streams.some(s => s.codec_type === 'subtitle'); if (hasSubtitles) { streamMap += ' -map 0:s'; } if (hasAttachments) { // Add attachments separately with copy codec streamMap += ' -map 0:t -c:t copy'; } let ffmpegArgs = `${streamMap} -c:v copy`; if (hasSubtitles) { ffmpegArgs += ' -c:s copy'; } let audioIdx = 0; let processNeeded = false; let is2channelAdded = false; let transcodedStreams = 0; let copiedStreams = 0; let downmixStreams = 0; try { for (const stream of audioStreams) { let streamNeedsTranscode = needsTranscoding(stream, inputs, targetCodec); let forcePerStreamDownmix = false; if (inputs.codec === CODECS.OPUS && isOpusIncompatibleLayout(stream.channel_layout)) { if (!streamNeedsTranscode) { streamNeedsTranscode = true; } if (inputs.channel_mode === CHANNEL_MODES.PRESERVE) { forcePerStreamDownmix = true; } } if (streamNeedsTranscode) { const targetBitrate = calculateBitrate(inputs, stream.channels, stream.bit_rate); const codecArgs = buildCodecArgs(audioIdx, inputs, targetBitrate); let channelArgs = buildChannelArgs(audioIdx, inputs); const sampleRateArgs = getSampleRateArgs(audioIdx, inputs); const metadataArgs = getMetadataArgs(audioIdx, stream, inputs); if (forcePerStreamDownmix) { channelArgs = ` -ac:a:${audioIdx} 2`; } ffmpegArgs += ` ${codecArgs}${channelArgs}${sampleRateArgs}${metadataArgs}`; processNeeded = true; transcodedStreams++; response.infoLog += `āœ… Converting ${stream.codec_name} (${stream.channels}ch, ${stream.bit_rate ? Math.round(stream.bit_rate / 1000) + 'kbps' : 'unknown'}) to ${inputs.codec} at stream ${audioIdx}.\n`; if (inputs.codec === CODECS.OPUS) { if (forcePerStreamDownmix) { response.infoLog += ` Detected incompatible layout "${stream.channel_layout}" → per-stream stereo downmix applied.\n`; } else if (stream.channel_layout) { response.infoLog += ` Layout "${stream.channel_layout}" deemed Opus-compatible.\n`; } } if (targetBitrate) { response.infoLog += ` Target bitrate: ${targetBitrate}kbps${inputs.bitrate_per_channel === 'original' ? ' (from source)' : ` (${inputs.bitrate_per_channel}kbps per channel)`}\n`; } } else { ffmpegArgs += ` -c:a:${audioIdx} copy`; copiedStreams++; if (inputs.skip_if_compatible === 'true' && COMPATIBLE_CODECS.includes(stream.codec_name)) { response.infoLog += `āœ… Keeping ${stream.codec_name} (${stream.channels}ch) - compatible format.\n`; } } audioIdx++; } } catch (error) { response.infoLog += `āŒ Error processing audio streams: ${error.message}\n`; response.processFile = false; return response; } if (inputs.create_downmix === 'true') { const existing2chTracks = audioStreams.filter(s => s.channels === 2); if (existing2chTracks.length > 0) { response.infoLog += `ā„¹ļø Skipping 2ch downmix - ${existing2chTracks.length} stereo track(s) already exist.\n`; } else { try { for (const stream of audioStreams) { if ((stream.channels === 6 || stream.channels === 8) && (inputs.downmix_single_track === 'false' || !is2channelAdded)) { const downmixArgs = buildDownmixArgs(audioIdx, stream.index, stream, inputs, 2); ffmpegArgs += downmixArgs; response.infoLog += `āœ… Creating 2ch downmix from ${stream.channels}ch audio.\n`; processNeeded = true; is2channelAdded = true; downmixStreams++; audioIdx++; } } } catch (error) { response.infoLog += `āŒ Error creating downmix tracks: ${error.message}\n`; response.processFile = false; return response; } } } // Create 6ch (5.1) downmix from 8ch (7.1) if enabled if (inputs.create_6ch_downmix === 'true') { const existing6chTracks = audioStreams.filter(s => s.channels === 6); const available8chTracks = audioStreams.filter(s => s.channels === 8); if (existing6chTracks.length > 0) { response.infoLog += `ā„¹ļø Skipping 6ch downmix - ${existing6chTracks.length} 5.1 track(s) already exist.\n`; } else if (available8chTracks.length === 0) { response.infoLog += 'ā„¹ļø Skipping 6ch downmix - no 7.1 (8ch) tracks available to downmix.\n'; } else { try { let is6channelAdded = false; for (const stream of audioStreams) { if (stream.channels === 8 && (inputs.downmix_single_track === 'false' || !is6channelAdded)) { const downmixArgs = buildDownmixArgs(audioIdx, stream.index, stream, inputs, 6); ffmpegArgs += downmixArgs; response.infoLog += 'āœ… Creating 6ch (5.1) downmix from 8ch (7.1) audio.\n'; processNeeded = true; is6channelAdded = true; downmixStreams++; audioIdx++; } } } catch (error) { response.infoLog += `āŒ Error creating 6ch downmix tracks: ${error.message}\n`; response.processFile = false; return response; } } } if (processNeeded) { try { response.processFile = true; // Add global Opus encoder options once at the end if using Opus const opusGlobalArgs = getOpusGlobalArgs(inputs); response.preset = `${ffmpegArgs}${opusGlobalArgs} -max_muxing_queue_size 9999`; response.ffmpegMode = true; response.reQueueAfter = true; // Calculate actual numerical bitrate for display (not 'auto' or 'original') const displayBitrate = calculateBitrate(inputs, 2, null); const bitratePerChannelDisplay = inputs.bitrate_per_channel === 'auto' ? '64 (auto)' : inputs.bitrate_per_channel === 'original' ? 'original' : inputs.bitrate_per_channel; response.infoLog += '\nšŸ“‹ Final Processing Summary:\n'; response.infoLog += ` Codec: ${inputs.codec}\n`; response.infoLog += ` Quality preset: ${inputs.quality_preset}\n`; response.infoLog += ` Channel mode: ${inputs.channel_mode}\n`; response.infoLog += ` Bitrate per channel: ${bitratePerChannelDisplay}kbps\n`; response.infoLog += ` Stereo downmix bitrate: ${displayBitrate}kbps\n`; response.infoLog += ` Streams to transcode: ${transcodedStreams}\n`; response.infoLog += ` Streams to copy: ${copiedStreams}\n`; response.infoLog += ` Downmix tracks to create: ${downmixStreams}\n`; if (inputs.skip_if_compatible === 'true') { response.infoLog += ' Compatibility mode: accepting both AAC and Opus\n'; } if (inputs.create_downmix === 'true') { response.infoLog += ' 2ch downmix creation enabled\n'; } if (inputs.create_6ch_downmix === 'true') { response.infoLog += ' 6ch downmix creation enabled\n'; } } catch (error) { response.infoLog += `āŒ Error building FFmpeg command: ${error.message}\n`; response.processFile = false; return response; } } else { response.infoLog += 'āœ… File already meets all requirements.\n'; response.processFile = false; } 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;