feat: Tdarr Local Flow Plugin Suite v1.0

7 atomic plugins for automated x265 transcoding pipeline:
- utility/mediaAnalyzerGOP    : GOP calc, FPS detection, film/series/anime routing
- utility/setAnimationFlag    : manual animation flag override
- utility/extractChaptersXml  : mkvextract chapters to XML
- audio/audioSelectorClamping : codec scoring, clamped Opus bitrates, 2 outputs
- audio/backupSourceAudio     : lossless DTS/TrueHD ITA backup (-c:a copy)
- subtitles/extractSubtitlesAndOcr : SRT extract + SubtitleEdit OCR for PGS
- video/transcode2PassX265    : 2-pass x265, Lanczos 1080p bound, Opus mux

Test suite: 34 unit tests + 3 integration tests (FFmpeg real files)
FlowHelpers mock for local testing included
Docs: integration-guide.md with canvas wiring and deploy instructions
This commit is contained in:
PickleRick committed 2026-09-18 23:23:05 +02:00
commit c2074be826
19 files changed
+2134

No files matched your search

+102
View File
@@ -0,0 +1,102 @@
'use strict';
const { execSync, spawnSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const FFMPEG = process.env.FFMPEG_PATH || 'ffmpeg';
const SAMPLES = {
test1: 'tests/samples/media/toshiba/Radarr/Film Test (2023)/Film Test (2023).mkv',
test2: 'tests/samples/media/toshiba/Sonarr/Naruto S01/Naruto S01E01.mkv',
test3: 'tests/samples/test3_ultrawide.mkv',
test4: 'tests/samples/test4_subtitles.mkv',
};
function run(cmd) {
console.log(`\n>> ${cmd}\n`);
const result = spawnSync(cmd, { shell: true, stdio: 'inherit' });
if (result.status !== 0) {
console.error(`Command failed with exit ${result.status}`);
process.exit(1);
}
}
function ensureDir(filePath) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
}
// Test 1 — skip if already generated
if (!fs.existsSync(SAMPLES.test1)) {
console.log('Generating Test 1: 23.976fps Film (AC3 ITA 448k + EAC3 ENG 640k)...');
ensureDir(SAMPLES.test1);
run([
FFMPEG, '-y',
'-f', 'lavfi', '-i', 'color=c=blue:s=1920x1080:r=24000/1001:d=30',
'-f', 'lavfi', '-i', 'sine=f=440:r=48000:d=30',
'-f', 'lavfi', '-i', 'sine=f=660:r=48000:d=30',
'-map', '0:v', '-map', '1:a', '-map', '2:a',
'-c:v', 'libx264', '-crf', '28', '-preset', 'ultrafast',
'-c:a:0', 'ac3', '-b:a:0', '448k', '-ac:a:0', '6', '-metadata:s:a:0', 'language=ita',
'-c:a:1', 'eac3', '-b:a:1', '640k', '-ac:a:1', '6', '-metadata:s:a:1', 'language=eng',
`"${SAMPLES.test1}"`,
].join(' '));
console.log('Test 1 OK.\n');
} else {
console.log('Test 1 already exists — skipping.\n');
}
// Test 2 — EAC3 ITA 640k 5.1 at 25fps (DTS is experimental in FFmpeg)
// Note: DTS logic is fully covered by unit tests with synthetic streams.
// For integration, we use EAC3 640k which triggers series clamping: 640>=384 -> Opus=384
console.log('Generating Test 2: 25fps Anime EAC3 ITA 640k 5.1 (Naruto)...');
ensureDir(SAMPLES.test2);
run([
FFMPEG, '-y',
'-f', 'lavfi', '-i', 'color=c=green:s=1280x720:r=25:d=30',
'-f', 'lavfi', '-i', 'sine=f=660:r=48000:d=30',
'-map', '0:v', '-map', '1:a',
'-c:v', 'libx264', '-crf', '28', '-preset', 'ultrafast',
'-c:a:0', 'eac3', '-b:a:0', '640k', '-ac:a:0', '6', '-metadata:s:a:0', 'language=ita',
`"${SAMPLES.test2}"`,
].join(' '));
console.log('Test 2 OK.\n');
// Test 3 — Ultrawide 3840x1600
console.log('Generating Test 3: Ultrawide 3840x1600...');
ensureDir(SAMPLES.test3);
run([
FFMPEG, '-y',
'-f', 'lavfi', '-i', 'color=c=red:s=3840x1600:r=24:d=10',
'-f', 'lavfi', '-i', 'sine=f=440:r=48000:d=10',
'-map', '0:v', '-map', '1:a',
'-c:v', 'libx264', '-crf', '28', '-preset', 'ultrafast',
'-c:a', 'aac', '-b:a', '128k', '-metadata:s:a:0', 'language=eng',
`"${SAMPLES.test3}"`,
].join(' '));
console.log('Test 3 OK.\n');
// Test 4 — SRT forced + SRT SDH
console.log('Generating Test 4: Subtitles (SRT forced + SRT SDH)...');
ensureDir(SAMPLES.test4);
const srtContent = '1\n00:00:01,000 --> 00:00:03,000\nTest subtitle line 1\n\n2\n00:00:04,000 --> 00:00:06,000\nTest subtitle line 2\n';
const tmpSrt = 'tests/samples/tmp_sub.srt';
fs.writeFileSync(tmpSrt, srtContent);
run([
FFMPEG, '-y',
'-f', 'lavfi', '-i', 'color=c=gray:s=1920x1080:r=25:d=30',
'-f', 'lavfi', '-i', 'sine=f=440:r=48000:d=30',
'-i', `"${tmpSrt}"`,
'-i', `"${tmpSrt}"`,
'-map', '0:v', '-map', '1:a', '-map', '2:s', '-map', '3:s',
'-c:v', 'libx264', '-crf', '28', '-preset', 'ultrafast',
'-c:a', 'aac', '-b:a', '128k', '-metadata:s:a:0', 'language=ita',
'-c:s:0', 'srt', '-metadata:s:s:0', 'language=ita', '-disposition:s:0', 'forced',
'-c:s:1', 'srt', '-metadata:s:s:1', 'language=ita', '-disposition:s:1', 'hearing_impaired',
`"${SAMPLES.test4}"`,
].join(' '));
try { fs.unlinkSync(tmpSrt); } catch(_) {}
console.log('Test 4 OK.\n');
console.log('All test files generated. Run: node tests/run-tests.js --skip-transcode');