From 35e873a041505dc056701300ae3e65ef998a8ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AE=D1=80=D0=B8=D0=B9=20=D0=A7=D0=B5=D1=80=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE?= Date: Thu, 13 Nov 2025 13:06:14 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BE=D1=82=D0=B4=D0=B0=D1=87=D0=B0=20=D0=BF?= =?UTF-8?q?=D1=83=D1=81=D1=82=D1=8B=D1=85=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D0=B4=D0=BB=D1=8F=20vMix,=20=D0=B2=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=82=20=D0=BA=D0=BE=D0=B3=D0=B4=D0=B0=20?= =?UTF-8?q?=D1=82=D1=80=D0=B5=D0=B4=D1=8B=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D0=BB=D1=8E=D1=87=D0=B0=D1=8E=D1=82=D1=81=D1=8F=20=D0=B8=D0=B7?= =?UTF-8?q?=20=D0=BE=D1=84=D0=BB=D0=B0=D0=B9=D0=BD=20=D0=B2=20=D0=BE=D0=BD?= =?UTF-8?q?=D0=BB=D0=B0=D0=B9=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- get_data.py | 752 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 447 insertions(+), 305 deletions(-) diff --git a/get_data.py b/get_data.py index 967a832..ea99a26 100644 --- a/get_data.py +++ b/get_data.py @@ -98,9 +98,9 @@ OFFLINE_SWITCH_AT = None # timestamp, когда надо уйти в оффл OFFLINE_DELAY_SEC = 600 # 10 минут SLEEP_NOTICE_SENT = False # 👈 чтобы не слать уведомление повторно # --- preload lock --- -PRELOAD_LOCK = False # когда True — consumer будет принимать только preloaded game -PRELOADED_GAME_ID = None # ID матча, который мы держим «тёплым» -PRELOAD_HOLD_UNTIL = None # timestamp, до какого момента держим (T-1:15) +PRELOAD_LOCK = False # когда True — consumer будет принимать только preloaded game +PRELOADED_GAME_ID = None # ID матча, который мы держим «тёплым» +PRELOAD_HOLD_UNTIL = None # timestamp, до какого момента держим (T-1:15) # общая очередь @@ -120,6 +120,8 @@ threads_offline = [] # какой режим сейчас запущен: "live" / "offline" / None CURRENT_THREADS_MODE = None +CLEAR_OUTPUT_FOR_VMIX = False +EMPTY_PHOTO_PATH = r"D:\Графика\БАСКЕТБОЛ\VTB League\ASSETS\EMPTY.png" URLS = { @@ -134,6 +136,17 @@ URLS = { "play-by-play": "{host}/api/abc/games/play-by-play?id={game_id}", } +def maybe_clear_for_vmix(payload): + """ + Если включён режим очистки — возвращаем payload, + где все значения заменены на "". + Иначе — возвращаем как есть. + """ + print(f"maybe_clear_for_vmix(): {CLEAR_OUTPUT_FOR_VMIX}") + if CLEAR_OUTPUT_FOR_VMIX: + return wipe_json_values(payload) + return payload + def start_offline_threads(season, game_id): """Запускаем редкие запросы, когда матча нет или он уже сыгран.""" @@ -146,17 +159,20 @@ def start_offline_threads(season, game_id): stop_live_threads() stop_offline_threads() logger.info("[threads] switching to OFFLINE mode ...") + # for key in latest_data: + # latest_data[key] = wipe_json_values(latest_data[key]) # 🔹 очищаем latest_data безопасно, чтобы не ломать структуру - keep_keys = { - "game", - "pregame", - "pregame-full-stats", - "actual-standings", - "calendar", - } - for key in list(latest_data.keys()): - if key not in keep_keys: - del latest_data[key] + # keep_keys = { + # "game", + # "pregame", + # "pregame-full-stats", + # "actual-standings", + # "calendar", + # } + # for key in list(latest_data.keys()): + # if key not in keep_keys: + # del latest_data[key] + stop_event_offline.clear() @@ -336,7 +352,7 @@ def stop_live_threads(): ) else: logger.info("[threads] LIVE threads stopped") - CURRENT_THREADS_MODE = None # 👈 сбрасываем режим + CURRENT_THREADS_MODE = None # 👈 сбрасываем режим def stop_offline_threads(): @@ -348,7 +364,7 @@ def stop_offline_threads(): for t in threads_offline: t.join(timeout=1) threads_offline = [] - CURRENT_THREADS_MODE = None # 👈 сбрасываем режим + CURRENT_THREADS_MODE = None # 👈 сбрасываем режим logger.info("[threads] OFFLINE threads stopped") @@ -364,6 +380,7 @@ def has_full_game_ready() -> bool: and "teams" in payload["data"]["result"] ) + # Функция запускаемая в потоках def get_data_from_API( name: str, @@ -371,15 +388,21 @@ def get_data_from_API( sleep_time: float, stop_event: threading.Event, stop_when_live=False, - stop_after_success: bool = False, # 👈 новый флаг + stop_after_success: bool = False, # 👈 новый флаг ): did_first_fetch = False while not stop_event.is_set(): current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") - if stop_when_live and globals().get("STATUS") == "live" and has_full_game_ready(): - logger.info(f"{[{current_time}]} [{name}] stopping because STATUS='live' and full game is ready") + if ( + stop_when_live + and globals().get("STATUS") == "live" + and has_full_game_ready() + ): + logger.info( + f"{[{current_time}]} [{name}] stopping because STATUS='live' and full game is ready" + ) break - + try: value = requests.get(url, timeout=5).json() did_first_fetch = True # помечаем, что один заход сделали @@ -417,7 +440,9 @@ def get_data_from_API( ) if stop_after_success and ok_status: - logger.info(f"[{name}] got successful response → stopping thread (stop_after_success)") + logger.info( + f"[{name}] got successful response → stopping thread (stop_after_success)" + ) return # сколько уже заняло @@ -431,8 +456,14 @@ def get_data_from_API( while slept < sleep_time: if stop_event.is_set(): break - if stop_when_live and globals().get("STATUS") == "live" and has_full_game_ready(): - logger.info(f"[{name}] stopping during sleep because STATUS='live' and full game is ready") + if ( + stop_when_live + and globals().get("STATUS") == "live" + and has_full_game_ready() + ): + logger.info( + f"[{name}] stopping during sleep because STATUS='live' and full game is ready" + ) return time.sleep(1) @@ -471,7 +502,9 @@ def results_consumer(): try: if PRELOAD_LOCK: incoming_gid = extract_game_id_from_payload(payload) - if not incoming_gid or str(incoming_gid) != str(PRELOADED_GAME_ID): + if not incoming_gid or str(incoming_gid) != str( + PRELOADED_GAME_ID + ): logger.debug( f"results_consumer: skip game (gid={incoming_gid}) due to PRELOAD_LOCK; keep {PRELOADED_GAME_ID}" ) @@ -587,8 +620,10 @@ def results_consumer(): and GAME_START_DT.date() == datetime.now().date() ): globals()["STATUS"] = "finished_wait" + globals()["CLEAR_OUTPUT_FOR_VMIX"] = True # 👈 включаем режим "пустых" данных else: globals()["STATUS"] = "finished_wait" + globals()["CLEAR_OUTPUT_FOR_VMIX"] = True # 👈 включаем режим "пустых" данных human_time = datetime.fromtimestamp(switch_at).strftime( "%H:%M:%S" @@ -608,6 +643,7 @@ def results_consumer(): "online" in raw_ls_status_low or "live" in raw_ls_status_low ): # если до этого стояла отложка — уберём + globals()["CLEAR_OUTPUT_FOR_VMIX"] = False # 👈 выключаем очистку if globals().get("OFFLINE_SWITCH_AT") is not None: logger.info( "[status] match back to LIVE → cancel scheduled OFFLINE" @@ -628,7 +664,9 @@ def results_consumer(): else: if source == "game": - has_game_already = "game" in latest_data and isinstance(latest_data.get("game"), dict) + has_game_already = "game" in latest_data and isinstance( + latest_data.get("game"), dict + ) # Полная структура? is_full = ( @@ -644,32 +682,41 @@ def results_consumer(): # чтобы /status видел "живость" раз в 5 минут независимо от полноты JSON. if globals().get("STATUS") != "live": latest_data["game"] = {"ts": msg["ts"], "data": payload} - logger.debug("results_consumer: pre-live game → updated (full=%s)", is_full) + logger.debug( + "results_consumer: pre-live game → updated (full=%s)", + is_full, + ) else: # ✅ если игры ещё НЕТ в кэше — примем ПЕРВЫЙ game даже неполный, # чтобы box-score/play-by-play могли его дорастить if is_full or not has_game_already: latest_data["game"] = {"ts": msg["ts"], "data": payload} - logger.debug("results_consumer: LIVE → stored (full=%s, had=%s)", is_full, has_game_already) + logger.debug( + "results_consumer: LIVE → stored (full=%s, had=%s)", + is_full, + has_game_already, + ) else: - logger.debug("results_consumer: LIVE & partial game → keep previous one") + logger.debug( + "results_consumer: LIVE & partial game → keep previous one" + ) # 2) Когда матч УЖЕ online (STATUS == 'live'): # - поток 'game' в live-режиме погаснет сам (stop_when_live=True), # но если вдруг что-то долетит, кладём только полный JSON. continue - # # game неполный - # if not has_game_already: - # # 👉 раньше game вообще не было — лучше положить хоть что-то - # latest_data["game"] = { - # "ts": msg["ts"], - # "data": payload, - # } - # else: - # # 👉 уже есть какой-то game — неполным НЕ затираем - # logger.debug( - # "results_consumer: got partial game, keeping previous one" - # ) + # # game неполный + # if not has_game_already: + # # 👉 раньше game вообще не было — лучше положить хоть что-то + # latest_data["game"] = { + # "ts": msg["ts"], + # "data": payload, + # } + # else: + # # 👉 уже есть какой-то game — неполным НЕ затираем + # logger.debug( + # "results_consumer: got partial game, keeping previous one" + # ) # и обязательно continue/return из этого elif/if else: @@ -927,7 +974,8 @@ def get_cached_game_id() -> str | None: # структура может быть {"data":{"result":{...}}} или {"result":{...}} result = ( payload.get("data", {}).get("result") - if "data" in payload else payload.get("result") + if "data" in payload + else payload.get("result") ) if not isinstance(result, dict): return None @@ -941,7 +989,7 @@ def extract_game_id_from_payload(payload: dict) -> str | None: if not isinstance(payload, dict): return None root = payload.get("data") if isinstance(payload.get("data"), dict) else payload - res = root.get("result") if isinstance(root.get("result"), dict) else None + res = root.get("result") if isinstance(root.get("result"), dict) else None if not isinstance(res, dict): return None g = res.get("game") @@ -949,6 +997,7 @@ def extract_game_id_from_payload(payload: dict) -> str | None: return g.get("id") return None + def start_offline_prevgame(season, prev_game_id: str): """ Специальный оффлайн для ПРЕДЫДУЩЕЙ игры: @@ -965,10 +1014,18 @@ def start_offline_prevgame(season, prev_game_id: str): logger.info("[threads] switching to OFFLINE mode (previous game) ...") # оставим только полезные ключи - keep_keys = {"game", "pregame", "pregame-full-stats", "actual-standings", "calendar"} - for key in list(latest_data.keys()): - if key not in keep_keys: - del latest_data[key] + keep_keys = { + "game", + "pregame", + "pregame-full-stats", + "actual-standings", + "calendar", + } + # for key in list(latest_data.keys()): + # if key not in keep_keys: + # del latest_data[key] + # for key in latest_data: + # latest_data[key] = wipe_json_values(latest_data[key]) stop_event_offline.clear() threads_offline = [ @@ -977,10 +1034,10 @@ def start_offline_prevgame(season, prev_game_id: str): args=( "game", URLS["game"].format(host=HOST, game_id=prev_game_id, lang=LANG), - 300, # редкий опрос + 300, # редкий опрос stop_event_offline, - False, # stop_when_live - False, # ✅ stop_after_success=False (держим тред) + False, # stop_when_live + False, # ✅ stop_after_success=False (держим тред) ), daemon=True, ), @@ -989,7 +1046,11 @@ def start_offline_prevgame(season, prev_game_id: str): args=( "pregame-full-stats", URLS["pregame-full-stats"].format( - host=HOST, league=LEAGUE, season=season, game_id=prev_game_id, lang=LANG + host=HOST, + league=LEAGUE, + season=season, + game_id=prev_game_id, + lang=LANG, ), 600, stop_event_offline, @@ -1043,20 +1104,23 @@ def start_prestart_watcher(game_dt: datetime | None): def _runner(): from datetime import time as dtime # для резервного парсинга времени + global STATUS - PRELOAD_LEAD = timedelta(hours=1, minutes=15) # T-1:15 → сброс - LIVE_LEAD = timedelta(hours=1, minutes=10) # T-1:10 → live - RESET_AT = game_dt - PRELOAD_LEAD - LIVE_AT = game_dt - LIVE_LEAD - PRELOAD_MAXWAIT_SEC = 180 # ждём до 3 мин готовности full game при предзагрузке + PRELOAD_LEAD = timedelta(hours=1, minutes=15) # T-1:15 → сброс + LIVE_LEAD = timedelta(hours=1, minutes=10) # T-1:10 → live + RESET_AT = game_dt - PRELOAD_LEAD + LIVE_AT = game_dt - LIVE_LEAD + PRELOAD_MAXWAIT_SEC = 180 # ждём до 3 мин готовности full game при предзагрузке did_preload = False - did_reset = False - did_live = False + did_reset = False + did_live = False # --- вспомогательное: поиск предыдущей игры команды ДО сегодняшнего матча --- - def _find_prev_game_id(calendar_json: dict, cutoff_dt: datetime) -> tuple[str | None, datetime | None]: + def _find_prev_game_id( + calendar_json: dict, cutoff_dt: datetime + ) -> tuple[str | None, datetime | None]: items = get_items(calendar_json) or [] prev_id, prev_dt = None, None team_norm = (TEAM or "").strip().casefold() @@ -1071,7 +1135,9 @@ def start_prestart_watcher(game_dt: datetime | None): gdt = extract_game_datetime(g) if not gdt: try: - gd = datetime.strptime(g["game"]["localDate"], "%d.%m.%Y").date() + gd = datetime.strptime( + g["game"]["localDate"], "%d.%m.%Y" + ).date() gdt = datetime.combine(gd, dtime(0, 0)) except Exception: continue @@ -1079,18 +1145,22 @@ def start_prestart_watcher(game_dt: datetime | None): prev_id, prev_dt = g["game"]["id"], gdt break return prev_id, prev_dt - + # --- Шаг 1: сразу включаем оффлайн по ПРЕДЫДУЩЕЙ игре и держим до T-1:15 --- try: now = datetime.now() if now < RESET_AT: calendar_resp = requests.get( - URLS["calendar"].format(host=HOST, league=LEAGUE, season=SEASON, lang=LANG), - timeout=6 + URLS["calendar"].format( + host=HOST, league=LEAGUE, season=SEASON, lang=LANG + ), + timeout=6, ).json() prev_game_id, prev_game_dt = _find_prev_game_id(calendar_resp, game_dt) if prev_game_id and str(prev_game_id) != str(GAME_ID): - logger.info(f"[preload] старт оффлайна по предыдущей игре {prev_game_id} ({prev_game_dt})") + logger.info( + f"[preload] старт оффлайна по предыдущей игре {prev_game_id} ({prev_game_dt})" + ) # включаем «замок», чтобы consumer принимал только старую игру globals()["PRELOAD_LOCK"] = True @@ -1103,11 +1173,12 @@ def start_prestart_watcher(game_dt: datetime | None): else: logger.warning("[preload] предыдущая игра не найдена — пропускаем") else: - logger.info("[preload] уже поздно для предзагрузки (прошло T-1:15) — пропуск") + logger.info( + "[preload] уже поздно для предзагрузки (прошло T-1:15) — пропуск" + ) except Exception as e: logger.warning(f"[preload] ошибка предзагрузки прошлой игры: {e}") - # --- Основной цикл ожидания контрольных моментов --- while not stop_event.is_set(): now = datetime.now() @@ -1118,18 +1189,25 @@ def start_prestart_watcher(game_dt: datetime | None): # Шаг 2: ровно T-1:15 — сбрасываем предзагруженные данные if not did_reset and now >= RESET_AT: - logger.info(f"[reset] {now:%H:%M:%S} → T-1:15: сбрасываем предзагруженные данные") + logger.info( + f"[reset] {now:%H:%M:%S} → T-1:15: сбрасываем предзагруженные данные" + ) try: - stop_offline_threads() # на всякий - latest_data.clear() # полный сброс кэша + stop_offline_threads() # на всякий + # for key in latest_data: + # latest_data[key] = wipe_json_values(latest_data[key]) + # latest_data.clear() # полный сброс кэша # снять замок предзагрузки globals()["PRELOAD_LOCK"] = False globals()["PRELOADED_GAME_ID"] = None globals()["PRELOAD_HOLD_UNTIL"] = None - - logger.info("[reset] latest_data очищен; ждём T-1:10 для запуска live") + + logger.info( + "[reset] latest_data очищен; ждём T-1:10 для запуска live" + ) except Exception as e: logger.warning(f"[reset] ошибка при очистке: {e}") + globals()["CLEAR_OUTPUT_FOR_VMIX"] = True did_reset = True # Шаг 3: T-1:10 — включаем live-треды @@ -1138,7 +1216,8 @@ def start_prestart_watcher(game_dt: datetime | None): f"[prestart] {now:%H:%M:%S}, игра в {game_dt:%H:%M}, включаем LIVE threads по правилу T-1:10" ) STATUS = "live_soon" - stop_offline_threads() # на всякий случай + globals()["CLEAR_OUTPUT_FOR_VMIX"] = False # можно оставить пустоту до первых живых данных + stop_offline_threads() # на всякий случай start_live_threads(SEASON, GAME_ID) did_live = True break @@ -1186,9 +1265,7 @@ async def lifespan(app: FastAPI): GAME_START_DT = game_dt GAME_TODAY = is_today - logger.info( - f"Лига: {LEAGUE}\nСезон: {season}\nКоманда: {TEAM}\nGame ID: {game_id}" - ) + logger.info(f"Лига: {LEAGUE}\nСезон: {season}\nКоманда: {TEAM}\nGame ID: {game_id}") # 4. запускаем "длинные" потоки (они у тебя и так всегда) thread_result_consumer = threading.Thread( @@ -1252,9 +1329,9 @@ async def lifespan(app: FastAPI): app = FastAPI( lifespan=lifespan, - docs_url=None, # ❌ отключает /docs - redoc_url=None, # ❌ отключает /redoc - openapi_url=None # ❌ отключает /openapi.json + docs_url=None, # ❌ отключает /docs + redoc_url=None, # ❌ отключает /redoc + openapi_url=None, # ❌ отключает /openapi.json ) @@ -1278,8 +1355,10 @@ def format_time(seconds: float | int) -> str: async def team1(): game = get_latest_game_safe("game") if not game: + # если данных вообще нет (ещё ни одной игры) — тут реально нечего отдавать raise HTTPException(status_code=503, detail="game data not ready") - return await team("team1") + data = await team("team1") + return maybe_clear_for_vmix(data) @app.get("/team2") @@ -1287,29 +1366,71 @@ async def team2(): game = get_latest_game_safe("game") if not game: raise HTTPException(status_code=503, detail="game data not ready") - return await team("team2") + data = await team("team2") + return maybe_clear_for_vmix(data) @app.get("/top_team1") async def top_team1(): data = await team("team1") - return await top_sorted_team(data) + top = await top_sorted_team(data) + return maybe_clear_for_vmix(top) @app.get("/top_team2") async def top_team2(): data = await team("team2") - return await top_sorted_team(data) + top = await top_sorted_team(data) + return maybe_clear_for_vmix(top) def _b(v) -> bool: - if isinstance(v, bool): return v - if isinstance(v, (int, float)): return v != 0 - if isinstance(v, str): return v.strip().lower() in ("1","true","yes","on") + if isinstance(v, bool): + return v + if isinstance(v, (int, float)): + return v != 0 + if isinstance(v, str): + return v.strip().lower() in ("1", "true", "yes", "on") return False + def _placeholders(n=5): - return [{"NameGFX":"", "Name1GFX":"", "Name2GFX":"", "isOnCourt":False, "num":"", } for _ in range(n)] + return [ + { + "NameGFX": "", + "Name1GFX": "", + "Name2GFX": "", + "isOnCourt": False, + "num": "", + "photoGFX": EMPTY_PHOTO_PATH, + } + for _ in range(n) + ] + + +def wipe_json_values(obj): + """ + Рекурсивно заменяет все значения JSON на пустые строки. + Если ключ содержит "photo", заменяет значение на EMPTY_PHOTO_PATH. + """ + # если словарь — обрабатываем ключи + if isinstance(obj, dict): + new_dict = {} + for k, v in obj.items(): + if "photo" in str(k).lower(): + # ключ содержит photo → отдаём пустую картинку + new_dict[k] = EMPTY_PHOTO_PATH + else: + new_dict[k] = wipe_json_values(v) + return new_dict + + # если список — рекурсивно обработать элементы + elif isinstance(obj, list): + return [wipe_json_values(v) for v in obj] + + # любое конечное значение → "" + else: + return "" @app.get("/started_team1") async def started_team1(sort_by: str = None): @@ -1323,14 +1444,15 @@ async def started_team1(sort_by: str = None): if sort_by and sort_by.strip().lower() == "isstart": starters = [p for p in players if p["isStart"]] - return starters[:5] if starters else _placeholders(5) + return maybe_clear_for_vmix(starters[:5] if starters else _placeholders(5)) if sort_by and sort_by.strip().lower() == "isoncourt": on_court = [p for p in players if p["isOnCourt"]] - return on_court[:5] if on_court else _placeholders(5) + return maybe_clear_for_vmix(on_court[:5] if on_court else _placeholders(5)) # дефолт — без фильтра, как раньше - return players + return maybe_clear_for_vmix(players) + @app.get("/started_team2") async def started_team2(sort_by: str = None): @@ -1343,13 +1465,13 @@ async def started_team2(sort_by: str = None): if sort_by and sort_by.strip().lower() == "isstart": starters = [p for p in players if p["isStart"]] - return starters[:5] if starters else _placeholders(5) + return maybe_clear_for_vmix(starters[:5] if starters else _placeholders(5)) if sort_by and sort_by.strip().lower() == "isoncourt": on_court = [p for p in players if p["isOnCourt"]] - return on_court[:5] if on_court else _placeholders(5) + return maybe_clear_for_vmix(on_court[:5] if on_court else _placeholders(5)) - return players + return maybe_clear_for_vmix(players) @app.get("/game") @@ -1388,7 +1510,9 @@ async def status(request: Request): cached_game_id = get_cached_game_id() or GAME_ID note = "" if cached_game_id and GAME_ID and str(cached_game_id) != str(GAME_ID): - note = f' (предзагружены данные прошлой игры)' + note = ( + f' (предзагружены данные прошлой игры)' + ) data = { "league": LEAGUE, "team": TEAM, @@ -1550,7 +1674,7 @@ async def status(request: Request): formatted = json.dumps(data, indent=4, ensure_ascii=False) response = Response(content=formatted, media_type="application/json") response.headers["Refresh"] = "1" - return response + return maybe_clear_for_vmix(response) @app.get("/scores") @@ -1591,7 +1715,7 @@ async def scores(): score_by_quarter[i]["score1"] = parts[0] score_by_quarter[i]["score2"] = parts[1] - return score_by_quarter + return maybe_clear_for_vmix(score_by_quarter) async def top_sorted_team(data): @@ -1617,7 +1741,7 @@ async def top_sorted_team(data): return top_sorted_team -def get_latest_game_safe(name:str): +def get_latest_game_safe(name: str): """ Безопасно достаём актуальный game из latest_data. Возвращаем None, если структура ещё не готова или прилетел "плохой" game @@ -1673,15 +1797,31 @@ def _pick_last_avg_and_sum(stats_list: list) -> tuple[dict, dict]: last = stats_list[-1] if stats_list else None prev = stats_list[-2] if len(stats_list) >= 2 else None - season_avg = last.get("stats", {}) if isinstance(last, dict) and str(last.get("class")).lower() == "avg" else {} - season_sum = prev.get("stats", {}) if isinstance(prev, dict) and str(prev.get("class")).lower() == "sum" else {} + season_avg = ( + last.get("stats", {}) + if isinstance(last, dict) and str(last.get("class")).lower() == "avg" + else {} + ) + season_sum = ( + prev.get("stats", {}) + if isinstance(prev, dict) and str(prev.get("class")).lower() == "sum" + else {} + ) # Бывают инверсии порядка (на всякий случай): попробуем найти явно if not season_avg or not season_sum: for x in reversed(stats_list): - if isinstance(x, dict) and str(x.get("class")).lower() == "avg" and not season_avg: + if ( + isinstance(x, dict) + and str(x.get("class")).lower() == "avg" + and not season_avg + ): season_avg = x.get("stats", {}) or {} - if isinstance(x, dict) and str(x.get("class")).lower() == "sum" and not season_sum: + if ( + isinstance(x, dict) + and str(x.get("class")).lower() == "sum" + and not season_sum + ): season_sum = x.get("stats", {}) or {} if season_avg and season_sum: break @@ -1732,11 +1872,13 @@ async def team(who: str): if not game: # игра ещё не подгружена или структура кривоватая raise HTTPException(status_code=503, detail="game data not ready") - + full_stat = get_latest_game_safe("pregame-full-stats") if not full_stat: # ⚠️ full_stat_data отсутствует — работаем только с game_data - logger.debug(f"[{who}] full_stat_data not found → continuing with game_data only") + logger.debug( + f"[{who}] full_stat_data not found → continuing with game_data only" + ) full_stat_data = {} else: full_stat_data = full_stat["data"] if "data" in full_stat else full_stat @@ -1747,7 +1889,7 @@ async def team(who: str): "result" ] # здесь уже безопасно, мы проверили в get_latest_game_safe result_full = full_stat_data.get("result", {}) if full_stat_data else {} - + # в result ожидаем "teams" teams = result.get("teams") @@ -1779,87 +1921,93 @@ async def team(who: str): ("Forward-Center", "FC"), ] starts = payload.get("starts", []) - + team_rows = [] for item in starts: stats = item.get("stats") or {} pid = str(item.get("personId")) - full_obj = next((p for p in (payload_full or []) if str(p.get("personId")) == pid), None) + full_obj = next( + (p for p in (payload_full or []) if str(p.get("personId")) == pid), None + ) season_sum = season_avg = career_sum = career_avg = {} if full_obj: # сезон - season_sum, season_avg = _pick_last_avg_and_sum(full_obj.get("seasonStats") or []) + season_sum, season_avg = _pick_last_avg_and_sum( + full_obj.get("seasonStats") or [] + ) # карьера - career_sum, career_avg = _pick_career_sum_and_avg(full_obj.get("carrier") or []) - + career_sum, career_avg = _pick_career_sum_and_avg( + full_obj.get("carrier") or [] + ) + season_sum = _safe(season_sum) season_avg = _safe(season_avg) career_sum = _safe(career_sum) career_avg = _safe(career_avg) - + # Полезные числа для Totals+Live # live-поля в box-score называются goal1/2/3, shot1/2/3, defReb/offReb и т.п. - g1 = _as_int(stats.get("goal1")) - s1 = _as_int(stats.get("shot1")) - g2 = _as_int(stats.get("goal2")) - s2 = _as_int(stats.get("shot2")) - g3 = _as_int(stats.get("goal3")) - s3 = _as_int(stats.get("shot3")) + g1 = _as_int(stats.get("goal1")) + s1 = _as_int(stats.get("shot1")) + g2 = _as_int(stats.get("goal2")) + s2 = _as_int(stats.get("shot2")) + g3 = _as_int(stats.get("goal3")) + s3 = _as_int(stats.get("shot3")) # Сезонные суммы из pregame-full-stats - ss_pts = _as_int(season_sum.get("points")) - ss_ast = _as_int(season_sum.get("assist")) - ss_blk = _as_int(season_sum.get("blockShot")) + ss_pts = _as_int(season_sum.get("points")) + ss_ast = _as_int(season_sum.get("assist")) + ss_blk = _as_int(season_sum.get("blockShot")) ss_dreb = _as_int(season_sum.get("defRebound")) ss_oreb = _as_int(season_sum.get("offRebound")) - ss_reb = _as_int(season_sum.get("rebound")) - ss_stl = _as_int(season_sum.get("steal")) - ss_to = _as_int(season_sum.get("turnover")) + ss_reb = _as_int(season_sum.get("rebound")) + ss_stl = _as_int(season_sum.get("steal")) + ss_to = _as_int(season_sum.get("turnover")) ss_foul = _as_int(season_sum.get("foul")) - ss_sec = _as_int(season_sum.get("second")) - ss_gms = _as_int(season_sum.get("games")) - ss_st = _as_int(season_sum.get("isStarts")) - ss_g1 = _as_int(season_sum.get("goal1")) - ss_s1 = _as_int(season_sum.get("shot1")) - ss_g2 = _as_int(season_sum.get("goal2")) - ss_s2 = _as_int(season_sum.get("shot2")) - ss_g3 = _as_int(season_sum.get("goal3")) - ss_s3 = _as_int(season_sum.get("shot3")) - + ss_sec = _as_int(season_sum.get("second")) + ss_gms = _as_int(season_sum.get("games")) + ss_st = _as_int(season_sum.get("isStarts")) + ss_g1 = _as_int(season_sum.get("goal1")) + ss_s1 = _as_int(season_sum.get("shot1")) + ss_g2 = _as_int(season_sum.get("goal2")) + ss_s2 = _as_int(season_sum.get("shot2")) + ss_g3 = _as_int(season_sum.get("goal3")) + ss_s3 = _as_int(season_sum.get("shot3")) + # Карьерные суммы из pregame-full-stats - car_ss_pts = _as_int(career_sum.get("points")) - car_ss_ast = _as_int(career_sum.get("assist")) - car_ss_blk = _as_int(career_sum.get("blockShot")) + car_ss_pts = _as_int(career_sum.get("points")) + car_ss_ast = _as_int(career_sum.get("assist")) + car_ss_blk = _as_int(career_sum.get("blockShot")) car_ss_dreb = _as_int(career_sum.get("defRebound")) car_ss_oreb = _as_int(career_sum.get("offRebound")) - car_ss_reb = _as_int(career_sum.get("rebound")) - car_ss_stl = _as_int(career_sum.get("steal")) - car_ss_to = _as_int(career_sum.get("turnover")) + car_ss_reb = _as_int(career_sum.get("rebound")) + car_ss_stl = _as_int(career_sum.get("steal")) + car_ss_to = _as_int(career_sum.get("turnover")) car_ss_foul = _as_int(career_sum.get("foul")) - car_ss_sec = _as_int(career_sum.get("second")) - car_ss_gms = _as_int(career_sum.get("games")) - car_ss_st = _as_int(career_sum.get("isStarts")) - car_ss_g1 = _as_int(career_sum.get("goal1")) - car_ss_s1 = _as_int(career_sum.get("shot1")) - car_ss_g2 = _as_int(career_sum.get("goal2")) - car_ss_s2 = _as_int(career_sum.get("shot2")) - car_ss_g3 = _as_int(career_sum.get("goal3")) - car_ss_s3 = _as_int(career_sum.get("shot3")) + car_ss_sec = _as_int(career_sum.get("second")) + car_ss_gms = _as_int(career_sum.get("games")) + car_ss_st = _as_int(career_sum.get("isStarts")) + car_ss_g1 = _as_int(career_sum.get("goal1")) + car_ss_s1 = _as_int(career_sum.get("shot1")) + car_ss_g2 = _as_int(career_sum.get("goal2")) + car_ss_s2 = _as_int(career_sum.get("shot2")) + car_ss_g3 = _as_int(career_sum.get("goal3")) + car_ss_s3 = _as_int(career_sum.get("shot3")) # Totals по сезону, «с учётом текущего матча»: T_points = ss_pts + _as_int(stats.get("points")) T_assist = ss_ast + _as_int(stats.get("assist")) - T_block = ss_blk + _as_int(stats.get("block")) - T_dreb = ss_dreb + _as_int(stats.get("defReb")) - T_oreb = ss_oreb + _as_int(stats.get("offReb")) - T_reb = ss_reb + (_as_int(stats.get("defReb")) + _as_int(stats.get("offReb"))) - T_steal = ss_stl + _as_int(stats.get("steal")) - T_turn = ss_to + _as_int(stats.get("turnover")) - T_foul = ss_foul + _as_int(stats.get("foul")) - T_sec = ss_sec + _as_int(stats.get("second")) - T_gms = ss_gms + (1 if _as_int(stats.get("second")) > 0 else 0) - T_starts = ss_st + (1 if bool(stats.get("isStart")) else 0) + T_block = ss_blk + _as_int(stats.get("block")) + T_dreb = ss_dreb + _as_int(stats.get("defReb")) + T_oreb = ss_oreb + _as_int(stats.get("offReb")) + T_reb = ss_reb + (_as_int(stats.get("defReb")) + _as_int(stats.get("offReb"))) + T_steal = ss_stl + _as_int(stats.get("steal")) + T_turn = ss_to + _as_int(stats.get("turnover")) + T_foul = ss_foul + _as_int(stats.get("foul")) + T_sec = ss_sec + _as_int(stats.get("second")) + T_gms = ss_gms + (1 if _as_int(stats.get("second")) > 0 else 0) + T_starts = ss_st + (1 if bool(stats.get("isStart")) else 0) T_g1 = ss_g1 + g1 T_s1 = ss_s1 + s1 @@ -1867,20 +2015,22 @@ async def team(who: str): T_s2 = ss_s2 + s2 T_g3 = ss_g3 + g3 T_s3 = ss_s3 + s3 - + # Totals по карьере, «с учётом текущего матча»: car_T_points = car_ss_pts + _as_int(stats.get("points")) car_T_assist = car_ss_ast + _as_int(stats.get("assist")) - car_T_block = car_ss_blk + _as_int(stats.get("block")) - car_T_dreb = car_ss_dreb + _as_int(stats.get("defReb")) - car_T_oreb = car_ss_oreb + _as_int(stats.get("offReb")) - car_T_reb = car_ss_reb + (_as_int(stats.get("defReb")) + _as_int(stats.get("offReb"))) - car_T_steal = car_ss_stl + _as_int(stats.get("steal")) - car_T_turn = car_ss_to + _as_int(stats.get("turnover")) - car_T_foul = car_ss_foul + _as_int(stats.get("foul")) - car_T_sec = car_ss_sec + _as_int(stats.get("second")) - car_T_gms = car_ss_gms + (1 if _as_int(stats.get("second")) > 0 else 0) - car_T_starts = car_ss_st + (1 if bool(stats.get("isStart")) else 0) + car_T_block = car_ss_blk + _as_int(stats.get("block")) + car_T_dreb = car_ss_dreb + _as_int(stats.get("defReb")) + car_T_oreb = car_ss_oreb + _as_int(stats.get("offReb")) + car_T_reb = car_ss_reb + ( + _as_int(stats.get("defReb")) + _as_int(stats.get("offReb")) + ) + car_T_steal = car_ss_stl + _as_int(stats.get("steal")) + car_T_turn = car_ss_to + _as_int(stats.get("turnover")) + car_T_foul = car_ss_foul + _as_int(stats.get("foul")) + car_T_sec = car_ss_sec + _as_int(stats.get("second")) + car_T_gms = car_ss_gms + (1 if _as_int(stats.get("second")) > 0 else 0) + car_T_starts = car_ss_st + (1 if bool(stats.get("isStart")) else 0) car_T_g1 = car_ss_g1 + g1 car_T_s1 = car_ss_s1 + s1 @@ -1895,9 +2045,9 @@ async def team(who: str): # Для «23» используем сумму 2-х и 3-х T_g23 = T_g2 + T_g3 - T_s23 = T_s2 + T_s3 + T_s23 = T_s2 + T_s3 car_T_g23 = car_T_g2 + car_T_g3 - car_T_s23 = car_T_s2 + car_T_s3 + car_T_s23 = car_T_s2 + car_T_s3 # print(avg_season, total_season) row = { "id": item.get("personId") or "", @@ -1922,8 +2072,8 @@ async def team(who: str): and item.get("lastName") is not None else "Команда" ), - "Name1GFX": (item.get('firstName') or '').strip(), - "Name2GFX": (item.get('lastName') or '').strip(), + "Name1GFX": (item.get("firstName") or "").strip(), + "Name2GFX": (item.get("lastName") or "").strip(), "captain": item.get("isCapitan", False), "age": item.get("age") or 0, "height": f"{item.get('height')} cm" if item.get("height") else 0, @@ -1959,121 +2109,122 @@ async def team(who: str): if item.get("startRole") == "Player" else "" ), - # live-стата - "pts": _as_int(stats.get("points")), - "pt-2": f"{g2}/{s2}", - "pt-3": f"{g3}/{s3}", - "pt-1": f"{g1}/{s1}", - "fg": f"{g2+g3}/{s2+s3}", - "ast": _as_int(stats.get("assist")), - "stl": _as_int(stats.get("steal")), - "blk": _as_int(stats.get("block")), - "blkVic": _as_int(stats.get("blocked")), - "dreb": _as_int(stats.get("defReb")), - "oreb": _as_int(stats.get("offReb")), - "reb": _as_int(stats.get("defReb")) + _as_int(stats.get("offReb")), - "to": _as_int(stats.get("turnover")), - "foul": _as_int(stats.get("foul")), - "foulT": _as_int(stats.get("foulT")), - "foulD": _as_int(stats.get("foulD")), - "foulC": _as_int(stats.get("foulC")), - "foulB": _as_int(stats.get("foulB")), - "fouled": _as_int(stats.get("foulsOn")), - "plusMinus": _as_int(stats.get("plusMinus")), - "dunk": _as_int(stats.get("dunk")), - "kpi": ( - _as_int(stats.get("points")) - + _as_int(stats.get("defReb")) + _as_int(stats.get("offReb")) - + _as_int(stats.get("assist")) + _as_int(stats.get("steal")) + _as_int(stats.get("block")) - + _as_int(stats.get("foulsOn")) - + (g1 - s1) + (g2 - s2) + (g3 - s3) - - _as_int(stats.get("turnover")) - _as_int(stats.get("foul")) - ), - "time": format_time(_as_int(stats.get("second"))), - - # сезон — средние (из последнего Avg) - "AvgPoints": season_avg.get("points") or "0.0", - "AvgAssist": season_avg.get("assist") or "0.0", - "AvgBlocks": season_avg.get("blockShot") or "0.0", - "AvgDefRebound": season_avg.get("defRebound") or "0.0", - "AvgOffRebound": season_avg.get("offRebound") or "0.0", - "AvgRebound": season_avg.get("rebound") or "0.0", - "AvgSteal": season_avg.get("steal") or "0.0", - "AvgTurnover": season_avg.get("turnover") or "0.0", - "AvgFoul": season_avg.get("foul") or "0.0", - "AvgOpponentFoul": season_avg.get("foulsOnPlayer") or "0.0", - "AvgDunk": season_avg.get("dunk") or "0.0", - "AvgPlayedTime": season_avg.get("playedTime") or "0:00", - "Shot1Percent": season_avg.get("shot1Percent") or "0.0%", - "Shot2Percent": season_avg.get("shot2Percent") or "0.0%", - "Shot3Percent": season_avg.get("shot3Percent") or "0.0%", - "Shot23Percent": season_avg.get("shot23Percent") or "0.0%", - - # сезон — Totals (суммы из Sum + live) - "TPoints": T_points, - "TShots1": f"{T_g1}/{T_s1}", - "TShots2": f"{T_g2}/{T_s2}", - "TShots3": f"{T_g3}/{T_s3}", - "TShots23": f"{T_g23}/{T_s23}", - "TShot1Percent": _pct(T_g1, T_s1), - "TShot2Percent": _pct(T_g2, T_s2), - "TShot3Percent": _pct(T_g3, T_s3), - "TShot23Percent": _pct(T_g23, T_s23), - "TAssist": T_assist, - "TBlocks": T_block, - "TDefRebound": T_dreb, - "TOffRebound": T_oreb, - "TRebound": T_reb, - "TSteal": T_steal, - "TTurnover": T_turn, - "TFoul": T_foul, - "TPlayedTime": format_time(T_sec), - "TGameCount": T_gms, - "TStartCount": T_starts, - - # карьера — средние (из последнего Avg) - "Career_AvgPoints": career_avg.get("points") or "0.0", - "Career_AvgAssist": career_avg.get("assist") or "0.0", - "Career_AvgBlocks": career_avg.get("blockShot") or "0.0", - "Career_AvgDefRebound": career_avg.get("defRebound") or "0.0", - "Career_AvgOffRebound": career_avg.get("offRebound") or "0.0", - "Career_AvgRebound": career_avg.get("rebound") or "0.0", - "Career_AvgSteal": career_avg.get("steal") or "0.0", - "Career_AvgTurnover": career_avg.get("turnover") or "0.0", - "Career_AvgFoul": career_avg.get("foul") or "0.0", - "Career_AvgOpponentFoul": career_avg.get("foulsOnPlayer") or "0.0", - "Career_AvgDunk": career_avg.get("dunk") or "0.0", - "Career_AvgPlayedTime": career_avg.get("playedTime") or "0:00", - "Career_Shot1Percent": career_avg.get("shot1Percent") or "0.0%", - "Career_Shot2Percent": career_avg.get("shot2Percent") or "0.0%", - "Career_Shot3Percent": career_avg.get("shot3Percent") or "0.0%", - "Career_Shot23Percent": career_avg.get("shot23Percent") or "0.0%", - - # карьера — Totals (суммы из Sum + live) - "Career_TPoints": car_T_points, - "Career_TShots1": f"{car_T_g1}/{car_T_s1}", - "Career_TShots2": f"{car_T_g2}/{car_T_s2}", - "Career_TShots3": f"{car_T_g3}/{car_T_s3}", - "Career_TShots23": f"{car_T_g23}/{car_T_s23}", - "Career_TShot1Percent": _pct(car_T_g1, car_T_s1), - "Career_TShot2Percent": _pct(car_T_g2, car_T_s2), - "Career_TShot3Percent": _pct(car_T_g3, car_T_s3), - "Career_TShot23Percent": _pct(car_T_g23, car_T_s23), - "Career_TAssist": car_T_assist, - "Career_TBlocks": car_T_block, - "Career_TDefRebound": car_T_dreb, - "Career_TOffRebound": car_T_oreb, - "Career_TRebound": car_T_reb, - "Career_TSteal": car_T_steal, - "Career_TTurnover": car_T_turn, - "Career_TFoul": car_T_foul, - "Career_TPlayedTime": format_time(car_T_sec), - "Career_TGameCount": car_T_gms, - "Career_TStartCount": car_T_starts, - - } + # live-стата + "pts": _as_int(stats.get("points")), + "pt-2": f"{g2}/{s2}", + "pt-3": f"{g3}/{s3}", + "pt-1": f"{g1}/{s1}", + "fg": f"{g2+g3}/{s2+s3}", + "ast": _as_int(stats.get("assist")), + "stl": _as_int(stats.get("steal")), + "blk": _as_int(stats.get("block")), + "blkVic": _as_int(stats.get("blocked")), + "dreb": _as_int(stats.get("defReb")), + "oreb": _as_int(stats.get("offReb")), + "reb": _as_int(stats.get("defReb")) + _as_int(stats.get("offReb")), + "to": _as_int(stats.get("turnover")), + "foul": _as_int(stats.get("foul")), + "foulT": _as_int(stats.get("foulT")), + "foulD": _as_int(stats.get("foulD")), + "foulC": _as_int(stats.get("foulC")), + "foulB": _as_int(stats.get("foulB")), + "fouled": _as_int(stats.get("foulsOn")), + "plusMinus": _as_int(stats.get("plusMinus")), + "dunk": _as_int(stats.get("dunk")), + "kpi": ( + _as_int(stats.get("points")) + + _as_int(stats.get("defReb")) + + _as_int(stats.get("offReb")) + + _as_int(stats.get("assist")) + + _as_int(stats.get("steal")) + + _as_int(stats.get("block")) + + _as_int(stats.get("foulsOn")) + + (g1 - s1) + + (g2 - s2) + + (g3 - s3) + - _as_int(stats.get("turnover")) + - _as_int(stats.get("foul")) + ), + "time": format_time(_as_int(stats.get("second"))), + # сезон — средние (из последнего Avg) + "AvgPoints": season_avg.get("points") or "0.0", + "AvgAssist": season_avg.get("assist") or "0.0", + "AvgBlocks": season_avg.get("blockShot") or "0.0", + "AvgDefRebound": season_avg.get("defRebound") or "0.0", + "AvgOffRebound": season_avg.get("offRebound") or "0.0", + "AvgRebound": season_avg.get("rebound") or "0.0", + "AvgSteal": season_avg.get("steal") or "0.0", + "AvgTurnover": season_avg.get("turnover") or "0.0", + "AvgFoul": season_avg.get("foul") or "0.0", + "AvgOpponentFoul": season_avg.get("foulsOnPlayer") or "0.0", + "AvgDunk": season_avg.get("dunk") or "0.0", + "AvgPlayedTime": season_avg.get("playedTime") or "0:00", + "Shot1Percent": season_avg.get("shot1Percent") or "0.0%", + "Shot2Percent": season_avg.get("shot2Percent") or "0.0%", + "Shot3Percent": season_avg.get("shot3Percent") or "0.0%", + "Shot23Percent": season_avg.get("shot23Percent") or "0.0%", + # сезон — Totals (суммы из Sum + live) + "TPoints": T_points, + "TShots1": f"{T_g1}/{T_s1}", + "TShots2": f"{T_g2}/{T_s2}", + "TShots3": f"{T_g3}/{T_s3}", + "TShots23": f"{T_g23}/{T_s23}", + "TShot1Percent": _pct(T_g1, T_s1), + "TShot2Percent": _pct(T_g2, T_s2), + "TShot3Percent": _pct(T_g3, T_s3), + "TShot23Percent": _pct(T_g23, T_s23), + "TAssist": T_assist, + "TBlocks": T_block, + "TDefRebound": T_dreb, + "TOffRebound": T_oreb, + "TRebound": T_reb, + "TSteal": T_steal, + "TTurnover": T_turn, + "TFoul": T_foul, + "TPlayedTime": format_time(T_sec), + "TGameCount": T_gms, + "TStartCount": T_starts, + # карьера — средние (из последнего Avg) + "Career_AvgPoints": career_avg.get("points") or "0.0", + "Career_AvgAssist": career_avg.get("assist") or "0.0", + "Career_AvgBlocks": career_avg.get("blockShot") or "0.0", + "Career_AvgDefRebound": career_avg.get("defRebound") or "0.0", + "Career_AvgOffRebound": career_avg.get("offRebound") or "0.0", + "Career_AvgRebound": career_avg.get("rebound") or "0.0", + "Career_AvgSteal": career_avg.get("steal") or "0.0", + "Career_AvgTurnover": career_avg.get("turnover") or "0.0", + "Career_AvgFoul": career_avg.get("foul") or "0.0", + "Career_AvgOpponentFoul": career_avg.get("foulsOnPlayer") or "0.0", + "Career_AvgDunk": career_avg.get("dunk") or "0.0", + "Career_AvgPlayedTime": career_avg.get("playedTime") or "0:00", + "Career_Shot1Percent": career_avg.get("shot1Percent") or "0.0%", + "Career_Shot2Percent": career_avg.get("shot2Percent") or "0.0%", + "Career_Shot3Percent": career_avg.get("shot3Percent") or "0.0%", + "Career_Shot23Percent": career_avg.get("shot23Percent") or "0.0%", + # карьера — Totals (суммы из Sum + live) + "Career_TPoints": car_T_points, + "Career_TShots1": f"{car_T_g1}/{car_T_s1}", + "Career_TShots2": f"{car_T_g2}/{car_T_s2}", + "Career_TShots3": f"{car_T_g3}/{car_T_s3}", + "Career_TShots23": f"{car_T_g23}/{car_T_s23}", + "Career_TShot1Percent": _pct(car_T_g1, car_T_s1), + "Career_TShot2Percent": _pct(car_T_g2, car_T_s2), + "Career_TShot3Percent": _pct(car_T_g3, car_T_s3), + "Career_TShot23Percent": _pct(car_T_g23, car_T_s23), + "Career_TAssist": car_T_assist, + "Career_TBlocks": car_T_block, + "Career_TDefRebound": car_T_dreb, + "Career_TOffRebound": car_T_oreb, + "Career_TRebound": car_T_reb, + "Career_TSteal": car_T_steal, + "Career_TTurnover": car_T_turn, + "Career_TFoul": car_T_foul, + "Career_TPlayedTime": format_time(car_T_sec), + "Career_TGameCount": car_T_gms, + "Career_TStartCount": car_T_starts, + } team_rows.append(row) - + # добиваем до 12 строк, чтобы UI был ровный count_player = sum(1 for x in team_rows if x["startRole"] == "Player") if count_player < 12 and team_rows: @@ -2132,14 +2283,6 @@ async def team(who: str): async def started_team(data): - # started_team = sorted( - # ( - # p - # for p in data - # if p.get("startRole") == "Player" and p.get("isStart") is True - # ), - # key=lambda x: int(x.get("num") or 0), - # ) return data @@ -2397,7 +2540,7 @@ async def team_stats(): "val2": val2, } ) - return result_json + return maybe_clear_for_vmix(result_json) @app.get("/referee") @@ -2449,7 +2592,7 @@ async def referee(): else len(desired_order) ), ) - return referees + return maybe_clear_for_vmix(referees) @app.get("/team_comparison") @@ -2523,7 +2666,7 @@ async def team_comparison(): ), } teams.append(temp_team) - return teams + return maybe_clear_for_vmix(teams) else: return [{"Данных о сравнении команд нет!"}] @@ -2594,7 +2737,7 @@ async def regular_standings(): df["plus_minus"] = tg_plus - tg_minus standings_payload = df.to_dict(orient="records") - return standings_payload + return maybe_clear_for_vmix(standings_payload) @app.get("/live_status") @@ -2605,7 +2748,7 @@ async def live_status(): if not ls: # live-status ещё не прилетел - return [{"foulsA": 0, "foulsB": 0}] + return maybe_clear_for_vmix([{"foulsA": 0, "foulsB": 0, "scoreA": 0, "scoreB": 0}]) raw = ls.get("data") @@ -2613,20 +2756,20 @@ async def live_status(): if isinstance(raw, dict): # иногда API кладёт всё прямо в root, иногда внутрь result if "result" in raw and isinstance(raw["result"], dict): - return [raw["result"]] + return maybe_clear_for_vmix([raw["result"]]) else: # отдадим как есть, но в списке, чтобы фронт не сломать - return [raw] + return maybe_clear_for_vmix([raw]) # 2) если это просто строка статуса ("ok" / "no-status" / "error") if isinstance(raw, str): - return [{"status": raw}] + return maybe_clear_for_vmix([{"status": raw}]) # fallback - return [{"foulsA": 0, "foulsB": 0}] + return maybe_clear_for_vmix([{"foulsA": 0, "foulsB": 0, "scoreA": 0, "scoreB": 0}]) else: # матч не идёт — как у тебя было - return [{"foulsA": 0, "foulsB": 0}] + return maybe_clear_for_vmix([{"foulsA": 0, "foulsB": 0, "scoreA": 0, "scoreB": 0}]) @app.get("/info") @@ -2652,9 +2795,8 @@ async def info(): except ValueError: full_format = date_obj.strftime("%A, %#d %B %Y") short_format = date_obj.strftime("%A, %#d %b") - - - return [ + + return maybe_clear_for_vmix([ { "team1": team1_name, "team2": team2_name, @@ -2672,7 +2814,7 @@ async def info(): "date1": str(full_format), "date2": str(short_format), } - ] + ]) if __name__ == "__main__":