diff --git a/get_data_new.py b/get_data_new.py index 0462fcd..4725993 100644 --- a/get_data_new.py +++ b/get_data_new.py @@ -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 в .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)