отдача пустых данных для vMix, в момент когда треды переключаются из офлайн в онлайн
This commit is contained in:
338
get_data.py
338
get_data.py
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user