Compare commits
No commits in common. "468ca33bb33cedd60091081dd8d12b832102dae1" and "d32600aaa262cb4be35a9a3af492c3ce6a2e60d6" have entirely different histories.
468ca33bb3
...
d32600aaa2
2 changed files with 1 additions and 216 deletions
|
|
@ -4,10 +4,8 @@
|
||||||
* behandelt Untertitel (Text-Subs als .srt-Sidecar, Bild-Subs Disposition neutralisieren).
|
* behandelt Untertitel (Text-Subs als .srt-Sidecar, Bild-Subs Disposition neutralisieren).
|
||||||
* SICHERHEITS-FIX 17.07.2026: atomare Sidecar-Erzeugung, 0-Byte-Pruefung,
|
* SICHERHEITS-FIX 17.07.2026: atomare Sidecar-Erzeugung, 0-Byte-Pruefung,
|
||||||
* nur erfolgreich extrahierte Textspuren entfernen, Remux-Verifikation.
|
* 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
|
* @author Sascha + Trulla
|
||||||
* @revision 3
|
* @revision 2
|
||||||
* @output OK
|
* @output OK
|
||||||
* @output Fehler
|
* @output Fehler
|
||||||
*/
|
*/
|
||||||
|
|
@ -60,7 +58,6 @@ function Script(SourceRoot, DestRoot)
|
||||||
if (origPath !== destAbs) {
|
if (origPath !== destAbs) {
|
||||||
Flow.Execute({ command: '/bin/rm', argumentList: ['-f', origPath] });
|
Flow.Execute({ command: '/bin/rm', argumentList: ['-f', origPath] });
|
||||||
Logger.ILog('Deleted original: ' + origPath);
|
Logger.ILog('Deleted original: ' + origPath);
|
||||||
cleanupSourceDir(origPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
|
|
@ -90,52 +87,6 @@ function Script(SourceRoot, DestRoot)
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
// -- Helfer --------------------------------------------------------------
|
// -- 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) {
|
function handleSubtitles(file) {
|
||||||
var ffprobe = Flow.GetToolPath('ffprobe') || Variables['ffprobe'] || 'ffprobe';
|
var ffprobe = Flow.GetToolPath('ffprobe') || Variables['ffprobe'] || 'ffprobe';
|
||||||
var ffmpeg = Flow.GetToolPath('ffmpeg') || Variables['ffmpeg'] || 'ffmpeg';
|
var ffmpeg = Flow.GetToolPath('ffmpeg') || Variables['ffmpeg'] || 'ffmpeg';
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue