diff --git a/FlowHelpers/1.0.0/cliUtils.js b/FlowHelpers/1.0.0/cliUtils.js index 80146a2..3092d33 100644 --- a/FlowHelpers/1.0.0/cliUtils.js +++ b/FlowHelpers/1.0.0/cliUtils.js @@ -4,8 +4,10 @@ * Used only for local testing — on real Tdarr this file comes from the server. * * Matches the real Tdarr CLI constructor signature: - * new CLI({ cli, spawnArgs, WorkDir, jobLog, inputFileObj }) - * inputFileObj is accepted but unused here (only used by real Tdarr for progress tracking). + * 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). */ const { spawn } = require('child_process'); diff --git a/LocalFlowPlugins/audio/backupSourceAudio/1.0.0/index.js b/LocalFlowPlugins/audio/backupSourceAudio/1.0.0/index.js index 8eca5b3..a13503d 100644 --- a/LocalFlowPlugins/audio/backupSourceAudio/1.0.0/index.js +++ b/LocalFlowPlugins/audio/backupSourceAudio/1.0.0/index.js @@ -34,7 +34,7 @@ const details = () => ({ }); const plugin = async (args) => { - const { jobLog, inputFileObj, variables, workDir } = args; + const { jobLog, inputFileObj, variables, workDir, updateWorker } = args; const inputFile = inputFileObj._id; jobLog(`[backupSourceAudio] Processing: ${inputFile}\n`); @@ -77,6 +77,7 @@ const plugin = async (args) => { WorkDir: workDir, jobLog, inputFileObj, + updateWorker, }); const { cliExitCode } = await cli.runCli(); diff --git a/LocalFlowPlugins/subtitles/extractSubtitlesAndOcr/1.0.0/index.js b/LocalFlowPlugins/subtitles/extractSubtitlesAndOcr/1.0.0/index.js index d384c29..06054f8 100644 --- a/LocalFlowPlugins/subtitles/extractSubtitlesAndOcr/1.0.0/index.js +++ b/LocalFlowPlugins/subtitles/extractSubtitlesAndOcr/1.0.0/index.js @@ -79,10 +79,10 @@ const buildSuffix = (lang, stream) => { return parts.join('.'); }; -const runFFmpeg = async (spawnArgs, workDir, ffmpegPath, jobLog, inputFileObj) => { +const runFFmpeg = async (spawnArgs, workDir, ffmpegPath, jobLog, inputFileObj, updateWorker) => { const { CLI } = require('../../../../FlowHelpers/1.0.0/cliUtils'); const cli = new CLI({ - cli: ffmpegPath, spawnArgs, WorkDir: workDir, jobLog, inputFileObj, + cli: ffmpegPath, spawnArgs, WorkDir: workDir, jobLog, inputFileObj, updateWorker, }); return cli.runCli(); }; @@ -90,7 +90,7 @@ const runFFmpeg = async (spawnArgs, workDir, ffmpegPath, jobLog, inputFileObj) = // ─── Plugin ─────────────────────────────────────────────────────────────────── const plugin = async (args) => { - const { jobLog, inputFileObj, variables, workDir } = args; + const { jobLog, inputFileObj, variables, workDir, updateWorker } = args; const inputFile = inputFileObj._id; const sourceDir = path.dirname(inputFile); @@ -129,7 +129,7 @@ const plugin = async (args) => { if (TEXT_CODECS.has(codec)) { const { cliExitCode } = await runFFmpeg( ['-y', '-i', inputFile, '-map', `0:${stream.index}`, '-c:s', 'srt', outSrt], - workDir, args.ffmpegPath, jobLog, inputFileObj, + workDir, args.ffmpegPath, jobLog, inputFileObj, updateWorker, ); if (cliExitCode !== 0) { jobLog(`[extractSubtitlesAndOcr] WARNING: FFmpeg failed for stream #${stream.index} (exit ${cliExitCode}).\n`); @@ -146,7 +146,7 @@ const plugin = async (args) => { // Step 1: Extract to .sup const { cliExitCode: extractExit } = await runFFmpeg( ['-y', '-i', inputFile, '-map', `0:${stream.index}`, '-c:s', 'copy', tmpSup], - workDir, args.ffmpegPath, jobLog, inputFileObj, + workDir, args.ffmpegPath, jobLog, inputFileObj, updateWorker, ); if (extractExit !== 0) { @@ -167,6 +167,7 @@ const plugin = async (args) => { WorkDir: workDir, jobLog, inputFileObj, + updateWorker, }); const { cliExitCode: seExit } = await seCli.runCli(); diff --git a/LocalFlowPlugins/utility/copyToWorkDir/1.0.0/index.js b/LocalFlowPlugins/utility/copyToWorkDir/1.0.0/index.js new file mode 100644 index 0000000..08640db --- /dev/null +++ b/LocalFlowPlugins/utility/copyToWorkDir/1.0.0/index.js @@ -0,0 +1,114 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +/* eslint-disable no-param-reassign */ + +/** + * copyToWorkDir + * Copies the source file to the worker's local workDir before processing. + * + * Use this plugin BEFORE any transcode/extract plugin when: + * - The source file lives on a network share / mapped drive + * - You want the worker to encode from a local copy to avoid I/O bottleneck + * - The network path is slow or unreliable during long encodes + * + * Flow: + * 1. Copies source → workDir/ (streams, no RAM load) + * 2. Updates outputFileObj._id to the local copy path + * 3. Downstream plugins operate on the local copy + * 4. Tdarr replaces the original with the final output as usual + */ + +const details = () => ({ + name: 'Copy Source to Work Dir', + description: + 'Copies the source file into the worker local workDir before processing. ' + + 'Use before transcode plugins when the source is on a slow network share ' + + 'to avoid I/O bottleneck during encode. ' + + 'The local copy is used by all subsequent plugins in this flow.', + style: { borderColor: '#607D8B' }, + tags: 'utility', + isStartPlugin: false, + pType: '', + requiresVersion: '2.11.01', + sidebarPosition: 0, + icon: 'faCopy', + inputs: [ + { + label: 'Skip if already local', + name: 'skipIfLocal', + type: 'boolean', + defaultValue: 'true', + inputUI: { + type: 'dropdown', + options: ['true', 'false'], + }, + tooltip: + 'If true, skip the copy when the source file is already inside workDir ' + + 'or on the same drive as workDir (avoids double-copy on mapped nodes).', + }, + ], + outputs: [ + { number: 1, tooltip: 'File is ready in workDir — continue to next plugin' }, + ], +}); + +// ─── Plugin ─────────────────────────────────────────────────────────────────── + +const plugin = async (args) => { + const { jobLog, inputFileObj, variables, workDir } = args; + + const inputFile = inputFileObj._id; + const ext = path.extname(inputFile); + const baseName = path.basename(inputFile, ext); + const localCopy = path.join(workDir, `${baseName}${ext}`); + + jobLog(`[copyToWorkDir] Source : ${inputFile}\n`); + jobLog(`[copyToWorkDir] WorkDir: ${workDir}\n`); + + // ── Optional: skip if source is already local ───────────────────────────── + const skipIfLocal = (args.inputs.skipIfLocal || 'true') === 'true'; + if (skipIfLocal) { + const srcDrive = inputFile.split(/[/\\]/)[0].toUpperCase(); // e.g. "T:" + const workDrive = workDir.split(/[/\\]/)[0].toUpperCase(); // e.g. "T:" + const alreadyLocal = ( + inputFile.startsWith(workDir) + || (srcDrive && workDrive && srcDrive === workDrive) + ); + if (alreadyLocal) { + jobLog(`[copyToWorkDir] Source is already local (same drive: ${srcDrive}) — skipping copy.\n`); + return { outputNumber: 1, outputFileObj: inputFileObj, variables }; + } + } + + // ── Copy using streams (no RAM spike for large files) ──────────────────── + jobLog(`[copyToWorkDir] Copying to: ${localCopy}\n`); + + const startMs = Date.now(); + + await new Promise((resolve, reject) => { + const rd = fs.createReadStream(inputFile); + const wr = fs.createWriteStream(localCopy); + + rd.on('error', reject); + wr.on('error', reject); + wr.on('finish', resolve); + rd.pipe(wr); + }); + + const elapsed = ((Date.now() - startMs) / 1000).toFixed(1); + const sizeMb = (fs.statSync(localCopy).size / 1024 / 1024).toFixed(1); + jobLog(`[copyToWorkDir] Done: ${sizeMb} MB copied in ${elapsed}s\n`); + + // Return updated inputFileObj pointing to the local copy + return { + outputNumber: 1, + outputFileObj: { ...inputFileObj, _id: localCopy }, + variables, + }; +}; + +module.exports.details = details; +module.exports.plugin = plugin; diff --git a/LocalFlowPlugins/utility/extractChaptersXml/1.0.0/index.js b/LocalFlowPlugins/utility/extractChaptersXml/1.0.0/index.js index f79a9e5..5732546 100644 --- a/LocalFlowPlugins/utility/extractChaptersXml/1.0.0/index.js +++ b/LocalFlowPlugins/utility/extractChaptersXml/1.0.0/index.js @@ -25,7 +25,7 @@ const details = () => ({ }); const plugin = async (args) => { - const { jobLog, inputFileObj, variables, workDir } = args; + const { jobLog, inputFileObj, variables, workDir, updateWorker } = args; const inputFile = inputFileObj._id; const baseName = path.basename(inputFile, path.extname(inputFile)); @@ -50,6 +50,7 @@ const plugin = async (args) => { WorkDir: workDir, jobLog, inputFileObj, + updateWorker, }); const { cliExitCode } = await cli.runCli(); diff --git a/LocalFlowPlugins/video/transcode2PassX265/1.0.0/index.js b/LocalFlowPlugins/video/transcode2PassX265/1.0.0/index.js index 0e2747b..fbe36ab 100644 --- a/LocalFlowPlugins/video/transcode2PassX265/1.0.0/index.js +++ b/LocalFlowPlugins/video/transcode2PassX265/1.0.0/index.js @@ -98,7 +98,7 @@ const buildAudioArgs = (variables) => { // ─── Plugin ─────────────────────────────────────────────────────────────────── const plugin = async (args) => { - const { jobLog, inputFileObj, variables, workDir } = args; + const { jobLog, inputFileObj, variables, workDir, updateWorker } = args; const inputFile = inputFileObj._id; jobLog(`[transcode2PassX265] Source: ${inputFile}\n`); @@ -178,6 +178,7 @@ const plugin = async (args) => { WorkDir: workDir, jobLog, inputFileObj, + updateWorker, }); const { cliExitCode: pass1Exit } = await pass1Cli.runCli(); @@ -209,6 +210,7 @@ const plugin = async (args) => { WorkDir: workDir, jobLog, inputFileObj, + updateWorker, }); const { cliExitCode: pass2Exit } = await pass2Cli.runCli();