поправленны данные, когда матч онлайн, но нет live-status и box-score
This commit is contained in:
236
get_data_new.py
236
get_data_new.py
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user