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__":