This commit is contained in:
2025-10-27 18:39:25 +03:00
parent b27e02bb49
commit b0aba27764

View File

@@ -410,33 +410,38 @@ def run_live_loop(
def poll_game_live( 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-status", "box-score", "play-by-play" раз в 1 сек
Всё, что надо сейчас дернуть, дергаем параллельно. Цикл выходит, когда матч перестаёт быть live ИЛИ когда сработал stop_event.
Цикл выходит, когда матч перестаёт быть live.
""" """
slow_endpoints = [ slow_endpoints = ["game"]
"game",
] # "pregame-fullstats"]
fast_endpoints = ["live-status", "box-score", "play-by-play"] fast_endpoints = ["live-status", "box-score", "play-by-play"]
last_call = {} last_call = {ep: 0 for ep in slow_endpoints + fast_endpoints}
now = time.time()
for name in slow_endpoints + fast_endpoints:
last_call[name] = 0 # форсим первый вызов сразу
# пул потоков: 5 нам хватит (у нас максимум 5 ручек одновременно) # пул потоков живет весь матч
with ThreadPoolExecutor(max_workers=5) as executor: with ThreadPoolExecutor(max_workers=5) as executor:
while True: 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() now = time.time()
# собрать, какие ручки надо дёрнуть прямо сейчас # какие ручки надо дёрнуть прямо сейчас
to_run = [] to_run = []
for ep in fast_endpoints + slow_endpoints: for ep in fast_endpoints + slow_endpoints:
interval = get_interval_by_name(ep) interval = get_interval_by_name(ep)
@@ -458,18 +463,19 @@ def poll_game_live(
) )
) )
# будем смотреть на ответы, особенно live-status
game_finished = False game_finished = False
for fut in as_completed(futures): for fut in as_completed(futures):
try: try:
ep_name, data = fut.result() ep_name, data = fut.result()
last_call[ep_name] = now # помечаем как обновлённый last_call[ep_name] = now
if ep_name == "live-status": if ep_name == "live-status":
# проверяем статус, не закончилась ли игра
if isinstance(data, dict): if isinstance(data, dict):
st = ( st = (
data.get("status") or data.get("gameStatus") or "" data.get("status")
or data.get("gameStatus")
or ""
).lower() ).lower()
if st in ("resultconfirmed", "finished", "final"): if st in ("resultconfirmed", "finished", "final"):
logger.info( logger.info(
@@ -480,7 +486,7 @@ def poll_game_live(
except Exception as e: except Exception as e:
logger.exception(f"poll endpoint error: {e}") logger.exception(f"poll endpoint error: {e}")
# вторая страховка (инфо из календаря) # страховка по календарю (game_meta мог устареть, но лучше чем ничего)
if not is_game_live(game_meta): if not is_game_live(game_meta):
logger.info( logger.info(
f"Game {game_id} no longer live by calendar meta -> stop loop" f"Game {game_id} no longer live by calendar meta -> stop loop"
@@ -490,7 +496,6 @@ def poll_game_live(
if game_finished: if game_finished:
break break
# чуть притормозим, чтобы не жарить CPU
time.sleep(0.2) time.sleep(0.2)
logger.debug("live poll tick ok") 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) today_game, last_played = get_game_id(json_calendar, team)
# если есть завершённая последняя игра — просто сохраним её статические данные # если есть завершённая последняя игра — просто сохраним статический срез и выходим
if last_played and not today_game: if last_played and not today_game:
game_id = last_played["game"]["id"] game_id = last_played["game"]["id"]
logger.info(f"Последний завершённый матч id={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"] game_id = today_game["game"]["id"]
logger.info(f"Онлайн матч id={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, "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"]): if is_game_live(today_game["game"]):
t = threading.Thread( # отдельная сессия для live-пула (как раньше)
target=run_live_loop, live_session = create_session()
args=(session, league, season, game_id, lang, today_game["game"]),
daemon=False, # <-- ключевое изменение # единый stop_event для всего матча
) stop_event = threading.Event()
t.start()
logger.info("live thread spawned, waiting for it to finish...") # поток рендера
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 return
logger.info("Для этой команды игр сегодня нет и нет завершённой последней игры.") logger.info("Для этой команды игр сегодня нет и нет завершённой последней игры.")
def read_local_json(name: str, in_dir: str = "static"): def read_local_json(name: str, in_dir: str = "static"):
""" """
Безопасно читает static/<name>.json. Безопасно читает static/<name>.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"): def render_loop(stop_event: threading.Event, out_name: str = "game"):
""" """
Крутится в отдельном потоке. Крутится в отдельном потоке.
Постоянно читает сырые api_*.json, собирает финальный state Читает api_*.json, собирает финальный state
и сохраняет в static/<out_name>.json. и сохраняет в static/<out_name>.json.
Работает, пока stop_event не установлен. Работает, пока stop_event не установлен.
""" """
@@ -1562,8 +1596,6 @@ def render_loop(stop_event: threading.Event, out_name: str = "game"):
except Exception as ex: except Exception as ex:
logger.exception(f"[RENDER_THREAD] error while building render state: {ex}") logger.exception(f"[RENDER_THREAD] error while building render state: {ex}")
# частота обновления отрисовки.
# 0.2с достаточно быстро для ТВ-графики и не жарит CPU.
time.sleep(0.2) time.sleep(0.2)
logger.info("[RENDER_THREAD] stop render loop") logger.info("[RENDER_THREAD] stop render loop")
@@ -1578,7 +1610,19 @@ def main():
session = create_session() 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__": if __name__ == "__main__":