196 lines
6.7 KiB
Python
196 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Расчёт суммарного размера библиотеки Steam по данным из steam_library.json.
|
||
Приоритет: api.steamcmd.net (реальные размеры депо). Запас: системные требования
|
||
в магазине Steam (часто занижены). Результаты кэшируются в steam_app_sizes.json.
|
||
"""
|
||
|
||
import json
|
||
import re
|
||
import time
|
||
import urllib.request
|
||
from pathlib import Path
|
||
from typing import Optional
|
||
|
||
LIBRARY_FILE = "steam_library.json"
|
||
SIZES_CACHE_FILE = "steam_app_sizes.json"
|
||
OUTPUT_FILE = "steam_library_with_sizes.json"
|
||
|
||
REQUEST_DELAY = 0.4
|
||
|
||
|
||
def get_app_size_from_steamcmd(appid: int) -> Optional[float]:
|
||
"""
|
||
Размер в ГБ из api.steamcmd.net (депо, реальный объём установки).
|
||
Учитываются только депо для Windows, для языков — только english, чтобы не суммировать все локали.
|
||
"""
|
||
url = f"https://api.steamcmd.net/v1/info/{appid}"
|
||
try:
|
||
req = urllib.request.Request(url, headers={"Accept": "application/json"})
|
||
with urllib.request.urlopen(req, timeout=20) as resp:
|
||
data = json.loads(resp.read().decode())
|
||
except Exception:
|
||
return None
|
||
if data.get("status") != "success" or "data" not in data:
|
||
return None
|
||
app_data = data["data"].get(str(appid))
|
||
if not app_data or "depots" not in app_data:
|
||
return None
|
||
depots = app_data["depots"]
|
||
total_bytes = 0
|
||
for depot_id, depot in depots.items():
|
||
if depot_id in ("appmanagesdlc", "baselanguages", "branches", "privatebranches"):
|
||
continue
|
||
if not isinstance(depot, dict):
|
||
continue
|
||
config = depot.get("config") or {}
|
||
oslist = config.get("oslist", "")
|
||
if "windows" not in oslist.lower():
|
||
continue
|
||
manifests = depot.get("manifests")
|
||
if not manifests or "public" not in manifests:
|
||
continue
|
||
size_str = manifests["public"].get("size")
|
||
if not size_str:
|
||
continue
|
||
try:
|
||
size_bytes = int(size_str)
|
||
except (TypeError, ValueError):
|
||
continue
|
||
language = config.get("language", "")
|
||
if language and language != "english":
|
||
continue
|
||
total_bytes += size_bytes
|
||
if total_bytes == 0:
|
||
return None
|
||
return total_bytes / (1024 ** 3)
|
||
|
||
|
||
def parse_size_from_text(text: str) -> Optional[float]:
|
||
"""Извлекает размер в ГБ из строки системных требований."""
|
||
if not text:
|
||
return None
|
||
text = text.replace(",", ".")
|
||
m = re.search(r"(\d+(?:\.\d+)?)\s*GB", text, re.IGNORECASE)
|
||
if m:
|
||
return float(m.group(1))
|
||
m = re.search(r"(\d+(?:\.\d+)?)\s*MB", text, re.IGNORECASE)
|
||
if m:
|
||
return float(m.group(1)) / 1024
|
||
return None
|
||
|
||
|
||
def get_app_size_from_store(appid: int) -> Optional[float]:
|
||
"""Размер в ГБ из системных требований store.steampowered.com (часто занижен)."""
|
||
url = f"https://store.steampowered.com/api/appdetails?appids={appid}&l=english"
|
||
try:
|
||
req = urllib.request.Request(url, headers={"Accept": "application/json"})
|
||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||
data = json.loads(resp.read().decode())
|
||
except Exception:
|
||
return None
|
||
block = data.get(str(appid))
|
||
if not block or not block.get("success") or "data" not in block:
|
||
return None
|
||
reqs = block["data"].get("pc_requirements")
|
||
if not reqs:
|
||
return None
|
||
if isinstance(reqs, dict):
|
||
for part in (reqs.get("minimum"), reqs.get("recommended")):
|
||
size = parse_size_from_text(part)
|
||
if size is not None:
|
||
return size
|
||
elif isinstance(reqs, str):
|
||
return parse_size_from_text(reqs)
|
||
return None
|
||
|
||
|
||
def get_app_size(appid: int) -> Optional[float]:
|
||
"""Сначала steamcmd (реальный размер), при отсутствии — магазин."""
|
||
size = get_app_size_from_steamcmd(appid)
|
||
if size is not None:
|
||
return size
|
||
return get_app_size_from_store(appid)
|
||
|
||
|
||
def load_library(path: Path) -> list[dict]:
|
||
with open(path, encoding="utf-8") as f:
|
||
return json.load(f)
|
||
|
||
|
||
def load_sizes_cache(path: Path) -> dict:
|
||
if not path.exists():
|
||
return {}
|
||
with open(path, encoding="utf-8") as f:
|
||
return {int(k): v for k, v in json.load(f).items()}
|
||
|
||
|
||
def save_sizes_cache(path: Path, cache: dict) -> None:
|
||
with open(path, "w", encoding="utf-8") as f:
|
||
json.dump(cache, f, indent=2)
|
||
|
||
|
||
def progress_bar(current: int, total: int, width: int = 40) -> str:
|
||
"""Строка прогресс-бара: [=======> ] 45/289 15%"""
|
||
if total <= 0:
|
||
return ""
|
||
pct = current / total
|
||
filled = int(width * pct)
|
||
bar = "=" * filled + ">" * (1 if filled < width else 0) + " " * (width - filled - 1)
|
||
return f"[{bar}] {current}/{total} {pct*100:.0f}%"
|
||
|
||
|
||
def main() -> None:
|
||
base = Path(__file__).parent
|
||
lib_path = base / LIBRARY_FILE
|
||
cache_path = base / SIZES_CACHE_FILE
|
||
out_path = base / OUTPUT_FILE
|
||
|
||
if not lib_path.exists():
|
||
print(f"Сначала запусти steam-library-to-json.py — нужен файл {LIBRARY_FILE}")
|
||
return
|
||
|
||
library = load_library(lib_path)
|
||
cache = load_sizes_cache(cache_path)
|
||
total_gb = 0.0
|
||
unknown_count = 0
|
||
results = []
|
||
|
||
total_games = len(library)
|
||
for i, game in enumerate(library):
|
||
appid = game["appid"]
|
||
name = game.get("name", "")
|
||
if appid in cache:
|
||
size_gb = cache[appid]
|
||
else:
|
||
size_gb = get_app_size(appid)
|
||
cache[appid] = size_gb
|
||
time.sleep(REQUEST_DELAY)
|
||
if size_gb is not None:
|
||
total_gb += size_gb
|
||
else:
|
||
unknown_count += 1
|
||
results.append({
|
||
"appid": appid,
|
||
"name": name,
|
||
"size_gb": round(size_gb, 2) if size_gb is not None else None,
|
||
})
|
||
print(f"\r{progress_bar(i + 1, total_games)} {name[:35]:<35}", end="", flush=True)
|
||
print()
|
||
|
||
save_sizes_cache(cache_path, cache)
|
||
with open(out_path, "w", encoding="utf-8") as f:
|
||
json.dump(results, f, ensure_ascii=False, indent=2)
|
||
|
||
print()
|
||
print(f"Игр в библиотеке: {len(library)}")
|
||
print(f"С известным размером: {len(library) - unknown_count}")
|
||
print(f"Без размера: {unknown_count}")
|
||
print(f"Суммарный размер: {total_gb:.1f} ГБ")
|
||
print(f"Результат: {out_path}")
|
||
print(f"Кэш: {cache_path}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|