#!/usr/bin/env python3 """live_readback.py — Part B6: the one-drive chain on a box with the LIVE templates, run INSIDE the guest. Does what the new first steps tell the household to do — Radarr: root folder `.../media/movies`; one qBittorrent entry (host qbittorrent, port 8080, admin, the install's password, category `movies`) — then hands ONE public-domain archive.org film to Radarr by its release push and waits for the import. Proof: download path, library path, link count, inode, Radarr's history entry. Keys and passwords are read at runtime and never printed. run_in_guest-style: python3 live_readback.py """ import datetime, json, os, re, subprocess, sys, time, urllib.error, urllib.request HDD = sys.argv[1] LIB = f"{HDD}/userdata/media/movies" ITEM = "night-of-the-living-dead-1968_202508" # CC Public Domain Mark, 597 MB SECRETS = [] def red(s): s = str(s) for x in SECRETS: s = s.replace(x, "") if x else s return s def say(*a): print(red(" ".join(str(x) for x in a)), flush=True) 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}}}}'") key = re.search(r"([^<]+)", open("/var/lib/docker/volumes/radarr_radarr_config/_data/config.xml").read()).group(1) pw = sh("docker exec qbittorrent printenv QBT_ADMIN_PASSWORD") SECRETS += [key, pw] RA = f"http://{ip('radarr')}:7878/api/v3" H = {"X-Api-Key": key, "Host": "radarr", "Content-Type": "application/json"} def ra(m, p, body=None): r = urllib.request.Request(RA + p, data=json.dumps(body).encode() if body is not None else None, headers=H, method=m) try: with urllib.request.urlopen(r, timeout=120) as x: t = x.read(); return x.status, (json.loads(t) if t else None) except urllib.error.HTTPError as e: b = e.read().decode()[:400]; say(f" !! {m} {p} -> {e.code} {b}"); return e.code, None say("== Radarr: what the first steps say") print(sh(f"docker exec radarr sh -c 'ls -ld {LIB} {HDD}/userdata/media/torrents; ls {HDD}/userdata/media | tr \"\\n\" \" \"'")) if not any(r["path"].rstrip("/") == LIB for r in ra("GET", "/rootfolder")[1]): c, r = ra("POST", "/rootfolder", {"path": LIB}); say(f" root folder {LIB} -> {c}") sch = {s["implementation"]: s for s in ra("GET", "/downloadclient/schema")[1]} if not ra("GET", "/downloadclient")[1]: b = json.loads(json.dumps(sch["QBittorrent"])) vals = {"host": "qbittorrent", "port": 8080, "username": "admin", "password": pw, "movieCategory": "movies"} for f in b["fields"]: if f["name"] in vals: f["value"] = vals[f["name"]] b.update({"name": "qBittorrent", "enable": True}) c, r = ra("POST", "/downloadclient", b); say(f" download client qBittorrent (its own connection test) -> {c}") c, lk = ra("GET", "/movie/lookup/tmdb?tmdbId=10331") qp = ra("GET", "/qualityprofile")[1][0] c, m = ra("POST", "/movie", dict(lk, qualityProfileId=qp["id"], rootFolderPath=LIB, monitored=True, tags=[], minimumAvailability="released", addOptions={"searchForMovie": False})) say(f" movie added: {m['title']} ({m['year']}) path={m['path']}") rel = {"title": "Night.of.the.Living.Dead.1968.480p.WEB-DL.x264-ARCHIVEORG", "protocol": "torrent", "size": 596645320, "downloadUrl": f"https://archive.org/download/{ITEM}/{ITEM}_archive.torrent", "indexer": "archive.org (release push)", "publishDate": datetime.datetime.now(datetime.timezone.utc).isoformat()} t0 = time.time() c, d = ra("POST", "/release/push", rel) dd = d[0] if isinstance(d, list) else (d or {}) say(f" release push -> {c} approved={dd.get('approved')} rejections={dd.get('rejections')}") imp = None for i in range(150): time.sleep(10) for e in ra("GET", f"/history/movie?movieId={m['id']}")[1] or []: if e["eventType"] == "grabbed" and i == 0: say(f" grabbed by {e['data'].get('downloadClientName')}") if e["eventType"] == "downloadFolderImported": imp = e if imp: break if i % 6 == 5: for r in (ra("GET", f"/queue?movieIds={m['id']}")[1] or {}).get("records", []): say(f" +{int(time.time()-t0)}s queue: {r.get('status')} {r.get('trackedDownloadState')} left={r.get('sizeleft')} output={r.get('outputPath')}") if not imp: say(f" RESULT: not imported within {int(time.time()-t0)}s"); sys.exit(1) e = imp["data"] say(f" +{int(time.time()-t0)}s history downloadFolderImported: {json.dumps({k: e.get(k) for k in ('downloadClientName', 'droppedPath', 'importedPath')})}") a, b = os.stat(e["droppedPath"]), os.stat(e["importedPath"]) say(f" download: links={a.st_nlink} inode={a.st_ino} library: links={b.st_nlink} inode={b.st_ino}") say(" RESULT:", "HARDLINK — one file, two names" if (a.st_ino, a.st_dev) == (b.st_ino, b.st_dev) else "COPY")