поправленны данные, когда матч онлайн, но нет live-status и box-score
This commit is contained in:
216
get_data_new.py
216
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"
|
||||
)
|
||||
# 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 = None
|
||||
if isinstance(box_team.get("starts"), list):
|
||||
stat = next(
|
||||
(
|
||||
s
|
||||
for s in box_team.get("starts", [])
|
||||
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:
|
||||
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
|
||||
|
||||
# пробуем каждую генерацию отдельно, чтобы одна не убила остальные
|
||||
try:
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user