Files
Tdarr-plugin/tests/test-cases/test-copy-and-subtitles.js
T

222 lines
10 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: 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'), '<chapters/>');
// 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'), '<chapters ep2/>');
// 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);
});
}