Fix copy progress, remove v2.0.0, fix subtitle default naming, enforce strict no-upcoding on audio
Build / Lint & Test (push) Successful in 3m15s
Build / Lint & Test (push) Successful in 3m15s
This commit is contained in:
1 parent
7ee5a5bc48
commit
86448988ed
12 files changed
+470
-348
No files matched your search
+61
-20
@@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
@@ -127,22 +127,53 @@ const AUDIO_INPUTS = {
|
||||
eq(o.variables.audioItaOpusBitrate, '384', 'Series DTS 768k >= 384 -> Opus=384');
|
||||
cleanup(a); }
|
||||
|
||||
// Series AC3 320k < 384 threshold -> 256
|
||||
// Series AC3 320k < 384 target -> 320 (strict no upcoding)
|
||||
{ const streams = [
|
||||
makeStream({ index:0, codec_type:'video' }),
|
||||
makeStream({ index:1, codec_type:'audio', codec_name:'ac3', bit_rate:'320000', channels:6, tags:{language:'ita'} }),
|
||||
];
|
||||
const a = makeArgs('/media/Sonarr/Show/ep.mkv', streams, AUDIO_INPUTS, { isMovie:'false' });
|
||||
const a = makeArgs('/media/Sonarr/Show/ep.mkv', streams, AUDIO_INPUTS, { isMovie:'false', isAnimation:'false' });
|
||||
const o = await audioPlugin(a);
|
||||
eq(o.variables.audioItaOpusBitrate, '256', 'Series AC3 320k < 384 -> Opus=256');
|
||||
eq(o.variables.audioItaOpusBitrate, '320', 'Series AC3 320k -> min(384,320)=320 (no upcoding)');
|
||||
cleanup(a); }
|
||||
|
||||
// NEW: Series bitrate unknown (0) -> safe default 384k
|
||||
// Series multichannel low bitrate 192k -> 192 (strict no upcoding to 256)
|
||||
{ const streams = [
|
||||
makeStream({ index:0, codec_type:'video' }),
|
||||
makeStream({ index:1, codec_type:'audio', codec_name:'aac', bit_rate:'192000', channels:6, tags:{language:'ita'} }),
|
||||
];
|
||||
const a = makeArgs('/media/Sonarr/Show/ep.mkv', streams, AUDIO_INPUTS, { isMovie:'false', isAnimation:'false' });
|
||||
const o = await audioPlugin(a);
|
||||
eq(o.variables.audioItaOpusBitrate, '192', 'Series 192k multichannel -> stays 192k (never upcoded to 256)');
|
||||
cleanup(a); }
|
||||
|
||||
// Series / Animation with 256k threshold -> capped at 256k
|
||||
{ const streams = [
|
||||
makeStream({ index:0, codec_type:'video' }),
|
||||
makeStream({ index:1, codec_type:'audio', codec_name:'ac3', bit_rate:'448000', channels:6, tags:{language:'ita'} }),
|
||||
];
|
||||
const customInputs = Object.assign({}, AUDIO_INPUTS, { seriesMultiChannelThreshold: '256' });
|
||||
const a = makeArgs('/media/Sonarr/Naruto/ep.mkv', streams, customInputs, { isMovie:'false' });
|
||||
const o = await audioPlugin(a);
|
||||
eq(o.variables.audioItaOpusBitrate, '256', 'Animation 448k multichannel with 256k threshold -> capped at 256k');
|
||||
cleanup(a); }
|
||||
|
||||
// Detection via tags.BPS when stream.bit_rate is 0
|
||||
{ const streams = [
|
||||
makeStream({ index:0, codec_type:'video' }),
|
||||
makeStream({ index:1, codec_type:'audio', codec_name:'ac3', bit_rate:'0', channels:6, tags:{language:'ita', BPS:'384000'} }),
|
||||
];
|
||||
const a = makeArgs('/media/Sonarr/Show/ep.mkv', streams, AUDIO_INPUTS, { isMovie:'false', isAnimation:'false' });
|
||||
const o = await audioPlugin(a);
|
||||
eq(o.variables.audioItaOpusBitrate, '384', 'Bitrate detected from tags.BPS -> 384k');
|
||||
cleanup(a); }
|
||||
|
||||
// Series bitrate unknown (0) and no BPS tag -> safe default 384k
|
||||
{ const streams = [
|
||||
makeStream({ index:0, codec_type:'video' }),
|
||||
makeStream({ index:1, codec_type:'audio', codec_name:'dts', bit_rate:'0', channels:6, tags:{language:'ita'} }),
|
||||
];
|
||||
const a = makeArgs('/media/Sonarr/Show/ep.mkv', streams, AUDIO_INPUTS, { isMovie:'false' });
|
||||
const a = makeArgs('/media/Sonarr/Show/ep.mkv', streams, AUDIO_INPUTS, { isMovie:'false', isAnimation:'false' });
|
||||
const o = await audioPlugin(a);
|
||||
eq(o.variables.audioItaOpusBitrate, '384', 'Series unknown bitrate (0) -> safe default 384k');
|
||||
cleanup(a); }
|
||||
@@ -208,25 +239,35 @@ const AUDIO_INPUTS = {
|
||||
ok(!transcodeCode.includes("'-map_chapters -1'"), 'pass 1 separates -map_chapters from its value');
|
||||
ok(transcodeCode.includes('const statsForX265 = path.basename(statsFile)'), 'x265 stats file is relative to workDir on Windows');
|
||||
|
||||
// ── copyToWorkDir & replaceOriginalFile v1.0.0 checks ─────────────────────
|
||||
const copyPluginCode = require('fs').readFileSync(path.join(ROOT, 'LocalFlowPlugins/utility/copyToWorkDir/1.0.0/index.js'), 'utf8');
|
||||
ok(copyPluginCode.includes('[copyToWorkDir] Progress:'), 'copyToWorkDir reports copy progress in the job log');
|
||||
const copyPluginV2Code = require('fs').readFileSync(path.join(ROOT, 'LocalFlowPlugins/utility/copyToWorkDir/2.0.0/index.js'), 'utf8');
|
||||
ok(copyPluginV2Code.includes('updateProgress: args.updateProgress'), 'copyToWorkDir v2 forwards UI progress');
|
||||
const replacePluginCode = require('fs').readFileSync(path.join(ROOT, 'LocalFlowPlugins/utility/replaceOriginalFile/2.0.0/index.js'), 'utf8');
|
||||
ok(replacePluginCode.includes('.partial.old'), 'replaceOriginalFile v2 uses a rollback backup');
|
||||
ok(replacePluginCode.includes('fs.renameSync(temporary, destination)'), 'replaceOriginalFile v2 atomically installs the verified copy');
|
||||
const replacePluginV1Code = require('fs').readFileSync(path.join(ROOT, 'LocalFlowPlugins/utility/replaceOriginalFile/1.0.0/index.js'), 'utf8');
|
||||
ok(replacePluginV1Code.includes('copyAndVerify'), 'replaceOriginalFile v1 verifies the copied file');
|
||||
ok(!replacePluginV1Code.includes('updateProgress'), 'replaceOriginalFile v1 has no progress callback');
|
||||
ok(copyPluginCode.includes('copyWithProgress'), 'copyToWorkDir v1 uses copyWithProgress');
|
||||
ok(copyPluginCode.includes('updateWorker'), 'copyToWorkDir v1 forwards updateWorker callback');
|
||||
ok(!fs.existsSync(path.join(ROOT, 'LocalFlowPlugins/utility/copyToWorkDir/2.0.0')), 'copyToWorkDir v2.0.0 is deleted');
|
||||
|
||||
const replacePluginCode = require('fs').readFileSync(path.join(ROOT, 'LocalFlowPlugins/utility/replaceOriginalFile/1.0.0/index.js'), 'utf8');
|
||||
ok(replacePluginCode.includes('copyWithProgress'), 'replaceOriginalFile v1 uses copyWithProgress');
|
||||
ok(replacePluginCode.includes('updateWorker'), 'replaceOriginalFile v1 forwards updateWorker callback');
|
||||
ok(replacePluginCode.includes('.partial.old'), 'replaceOriginalFile v1 uses a rollback backup');
|
||||
ok(replacePluginCode.includes('fs.renameSync(temporary, destination)'), 'replaceOriginalFile v1 atomically installs the verified copy');
|
||||
ok(!fs.existsSync(path.join(ROOT, 'LocalFlowPlugins/utility/replaceOriginalFile/2.0.0')), 'replaceOriginalFile v2.0.0 is deleted');
|
||||
|
||||
const copyHelperCode = require('fs').readFileSync(path.join(ROOT, 'FlowHelpers/1.0.0/copyWithProgress.js'), 'utf8');
|
||||
ok(copyHelperCode.includes('MAX_RETRIES = 2'), 'copy helper retries failed writes');
|
||||
ok(copyHelperCode.includes('Copy size mismatch'), 'copy helper verifies destination size');
|
||||
ok(copyHelperCode.includes('fs.unlinkSync(destination)'), 'copy helper removes partial copies before retrying');
|
||||
ok(copyHelperCode.includes('fs.promises.copyFile(source, destination)'), 'copy helper uses native file copying');
|
||||
ok(copyHelperCode.includes('PROGRESS_INTERVAL_MS = 250'), 'copy helper polls progress frequently');
|
||||
ok(copyHelperCode.includes('intervalSpeed'), 'copy helper reports interval speed');
|
||||
ok(!copyPluginV2Code.includes('copyMethod'), 'copyToWorkDir v2 has no copy method setting');
|
||||
ok(!replacePluginCode.includes('bufferSizeMb'), 'replaceOriginalFile v2 has no buffer setting');
|
||||
ok(copyHelperCode.includes('formatEta'), 'copy helper formats ETA');
|
||||
ok(copyHelperCode.includes('updateWorker'), 'copy helper updates Tdarr worker progress');
|
||||
|
||||
// ── Subtitle default naming checks ─────────────────────────────────────────
|
||||
console.log('\n── Subtitle default naming rules ────────────────────');
|
||||
const subPluginCode = require('fs').readFileSync(path.join(ROOT, 'LocalFlowPlugins/subtitles/extractSubtitlesAndOcr/1.0.0/index.js'), 'utf8');
|
||||
ok(subPluginCode.includes("lang === 'ita' && isForced"), 'extractSubtitlesAndOcr: only ita.forced gets default flag');
|
||||
ok(!subPluginCode.includes("if (isForced || (stream.disposition && stream.disposition.default))"), 'extractSubtitlesAndOcr: old buggy default condition removed');
|
||||
|
||||
const finalizeCode = require('fs').readFileSync(path.join(ROOT, 'LocalFlowPlugins/utility/finalizeMedia/1.0.0/index.js'), 'utf8');
|
||||
ok(finalizeCode.includes("value === 'default'"), 'finalizeMedia: strips default before checking for ita.forced');
|
||||
ok(finalizeCode.includes("isItaForced ? '.default' : ''"), 'finalizeMedia: expected subtitle sidecar only expects .default on ita.forced');
|
||||
|
||||
// ── Summary ───────────────────────────────────────────────────────────────
|
||||
console.log(`\n${'='.repeat(55)}`);
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* run-tests.js
|
||||
@@ -17,6 +17,7 @@ const tests = [
|
||||
{ name: 'Test 2 — Anime 25fps DTS ITA (Naruto)', file: './test-cases/test2-anime-25fps', heavy: false },
|
||||
{ name: 'Test 3 — Ultrawide 3840x1600 Scale', file: './test-cases/test3-ultrawide', heavy: true },
|
||||
{ name: 'Test 4 — Subtitle Extraction (forced + SDH)', file: './test-cases/test4-subtitles', heavy: false },
|
||||
{ name: 'Test 5 — Copy/Replace Progress & Subtitle Rules', file: './test-cases/test-copy-and-subtitles', heavy: false },
|
||||
];
|
||||
|
||||
async function main() {
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
'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)');
|
||||
|
||||
console.log('\n=== ALL COPY, MOVE & SUBTITLE 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);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user