Separate the local recognition, web publishing, and shared data paths while preserving direct script execution and existing site content. Co-authored-by: Cursor <cursoragent@cursor.com>
118 lines
3.5 KiB
Python
118 lines
3.5 KiB
Python
"""Fetch Valve Immortal regional leaderboards into data/leaderboards.json.
|
|
|
|
Pulls the official division boards (americas / europe / se_asia / china) and
|
|
keeps Top 100 per region for the Climperor web「排行」page.
|
|
|
|
Valve does not expose MMR or account_id on this endpoint — only rank, name,
|
|
and optional team/country. Divisions use separate MMR scales (no global board).
|
|
|
|
Preview only — do not merge into relations/heroes or recommend.
|
|
|
|
Usage:
|
|
python fetch_leaderboards.py
|
|
python fetch_leaderboards.py --out data/leaderboards.json
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
import argparse
|
|
import json
|
|
from datetime import datetime, timezone
|
|
|
|
from shared.http_utils import http_json
|
|
from shared.paths import DATA
|
|
|
|
OUT = DATA / "leaderboards.json"
|
|
API = (
|
|
"https://www.dota2.com/webapi/ILeaderboard/GetDivisionLeaderboard/v0001"
|
|
"?division={division}&leaderboard=0"
|
|
)
|
|
|
|
# Display order for the web rankings tab (China first for CN audience).
|
|
REGION_ORDER = ("china", "europe", "americas", "se_asia")
|
|
REGION_LABELS = {
|
|
"china": "中国",
|
|
"europe": "欧洲",
|
|
"americas": "美洲",
|
|
"se_asia": "东南亚",
|
|
}
|
|
TOP_N = 100
|
|
|
|
|
|
def slim_entry(row: dict) -> dict:
|
|
out: dict = {
|
|
"rank": int(row.get("rank") or 0),
|
|
"name": str(row.get("name") or ""),
|
|
}
|
|
team = row.get("team_tag")
|
|
if isinstance(team, str) and team.strip():
|
|
out["team_tag"] = team.strip()
|
|
country = row.get("country")
|
|
if isinstance(country, str) and country.strip():
|
|
out["country"] = country.strip().lower()
|
|
return out
|
|
|
|
|
|
def fetch_division(division: str) -> dict:
|
|
url = API.format(division=division)
|
|
print(f"fetching {division} ...", flush=True)
|
|
raw = http_json(url)
|
|
if not isinstance(raw, dict):
|
|
raise SystemExit(f"{division}: unexpected payload type {type(raw).__name__}")
|
|
lb = raw.get("leaderboard")
|
|
if not isinstance(lb, list):
|
|
raise SystemExit(f"{division}: missing leaderboard array")
|
|
top = [slim_entry(e) for e in lb[:TOP_N] if isinstance(e, dict)]
|
|
return {
|
|
"division": division,
|
|
"label_zh": REGION_LABELS.get(division, division),
|
|
"time_posted": raw.get("time_posted"),
|
|
"count": len(lb),
|
|
"top100": top,
|
|
}
|
|
|
|
|
|
def build_payload(regions: dict[str, dict]) -> dict:
|
|
return {
|
|
"fetched_at": datetime.now(timezone.utc).isoformat(),
|
|
"source": "valve",
|
|
"attribution": "https://www.dota2.com/leaderboards",
|
|
"note": (
|
|
"Official Immortal division boards; no MMR/account_id in payload; "
|
|
"region MMR scales are not comparable across divisions"
|
|
),
|
|
"default_region": "china",
|
|
"region_order": list(REGION_ORDER),
|
|
"regions": regions,
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
ap.add_argument("--out", type=Path, default=OUT)
|
|
args = ap.parse_args()
|
|
|
|
regions: dict[str, dict] = {}
|
|
for div in REGION_ORDER:
|
|
regions[div] = fetch_division(div)
|
|
|
|
payload = build_payload(regions)
|
|
args.out.parent.mkdir(parents=True, exist_ok=True)
|
|
args.out.write_text(
|
|
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
parts = [
|
|
f"{div}={len((regions[div].get('top100') or []))}" for div in REGION_ORDER
|
|
]
|
|
print(f"done: {', '.join(parts)} -> {args.out}", flush=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|