305 lines
15 KiB
JavaScript
305 lines
15 KiB
JavaScript
'use strict';
|
|
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
|
|
const { copyWithProgress } = require('../../FlowHelpers/1.0.0/copyWithProgress');
|
|
const { plugin: copyPlugin } = require('../../LocalFlowPlugins/utility/copyToWorkDir/1.0.0/index');
|
|
const { plugin: replacePlugin } = require('../../LocalFlowPlugins/utility/replaceOriginalFile/1.0.0/index');
|
|
const { plugin: finalizePlugin } = require('../../LocalFlowPlugins/utility/finalizeMedia/1.0.0/index');
|
|
|
|
const assert = (condition, message) => {
|
|
if (!condition) throw new Error(`FAILED: ${message}`);
|
|
console.log(` [OK] ${message}`);
|
|
};
|
|
|
|
async function testCopyAndSubtitles() {
|
|
console.log('\n=== Testing copyWithProgress, copyToWorkDir, replaceOriginalFile & subtitle naming ===\n');
|
|
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tdarr-test-copy-'));
|
|
|
|
try {
|
|
// 1. Test copyWithProgress with mock worker updates
|
|
const srcFile = path.join(tmpDir, 'source.bin');
|
|
const dstFile = path.join(tmpDir, 'dest.bin');
|
|
const dummyData = Buffer.alloc(5 * 1024 * 1024, 0xAB); // 5 MB
|
|
fs.writeFileSync(srcFile, dummyData);
|
|
|
|
const workerUpdates = [];
|
|
const jobLogs = [];
|
|
|
|
await copyWithProgress({
|
|
source: srcFile,
|
|
destination: dstFile,
|
|
jobLog: (msg) => jobLogs.push(msg),
|
|
updateWorker: (data) => workerUpdates.push(data),
|
|
label: 'testCopy',
|
|
});
|
|
|
|
assert(fs.existsSync(dstFile), 'Destination file created');
|
|
assert(fs.statSync(dstFile).size === dummyData.length, 'Destination file size matches source');
|
|
assert(workerUpdates.length > 0, 'Worker updates were received');
|
|
assert(workerUpdates[workerUpdates.length - 1].percentage === 100, 'Final worker percentage is 100');
|
|
assert(jobLogs.some((l) => l.includes('Done:')), 'jobLog recorded completion');
|
|
|
|
// 2. Test copyToWorkDir plugin
|
|
const workDir = path.join(tmpDir, 'workdir');
|
|
fs.mkdirSync(workDir);
|
|
|
|
const copyArgs = {
|
|
inputFileObj: { _id: srcFile },
|
|
variables: {},
|
|
workDir,
|
|
inputs: { skipIfLocal: 'true' },
|
|
jobLog: () => {},
|
|
updateWorker: (data) => workerUpdates.push(data),
|
|
};
|
|
|
|
const copyResult = await copyPlugin(copyArgs);
|
|
assert(copyResult.outputNumber === 1, 'copyToWorkDir returned output 1');
|
|
assert(fs.existsSync(copyResult.outputFileObj._id), 'Local copy file exists in workDir');
|
|
assert(copyResult.variables.originalLibraryFile === srcFile, 'originalLibraryFile saved in variables');
|
|
assert(copyResult.variables.workDirCopyApplied === 'true', 'workDirCopyApplied marked true');
|
|
|
|
// 3. Test replaceOriginalFile plugin
|
|
const processedFile = path.join(workDir, 'source_processed.bin');
|
|
const processedData = Buffer.alloc(3 * 1024 * 1024, 0xCD); // 3 MB
|
|
fs.writeFileSync(processedFile, processedData);
|
|
|
|
const replaceArgs = {
|
|
inputFileObj: { _id: processedFile },
|
|
variables: { originalLibraryFile: srcFile },
|
|
workDir,
|
|
inputs: { deleteSource: 'true' },
|
|
jobLog: () => {},
|
|
updateWorker: (data) => workerUpdates.push(data),
|
|
};
|
|
|
|
const replaceResult = await replacePlugin(replaceArgs);
|
|
assert(replaceResult.outputNumber === 1, 'replaceOriginalFile returned output 1');
|
|
assert(fs.existsSync(srcFile), 'Original library file exists at destination');
|
|
assert(fs.statSync(srcFile).size === processedData.length, 'Original file successfully replaced with processed file');
|
|
assert(!fs.existsSync(processedFile), 'Processed work file deleted from workDir');
|
|
assert(!fs.existsSync(`${srcFile}.partial.old`), 'Temporary backup deleted');
|
|
assert(!fs.existsSync(`${srcFile}.tdarr.tmp`), 'Temporary file deleted');
|
|
|
|
// 4. Test finalizeMedia subtitle renaming & default stripping
|
|
const libraryDir = path.join(tmpDir, 'library');
|
|
fs.mkdirSync(libraryDir);
|
|
const mediaFile = path.join(libraryDir, 'Movie.mkv');
|
|
fs.writeFileSync(mediaFile, Buffer.alloc(100));
|
|
|
|
// Create a variety of subtitles:
|
|
// - eng.forced.default.srt (default should be stripped -> eng.forced.srt)
|
|
// - ita.forced.srt (default should be added -> ita.forced.default.srt)
|
|
// - ita.forced.default.srt (already correct -> stays ita.forced.default.srt)
|
|
// - ita.default.srt (default should be stripped -> ita.srt)
|
|
// - eng.default.srt (default should be stripped -> eng.srt)
|
|
// - eng.sdh.default.srt (default should be stripped -> eng.sdh.srt)
|
|
fs.writeFileSync(path.join(libraryDir, 'Movie.eng.forced.default.srt'), 'dummy 1');
|
|
fs.writeFileSync(path.join(libraryDir, 'Movie.ita.forced.srt'), 'dummy 2');
|
|
fs.writeFileSync(path.join(libraryDir, 'Movie.ita.default.srt'), 'dummy 3');
|
|
fs.writeFileSync(path.join(libraryDir, 'Movie.eng.default.srt'), 'dummy 4');
|
|
fs.writeFileSync(path.join(libraryDir, 'Movie.eng.sdh.default.srt'), 'dummy 5');
|
|
|
|
const finalizeArgs = {
|
|
inputFileObj: {
|
|
_id: mediaFile,
|
|
ffProbeData: { streams: [] },
|
|
},
|
|
variables: { originalLibraryDir: libraryDir },
|
|
inputs: { removeEmptySourceDir: 'false' },
|
|
jobLog: () => {},
|
|
};
|
|
|
|
await finalizePlugin(finalizeArgs);
|
|
|
|
assert(!fs.existsSync(path.join(libraryDir, 'Movie.eng.forced.default.srt')), 'eng.forced.default.srt was renamed');
|
|
assert(fs.existsSync(path.join(libraryDir, 'Movie.eng.forced.srt')), 'eng.forced.srt exists (default stripped)');
|
|
|
|
assert(!fs.existsSync(path.join(libraryDir, 'Movie.ita.forced.srt')), 'ita.forced.srt was normalized');
|
|
assert(fs.existsSync(path.join(libraryDir, 'Movie.ita.forced.default.srt')), 'ita.forced.default.srt exists');
|
|
|
|
assert(!fs.existsSync(path.join(libraryDir, 'Movie.ita.default.srt')), 'ita.default.srt was renamed');
|
|
assert(fs.existsSync(path.join(libraryDir, 'Movie.ita.srt')), 'ita.srt exists (default stripped)');
|
|
|
|
assert(!fs.existsSync(path.join(libraryDir, 'Movie.eng.default.srt')), 'eng.default.srt was renamed');
|
|
assert(fs.existsSync(path.join(libraryDir, 'Movie.eng.srt')), 'eng.srt exists (default stripped)');
|
|
|
|
assert(!fs.existsSync(path.join(libraryDir, 'Movie.eng.sdh.default.srt')), 'eng.sdh.default.srt was renamed');
|
|
assert(fs.existsSync(path.join(libraryDir, 'Movie.eng.sdh.srt')), 'eng.sdh.srt exists (default stripped)');
|
|
|
|
// 5. Test TV Series: Complete Folder-to-Folder Workflow (Sonarr -> Serie)
|
|
const sonarrRoot = path.join(tmpDir, 'Sonarr');
|
|
const seriesSourceDir = path.join(sonarrRoot, 'Halo.2022 [tvdbid-12345]', 'After.Life.S01');
|
|
fs.mkdirSync(seriesSourceDir, { recursive: true });
|
|
|
|
const serieOutputRoot = path.join(tmpDir, 'Serie');
|
|
|
|
// Create Episode 1 source files in Sonarr
|
|
const ep1SourceFile = path.join(seriesSourceDir, 'Halo.S01E01.mkv');
|
|
const ep1SubFile = path.join(seriesSourceDir, 'Halo.S01E01.ita.forced.srt');
|
|
const ep1ChapFile = path.join(seriesSourceDir, 'Halo.S01E01_chapters.xml');
|
|
fs.writeFileSync(ep1SourceFile, Buffer.alloc(100));
|
|
fs.writeFileSync(ep1SubFile, 'ep1 sub');
|
|
fs.writeFileSync(ep1ChapFile, '<chapters ep1/>');
|
|
|
|
// Create Episode 2 source files in Sonarr (still waiting to be processed)
|
|
const ep2SourceFile = path.join(seriesSourceDir, 'Halo.S01E02.mkv');
|
|
const ep2SubFile = path.join(seriesSourceDir, 'Halo.S01E02.eng.forced.default.srt');
|
|
const ep2ChapFile = path.join(seriesSourceDir, 'Halo.S01E02_chapters.xml');
|
|
fs.writeFileSync(ep2SourceFile, Buffer.alloc(100));
|
|
fs.writeFileSync(ep2SubFile, 'ep2 sub');
|
|
fs.writeFileSync(ep2ChapFile, '<chapters ep2/>');
|
|
|
|
// Ep1 encoded file in worker workDir
|
|
const ep1WorkFile = path.join(workDir, 'Halo.S01E01_encoded.mkv');
|
|
fs.writeFileSync(ep1WorkFile, Buffer.alloc(200));
|
|
|
|
// A. Run replaceOriginalFile for Ep1 with folder-to-folder
|
|
const ep1ReplaceResult = await replacePlugin({
|
|
inputFileObj: { _id: ep1WorkFile },
|
|
variables: { originalLibraryFile: ep1SourceFile, originalLibraryDir: seriesSourceDir },
|
|
workDir,
|
|
librarySettings: { folder: sonarrRoot, output: serieOutputRoot },
|
|
inputs: { deleteSource: 'true' },
|
|
jobLog: () => {},
|
|
updateWorker: () => {},
|
|
});
|
|
|
|
assert(fs.existsSync(ep1ReplaceResult.outputFileObj._id), 'Ep1 installed in Serie destination library');
|
|
assert(ep1ReplaceResult.outputFileObj._id.includes(path.join('Serie', 'Halo.2022 [tvdbid-12345]', 'After.Life.S01')), 'Ep1 relative season subfolder preserved');
|
|
assert(!fs.existsSync(ep1SourceFile), 'Ep1 source video unlinked from Sonarr intake');
|
|
assert(fs.existsSync(ep2SourceFile), 'Ep2 source video untouched in Sonarr intake');
|
|
assert(fs.existsSync(ep1SubFile), 'Ep1 subtitle sidecar still in Sonarr before finalize');
|
|
assert(fs.existsSync(ep2SubFile), 'Ep2 subtitle sidecar untouched in Sonarr');
|
|
|
|
// B. Run finalizeMedia for Ep1
|
|
const ep1FinalDir = path.dirname(ep1ReplaceResult.outputFileObj._id);
|
|
await finalizePlugin({
|
|
inputFileObj: {
|
|
_id: ep1ReplaceResult.outputFileObj._id,
|
|
ffProbeData: { streams: [] },
|
|
},
|
|
variables: ep1ReplaceResult.variables,
|
|
librarySettings: { folder: sonarrRoot, output: serieOutputRoot },
|
|
inputs: { removeEmptySourceDir: 'true', removeEmptyParentSeriesDir: 'true' },
|
|
jobLog: () => {},
|
|
});
|
|
|
|
// Check Ep1 sidecars migrated and normalized in Serie
|
|
assert(fs.existsSync(path.join(ep1FinalDir, 'Halo.S01E01.ita.forced.default.srt')), 'Ep1 subtitle moved to Serie and normalized with .default');
|
|
assert(fs.existsSync(path.join(ep1FinalDir, 'Halo.S01E01_chapters.xml')), 'Ep1 chapters moved to Serie');
|
|
assert(!fs.existsSync(ep1SubFile), 'Ep1 subtitle removed from Sonarr sourceDir');
|
|
assert(!fs.existsSync(ep1ChapFile), 'Ep1 chapters removed from Sonarr sourceDir');
|
|
|
|
// Check Ep2 sidecars in Sonarr were completely untouched
|
|
assert(fs.existsSync(ep2SubFile), 'Ep2 subtitle untouched in Sonarr sourceDir');
|
|
assert(fs.existsSync(ep2ChapFile), 'Ep2 chapters untouched in Sonarr sourceDir');
|
|
|
|
// Season folder in Sonarr preserved because Ep2 is still there
|
|
assert(fs.existsSync(seriesSourceDir), 'Sonarr season folder preserved while Ep2 remains');
|
|
|
|
// C. Now process Episode 2
|
|
const ep2WorkFile = path.join(workDir, 'Halo.S01E02_encoded.mkv');
|
|
fs.writeFileSync(ep2WorkFile, Buffer.alloc(200));
|
|
|
|
const ep2ReplaceResult = await replacePlugin({
|
|
inputFileObj: { _id: ep2WorkFile },
|
|
variables: { originalLibraryFile: ep2SourceFile, originalLibraryDir: seriesSourceDir },
|
|
workDir,
|
|
librarySettings: { folder: sonarrRoot, output: serieOutputRoot },
|
|
inputs: { deleteSource: 'true' },
|
|
jobLog: () => {},
|
|
updateWorker: () => {},
|
|
});
|
|
|
|
assert(fs.existsSync(ep2ReplaceResult.outputFileObj._id), 'Ep2 installed in Serie destination library');
|
|
assert(!fs.existsSync(ep2SourceFile), 'Ep2 source video unlinked from Sonarr');
|
|
|
|
const ep2FinalDir = path.dirname(ep2ReplaceResult.outputFileObj._id);
|
|
await finalizePlugin({
|
|
inputFileObj: {
|
|
_id: ep2ReplaceResult.outputFileObj._id,
|
|
ffProbeData: { streams: [] },
|
|
},
|
|
variables: ep2ReplaceResult.variables,
|
|
librarySettings: { folder: sonarrRoot, output: serieOutputRoot },
|
|
inputs: { removeEmptySourceDir: 'true', removeEmptyParentSeriesDir: 'true' },
|
|
jobLog: () => {},
|
|
});
|
|
|
|
// Ep2 sidecars normalized in Serie (default stripped for eng)
|
|
assert(fs.existsSync(path.join(ep2FinalDir, 'Halo.S01E02.eng.forced.srt')), 'Ep2 subtitle moved to Serie and default stripped');
|
|
assert(!fs.existsSync(path.join(ep2FinalDir, 'Halo.S01E02.eng.forced.default.srt')), 'Ep2 old subtitle removed');
|
|
assert(fs.existsSync(path.join(ep2FinalDir, 'Halo.S01E02_chapters.xml')), 'Ep2 chapters moved to Serie');
|
|
|
|
// Season 01 and parent Halo directory removed from Sonarr, but sonarrRoot preserved!
|
|
assert(!fs.existsSync(seriesSourceDir), 'Empty Season 01 directory removed from Sonarr');
|
|
assert(!fs.existsSync(path.join(sonarrRoot, 'Halo.2022 [tvdbid-12345]')), 'Empty Series parent directory removed from Sonarr');
|
|
assert(fs.existsSync(sonarrRoot), 'Sonarr library root preserved');
|
|
|
|
// 6. Test Movie: Complete Folder-to-Folder Workflow (Radarr -> Film)
|
|
const radarrRoot = path.join(tmpDir, 'Radarr');
|
|
const movieSourceDir = path.join(radarrRoot, 'Avatar.2009');
|
|
fs.mkdirSync(movieSourceDir, { recursive: true });
|
|
|
|
const filmOutputRoot = path.join(tmpDir, 'Film');
|
|
|
|
const movieSourceFile = path.join(movieSourceDir, 'Avatar.2009.mkv');
|
|
const movieSubFile = path.join(movieSourceDir, 'Avatar.2009.ita.forced.srt');
|
|
const movieChapFile = path.join(movieSourceDir, 'Avatar.2009_chapters.xml');
|
|
fs.writeFileSync(movieSourceFile, Buffer.alloc(100));
|
|
fs.writeFileSync(movieSubFile, 'movie sub');
|
|
fs.writeFileSync(movieChapFile, '<chapters movie/>');
|
|
|
|
const movieWorkFile = path.join(workDir, 'Avatar.2009_encoded.mkv');
|
|
fs.writeFileSync(movieWorkFile, Buffer.alloc(200));
|
|
|
|
const movieReplaceResult = await replacePlugin({
|
|
inputFileObj: { _id: movieWorkFile },
|
|
variables: { originalLibraryFile: movieSourceFile, originalLibraryDir: movieSourceDir },
|
|
workDir,
|
|
librarySettings: { folder: radarrRoot, output: filmOutputRoot },
|
|
inputs: { deleteSource: 'true' },
|
|
jobLog: () => {},
|
|
updateWorker: () => {},
|
|
});
|
|
|
|
assert(fs.existsSync(movieReplaceResult.outputFileObj._id), 'Movie installed in Film library');
|
|
assert(movieReplaceResult.outputFileObj._id.includes(path.join('Film', 'Avatar.2009')), 'Movie subfolder preserved in Film');
|
|
assert(!fs.existsSync(movieSourceFile), 'Movie source video unlinked from Radarr');
|
|
|
|
const movieFinalDir = path.dirname(movieReplaceResult.outputFileObj._id);
|
|
await finalizePlugin({
|
|
inputFileObj: {
|
|
_id: movieReplaceResult.outputFileObj._id,
|
|
ffProbeData: { streams: [] },
|
|
},
|
|
variables: movieReplaceResult.variables,
|
|
librarySettings: { folder: radarrRoot, output: filmOutputRoot },
|
|
inputs: { removeEmptySourceDir: 'true', removeEmptyParentSeriesDir: 'true' },
|
|
jobLog: () => {},
|
|
});
|
|
|
|
assert(fs.existsSync(path.join(movieFinalDir, 'Avatar.2009.ita.forced.default.srt')), 'Movie subtitle moved to Film and normalized');
|
|
assert(fs.existsSync(path.join(movieFinalDir, 'Avatar.2009_chapters.xml')), 'Movie chapters moved to Film');
|
|
assert(!fs.existsSync(movieSourceDir), 'Empty movie directory removed from Radarr');
|
|
assert(fs.existsSync(radarrRoot), 'Radarr library root preserved');
|
|
|
|
console.log('\n=== ALL COPY, MOVE, SUBTITLE & SERIES TESTS PASSED ===\n');
|
|
} finally {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
module.exports = testCopyAndSubtitles;
|
|
|
|
if (require.main === module) {
|
|
testCopyAndSubtitles().catch((err) => {
|
|
console.error('Test failed:', err);
|
|
process.exit(1);
|
|
});
|
|
}
|