diff --git a/FlowHelpers/1.0.0/cliUtils.js b/FlowHelpers/1.0.0/cliUtils.js index 3092d33..d0e4a03 100644 --- a/FlowHelpers/1.0.0/cliUtils.js +++ b/FlowHelpers/1.0.0/cliUtils.js @@ -1,43 +1,92 @@ 'use strict'; /** - * Local mock of Tdarr FlowHelpers/1.0.0/cliUtils.js - * Used only for local testing — on real Tdarr this file comes from the server. + * Local fallback/mock of Tdarr FlowHelpers/1.0.0/cliUtils.js + * Used for local testing and as safe fallback. * * Matches the real Tdarr CLI constructor signature: - * new CLI({ cli, spawnArgs, WorkDir, jobLog, inputFileObj, updateWorker }) - * - inputFileObj : used by real Tdarr for ffprobe-based progress tracking - * - updateWorker : callback used by real Tdarr to push progress to the UI - * Both are accepted but unused here (local mock just logs to stdout). + * new CLI({ cli, spawnArgs, spawnOpts, WorkDir, jobLog, inputFileObj, updateWorker, logFullCliOutput }) */ const { spawn } = require('child_process'); class CLI { constructor(config) { - this.config = config; + this.config = config || {}; } runCli() { return new Promise((resolve) => { - const { cli, spawnArgs, WorkDir, jobLog } = this.config; + const { + cli, + spawnArgs = [], + spawnOpts = {}, + WorkDir, + jobLog, + updateWorker, + logFullCliOutput = false, + } = this.config; - if (jobLog) jobLog(`[CLI] Running: ${cli} ${spawnArgs.join(' ')}\n`); - - const proc = spawn(cli, spawnArgs, { - cwd: WorkDir || process.cwd(), + const opts = { + cwd: spawnOpts.cwd || WorkDir || process.cwd(), stdio: ['ignore', 'pipe', 'pipe'], shell: false, - }); + ...spawnOpts, + }; - proc.stdout.on('data', (d) => { if (jobLog) jobLog(d.toString()); }); - proc.stderr.on('data', (d) => { if (jobLog) jobLog(d.toString()); }); + if (jobLog) jobLog(`Running ${cli} ${spawnArgs.join(' ')}\n`); + + const proc = spawn(cli, spawnArgs, opts); + const errorLogFull = []; + + const handleData = (data) => { + const str = data.toString(); + errorLogFull.push(str); + + if (logFullCliOutput && jobLog) { + jobLog(str); + } + + if (typeof updateWorker === 'function') { + // ffmpeg progress matching: time=HH:MM:SS + const fpsMatch = str.match(/fps=\s*(\d+(?:\.\d+)?)/); + if (fpsMatch) { + updateWorker({ fps: parseFloat(fpsMatch[1]) }); + } + + const timeMatch = str.match(/time=(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)/); + if (timeMatch) { + const curSec = parseInt(timeMatch[1], 10) * 3600 + + parseInt(timeMatch[2], 10) * 60 + + parseFloat(timeMatch[3]); + const duration = this.config.inputFileObj?.meta?.Duration + || parseFloat(this.config.inputFileObj?.ffProbeData?.format?.duration) + || 0; + if (duration > 0) { + const perc = Math.min(100, Math.max(0, +((curSec / duration) * 100).toFixed(1))); + updateWorker({ percentage: perc }); + } + } else if (str.includes('%')) { + // mkvextract progress matching (e.g. "Progress: 45%" or "Progresso: 45%") + const pMatch = str.match(/(?:Progress|Progresso):\s*(\d+)%/i); + if (pMatch) { + updateWorker({ percentage: parseInt(pMatch[1], 10) }); + } + } + } + }; + + proc.stdout.on('data', handleData); + proc.stderr.on('data', handleData); proc.on('close', (code) => { - resolve({ cliExitCode: code || 0 }); + if (!logFullCliOutput && jobLog) { + jobLog(errorLogFull.slice(-1000).join('')); + } + resolve({ cliExitCode: code || 0, errorLogFull }); }); proc.on('error', (err) => { - if (jobLog) jobLog(`[CLI] Error: ${err.message}\n`); - resolve({ cliExitCode: 1 }); + if (jobLog) jobLog(`Error executing binary: ${cli}: ${err.message}\n`); + resolve({ cliExitCode: 1, errorLogFull }); }); }); } @@ -47,3 +96,4 @@ const getFFmpegVar = () => {}; module.exports = { CLI, getFFmpegVar }; + diff --git a/LocalFlowPlugins/audio/backupSourceAudio/1.0.0/index.js b/LocalFlowPlugins/audio/backupSourceAudio/1.0.0/index.js index 4f436ec..70e452f 100644 --- a/LocalFlowPlugins/audio/backupSourceAudio/1.0.0/index.js +++ b/LocalFlowPlugins/audio/backupSourceAudio/1.0.0/index.js @@ -74,10 +74,13 @@ const plugin = async (args) => { '-c:a', 'copy', outFile, ], + spawnOpts: { cwd: workDir }, WorkDir: workDir, jobLog, + outputFilePath: outFile, inputFileObj, updateWorker, + logFullCliOutput: args.logFullCliOutput || false, args, }); diff --git a/LocalFlowPlugins/subtitles/extractSubtitlesAndOcr/1.0.0/index.js b/LocalFlowPlugins/subtitles/extractSubtitlesAndOcr/1.0.0/index.js index 2db2e35..8fea564 100644 --- a/LocalFlowPlugins/subtitles/extractSubtitlesAndOcr/1.0.0/index.js +++ b/LocalFlowPlugins/subtitles/extractSubtitlesAndOcr/1.0.0/index.js @@ -97,12 +97,14 @@ const getMkvExtract = (args) => { const runCLI = async (cliBin, spawnArgs, args) => { const { CLI } = require('../../../../FlowHelpers/1.0.0/cliUtils'); const cli = new CLI({ - cli: cliBin, + cli: cliBin, spawnArgs, - WorkDir: args.workDir, - jobLog: args.jobLog, - inputFileObj: args.inputFileObj, - updateWorker: args.updateWorker, + spawnOpts: { cwd: args.workDir }, + WorkDir: args.workDir, + jobLog: args.jobLog, + inputFileObj: args.inputFileObj, + updateWorker: args.updateWorker, + logFullCliOutput: args.logFullCliOutput || false, args, }); return cli.runCli(); diff --git a/LocalFlowPlugins/utility/extractChaptersXml/1.0.0/index.js b/LocalFlowPlugins/utility/extractChaptersXml/1.0.0/index.js index bda443b..64da92f 100644 --- a/LocalFlowPlugins/utility/extractChaptersXml/1.0.0/index.js +++ b/LocalFlowPlugins/utility/extractChaptersXml/1.0.0/index.js @@ -50,10 +50,12 @@ const plugin = async (args) => { const cli = new CLI({ cli: mkvExtract, spawnArgs: [inputFile, 'chapters', xmlPath], + spawnOpts: { cwd: workDir }, WorkDir: workDir, jobLog, inputFileObj, updateWorker, + logFullCliOutput: args.logFullCliOutput || false, args, }); diff --git a/LocalFlowPlugins/video/transcode2PassX265/1.0.0/index.js b/LocalFlowPlugins/video/transcode2PassX265/1.0.0/index.js index 945d22f..0a3e8a1 100644 --- a/LocalFlowPlugins/video/transcode2PassX265/1.0.0/index.js +++ b/LocalFlowPlugins/video/transcode2PassX265/1.0.0/index.js @@ -266,10 +266,13 @@ const plugin = async (args) => { '-sn', outFile, ], + spawnOpts: { cwd: workDir }, WorkDir: workDir, jobLog, + outputFilePath: outFile, inputFileObj, updateWorker, + logFullCliOutput: args.logFullCliOutput || false, args, }); @@ -310,10 +313,12 @@ const plugin = async (args) => { '-f', 'null', nullDev, ], + spawnOpts: { cwd: workDir }, WorkDir: workDir, jobLog, inputFileObj, updateWorker, + logFullCliOutput: args.logFullCliOutput || false, args, }); @@ -345,10 +350,13 @@ const plugin = async (args) => { '-sn', outFile, ], + spawnOpts: { cwd: workDir }, WorkDir: workDir, jobLog, + outputFilePath: outFile, inputFileObj, updateWorker, + logFullCliOutput: args.logFullCliOutput || false, args, }); diff --git a/install.sh b/install.sh index 9fc49b7..81ada8a 100644 --- a/install.sh +++ b/install.sh @@ -105,6 +105,17 @@ download_flow_helpers() { curl -fsSL "$official_fu" -o "${dest}/fileUtils.js" || true fi + # Restore official Tdarr cliUtils.js if it was previously overwritten by a mock + local official_cu="https://raw.githubusercontent.com/HaveAGitGat/Tdarr_Plugins/master/FlowPlugins/FlowHelpers/1.0.0/cliUtils.js" + if [[ -f "${dest}/cliUtils.js" ]]; then + if grep -q "Local mock of Tdarr" "${dest}/cliUtils.js" || ! grep -q "ffmpegParser" "${dest}/cliUtils.js"; then + echo " 🔄 Restoring official Tdarr cliUtils.js..." + curl -fsSL "$official_cu" -o "${dest}/cliUtils.js" || true + fi + else + curl -fsSL "$official_cu" -o "${dest}/cliUtils.js" || true + fi + # Download our custom copyWithProgress helper local url="${RAW}/FlowHelpers/1.0.0/copyWithProgress.js" if curl -fsSL "$url" -o "${dest}/copyWithProgress.js"; then