99 lines
4.9 KiB
Python
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")
|