поправленны данные, когда матч онлайн, но нет live-status и box-score

This commit is contained in:
2025-10-28 14:25:23 +03:00
parent 15367b05fc
commit f11b315d2f

View File

@@ -487,20 +487,6 @@ def poll_game_live(
game_meta: dict,
stop_event: threading.Event,
) -> None:
"""
Главный цикл лайва.
Каждые ~0.2 сек:
- решаем, какие эндпоинты давно не опрашивали (live-status, box-score, play-by-play, game),
- параллельно дёргаем их через ThreadPoolExecutor,
- сохраняем результаты в static/api_*.json,
- проверяем статус матча в live-status.
Цикл завершится, когда:
- матч закончен (по live-status),
- календарь говорит, что игра не live,
- или выставлен stop_event (например, оператор нажал Ctrl+C).
"""
slow_endpoints = ["game"] # "pregame-fullstats" можно вернуть по желанию
fast_endpoints = ["live-status", "box-score", "play-by-play"]
@@ -508,16 +494,12 @@ def poll_game_live(
with ThreadPoolExecutor(max_workers=5) as executor:
while True:
# внешний стоп: операторская остановка или завершение run_live_loop
if stop_event.is_set():
logger.info(
f"[POLL] stop_event set -> break live poll for game {game_id}"
)
logger.info(f"[POLL] stop_event set -> break live poll for game {game_id}")
break
now = time.time()
# решаем, какие ручки надо дёрнуть прямо сейчас
to_run = []
for ep in fast_endpoints + slow_endpoints:
interval = get_interval_by_name(ep)
@@ -541,23 +523,26 @@ def poll_game_live(
game_finished = False
# собираем результаты параллельных вызовов
for fut in as_completed(futures):
try:
ep_name, data = fut.result()
last_call[ep_name] = now
# проверяем статус лайва
if ep_name == "live-status":
if isinstance(data, dict):
st = (data.get("result").get("gameStatus") or "").lower()
if st in ("resultconfirmed", "finished", "result"):
logger.info(
f"[POLL] Game {game_id} finished by live-status"
)
game_finished = True
# data может быть:
# {"status":"404","message":"Not found","result":None}
# или {"status":"200","result":{"gameStatus":"Online", ...}}
ls_result = data.get("result") if isinstance(data, dict) else None
game_status = ""
if isinstance(ls_result, dict):
game_status = (ls_result.get("gameStatus") or "").lower()
if game_status in ("resultconfirmed", "finished", "result"):
logger.info(f"[POLL] Game {game_id} finished by live-status")
game_finished = True
except Exception as e:
# логируем, но не роняем поток
logger.exception(f"[POLL] poll endpoint error: {e}")
# страховка: календарь говорит, что матч не лайв -> выходим
@@ -570,7 +555,6 @@ def poll_game_live(
time.sleep(1)
# вторая точка выхода по stop_event после sleep
if stop_event.is_set():
logger.info(
f"[POLL] stop_event set after sleep -> break live poll for game {game_id}"
@@ -581,66 +565,99 @@ def poll_game_live(
def build_render_state() -> dict:
"""
Собирает итоговое состояние матча (merged dict) для графики/внешки.
Читает из api_*.json:
- api_game
- api_live-status
- api_box-score
- api_play-by-play
Обогащает:
- мержит box-score в структуру команд/игроков
- добавляет plays, scoreByPeriods, fullScore, live_status
- добавляет служебные метаданные (generatedAt)
Возвращает словарь:
{
"meta": {...},
"result": {...} # <-- это пойдёт в game.json / ui_state.json и т.д.
}
Возвращает минимально возможный state, даже если часть данных ещё не доступна.
"""
game_data = read_local_json("api_game")
game_data_raw = read_local_json("api_game")
live_status_data = read_local_json("api_live-status")
box_score_data = read_local_json("api_box-score")
play_by_play_data = read_local_json("api_play-by-play")
# Минимальная защита: если ничего нет, рендер всё равно не должен падать жёстко.
if not game_data or "result" not in game_data:
# Без api_game у нас вообще нет каркаса матча -> это критично
if not game_data_raw or "result" not in game_data_raw:
raise RuntimeError("build_render_state(): api_game/result отсутствует")
game_data = game_data["result"]
game_data = game_data_raw["result"]
# проставляем статистику игроков из box-score внутрь game_data["teams"]
if box_score_data and "result" in box_score_data:
for index_team, team in enumerate(game_data["teams"][1:]):
box_team = box_score_data["result"]["teams"][index_team]
# Защитимся от отсутствия ключевых структур
# Убедимся, что есть поля, которые ждёт остальной код
game_data.setdefault("teams", [])
game_data.setdefault("team1", {})
game_data.setdefault("team2", {})
game_data.setdefault("game", {})
# plays - список событий
game_data["plays"] = (play_by_play_data or {}).get("result", []) or []
# live_status - текущее состояние периода/секунды
if live_status_data and isinstance(live_status_data.get("result"), dict):
game_data["live_status"] = live_status_data["result"]
else:
# дадим заготовку, чтобы Play_By_Play не падал
game_data["live_status"] = {
"period": 1,
"second": 0,
"gameStatus": game_data.get("game", {}).get("gameStatus", ""),
}
# box-score -> статы игроков и команд
if (
box_score_data
and isinstance(box_score_data.get("result"), dict)
and "teams" in box_score_data["result"]
and box_score_data["result"]["teams"] is not None
):
for index_team, team in enumerate(game_data.get("teams", [])[1:]):
# box_team может отсутствовать, если индексы не совпали или сервер отдал None
box_teams_list = box_score_data["result"]["teams"]
if (
isinstance(box_teams_list, list)
and index_team < len(box_teams_list)
and box_teams_list[index_team] is not None
):
box_team = box_teams_list[index_team]
else:
box_team = {}
# переносим статы игроков
for player in team.get("starts", []):
stat = next(
(
s
for s in box_team.get("starts", [])
if s.get("startNum") == player.get("startNum")
),
None,
)
stat = None
if isinstance(box_team.get("starts"), list):
stat = next(
(
s
for s in box_team["starts"]
if s.get("startNum") == player.get("startNum")
),
None,
)
if stat:
player["stats"] = stat
team["total"] = box_team.get("total", {})
game_data["scoreByPeriods"] = box_score_data["result"].get("scoreByPeriods", [])
game_data["fullScore"] = box_score_data["result"].get("fullScore", {})
# total по команде
team["total"] = box_team.get("total", {}) if isinstance(box_team, dict) else {}
# плей-бай-плей и live_status
game_data["plays"] = (play_by_play_data or {}).get("result", [])
if live_status_data and "result" in live_status_data:
game_data["live_status"] = live_status_data["result"]
# периоды и общий счёт
if isinstance(box_score_data["result"], dict):
game_data["scoreByPeriods"] = (
box_score_data["result"].get("scoreByPeriods") or []
)
game_data["fullScore"] = box_score_data["result"].get("fullScore") or {}
else:
# если box-score нет ещё:
game_data.setdefault("scoreByPeriods", [])
game_data.setdefault("fullScore", {})
# а ещё надо, чтобы у каждой команды было хотя бы .total = {}
for team in game_data.get("teams", []):
team.setdefault("total", {})
for starter in team.get("starts", []):
starter.setdefault("stats", {})
merged: Dict[str, Any] = {
"meta": {
"generatedAt": _now_iso(),
"sourceHints": {
"boxScoreHas": "",
"pbpLen": "",
"boxScoreHas": "yes" if box_score_data else "no",
"pbpLen": str(len(game_data["plays"])),
},
},
"result": game_data,
@@ -649,37 +666,68 @@ def build_render_state() -> dict:
def render_loop(stop_event: threading.Event, out_name: str = "game") -> None:
"""
Поток рендера.
Пока матч идёт (или пока мы не сказали стоп), крутится так:
- собрал текущее state через build_render_state()
- посчитал командную статистику (Team_Both_Stat)
- посчитал ростер/стартеров/лидеров (Json_Team_Generation)
- посчитал счёт по четвертям (Scores_Quarter)
- всё это положил в static/*.json
Цикл выходит, когда stop_event.is_set() == True.
"""
logger.info("[RENDER_THREAD] start render loop")
while not stop_event.is_set():
try:
state = build_render_state()
try:
state = build_render_state()
except Exception as build_err:
# до тех пор, пока api_game не готов или битый — просто ждём
logger.debug(f"[RENDER_THREAD] build_render_state not ready: {build_err}")
time.sleep(0.2)
continue
Team_Both_Stat(state)
Json_Team_Generation(state, who="team1")
Json_Team_Generation(state, who="team2")
Scores_Quarter(state)
Referee(state)
Play_By_Play(state)
# пробуем каждую генерацию отдельно, чтобы одна не убила остальные
try:
Team_Both_Stat(state)
except Exception as e:
logger.debug(f"[RENDER_THREAD] skip Team_Both_Stat: {e}")
# live_status отдельно, + общий state в <out_name>.json
atomic_write_json([state["result"]["live_status"]], "live_status")
atomic_write_json(state["result"], out_name)
try:
Json_Team_Generation(state, who="team1")
except Exception as e:
logger.debug(f"[RENDER_THREAD] skip Json_Team_Generation team1: {e}")
try:
Json_Team_Generation(state, who="team2")
except Exception as e:
logger.debug(f"[RENDER_THREAD] skip Json_Team_Generation team2: {e}")
try:
Scores_Quarter(state)
except Exception as e:
logger.debug(f"[RENDER_THREAD] skip Scores_Quarter: {e}")
try:
Referee(state)
except Exception as e:
logger.debug(f"[RENDER_THREAD] skip Referee: {e}")
try:
Play_By_Play(state)
except Exception as e:
logger.debug(f"[RENDER_THREAD] skip Play_By_Play: {e}")
# записываем live_status и общий state/game.json, но тоже мягко
try:
live_status_to_write = []
rs = state.get("result", {})
if isinstance(rs, dict) and "live_status" in rs:
live_status_to_write = [rs["live_status"]]
atomic_write_json(live_status_to_write, "live_status")
except Exception as e:
logger.debug(f"[RENDER_THREAD] skip live_status write: {e}")
try:
atomic_write_json(state.get("result", {}), out_name)
except Exception as e:
logger.debug(f"[RENDER_THREAD] skip {out_name}.json write: {e}")
except Exception as ex:
logger.exception(f"[RENDER_THREAD] error while building render state: {ex}")
# крайняя защита цикла — никогда не вываливаться из потока
logger.exception(f"[RENDER_THREAD] unexpected error: {ex}")
time.sleep(0.2)