Files
felhom.eu/documentation/audits/media-all-drives-2026-10-14/tools/snap.py
T

73 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""snap.py <label> — S5/S6: one snapshot of what every layer says about the drives, run INSIDE the guest.
Plex's token and Jellyfin's password are read at runtime (Plex: its Preferences.xml; Jellyfin: substituted into the
box's copy of this file by the runner, deleted after) and never printed."""
import json, os, re, subprocess, sys, time, urllib.parse, urllib.request
JFPW = "__JF_PW__"
L = sys.argv[1] if len(sys.argv) > 1 else "snap"
B = "/mnt/felhom-drives/media_b/userdata/media"
def sh(c, t=20):
try:
return subprocess.run(c, shell=True, capture_output=True, text=True, timeout=t).stdout.strip()
except subprocess.TimeoutExpired:
return "<timeout>"
def ip(c):
return sh(f"docker inspect {c} -f '{{{{range .NetworkSettings.Networks}}}}{{{{.IPAddress}}}}{{{{end}}}}'")
def get(url, h=None, t=20):
try:
with urllib.request.urlopen(urllib.request.Request(url, headers=h or {}), timeout=t) as r:
return r.status, json.loads(r.read() or b"null")
except urllib.error.HTTPError as e:
return e.code, None
except Exception as e:
return 0, str(e)[:80]
print(f"===== {L} {time.strftime('%Y-%m-%dT%H:%M:%S%z')}")
print("guest: is media_b a mountpoint:", sh(f"mountpoint -q /mnt/felhom-drives/media_b && echo yes || echo NO"),
"| ls B:", sh(f"timeout 10 ls {B} 2>&1 | head -3 | tr '\\n' ' '"))
print("guest rootfs under media_b (only if not a mountpoint):", sh("mountpoint -q /mnt/felhom-drives/media_b || du -sh /mnt/felhom-drives/media_b 2>&1"))
for c in ("sonarr", "radarr", "qbittorrent", "transmission", "plex", "jellyfin", "filebrowser"):
st = sh(f"docker inspect {c} -f '{{{{.State.Status}}}} {{{{.State.Health.Status}}}} started={{{{.State.StartedAt}}}}' 2>&1")
view = sh(f"docker exec {c} sh -c 'timeout 8 ls /mnt/felhom-drives/media_b/userdata/media/tv 2>&1 | head -2 | tr \"\\n\" \" \"' 2>&1") if c != "filebrowser" else sh("docker exec filebrowser sh -c 'ls /srv 2>&1 | tr \"\\n\" \" \"'")
print(f" {c:12} {st[:60]:60} | B/tv: {view[:110]}")
for app, port in (("sonarr", 8989), ("radarr", 7878)):
k = re.search(r"<ApiKey>([^<]+)", open(f"/var/lib/docker/volumes/{app}_{app}_config/_data/config.xml").read()).group(1)
H = {"X-Api-Key": k, "Host": app}
c, h = get(f"http://{ip(app)}:{port}/api/v3/health", H)
print(f" {app} health ({c}):", [(x['source'], x['message'][:90]) for x in (h or [])])
c, r = get(f"http://{ip(app)}:{port}/api/v3/rootfolder", H)
print(f" {app} root folders:", [(x['path'].split('/')[3], x.get('accessible')) for x in (r or [])])
kind = "series" if app == "sonarr" else "movie"
c, s = get(f"http://{ip(app)}:{port}/api/v3/{kind}", H)
print(f" {app} {kind}:", [(x['title'][:18], (x.get('statistics') or {}).get('episodeFileCount', x.get('hasFile'))) for x in (s or [])])
P = "/var/lib/docker/volumes/plex_plex_config/_data/Library/Application Support/Plex Media Server"
T = re.search(r'PlexOnlineToken="([^"]+)', open(f"{P}/Preferences.xml").read()).group(1)
for sec in (1, 2):
c, d = get(f"http://{ip('plex')}:32400/library/sections/{sec}/all?X-Plex-Token={T}", {"Accept": "application/json"})
mc = (d or {}).get("MediaContainer", {}) if isinstance(d, dict) else {}
print(f" plex section {sec} ({c}):", [m.get("title") for m in mc.get("Metadata", [])])
c, d = get(f"http://{ip('plex')}:32400/library/sections/2/allLeaves?X-Plex-Token={T}", {"Accept": "application/json"})
mc = (d or {}).get("MediaContainer", {}) if isinstance(d, dict) else {}
print(" plex episodes:", mc.get("size"))
A = 'MediaBrowser Client="felhom-test", Device="snap", DeviceId="felhom-snap-1", Version="1"'
try:
jb = f"http://{ip('jellyfin')}:8096"
rq = urllib.request.Request(jb + "/Users/AuthenticateByName", data=json.dumps({"Username": "admin", "Pw": JFPW}).encode(),
headers={"Content-Type": "application/json", "Authorization": A}, method="POST")
tok = json.load(urllib.request.urlopen(rq, timeout=20))["AccessToken"]
c, d = get(jb + "/Items?Recursive=true&IncludeItemTypes=Series,Movie,Episode", {"X-Emby-Token": tok})
items = (d or {}).get("Items", [])
print(" jellyfin:", {t: len([i for i in items if i["Type"] == t]) for t in ("Series", "Movie", "Episode")},
sorted(i["Name"] for i in items if i["Type"] in ("Series", "Movie")))
except Exception as e:
print(" jellyfin: error", str(e)[:100])
print(" controller log (last 10 min, gate/storage/event):")
print(" " + sh("docker logs --since 10m felhom-controller 2>&1 | grep -iE 'gate|storage|disconnect|absent|Event pushed|FileBrowser' | grep -v DEBUG | tail -10 | cut -c1-200").replace("\n", "\n "))