Files
Panama/server/home/Media/compact-tv/cost-report.py
T

66 lines
3.3 KiB
Python

#!/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)