From 0ce5a817a2ebfe7593841791f4d039fbb847af4a Mon Sep 17 00:00:00 2001 From: sascha Date: Sat, 5 Sep 2026 07:59:56 +0200 Subject: [PATCH] feat: keep SAB jobs leased until Arr import --- usenet-scripts/test_movetdarr.py | 107 +++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 usenet-scripts/test_movetdarr.py diff --git a/usenet-scripts/test_movetdarr.py b/usenet-scripts/test_movetdarr.py new file mode 100644 index 0000000..ad6c3db --- /dev/null +++ b/usenet-scripts/test_movetdarr.py @@ -0,0 +1,107 @@ +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", + "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"] + + 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()