Files
felhom.eu/documentation/audits/media-folder-fix-2026-10-14/tools/live_readback.py
T

99 lines
4.9 KiB
Python

#!/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 <drive mount, e.g. /mnt/felhom-drives/hdd_1>
"""
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, "<redacted>") 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"<ApiKey>([^<]+)", 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")