Files
Tdarr-plugin/LocalFlowPlugins/utility/replaceOriginalFile/1.0.0/index.js
T
2026-09-30 18:43:12 +02:00

198 lines
7.1 KiB
JavaScript

'use strict';
const path = require('path');
const fs = require('fs');
const { copyWithProgress } = require('../../../../FlowHelpers/1.0.0/copyWithProgress');
/* eslint-disable no-param-reassign */
const details = () => ({
name: 'Replace Original File',
description:
'Copies the processed file to the original library path with streamed progress, '
+ 'size verification, an atomic rename, and rollback if replacement fails.',
style: { borderColor: '#FF9800' },
tags: 'utility,replace,progress',
isStartPlugin: false,
pType: '',
requiresVersion: '2.11.01',
sidebarPosition: 1,
icon: 'faExchangeAlt',
inputs: [
{
label: 'Delete processed work file',
name: 'deleteSource',
type: 'boolean',
defaultValue: 'true',
inputUI: { type: 'dropdown', options: ['true', 'false'] },
tooltip: 'Delete the processed file after it has replaced the original.',
},
{
label: 'Output Directory (Optional)',
name: 'outputDirectory',
type: 'string',
defaultValue: '',
inputUI: { type: 'text' },
tooltip: 'Target library directory for folder-to-folder conversion. If empty, automatically uses Library Output if configured, or replaces in-place. Example: T:/Media/Film or T:/Media/Serie',
},
],
outputs: [
{ number: 1, tooltip: 'Original file replaced — continue to next plugin' },
],
});
const plugin = async (args) => {
const { inputFileObj, variables, jobLog, updateWorker } = args;
const source = inputFileObj._id;
const configuredDestination = variables.originalLibraryFile || args.originalLibraryFile;
let destination = typeof configuredDestination === 'string'
? configuredDestination
: configuredDestination && configuredDestination._id;
if (!destination || typeof destination !== 'string') {
throw new Error('[replaceOriginalFile] No original library path is available');
}
if (!fs.existsSync(source)) {
throw new Error(`[replaceOriginalFile] Processed file not found: ${source}`);
}
const originalSourceFile = destination;
const originalSourceDir = path.dirname(originalSourceFile);
// Check for folder-to-folder target directory (from plugin input or Tdarr librarySettings.output)
const configuredOutput = ((args.inputs && args.inputs.outputDirectory) || '').trim()
|| ((args.librarySettings && args.librarySettings.output) || '').trim();
const sourceLibraryRoot = ((args.librarySettings && args.librarySettings.folder) || '').trim();
if (configuredOutput && path.resolve(configuredOutput) !== path.resolve(sourceLibraryRoot || originalSourceDir)) {
let relPath = '';
if (sourceLibraryRoot) {
try {
relPath = path.relative(path.resolve(sourceLibraryRoot), path.resolve(originalSourceFile));
} catch (_) { relPath = ''; }
}
if (!relPath || relPath.startsWith('..') || path.isAbsolute(relPath)) {
const base = path.basename(originalSourceFile);
const parent = path.basename(originalSourceDir);
const grandParent = path.basename(path.dirname(originalSourceDir));
if (grandParent && grandParent !== '.' && !grandParent.includes(':')) {
relPath = path.join(grandParent, parent, base);
} else {
relPath = path.join(parent, base);
}
}
destination = path.join(path.resolve(configuredOutput), relPath);
jobLog(`[replaceOriginalFile] Folder-to-folder mode: target is ${destination}\n`);
}
if (path.resolve(source) === path.resolve(destination)) {
jobLog('[replaceOriginalFile] Processed file is already at the original path — skipping.\n');
return { outputNumber: 1, outputFileObj: inputFileObj, variables };
}
const destinationDir = path.dirname(destination);
if (!fs.existsSync(destinationDir)) {
fs.mkdirSync(destinationDir, { recursive: true });
jobLog(`[replaceOriginalFile] Created destination directory: ${destinationDir}\n`);
}
const sourceExt = path.extname(source);
const destExt = path.extname(destination);
const oldOriginalFile = destination;
if (sourceExt && destExt && sourceExt.toLowerCase() !== destExt.toLowerCase()) {
destination = path.join(destinationDir, `${path.basename(destination, destExt)}${sourceExt}`);
}
const temporary = `${destination}.tdarr.tmp`;
const backup = `${oldOriginalFile}.partial.old`;
let backupCreated = false;
let installed = false;
try {
if (fs.existsSync(temporary)) {
fs.unlinkSync(temporary);
}
jobLog(`[replaceOriginalFile] Copying processed file to temporary destination: ${temporary}\n`);
await copyWithProgress({
source,
destination: temporary,
jobLog,
updateWorker,
updateProgress: args.updateProgress,
label: 'replaceOriginalFile',
});
if (fs.existsSync(oldOriginalFile)) {
if (fs.existsSync(backup)) {
fs.unlinkSync(backup);
}
fs.renameSync(oldOriginalFile, backup);
backupCreated = true;
}
if (fs.existsSync(destination) && destination !== oldOriginalFile) {
try { fs.unlinkSync(destination); } catch (_) { /* ignore */ }
}
fs.renameSync(temporary, destination);
installed = true;
if (backupCreated) {
try {
fs.unlinkSync(backup);
} catch (error) {
jobLog(`[replaceOriginalFile] WARNING: Could not remove backup ${backup}: ${error.message}\n`);
}
}
if ((args.inputs.deleteSource || 'true') === 'true' && path.resolve(source) !== path.resolve(destination)) {
try {
fs.unlinkSync(source);
} catch (error) {
jobLog(`[replaceOriginalFile] WARNING: Could not delete processed file ${source}: ${error.message}\n`);
}
}
if (path.resolve(destination) !== path.resolve(originalSourceFile) && fs.existsSync(originalSourceFile)) {
try {
fs.unlinkSync(originalSourceFile);
jobLog(`[replaceOriginalFile] Removed source file from intake directory: ${originalSourceFile}\n`);
} catch (error) {
jobLog(`[replaceOriginalFile] WARNING: Could not delete intake source file ${originalSourceFile}: ${error.message}\n`);
}
}
// Clean up temporary workDir copy (_old) if still present
if (variables.workDirCopyApplied === 'true') {
const workDir = path.dirname(source);
const oldBase = variables.workDirSourceBaseName || path.basename(destination, sourceExt);
const oldCandidate = path.join(workDir, `${oldBase}_old${destExt}`);
if (fs.existsSync(oldCandidate)) {
try { fs.unlinkSync(oldCandidate); } catch (_) { /* ignore */ }
}
}
variables.finalFile = destination;
variables.finalDir = destinationDir;
jobLog(`[replaceOriginalFile] Replaced original file: ${destination}\n`);
return {
outputNumber: 1,
outputFileObj: { ...inputFileObj, _id: destination },
variables,
};
} catch (error) {
if (fs.existsSync(temporary)) {
try { fs.unlinkSync(temporary); } catch (_) { /* ignore */ }
}
if (backupCreated && !installed && !fs.existsSync(destination)) {
try { fs.renameSync(backup, destination); } catch (_) { /* ignore */ }
}
throw new Error(`[replaceOriginalFile] Replacement failed: ${error.message}`);
}
};
module.exports.details = details;
module.exports.plugin = plugin;