'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: Multi-episode sidecar isolation & empty Season/Series directory cleanup const stagingRoot = path.join(tmpDir, 'staging'); const seriesSourceDir = path.join(stagingRoot, 'Halo', 'Season 01'); fs.mkdirSync(seriesSourceDir, { recursive: true }); const finalSeasonDir = path.join(tmpDir, 'final_library', 'Halo', 'Season 01'); fs.mkdirSync(finalSeasonDir, { recursive: true }); // In finalSeasonDir, put Episode 1 and Episode 2 with their sidecars const ep1File = path.join(finalSeasonDir, 'Halo - S01E01.mkv'); const ep2File = path.join(finalSeasonDir, 'Halo - S01E02.mkv'); fs.writeFileSync(ep1File, Buffer.alloc(100)); fs.writeFileSync(ep2File, Buffer.alloc(100)); // Sidecars for Ep1 (to be normalized when Ep1 is finalized) fs.writeFileSync(path.join(finalSeasonDir, 'Halo - S01E01.ita.forced.srt'), 'ep1 sub'); fs.writeFileSync(path.join(finalSeasonDir, 'Halo - S01E01_chapters.xml'), ''); // Sidecars for Ep2 (should NOT be touched when Ep1 is finalized!) fs.writeFileSync(path.join(finalSeasonDir, 'Halo - S01E02.eng.forced.default.srt'), 'ep2 sub'); fs.writeFileSync(path.join(finalSeasonDir, 'Halo - S01E02_chapters.xml'), ''); // In source directory, simulate that Ep2 is still waiting in staging fs.writeFileSync(path.join(seriesSourceDir, 'Halo - S01E02.mkv'), Buffer.alloc(100)); const finalizeEp1Args = { inputFileObj: { _id: ep1File, ffProbeData: { streams: [] }, }, variables: { originalLibraryDir: seriesSourceDir }, inputs: { removeEmptySourceDir: 'true', removeEmptyParentSeriesDir: 'true' }, jobLog: () => {}, }; await finalizePlugin(finalizeEp1Args); // Ep1 sidecars were processed assert(fs.existsSync(path.join(finalSeasonDir, 'Halo - S01E01.ita.forced.default.srt')), 'Ep1 subtitle normalized to .default'); assert(fs.existsSync(path.join(finalSeasonDir, 'Halo - S01E01_chapters.xml')), 'Ep1 chapters intact'); // Ep2 sidecars were COMPLETELY UNTOUCHED! assert(fs.existsSync(path.join(finalSeasonDir, 'Halo - S01E02.eng.forced.default.srt')), 'Ep2 subtitle was untouched when Ep1 was finalized'); assert(fs.existsSync(path.join(finalSeasonDir, 'Halo - S01E02_chapters.xml')), 'Ep2 chapters untouched'); // Source season directory was NOT removed because Ep2 was still in staging assert(fs.existsSync(seriesSourceDir), 'Source Season 01 directory kept while Ep2 remains'); // Now simulate Ep2 finishing and removing from staging, leaving only a junk file (e.g. Thumbs.db) fs.unlinkSync(path.join(seriesSourceDir, 'Halo - S01E02.mkv')); fs.writeFileSync(path.join(seriesSourceDir, 'Thumbs.db'), 'junk'); // Also put a junk file in parent series folder fs.writeFileSync(path.join(stagingRoot, 'Halo', 'desktop.ini'), 'junk'); const finalizeEp2Args = { inputFileObj: { _id: ep2File, ffProbeData: { streams: [] }, }, variables: { originalLibraryDir: seriesSourceDir }, inputs: { removeEmptySourceDir: 'true', removeEmptyParentSeriesDir: 'true' }, jobLog: () => {}, }; await finalizePlugin(finalizeEp2Args); // Ep2 subtitle normalized now assert(fs.existsSync(path.join(finalSeasonDir, 'Halo - S01E02.eng.forced.srt')), 'Ep2 subtitle normalized when Ep2 was finalized'); assert(!fs.existsSync(path.join(finalSeasonDir, 'Halo - S01E02.eng.forced.default.srt')), 'Ep2 default stripped'); // Season 01 and parent Halo directory removed, but stagingRoot (library root) preserved assert(!fs.existsSync(seriesSourceDir), 'Empty Season 01 directory removed'); assert(!fs.existsSync(path.join(stagingRoot, 'Halo')), 'Empty Series directory removed'); assert(fs.existsSync(stagingRoot), 'Staging root directory 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); }); }