поправленны данные, когда матч онлайн, но нет 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, game_meta: dict,
stop_event: threading.Event, stop_event: threading.Event,
) -> None: ) -> 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" можно вернуть по желанию slow_endpoints = ["game"] # "pregame-fullstats" можно вернуть по желанию
fast_endpoints = ["live-status", "box-score", "play-by-play"] fast_endpoints = ["live-status", "box-score", "play-by-play"]
@@ -508,16 +494,12 @@ def poll_game_live(
with ThreadPoolExecutor(max_workers=5) as executor: with ThreadPoolExecutor(max_workers=5) as executor:
while True: while True:
# внешний стоп: операторская остановка или завершение run_live_loop
if stop_event.is_set(): if stop_event.is_set():
logger.info( logger.info(f"[POLL] stop_event set -> break live poll for game {game_id}")
f"[POLL] stop_event set -> break live poll for game {game_id}"
)
break 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)
@@ -541,23 +523,26 @@ def poll_game_live(
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): # data может быть:
st = (data.get("result").get("gameStatus") or "").lower() # {"status":"404","message":"Not found","result":None}
if st in ("resultconfirmed", "finished", "result"): # или {"status":"200","result":{"gameStatus":"Online", ...}}
logger.info( ls_result = data.get("result") if isinstance(data, dict) else None
f"[POLL] Game {game_id} finished by live-status" game_status = ""
) if isinstance(ls_result, dict):
game_finished = True 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: except Exception as e:
# логируем, но не роняем поток
logger.exception(f"[POLL] poll endpoint error: {e}") logger.exception(f"[POLL] poll endpoint error: {e}")
# страховка: календарь говорит, что матч не лайв -> выходим # страховка: календарь говорит, что матч не лайв -> выходим
@@ -570,7 +555,6 @@ def poll_game_live(
time.sleep(1) time.sleep(1)
# вторая точка выхода по stop_event после sleep
if stop_event.is_set(): if stop_event.is_set():
logger.info( logger.info(
f"[POLL] stop_event set after sleep -> break live poll for game {game_id}" 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: def build_render_state() -> dict:
""" """
Собирает итоговое состояние матча (merged dict) для графики/внешки. Собирает итоговое состояние матча (merged dict) для графики/внешки.
Возвращает минимально возможный state, даже если часть данных ещё не доступна.
Читает из 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 и т.д.
}
""" """
game_data = read_local_json("api_game")
game_data_raw = read_local_json("api_game")
live_status_data = read_local_json("api_live-status") live_status_data = read_local_json("api_live-status")
box_score_data = read_local_json("api_box-score") box_score_data = read_local_json("api_box-score")
play_by_play_data = read_local_json("api_play-by-play") play_by_play_data = read_local_json("api_play-by-play")
# Минимальная защита: если ничего нет, рендер всё равно не должен падать жёстко. # Без api_game у нас вообще нет каркаса матча -> это критично
if not game_data or "result" not in game_data: if not game_data_raw or "result" not in game_data_raw:
raise RuntimeError("build_render_state(): api_game/result отсутствует") 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:]): game_data.setdefault("teams", [])
box_team = box_score_data["result"]["teams"][index_team] 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", []): for player in team.get("starts", []):
stat = next( stat = None
( if isinstance(box_team.get("starts"), list):
s stat = next(
for s in box_team.get("starts", []) (
if s.get("startNum") == player.get("startNum") s
), for s in box_team["starts"]
None, if s.get("startNum") == player.get("startNum")
) ),
None,
)
if stat: if stat:
player["stats"] = stat player["stats"] = stat
team["total"] = box_team.get("total", {})
game_data["scoreByPeriods"] = box_score_data["result"].get("scoreByPeriods", []) # total по команде
game_data["fullScore"] = box_score_data["result"].get("fullScore", {}) 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 isinstance(box_score_data["result"], dict):
if live_status_data and "result" in live_status_data: game_data["scoreByPeriods"] = (
game_data["live_status"] = live_status_data["result"] 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] = { merged: Dict[str, Any] = {
"meta": { "meta": {
"generatedAt": _now_iso(), "generatedAt": _now_iso(),
"sourceHints": { "sourceHints": {
"boxScoreHas": "", "boxScoreHas": "yes" if box_score_data else "no",
"pbpLen": "", "pbpLen": str(len(game_data["plays"])),
}, },
}, },
"result": game_data, "result": game_data,
@@ -649,37 +666,68 @@ def build_render_state() -> dict:
def render_loop(stop_event: threading.Event, out_name: str = "game") -> None: 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") logger.info("[RENDER_THREAD] start render loop")
while not stop_event.is_set(): while not stop_event.is_set():
try: 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") try:
Json_Team_Generation(state, who="team2") Team_Both_Stat(state)
Scores_Quarter(state) except Exception as e:
Referee(state) logger.debug(f"[RENDER_THREAD] skip Team_Both_Stat: {e}")
Play_By_Play(state)
# live_status отдельно, + общий state в <out_name>.json try:
atomic_write_json([state["result"]["live_status"]], "live_status") Json_Team_Generation(state, who="team1")
atomic_write_json(state["result"], out_name) 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: 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) time.sleep(0.2)