66 lines
3.3 KiB
Python
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)
|