#!/usr/bin/env python3 """k3.py create|state|pause|resume [drive] — K3 (a safe removal), run INSIDE the Tester 1 guest. create makes ONE torrent of our own random bytes (3 MB, private, no tracker) per client on Media C and adds it so it seeds: qBittorrent (category tv-drive-c → media_c/userdata/media/torrents/tv), Transmission (media_c/userdata/media/transmission/tv). state every torrent: client, drive, state, progress — the „did anything else stop / re-check" record. pause pause ONLY the torrents whose save folder is on (qBittorrent by save_path, Transmission by downloadDir). resume start them again. Passwords: the deploy secrets read from the containers' environment; never printed. """ import base64, hashlib, json, os, re, subprocess, sys, time, urllib.error, urllib.parse, urllib.request DRV = sys.argv[2] if len(sys.argv) > 2 else "media_c" M = f"/mnt/felhom-drives/{DRV}/userdata/media" def sh(c): return subprocess.run(c, shell=True, capture_output=True, text=True).stdout.strip() def ip(c): return sh(f"docker inspect {c} -f '{{{{range .NetworkSettings.Networks}}}}{{{{.IPAddress}}}}{{{{end}}}}'") def be(x): if isinstance(x, int): return b"i%de" % x if isinstance(x, bytes): return b"%d:%s" % (len(x), x) if isinstance(x, str): return be(x.encode()) if isinstance(x, list): return b"l" + b"".join(be(i) for i in x) + b"e" return b"d" + b"".join(be(k) + be(x[k]) for k in sorted(x)) + b"e" def mktorrent(d): L = 262144 files = sorted(os.listdir(d)); data = b"".join(open(os.path.join(d, f), "rb").read() for f in files) info = {"name": os.path.basename(d), "piece length": L, "private": 1, "pieces": b"".join(hashlib.sha1(data[i:i+L]).digest() for i in range(0, len(data), L)), "files": [{"length": os.path.getsize(os.path.join(d, f)), "path": [f]} for f in files]} return be({"info": info, "created by": "felhom K3 (own random bytes)"}) QB = f"http://{ip('qbittorrent')}:8080" qpw = sh("docker exec qbittorrent printenv QBT_ADMIN_PASSWORD") req = urllib.request.Request(QB + "/api/v2/auth/login", data=urllib.parse.urlencode({"username": "admin", "password": qpw}).encode(), headers={"Referer": QB}) with urllib.request.urlopen(req) as r: m = re.search(r"((?:QBT_)?SID[A-Za-z0-9_]*)=([^;]+)", r.headers.get("Set-Cookie", "")) QH = {"Referer": QB, "Cookie": f"{m.group(1)}={m.group(2)}"} def qb(path, form=None, files=None): if files: bnd = "felhomk3" body = b"" for k, v in (form or {}).items(): body += f"--{bnd}\r\nContent-Disposition: form-data; name=\"{k}\"\r\n\r\n{v}\r\n".encode() for k, (fn, data) in files.items(): body += f"--{bnd}\r\nContent-Disposition: form-data; name=\"{k}\"; filename=\"{fn}\"\r\nContent-Type: application/x-bittorrent\r\n\r\n".encode() + data + b"\r\n" body += f"--{bnd}--\r\n".encode() r = urllib.request.Request(QB + path, data=body, headers=dict(QH, **{"Content-Type": f"multipart/form-data; boundary={bnd}"})) elif form is not None: r = urllib.request.Request(QB + path, data=urllib.parse.urlencode(form).encode(), headers=QH) else: r = urllib.request.Request(QB + path, headers=QH) with urllib.request.urlopen(r, timeout=60) as x: t = x.read().decode() try: return json.loads(t) except ValueError: return t TR = f"http://{ip('transmission')}:9091/transmission/rpc" tpw = sh("docker exec transmission printenv PASS") TA = {"Authorization": "Basic " + base64.b64encode(f"admin:{tpw}".encode()).decode()} def tr(method, args=None): h = dict(TA) for _ in range(2): r = urllib.request.Request(TR, data=json.dumps({"method": method, "arguments": args or {}}).encode(), headers=h) try: with urllib.request.urlopen(r, timeout=60) as x: return json.load(x) except urllib.error.HTTPError as e: if e.code == 409: h["X-Transmission-Session-Id"] = e.headers.get("X-Transmission-Session-Id"); continue raise def state(): out = [] for t in qb("/api/v2/torrents/info"): out.append(("qbit", t["save_path"].split("/")[3], t["name"][:30], t["state"], round(t["progress"], 4))) for t in tr("torrent-get", {"fields": ["name", "downloadDir", "status", "percentDone", "error", "errorString"]})["arguments"]["torrents"]: st = {0: "stopped", 1: "check-wait", 2: "checking", 3: "dl-wait", 4: "downloading", 5: "seed-wait", 6: "seeding"}.get(t["status"], t["status"]) out.append(("trans", t["downloadDir"].split("/")[3], t["name"][:30], st + (" ERR " + t["errorString"][:30] if t["error"] else ""), round(t["percentDone"], 4))) return sorted(out) step = sys.argv[1] if step == "create": for client, sub in (("qbit", "torrents/tv"), ("trans", "transmission/tv")): d = f"{M}/{sub}/k3-own-bytes-{client}" os.makedirs(d, exist_ok=True) with open(f"{d}/data.bin", "wb") as f: f.write(os.urandom(3_000_000)) sh(f"chown -R 1000:1000 '{d}'") tor = mktorrent(d) if client == "qbit": print("qbit add ->", qb("/api/v2/torrents/add", {"category": (sys.argv[3] if len(sys.argv) > 3 else "tv-drive-c")}, {"torrents": ("k3.torrent", tor)})) else: print("trans add ->", json.dumps(tr("torrent-add", {"metainfo": base64.b64encode(tor).decode(), "download-dir": f"{M}/transmission/tv"}))[:160]) time.sleep(20) for s in state(): print(" ", s) elif step == "state": print(f"--- {time.strftime('%H:%M:%S')}") for s in state(): print(" ", s) elif step in ("pause", "resume"): hs = [t["hash"] for t in qb("/api/v2/torrents/info") if t["save_path"].startswith(M)] print(f"qbit {step} {len(hs)} torrent(s) on {DRV} ->", qb(f"/api/v2/torrents/{'stop' if step == 'pause' else 'start'}", {"hashes": "|".join(hs)}) if hs else "none") ids = [t["id"] for t in tr("torrent-get", {"fields": ["id", "downloadDir"]})["arguments"]["torrents"] if t["downloadDir"].startswith(M)] print(f"trans {step} {len(ids)} torrent(s) on {DRV} ->", tr("torrent-stop" if step == "pause" else "torrent-start", {"ids": ids})["result"] if ids else "none") time.sleep(5) for s in state(): print(" ", s)