diff --git a/fileflows/scripts/05_Finalize_Move.js b/fileflows/scripts/05_Finalize_Move.js index 4e209ee..2b8b69e 100644 --- a/fileflows/scripts/05_Finalize_Move.js +++ b/fileflows/scripts/05_Finalize_Move.js @@ -4,10 +4,8 @@ * behandelt Untertitel (Text-Subs als .srt-Sidecar, Bild-Subs Disposition neutralisieren). * SICHERHEITS-FIX 17.07.2026: atomare Sidecar-Erzeugung, 0-Byte-Pruefung, * nur erfolgreich extrahierte Textspuren entfernen, Remux-Verifikation. - * CLEANUP-FIX 16.09.2026: leeres Release-Verzeichnis unter SourceRoot - * entfernen (nur wenn keine Mediendatei mehr drin liegt). * @author Sascha + Trulla - * @revision 3 + * @revision 2 * @output OK * @output Fehler */ @@ -60,7 +58,6 @@ function Script(SourceRoot, DestRoot) if (origPath !== destAbs) { Flow.Execute({ command: '/bin/rm', argumentList: ['-f', origPath] }); Logger.ILog('Deleted original: ' + origPath); - cleanupSourceDir(origPath); } // ======================================================================= @@ -90,52 +87,6 @@ function Script(SourceRoot, DestRoot) return 1; // -- Helfer -------------------------------------------------------------- - - /** - * Loescht das Release-Verzeichnis des Originals, wenn darin keine - * Mediendatei mehr liegt. Ohne das bleiben nach jedem Import leere - * Ordner mit .nfo/.sfv-Resten unter /tdarr/complete/ liegen, die den - * Handoff-Diagnostics-Endpoint als "target_directories" verschmutzen - * und faelschlich wie haengende Jobs aussehen. - * Sicherheit: nur unter SourceRoot, nur wenn keine Mediendatei mehr da. - */ - function cleanupSourceDir(originalFile) { - var dir = originalFile.substring(0, originalFile.lastIndexOf('/')); - - if (!dir || dir.indexOf(SourceRoot) !== 0 || dir === SourceRoot || - dir === SourceRoot.replace(/\/$/, '')) { - Logger.ILog('Cleanup: "' + dir + '" nicht unter ' + SourceRoot + ' - uebersprungen'); - return; - } - - var mediaExts = ['mkv', 'mp4', 'avi', 'ts', 'm2ts', 'mov', 'wmv', 'mpg', 'mpeg', 'm4v', 'iso']; - var findArgs = [dir, '-type', 'f', '(']; - mediaExts.forEach(function (ext, i) { - if (i > 0) findArgs.push('-o'); - findArgs.push('-iname', '*.' + ext); - }); - findArgs.push(')'); - - var found = Flow.Execute({ command: '/usr/bin/find', argumentList: findArgs }); - if (!found || found.exitCode !== 0) { - Logger.WLog('Cleanup: find fehlgeschlagen - Verzeichnis bleibt: ' + dir); - return; - } - - var remaining = ('' + (found.standardOutput || '')).trim(); - if (remaining.length > 0) { - Logger.ILog('Cleanup: Mediendateien noch vorhanden - Verzeichnis bleibt: ' + dir); - return; - } - - var del = Flow.Execute({ command: '/bin/rm', argumentList: ['-rf', dir] }); - if (del && del.exitCode === 0) { - Logger.ILog('Cleanup: Release-Verzeichnis entfernt: ' + dir); - } else { - Logger.WLog('Cleanup: rm fehlgeschlagen (exit=' + (del ? del.exitCode : '?') + '): ' + dir); - } - } - function handleSubtitles(file) { var ffprobe = Flow.GetToolPath('ffprobe') || Variables['ffprobe'] || 'ffprobe'; var ffmpeg = Flow.GetToolPath('ffmpeg') || Variables['ffmpeg'] || 'ffmpeg'; diff --git a/fileflows/tests/test_finalize_move_cleanup.py b/fileflows/tests/test_finalize_move_cleanup.py deleted file mode 100644 index 56dd29b..0000000 --- a/fileflows/tests/test_finalize_move_cleanup.py +++ /dev/null @@ -1,166 +0,0 @@ -"""Tests fuer cleanupSourceDir in 05_Finalize_Move.js. - -Hintergrund (16.09.2026): Das Script loeschte nur die Original-Videodatei, nicht -das Release-Verzeichnis. Dadurch blieben unter /tdarr/complete/ leere Ordner mit -.nfo-Resten liegen. Die tauchten im Butler-Endpoint -GET /media/handoff/diagnostics als "target_directories" auf und sahen wie -haengende Jobs aus, obwohl der Import laengst durch war. - -Diese Tests fahren das echte Script in einem Node-VM-Sandkasten und pruefen, -dass rm -rf NUR bei leeren Release-Verzeichnissen unter SourceRoot laeuft. -""" - -import json -import subprocess -import unittest -from pathlib import Path - - -SCRIPT = Path(__file__).parents[1] / "scripts" / "05_Finalize_Move.js" - -NODE_HARNESS = r""" -const fs = require('fs'); -const vm = require('vm'); -const scenario = JSON.parse(process.argv[2]); -let source = fs.readFileSync(process.argv[3], 'utf8') - .replace(/^\/\*\*[\s\S]*?\*\/\s*/, ''); -source += '\n;globalThis.__exports = { Script };'; - -const executed = []; - -function fakeExecute(spec) { - const cmd = spec.command; - const args = spec.argumentList || []; - executed.push({command: cmd, args: args}); - - if (cmd === '/usr/bin/find') { - const dir = args[0]; - const hits = (scenario.mediaFiles || []).filter(f => f.startsWith(dir)); - return {exitCode: scenario.findExitCode === undefined ? 0 : scenario.findExitCode, - standardOutput: hits.join('\n')}; - } - if (cmd === '/usr/bin/test') { - return {exitCode: 1}; // Ziel existiert nicht - } - if (cmd === '/bin/mv') { - return {exitCode: scenario.mvExitCode === undefined ? 0 : scenario.mvExitCode}; - } - if (cmd === '/bin/rm') { - return {exitCode: scenario.rmExitCode === undefined ? 0 : scenario.rmExitCode}; - } - if (cmd === '/bin/mkdir') { - return {exitCode: 0}; - } - if (String(cmd).indexOf('ffprobe') !== -1) { - // keine Subtitle-Streams -> handleSubtitles steigt sofort aus - return {exitCode: 0, standardOutput: JSON.stringify({streams: []})}; - } - return {exitCode: 0, standardOutput: ''}; -} - -const context = { - Variables: { - 'file.Orig.FullName': scenario.origPath, - 'file.FullName': scenario.workingFile - }, - Flow: { - Execute: fakeExecute, - SetWorkingFile() {}, - GetToolPath: name => '/usr/bin/' + name, - TempPath: '/temp', - NewGuid: () => 'guid-0000' - }, - Logger: {ILog(){}, WLog(){}, ELog(){}, DLog(){}}, - JSON: JSON, - console -}; -vm.createContext(context); -vm.runInContext(source, context); - -const rc = context.__exports.Script( - scenario.sourceRoot || '/tdarr/complete/', - scenario.destRoot || '/tdarr/converted/' -); -console.log(JSON.stringify({rc: rc, executed: executed})); -""" - - -def run_script(scenario): - harness = Path("/tmp/ff_finalize_harness.js") - harness.write_text(NODE_HARNESS) - proc = subprocess.run( - ["node", str(harness), json.dumps(scenario), str(SCRIPT)], - capture_output=True, text=True, timeout=60, - ) - if proc.returncode != 0: - raise AssertionError(f"harness failed: {proc.stderr[-2000:]}") - return json.loads(proc.stdout.strip().splitlines()[-1]) - - -def rm_rf_targets(result): - return [e["args"][1] for e in result["executed"] - if e["command"] == "/bin/rm" and e["args"][:1] == ["-rf"]] - - -class CleanupSourceDirTests(unittest.TestCase): - - BASE = { - "origPath": "/tdarr/complete/serien4k/Slow.Horses.S06E01-W4K/slow.horses.s06e01-w4k.mkv", - "workingFile": "/temp/Runner-1/slow.horses.s06e01-w4k.mkv", - } - - def test_leeres_release_verzeichnis_wird_entfernt(self): - """Nur .nfo uebrig -> Verzeichnis muss weg.""" - result = run_script({**self.BASE, "mediaFiles": []}) - self.assertEqual(result["rc"], 1) - self.assertEqual( - rm_rf_targets(result), - ["/tdarr/complete/serien4k/Slow.Horses.S06E01-W4K"], - ) - - def test_verzeichnis_mit_restlicher_mediendatei_bleibt(self): - """Season-Pack: zweite Episode noch da -> nicht loeschen.""" - result = run_script({ - **self.BASE, - "mediaFiles": [ - "/tdarr/complete/serien4k/Slow.Horses.S06E01-W4K/slow.horses.s06e02-w4k.mkv" - ], - }) - self.assertEqual(result["rc"], 1) - self.assertEqual(rm_rf_targets(result), []) - - def test_find_fehler_laesst_verzeichnis_stehen(self): - """find schlaegt fehl -> fail safe, nichts loeschen.""" - result = run_script({**self.BASE, "mediaFiles": [], "findExitCode": 1}) - self.assertEqual(result["rc"], 1) - self.assertEqual(rm_rf_targets(result), []) - - def test_sourceroot_selbst_wird_nie_geloescht(self): - """Datei direkt in SourceRoot -> rm -rf /tdarr/complete darf NICHT passieren.""" - result = run_script({ - "origPath": "/tdarr/complete/lose.datei.mkv", - "workingFile": "/temp/Runner-1/lose.datei.mkv", - "mediaFiles": [], - }) - self.assertEqual(rm_rf_targets(result), []) - - def test_pfad_ausserhalb_sourceroot_wird_abgelehnt(self): - """Original nicht unter SourceRoot -> Script bricht mit 2 ab, kein rm -rf.""" - result = run_script({ - "origPath": "/mnt/anderswo/film/film.mkv", - "workingFile": "/temp/Runner-1/film.mkv", - "mediaFiles": [], - }) - self.assertEqual(result["rc"], 2) - self.assertEqual(rm_rf_targets(result), []) - - def test_originaldatei_wird_weiterhin_geloescht(self): - """Regression: rm -f auf das Original bleibt erhalten.""" - result = run_script({**self.BASE, "mediaFiles": []}) - rm_f = [e["args"][1] for e in result["executed"] - if e["command"] == "/bin/rm" and e["args"][:1] == ["-f"]] - self.assertIn(self.BASE["origPath"], rm_f) - - -if __name__ == "__main__": - unittest.main(verbosity=2)