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() print("PASS: success lease, category fail-closed, no-overwrite, stale-directory cleanup") finally: server.shutdown() server.server_close()