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

@@ -0,0 +1,72 @@
'use strict';
const path = require('path');
const fs = require('fs');
/* eslint-disable no-param-reassign */
const details = () => ({
name: 'Extract Chapters to XML',
description:
'Extracts MKV chapter markers to an XML file in workDir using mkvextract. '
+ 'Stores the output path in args.variables.chaptersXmlPath. '
+ 'Safe to run even if the source has no chapters.',
style: { borderColor: '#9C27B0' },
tags: 'utility,chapters,mkv',
isStartPlugin: false,
pType: '',
requiresVersion: '2.11.01',
sidebarPosition: -1,
icon: 'faList',
inputs: [],
outputs: [
{ number: 1, tooltip: 'Continue to next plugin' },
],
});
const plugin = async (args) => {
const { jobLog, inputFileObj, variables, workDir } = args;
const inputFile = inputFileObj._id;
const baseName = path.basename(inputFile, path.extname(inputFile));
const xmlPath = path.join(workDir, `${baseName}_chapters.xml`);
jobLog(`[extractChaptersXml] Source: ${inputFile}\n`);
jobLog(`[extractChaptersXml] Output: ${xmlPath}\n`);
// Derive mkvextract from mkvpropeditPath (same MKVToolNix bin dir)
let mkvExtract = 'mkvextract';
if (args.mkvpropeditPath) {
const dir = path.dirname(args.mkvpropeditPath);
const candidate = path.join(dir, process.platform === 'win32' ? 'mkvextract.exe' : 'mkvextract');
if (fs.existsSync(candidate)) mkvExtract = candidate;
}
jobLog(`[extractChaptersXml] Using: ${mkvExtract}\n`);
const { CLI } = require('../../../../FlowHelpers/1.0.0/cliUtils');
const cli = new CLI({
cli: mkvExtract,
spawnArgs: [inputFile, 'chapters', xmlPath],
WorkDir: workDir,
jobLog,
});
const { cliExitCode } = await cli.runCli();
// mkvextract: exit 0 = OK, exit 1 = no chapters (still OK), exit 2 = error
if (cliExitCode > 1) {
jobLog(`[extractChaptersXml] WARNING: mkvextract exit ${cliExitCode} — chapters may be missing.\n`);
variables.chaptersXmlPath = '';
} else if (fs.existsSync(xmlPath) && fs.statSync(xmlPath).size > 50) {
variables.chaptersXmlPath = xmlPath;
jobLog(`[extractChaptersXml] Chapters saved: ${xmlPath}\n`);
} else {
jobLog('[extractChaptersXml] No chapters found.\n');
variables.chaptersXmlPath = '';
}
return { outputNumber: 1, outputFileObj: inputFileObj, variables };
};
module.exports.details = details;
module.exports.plugin = plugin;