Die Kategorie-Whitelist kannte nur serien4k|serien|video4k|video. Die
englischen Arr-Instanzen sonarrEN (Port 8991, Root /data/FHD/serienen) und
radarrEN (Port 7880, Root /data/FHD/videoen) nutzen aber serienen/videoen.
Folge: Downloads dieser Kategorien wurden mit "nicht unterstuetzte Kategorie"
abgewiesen und blieben in /usenet/complete liegen, ohne je in den
FileFlows/Arr-Flow zu gelangen. Gefunden: Mutiny.2026 x2 (08./09.09.2026,
zusammen ~21 GB), beide Filme in radarrEN weiter monitored/hasFile=false.
/tdarr/complete/{serienen,videoen} existieren bereits; die FileFlows-Library
scannt /tdarr/complete rekursiv, es braucht also nur die Whitelist.
2 neue Testfaelle (serienen + videoen), test_movetdarr.py und
test_movetdarr_parallel.py gruen.
124 lines
4.9 KiB
Python
124 lines
4.9 KiB
Python
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()
|