import json import os import subprocess import tempfile import threading from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path SCRIPT = Path(__file__).with_name("movetdarr.sh") actions = [] status_polls = 0 class Handler(BaseHTTPRequestHandler): def do_POST(self): global status_polls data = json.loads(self.rfile.read(int(self.headers.get("Content-Length", "0")))) actions.append(data) action = data.get("action") if action == "start": result = {"ok": True, "jobId": "test-job-1234", "state": "registered"} elif action == "moved": result = {"ok": True, "jobId": "test-job-1234", "state": "processing"} elif action == "status": status_polls += 1 result = {"ok": True, "jobId": "test-job-1234", "state": "success" if status_polls >= 2 else "processing"} else: result = {"ok": True, "jobId": "test-job-1234", "state": "failed"} body = json.dumps(result).encode() self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, *_args): pass def run_case(root, category="serien4k", status="0", name="Show.S01E01", target_video=False): src_root = root / "source" dst_root = root / "dest" source = src_root / category / name source.mkdir(parents=True) (source / "episode.mkv").write_bytes(b"video") target = dst_root / category / name if target_video: target.mkdir(parents=True) (target / "existing.mkv").write_bytes(b"existing") env = os.environ | { "SOURCE_ROOT": str(src_root), "DEST_ROOT": str(dst_root), "LOGFILE": str(root / "postprocess.log"), "HANDOFF_URL": f"http://127.0.0.1:{server.server_port}/media/handoff", "LEASE_LOCK": str(root / "lease.lock"), "POLL_SECONDS": "0", "MAX_POLLS": "3", } result = subprocess.run( ["bash", str(SCRIPT), str(source), name + ".nzb", name, "", category, "", status], env=env, text=True, capture_output=True, timeout=20, ) return result, source, target server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() try: with tempfile.TemporaryDirectory() as td: actions.clear() status_polls = 0 result, source, target = run_case(Path(td)) assert result.returncode == 0, (result.stdout, result.stderr) assert not source.exists() and (target / "episode.mkv").exists() assert [a["action"] for a in actions] == ["start", "moved", "status", "status"] assert actions[0]["expectedFiles"] == "1" with tempfile.TemporaryDirectory() as td: actions.clear() status_polls = 0 result, source, _ = run_case(Path(td), category="other") assert result.returncode == 1 and source.exists() and actions == [] with tempfile.TemporaryDirectory() as td: actions.clear() status_polls = 0 result, source, target = run_case(Path(td), target_video=True) assert result.returncode == 2 and source.exists() and (target / "existing.mkv").exists() assert [a["action"] for a in actions] == ["start", "fail"] with tempfile.TemporaryDirectory() as td: actions.clear() status_polls = 0 root = Path(td) stale = root / "dest" / "video" / "Movie.2026" stale.mkdir(parents=True) (stale / "leftover.nfo").write_text("stale") result, source, target = run_case(root, category="video", name="Movie.2026") assert result.returncode == 0 and not source.exists() assert not (target / "leftover.nfo").exists() and (target / "episode.mkv").exists() # 16.09.2026: serienen/videoen (englische Arr-Instanzen sonarrEN:8991 / # radarrEN:7880 mit Root-Foldern /data/FHD/serienen bzw. /data/FHD/videoen) # fehlten in der Kategorie-Whitelist. Folge: Mutiny.2026 x2 blieben am # 08./09.09.2026 mit "nicht unterstuetzte Kategorie 'videoen'" in # /usenet/complete liegen und wurden nie importiert. for extra_category in ("serienen", "videoen"): with tempfile.TemporaryDirectory() as td: actions.clear() status_polls = 0 result, source, target = run_case(Path(td), category=extra_category) assert result.returncode == 0, (extra_category, result.stdout, result.stderr) assert not source.exists(), extra_category assert (target / "episode.mkv").exists(), extra_category assert [a["action"] for a in actions] == ["start", "moved", "status", "status"], extra_category print("PASS: success lease, category fail-closed, no-overwrite, stale-directory cleanup, EN-Kategorien") finally: server.shutdown() server.server_close()