#!/usr/bin/env python3 """Per-title cost of shrinking everything on Standard/Mini: re-encode vs redownload. For each show or movie on those profiles, probes every file with compact-tv's own planner and reports what the nightly job would do (encode, skip as already small, skip as HDR), the space it would save, and the GPU time it would take. Alongside that it estimates what a fresh small download would cost instead, using typical small-encode sizes. GPU time is measured, not guessed: the Shameless pilot took 669 s for 57.5 minutes of 1080p (about 0.19 h of GPU per hour of video). 4K sources decode slower; the 1.5x for them is an estimate until the first 4K night is logged. Usage: cost-report.py > report.tsv (also prints a summary to stderr) """ import importlib.util, json, pathlib, sys HERE = pathlib.Path(__file__).resolve().parent spec = importlib.util.spec_from_file_location("ct", HERE / "compact-tv.py") ct = importlib.util.module_from_spec(spec); spec.loader.exec_module(ct) GPU_H_PER_H = 669 / 3450 # pilot: hours of GPU per hour of 1080p video UHD_FACTOR = 1.5 # estimate for 2160p sources SMALL_GB_PER_H = {"Standard": 0.85, "Mini": 0.57} # MeGusta-class 1080p / 720p HEVC, measured on S.W.A.T. rows = [] for app, a in ct.APPS.items(): call = ct.arr(app) profiles = {p["id"]: p["name"] for p in call("/qualityprofile")} container, host = a["paths"] for t in call(a["titles"]): tier = profiles.get(t["qualityProfileId"]) if tier not in ct.TIERS: continue r = {"app": app, "title": t["title"], "tier": tier, "files": 0, "hours": 0.0, "size": 0, "enc_files": 0, "enc_size": 0, "enc_est": 0, "gpu_h": 0.0, "hdr_size": 0, "small_size": 0} for f in a["files"](call, t): p = pathlib.Path(f["path"].replace(container, host, 1)) info = ct.probe(p) if p.exists() else None if not info: continue size, dur = int(info["format"]["size"]), float(info["format"]["duration"]) / 3600 r["files"] += 1; r["hours"] += dur; r["size"] += size args, why = ct.plan(info, tier, app) if args: w = max((s.get("width") or 0) for s in info["streams"] if s["codec_type"] == "video") r["enc_files"] += 1; r["enc_size"] += size; r["enc_est"] += why r["gpu_h"] += dur * GPU_H_PER_H * (UHD_FACTOR if w > 1920 else 1) elif "HDR" in str(why): r["hdr_size"] += size else: r["small_size"] += size if r["files"]: r["redownload_gb"] = round(r["hours"] * SMALL_GB_PER_H[tier], 1) rows.append(r) rows.sort(key=lambda r: -(r["enc_size"] - r["enc_est"])) cols = ["app", "title", "tier", "files", "hours", "size", "enc_files", "enc_size", "enc_est", "gpu_h", "hdr_size", "small_size", "redownload_gb"] print("\t".join(cols)) for r in rows: print("\t".join(str(round(r[c], 2) if isinstance(r[c], float) else r[c]) for c in cols)) tot = lambda k: sum(r[k] for r in rows) print(f"{len(rows)} titles; encode {tot('enc_files')} files {tot('enc_size')/1e12:.2f} TB -> ~{tot('enc_est')/1e12:.2f} TB, " f"{tot('gpu_h'):.0f} GPU hours; HDR skipped {tot('hdr_size')/1e12:.2f} TB; already small {tot('small_size')/1e12:.2f} TB", file=sys.stderr)