diff --git a/.gitignore b/.gitignore
index a21c49b..5820b83 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
/__pycache__
/TestJson
/logs/*
-*.venv
\ No newline at end of file
+*.venv
+*.env
\ No newline at end of file
diff --git a/get_data.py b/get_data.py
index 62f0b09..734abb9 100644
--- a/get_data.py
+++ b/get_data.py
@@ -1,25 +1,20 @@
-from fastapi import FastAPI
-from fastapi.responses import Response, HTMLResponse
-from fastapi import HTTPException
-from fastapi import Request
+from fastapi import FastAPI, HTTPException, Request
+from fastapi.responses import Response, HTMLResponse, FileResponse, StreamingResponse
+from typing import Dict, Any
from contextlib import asynccontextmanager
-import requests
-import threading
-import time
-import queue
+import requests, uvicorn, json
+import threading, queue
import argparse
-import uvicorn
-import os
import pandas as pd
-import json
from datetime import datetime, time as dtime, timedelta
from fastapi.responses import Response
import logging
import logging.config
-import platform
-import socket
-
-# передадим параметры через аргументы или глобальные переменные
+from dotenv import load_dotenv
+from pprint import pprint
+import nasio
+import io, os, platform, time
+import xml.etree.ElementTree as ET
parser = argparse.ArgumentParser()
parser = argparse.ArgumentParser()
@@ -29,14 +24,12 @@ parser.add_argument("--lang", default="en")
args = parser.parse_args()
MYHOST = platform.node()
-user_name = socket.gethostname()
if not os.path.exists("logs"):
os.makedirs("logs")
-telegram_bot_token = "7639240596:AAH0YtdQoWZSC-_R_EW4wKAHHNLIA0F_ARY"
-# telegram_chat_id = 228977654
-telegram_chat_id = -4803699526
+telegram_bot_token = os.getenv("TELEGRAM_TOKEN")
+telegram_chat_id = os.getenv("TELEGRAM_CHAT_ID")
log_config = {
"version": 1,
"handlers": {
@@ -67,7 +60,7 @@ log_config = {
"formatters": {
"telegram": {
"class": "telegram_handler.HtmlFormatter",
- "format": f"%(levelname)s [{MYHOST.upper()}] [{user_name}]\n%(message)s",
+ "format": f"%(levelname)s [{MYHOST.upper()}]\n%(message)s",
"use_emoji": "True",
},
"simple": {
@@ -82,11 +75,19 @@ logging.config.dictConfig(log_config)
logger = logging.getLogger(__name__)
logger.handlers[2].formatter.use_emoji = True
+pprint(f"Локальный файл окружения ={load_dotenv(verbose=True)}")
LEAGUE = args.league
TEAM = args.team
LANG = args.lang
-HOST = "https://pro.russiabasket.org"
+HOST = os.getenv("API_BASE_URL")
+SYNO_PATH = f'{os.getenv("SYNO_PATH")}MATCH INFO.xlsx'
+SYNO_URL = os.getenv("SYNO_URL")
+SYNO_USERNAME = os.getenv("SYNO_USERNAME")
+SYNO_PASSWORD = os.getenv("SYNO_PASSWORD")
+SYNO_PATH_МVMIX = os.getenv("SYNO_PATH_МVMIX")
+
+
STATUS = False
GAME_ID = None
SEASON = None
@@ -95,6 +96,12 @@ GAME_TODAY = False # флаг: игра сегодня
GAME_SOON = False # флаг: игра сегодня и скоро (<1 часа)
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)
+
# общая очередь
results_q = queue.Queue()
@@ -113,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 = {
@@ -128,6 +137,17 @@ URLS = {
}
+def maybe_clear_for_vmix(payload):
+ """
+ Если включён режим очистки — возвращаем payload,
+ где все значения заменены на "".
+ Иначе — возвращаем как есть.
+ """
+ if CLEAR_OUTPUT_FOR_VMIX:
+ return wipe_json_values(payload)
+ return payload
+
+
def start_offline_threads(season, game_id):
"""Запускаем редкие запросы, когда матча нет или он уже сыгран."""
global threads_offline, CURRENT_THREADS_MODE, stop_event_offline, latest_data
@@ -137,18 +157,8 @@ def start_offline_threads(season, game_id):
return
stop_live_threads()
+ stop_offline_threads()
logger.info("[threads] switching to OFFLINE mode ...")
- # 🔹 очищаем 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]
stop_event_offline.clear()
@@ -165,6 +175,34 @@ def start_offline_threads(season, game_id):
),
daemon=True,
),
+ threading.Thread(
+ target=get_data_from_API,
+ args=(
+ "pregame-full-stats",
+ URLS["pregame-full-stats"].format(
+ host=HOST, league=LEAGUE, season=season, game_id=game_id, lang=LANG
+ ),
+ 600,
+ stop_event_offline,
+ False,
+ True,
+ ),
+ daemon=True,
+ ),
+ threading.Thread(
+ target=get_data_from_API,
+ args=(
+ "actual-standings",
+ URLS["actual-standings"].format(
+ host=HOST, league=LEAGUE, season=season, lang=LANG
+ ),
+ 600,
+ stop_event_offline,
+ False,
+ True,
+ ),
+ daemon=True,
+ ),
]
for t in threads_offline:
t.start()
@@ -300,6 +338,7 @@ def stop_live_threads():
)
else:
logger.info("[threads] LIVE threads stopped")
+ CURRENT_THREADS_MODE = None # 👈 сбрасываем режим
def stop_offline_threads():
@@ -311,6 +350,7 @@ def stop_offline_threads():
for t in threads_offline:
t.join(timeout=1)
threads_offline = []
+ CURRENT_THREADS_MODE = None # 👈 сбрасываем режим
logger.info("[threads] OFFLINE threads stopped")
@@ -326,6 +366,7 @@ def has_full_game_ready() -> bool:
and "teams" in payload["data"]["result"]
)
+
# Функция запускаемая в потоках
def get_data_from_API(
name: str,
@@ -333,15 +374,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 # помечаем, что один заход сделали
@@ -379,7 +426,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
# сколько уже заняло
@@ -393,8 +442,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)
@@ -429,8 +484,19 @@ def results_consumer():
incoming_status = payload.get("status") # может быть None
# print(source, incoming_status)
if source == "game":
- # обработка ТОЛЬКО в спец-ветке ниже
- pass
+ # принимаем ТОЛЬКО тот game_id, который держим в PRELOAD_LOCK
+ try:
+ if PRELOAD_LOCK:
+ incoming_gid = extract_game_id_from_payload(payload)
+ 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}"
+ )
+ continue
+ except Exception as _e:
+ logger.debug(f"results_consumer: preload lock check error: {_e}")
else:
latest_data[source] = {
"ts": msg["ts"],
@@ -540,8 +606,14 @@ 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"
@@ -561,6 +633,9 @@ 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"
@@ -581,7 +656,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 = (
@@ -597,34 +674,25 @@ 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")
-
- # 2) Когда матч УЖЕ online (STATUS == 'live'):
- # - поток 'game' в live-режиме погаснет сам (stop_when_live=True),
- # но если вдруг что-то долетит, кладём только полный JSON.
+ logger.debug(
+ "results_consumer: LIVE & partial game → keep previous one"
+ )
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"
- # )
-
- # и обязательно continue/return из этого elif/if
else:
latest_data[source] = {
"ts": msg["ts"],
@@ -735,8 +803,9 @@ def build_pretty_status_message():
Если game ещё нет — шлём хотя бы статусы источников.
"""
lines = []
+ cgid = get_cached_game_id()
lines.append(f"🏀 {LEAGUE.upper()} • {TEAM}")
- lines.append(f"📌 Game ID: {GAME_ID}")
+ lines.append(f"📌 Game ID: {cgid or GAME_ID}")
lines.append(f"🕒 {GAME_START_DT}")
# сначала попробуем собрать нормальный game
@@ -869,42 +938,253 @@ def status_broadcaster():
time.sleep(1)
+def get_cached_game_id() -> str | None:
+ game = latest_data.get("game")
+ if not game:
+ return None
+ payload = game.get("data", game)
+ if not isinstance(payload, dict):
+ return None
+ # структура может быть {"data":{"result":{...}}} или {"result":{...}}
+ result = (
+ payload.get("data", {}).get("result")
+ if "data" in payload
+ else payload.get("result")
+ )
+ if not isinstance(result, dict):
+ return None
+ g = result.get("game")
+ if isinstance(g, dict):
+ return g.get("id")
+ return None
+
+
+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
+ if not isinstance(res, dict):
+ return None
+ g = res.get("game")
+ if isinstance(g, dict):
+ return g.get("id")
+ return None
+
+
+def start_offline_prevgame(season, prev_game_id: str):
+ """
+ Специальный оффлайн для ПРЕДЫДУЩЕЙ игры:
+ - гасит любые текущие треды
+ - запускает только 'game' для prev_game_id
+ - НЕ останавливается после первого 'ok' (stop_after_success=False)
+ """
+ global threads_offline, CURRENT_THREADS_MODE, stop_event_offline, latest_data
+
+ # всегда переключаемся чисто
+ stop_live_threads()
+ stop_offline_threads()
+
+ logger.info("[threads] switching to OFFLINE mode (previous game) ...")
+
+ stop_event_offline.clear()
+ threads_offline = [
+ threading.Thread(
+ target=get_data_from_API,
+ args=(
+ "game",
+ URLS["game"].format(host=HOST, game_id=prev_game_id, lang=LANG),
+ 300, # редкий опрос
+ stop_event_offline,
+ False, # stop_when_live
+ False, # ✅ stop_after_success=False (держим тред)
+ ),
+ daemon=True,
+ ),
+ threading.Thread(
+ target=get_data_from_API,
+ args=(
+ "pregame-full-stats",
+ URLS["pregame-full-stats"].format(
+ host=HOST,
+ league=LEAGUE,
+ season=season,
+ game_id=prev_game_id,
+ lang=LANG,
+ ),
+ 600,
+ stop_event_offline,
+ False,
+ False,
+ ),
+ daemon=True,
+ ),
+ threading.Thread(
+ target=get_data_from_API,
+ args=(
+ "actual-standings",
+ URLS["actual-standings"].format(
+ host=HOST, league=LEAGUE, season=season, lang=LANG
+ ),
+ 600,
+ stop_event_offline,
+ False,
+ False,
+ ),
+ daemon=True,
+ ),
+ ]
+ for t in threads_offline:
+ t.start()
+
+ CURRENT_THREADS_MODE = "offline"
+ logger.info(f"[threads] OFFLINE prev-game thread started for {prev_game_id}")
+
+
def start_prestart_watcher(game_dt: datetime | None):
"""
- Следит за временем начала игры.
- Как только до матча остаётся <= 1ч10м — включает live-треды.
- Работает только для "игра сегодня".
+ Логика на день игры:
+ 1) Немедленно подгружаем ДАННЫЕ ПРОШЛОГО МАТЧА (один раз, оффлайн-поток 'game'),
+ чтобы программа имела данные до старта.
+ 2) Ровно за 1:15 до начала — СБРАСЫВАЕМ эти данные (останавливаем оффлайн, чистим latest_data).
+ 3) Ровно за 1:10 до начала — ВКЛЮЧАЕМ LIVE-треды.
"""
if not game_dt:
- return # нечего ждать
+ return
+
+ # разовое уведомление о "спячке"
+ global SLEEP_NOTICE_SENT, STATUS, SEASON, GAME_ID
+ now = datetime.now()
+ if not SLEEP_NOTICE_SENT and game_dt > now:
+ logger.info(
+ "🛌 Тред ушёл в спячку до начала игры.\n"
+ f"⏰ Матч начинается сегодня в {game_dt.strftime('%H:%M')}."
+ )
+ SLEEP_NOTICE_SENT = True
def _runner():
- global STATUS
- # за сколько включать live
- lead = timedelta(hours=1, minutes=10)
- switch_at = game_dt - lead
+ 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 при предзагрузке
+
+ did_preload = False
+ did_reset = False
+ did_live = False
+
+ # --- вспомогательное: поиск предыдущей игры команды ДО сегодняшнего матча ---
+ 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()
+ for g in reversed(items):
+ try:
+ t1 = (g["team1"]["name"] or "").strip().casefold()
+ t2 = (g["team2"]["name"] or "").strip().casefold()
+ if team_norm not in (t1, t2):
+ continue
+ except Exception:
+ continue
+ gdt = extract_game_datetime(g)
+ if not gdt:
+ try:
+ gd = datetime.strptime(
+ g["game"]["localDate"], "%d.%m.%Y"
+ ).date()
+ gdt = datetime.combine(gd, dtime(0, 0))
+ except Exception:
+ continue
+ if gdt < cutoff_dt:
+ 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,
+ ).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})"
+ )
+
+ # включаем «замок», чтобы consumer принимал только старую игру
+ globals()["PRELOAD_LOCK"] = True
+ globals()["PRELOADED_GAME_ID"] = str(prev_game_id)
+ globals()["PRELOAD_HOLD_UNTIL"] = RESET_AT.timestamp()
+
+ # поднимаем один оффлайн-тред по старой игре (без stop_after_success)
+ start_offline_prevgame(SEASON, prev_game_id)
+ did_preload = True
+ else:
+ logger.warning("[preload] предыдущая игра не найдена — пропускаем")
+ else:
+ logger.info(
+ "[preload] уже поздно для предзагрузки (прошло T-1:15) — пропуск"
+ )
+ except Exception as e:
+ logger.warning(f"[preload] ошибка предзагрузки прошлой игры: {e}")
+
+ # --- Основной цикл ожидания контрольных моментов ---
while not stop_event.is_set():
now = datetime.now()
- # если игра уже live/finished — не мешаем
+ # если матч уже в другом конечном состоянии — выходим
if STATUS in ("live", "finished", "finished_wait", "finished_today"):
break
- # если время подошло — включаем live и выходим
- if now >= switch_at:
+ # Шаг 2: ровно T-1:15 — сбрасываем предзагруженные данные
+ if not did_reset and now >= RESET_AT:
logger.info(
- f"[prestart] it's {now}, game at {game_dt}, enabling LIVE threads (1h10m rule)"
+ f"[reset] {now:%H:%M:%S} → T-1:15: сбрасываем предзагруженные данные"
+ )
+ try:
+ 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"
+ )
+ except Exception as e:
+ logger.warning(f"[reset] ошибка при очистке: {e}")
+ globals()["CLEAR_OUTPUT_FOR_VMIX"] = True
+ did_reset = True
+
+ # Шаг 3: T-1:10 — включаем live-треды
+ if not did_live and now >= LIVE_AT:
+ logger.info(
+ f"[prestart] {now:%H:%M:%S}, игра в {game_dt:%H:%M}, включаем LIVE threads по правилу T-1:10"
)
STATUS = "live_soon"
- # сначала гасим оффлайн, если он крутится
- stop_offline_threads()
- # а потом включаем live
+ globals()[
+ "CLEAR_OUTPUT_FOR_VMIX"
+ ] = False # можно оставить пустоту до первых живых данных
+ stop_offline_threads() # на всякий случай
start_live_threads(SEASON, GAME_ID)
+ did_live = True
break
- # иначе спим немного и проверяем снова
- time.sleep(30) # можно 15–60 сек
+ time.sleep(15)
t = threading.Thread(target=_runner, daemon=True)
t.start()
@@ -947,9 +1227,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(
@@ -986,10 +1264,10 @@ async def lifespan(app: FastAPI):
start_live_threads(SEASON, GAME_ID)
else:
STATUS = "today_not_started"
- start_offline_threads(SEASON, GAME_ID)
+ # start_offline_threads(SEASON, GAME_ID)
else:
STATUS = "today_not_started"
- start_offline_threads(SEASON, GAME_ID)
+ # start_offline_threads(SEASON, GAME_ID)
elif cal_status == "Online":
STATUS = "live"
@@ -1013,9 +1291,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
)
@@ -1039,8 +1317,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")
@@ -1048,36 +1328,118 @@ 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")
+ return False
+
+
+def _placeholders(n=5):
+ 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():
+async def started_team1(sort_by: str = None):
data = await team("team1")
- return await started_team(data)
+ players = await started_team(data) or []
+
+ # нормализуем флаги
+ for p in players:
+ p["isStart"] = _b(p.get("isStart", False))
+ p["isOnCourt"] = _b(p.get("isOnCourt", False))
+
+ if sort_by and sort_by.strip().lower() == "isstart":
+ starters = [p for p in players if p["isStart"]]
+ 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 maybe_clear_for_vmix(on_court[:5] if on_court else _placeholders(5))
+
+ # дефолт — без фильтра, как раньше
+ return maybe_clear_for_vmix(players)
@app.get("/started_team2")
-async def started_team2():
+async def started_team2(sort_by: str = None):
data = await team("team2")
- return await started_team(data)
+ players = await started_team(data) or []
+
+ for p in players:
+ p["isStart"] = _b(p.get("isStart", False))
+ p["isOnCourt"] = _b(p.get("isOnCourt", False))
+
+ if sort_by and sort_by.strip().lower() == "isstart":
+ starters = [p for p in players if p["isStart"]]
+ 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 maybe_clear_for_vmix(on_court[:5] if on_court else _placeholders(5))
+
+ return maybe_clear_for_vmix(players)
-@app.get("/game")
+@app.get("/latest_data")
async def game():
- return latest_data["game"]
+ return latest_data
@app.get("/status")
@@ -1108,10 +1470,16 @@ async def status(request: Request):
sorted_keys = [k for k in sort_order if k in latest_data] + sorted(
[k for k in latest_data if k not in sort_order]
)
+ 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' (предзагружены данные прошлой игры)'
+ )
data = {
"league": LEAGUE,
"team": TEAM,
- "game_id": GAME_ID,
+ "game_id": cached_game_id,
"game_status": STATUS,
"statuses": [
{
@@ -1139,7 +1507,7 @@ async def status(request: Request):
league=LEAGUE,
season=SEASON,
lang=LANG,
- game_id=GAME_ID,
+ game_id=cached_game_id,
),
"color": color_for_status(
latest_data[item]["data"]["status"]
@@ -1223,7 +1591,7 @@ async def status(request: Request):
League: {LEAGUE}
Team: {TEAM}
-Game ID: {GAME_ID}
+Game ID: {cached_game_id}{note}
Game Status: {gs_text}