отдача пустых данных для vMix, в момент когда треды переключаются из офлайн в онлайн

This commit is contained in:
2025-11-13 13:06:14 +03:00
parent 92e6a9f463
commit 35e873a041

View File

@@ -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()
@@ -364,6 +380,7 @@ def has_full_game_ready() -> bool:
and "teams" in payload["data"]["result"]
)
# Функция запускаемая в потоках
def get_data_from_API(
name: str,
@@ -376,8 +393,14 @@ def get_data_from_API(
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:
@@ -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,15 +682,24 @@ 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),
@@ -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
@@ -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 = [
@@ -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,6 +1104,7 @@ 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 → сброс
@@ -1056,7 +1118,9 @@ def start_prestart_watcher(game_dt: datetime | None):
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
@@ -1085,12 +1151,16 @@ def start_prestart_watcher(game_dt: datetime | None):
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() # полный сброс кэша
# 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,6 +1216,7 @@ 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"
globals()["CLEAR_OUTPUT_FOR_VMIX"] = False # можно оставить пустоту до первых живых данных
stop_offline_threads() # на всякий случай
start_live_threads(SEASON, GAME_ID)
did_live = True
@@ -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(
@@ -1254,7 +1331,7 @@ app = FastAPI(
lifespan=lifespan,
docs_url=None, # ❌ отключает /docs
redoc_url=None, # ❌ отключает /redoc
openapi_url=None # ❌ отключает /openapi.json
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' <span style="color:#ffb84d;">(предзагружены данные прошлой игры)</span>'
note = (
f' <span style="color:#ffb84d;">(предзагружены данные прошлой игры)</span>'
)
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):
@@ -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
@@ -1736,7 +1876,9 @@ async def team(who: str):
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
@@ -1784,14 +1926,20 @@ async def team(who: str):
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)
@@ -1874,7 +2022,9 @@ async def team(who: str):
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_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"))
@@ -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,
@@ -1983,14 +2133,19 @@ async def team(who: str):
"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("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"))
+ (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",
@@ -2008,7 +2163,6 @@ async def team(who: str):
"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}",
@@ -2030,7 +2184,6 @@ async def team(who: str):
"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",
@@ -2048,7 +2201,6 @@ async def team(who: str):
"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}",
@@ -2070,7 +2222,6 @@ async def team(who: str):
"Career_TPlayedTime": format_time(car_T_sec),
"Career_TGameCount": car_T_gms,
"Career_TStartCount": car_T_starts,
}
team_rows.append(row)
@@ -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")
@@ -2653,8 +2796,7 @@ async def info():
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__":