diff --git a/get_data_new.py b/get_data_new.py index e851016..322b78b 100644 --- a/get_data_new.py +++ b/get_data_new.py @@ -410,33 +410,38 @@ def run_live_loop( def poll_game_live( - session, league: str, season: str, game_id: int, lang: str, game_meta: dict + session, + league: str, + season: str, + game_id: int, + lang: str, + game_meta: dict, + stop_event: threading.Event, ): """ Онлайн-цикл: - - "game" и "pregame-fullstats" раз в 600 сек + - "game" раз в 600 сек (pregame-fullstats можно вернуть позже) - "live-status", "box-score", "play-by-play" раз в 1 сек - Всё, что надо сейчас дернуть, дергаем параллельно. - Цикл выходит, когда матч перестаёт быть live. + Цикл выходит, когда матч перестаёт быть live ИЛИ когда сработал stop_event. """ - slow_endpoints = [ - "game", - ] # "pregame-fullstats"] + slow_endpoints = ["game"] fast_endpoints = ["live-status", "box-score", "play-by-play"] - last_call = {} - now = time.time() - for name in slow_endpoints + fast_endpoints: - last_call[name] = 0 # форсим первый вызов сразу + last_call = {ep: 0 for ep in slow_endpoints + fast_endpoints} - # пул потоков: 5 нам хватит (у нас максимум 5 ручек одновременно) + # пул потоков живет весь матч with ThreadPoolExecutor(max_workers=5) as executor: while True: + # внешняя принудительная остановка + if stop_event.is_set(): + logger.info(f"[LIVE_LOOP] 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) @@ -458,18 +463,19 @@ def poll_game_live( ) ) - # будем смотреть на ответы, особенно live-status game_finished = False + for fut in as_completed(futures): try: ep_name, data = fut.result() - last_call[ep_name] = now # помечаем как обновлённый + last_call[ep_name] = now if ep_name == "live-status": - # проверяем статус, не закончилась ли игра if isinstance(data, dict): st = ( - data.get("status") or data.get("gameStatus") or "" + data.get("status") + or data.get("gameStatus") + or "" ).lower() if st in ("resultconfirmed", "finished", "final"): logger.info( @@ -480,7 +486,7 @@ def poll_game_live( except Exception as e: logger.exception(f"poll endpoint error: {e}") - # вторая страховка (инфо из календаря) + # страховка по календарю (game_meta мог устареть, но лучше чем ничего) if not is_game_live(game_meta): logger.info( f"Game {game_id} no longer live by calendar meta -> stop loop" @@ -490,7 +496,6 @@ def poll_game_live( if game_finished: break - # чуть притормозим, чтобы не жарить CPU time.sleep(0.2) logger.debug("live poll tick ok") @@ -518,7 +523,7 @@ def get_data_API(session, league: str, team: str, lang: str): today_game, last_played = get_game_id(json_calendar, team) - # если есть завершённая последняя игра — просто сохраним её статические данные + # если есть завершённая последняя игра — просто сохраним статический срез и выходим if last_played and not today_game: game_id = last_played["game"]["id"] logger.info(f"Последний завершённый матч id={game_id}") @@ -530,36 +535,65 @@ def get_data_API(session, league: str, team: str, lang: str): game_id = today_game["game"]["id"] logger.info(f"Онлайн матч id={game_id}") - # базовые данные прямо сейчас (до запуска фонового потока) + # базовые данные прямо сейчас fetch_api_data(session, "game", host=HOST, game_id=game_id, lang=lang) - # fetch_api_data( - # session, - # "pregame-fullstats", - # host=HOST, - # league=league, - # season=season, - # game_id=game_id, - # lang=lang, - # ) - # если матч реально идёт -> запускаем отдельный поток live-опроса + # если матч реально идёт -> запускаем live-петлю и рендер if is_game_live(today_game["game"]): - t = threading.Thread( - target=run_live_loop, - args=(session, league, season, game_id, lang, today_game["game"]), - daemon=False, # <-- ключевое изменение - ) - t.start() - logger.info("live thread spawned, waiting for it to finish...") + # отдельная сессия для live-пула (как раньше) + live_session = create_session() + + # единый stop_event для всего матча + stop_event = threading.Event() + + # поток рендера + render_thread = threading.Thread( + target=render_loop, + args=(stop_event, "ui_state"), # имя файла можешь менять + daemon=False, + ) + render_thread.start() + logger.info("[MAIN] render thread spawned") + + # поток live-пулинга API + def live_worker(): + try: + poll_game_live( + session=live_session, + league=league, + season=season, + game_id=game_id, + lang=lang, + game_meta=today_game["game"], + stop_event=stop_event, + ) + except Exception as e: + logger.exception(f"[LIVE_THREAD] crash in live loop: {e}") + + live_thread = threading.Thread( + target=live_worker, + daemon=False, + ) + live_thread.start() + logger.info("[MAIN] live thread spawned") + + # дожидаемся окончания live_thread (то есть завершения матча или ошибки) + live_thread.join() + logger.info("[MAIN] live thread finished") + + # говорим рендеру остановиться + stop_event.set() + + # ждём корректного завершения рендера + render_thread.join() + logger.info("[MAIN] render thread finished") - # блокируем main до конца матча - t.join() - logger.info("live thread finished") return logger.info("Для этой команды игр сегодня нет и нет завершённой последней игры.") + def read_local_json(name: str, in_dir: str = "static"): """ Безопасно читает static/.json. @@ -1544,7 +1578,7 @@ def Team_Both_Stat(merged: dict, *, out_dir: str = "static") -> None: def render_loop(stop_event: threading.Event, out_name: str = "game"): """ Крутится в отдельном потоке. - Постоянно читает сырые api_*.json, собирает финальный state + Читает api_*.json, собирает финальный state и сохраняет в static/.json. Работает, пока stop_event не установлен. """ @@ -1558,12 +1592,10 @@ def render_loop(stop_event: threading.Event, out_name: str = "game"): Json_Team_Generation(state, who="team2") atomic_write_json([state["result"]["live_status"]], "live_status") atomic_write_json(state["result"], out_name) - + except Exception as ex: logger.exception(f"[RENDER_THREAD] error while building render state: {ex}") - # частота обновления отрисовки. - # 0.2с достаточно быстро для ТВ-графики и не жарит CPU. time.sleep(0.2) logger.info("[RENDER_THREAD] stop render loop") @@ -1578,7 +1610,19 @@ def main(): session = create_session() - get_data_API(session, args.league, args.team, args.lang) + # единый флаг остановки на ВСЮ программу + stop_event = threading.Event() + + try: + get_data_API(session, args.league, args.team, args.lang, stop_event) + except KeyboardInterrupt: + # ручное прерывание: просим все рабочие циклы сворачиваться + logger.info("KeyboardInterrupt: stopping...") + stop_event.set() + except Exception as e: + logger.exception(f"Fatal in main(): {e}") + stop_event.set() + if __name__ == "__main__":