v0.5.84: matches origin filter, mobile gate, refresh reliability.

Ship Web refresh cache/lock, mobile demand gate, matches 职业/国服 filter, and related site updates through 0.5.84.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-29 18:31:55 +08:00
co-authored by Cursor
parent 7681fdb069
commit b01552ee6e
50 changed files with 3406 additions and 487 deletions
+27 -23
View File
@@ -1,16 +1,17 @@
"""Fetch recent league/tournament matches for OpenDota registered pros.
"""Fetch recent league/tournament (+ optional ranked) matches for OpenDota pros.
Default source: web/data/pro_player_watchlist.json (hand-curated star IDs).
Falls back to /proPlayers auto-pick only with --all-pros.
Per player: /players/{id}/matches (lobby practice + tournament), then
/matches/{id} for final items + skill builds.
Per player: /players/{id}/matches per lobby_type (practice/tournament, and
ranked with --include-pubs), up to --limit newest each; then /matches/{id}
for final items + skill builds. League rows do not crowd out ranked pubs.
Output: web/data/pro_matches.json (Climperor web only; not used by recommend).
Usage:
python web/fetch_pro_matches.py
python web/fetch_pro_matches.py --limit 12
python web/fetch_pro_matches.py --include-pubs --limit 8
python web/fetch_pro_matches.py --players 898754153,Ame
python web/fetch_pro_matches.py --all-pros --limit-pros 20 --with-team
"""
@@ -29,7 +30,7 @@ import urllib.error
from datetime import datetime, timedelta, timezone
from shared.grid import hero_table
from shared.http_utils import http_json
from shared.http_utils import http_json, write_json_atomic
from shared.paths import DATA
from fetch_hero_matches import (
@@ -220,11 +221,15 @@ def player_match_metas(
*,
lobby_types: tuple[int, ...] = LOBBY_LEAGUE,
) -> list[dict]:
"""Recent match list rows for a pro (deduped, newest first)."""
per_lt = max(limit, limit // max(1, len(lobby_types)) + 2)
"""Recent match list rows for a pro (deduped, newest first).
Fetches up to ``limit`` newest matches **per lobby_type**, then merges.
With league + ranked pubs this can return up to ``limit * len(lobby_types)``
rows — important so recent league games do not crowd out ranked pubs.
"""
by_id: dict[int, dict] = {}
for lt in lobby_types:
url = f"{OPENDOTA}/players/{account_id}/matches?limit={per_lt}&lobby_type={lt}"
url = f"{OPENDOTA}/players/{account_id}/matches?limit={limit}&lobby_type={lt}"
try:
raw = http_json(url)
except (
@@ -258,11 +263,10 @@ def player_match_metas(
if st_new >= st_old:
by_id[mid] = row
ranked = sorted(
return sorted(
by_id.values(),
key=lambda r: (-int(r.get("start_time") or 0), -int(r.get("match_id") or 0)),
)
return ranked[:limit]
def fetch_player_matches(
@@ -285,6 +289,12 @@ def fetch_player_matches(
continue
if mid <= 0 or hid <= 0:
continue
lt_raw = meta.get("lobby_type")
try:
lt_i = int(lt_raw) if lt_raw is not None else None
except (TypeError, ValueError):
lt_i = None
row_origin = "public" if lt_i == 7 else "pro"
detail = fetch_match(mid)
if delay > 0:
time.sleep(delay)
@@ -293,7 +303,7 @@ def fetch_player_matches(
slim = extract_player_row(
detail,
hid,
origin="pro",
origin=row_origin,
id_map=id_map,
item_catalog=catalog,
list_meta=meta,
@@ -309,11 +319,7 @@ def fetch_player_matches(
continue
slim["hero_id"] = hid
slim["hero_key"] = id_to_key.get(hid)
lt = meta.get("lobby_type")
try:
slim["lobby_type"] = int(lt) if lt is not None else None
except (TypeError, ValueError):
slim["lobby_type"] = None
slim["lobby_type"] = lt_i
rows.append(slim)
return rows
@@ -390,16 +396,17 @@ def write_out(
"attribution": "https://www.opendota.com",
"fetched_at": datetime.now(timezone.utc).isoformat(),
"player_source": player_source,
"limit_per_pro": limit,
"limit_per_lobby": limit,
"limit_pros": limit_pros,
"lobby_types": list(lobby_types),
"pro_count": len(by_pro),
"match_count": match_count,
"hero_count": len(by_hero),
"note_zh": (
"OpenDota 明星/职业选手近期联赛/锦标赛对局;"
"OpenDota 明星选手近期联赛/锦标赛对局(可选含天梯 lobby_type=7"
"默认名单见 web/data/pro_player_watchlist.json"
"lobby_type 1=训练/practice、2=tournament"
"每种 lobby 各保留最近 limit 场,避免联赛挤掉天梯"
"lobby_type 1=训练/practice、2=tournament、7=ranked"
"含终局出装、加点与联赛名(若有)。"
),
},
@@ -408,10 +415,7 @@ def write_out(
"by_pro": by_pro,
"by_hero": by_hero,
}
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
write_json_atomic(path, payload)
def main() -> None: