diff --git a/get_data.py b/get_data.py
index 62f0b09..00c8792 100644
--- a/get_data.py
+++ b/get_data.py
@@ -95,6 +95,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()
@@ -137,6 +143,7 @@ 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 = {
@@ -165,6 +172,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 +335,7 @@ def stop_live_threads():
)
else:
logger.info("[threads] LIVE threads stopped")
+ CURRENT_THREADS_MODE = None # 👈 сбрасываем режим
def stop_offline_threads():
@@ -311,6 +347,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")
@@ -429,8 +466,17 @@ 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"],
@@ -735,8 +781,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 +916,233 @@ 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) ...")
+
+ # оставим только полезные ключи
+ 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()
+ 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():
+ from datetime import time as dtime # для резервного парсинга времени
global STATUS
- # за сколько включать live
- lead = timedelta(hours=1, minutes=10)
- switch_at = game_dt - lead
+ 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"[reset] {now:%H:%M:%S} → T-1:15: сбрасываем предзагруженные данные")
+ try:
+ stop_offline_threads() # на всякий
+ 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}")
+ did_reset = True
+
+ # Шаг 3: T-1:10 — включаем live-треды
+ if not did_live and now >= LIVE_AT:
logger.info(
- f"[prestart] it's {now}, game at {game_dt}, enabling LIVE threads (1h10m rule)"
+ f"[prestart] {now:%H:%M:%S}, игра в {game_dt:%H:%M}, включаем LIVE threads по правилу T-1:10"
)
STATUS = "live_soon"
- # сначала гасим оффлайн, если он крутится
- stop_offline_threads()
- # а потом включаем live
+ 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()
@@ -986,10 +1224,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"
@@ -1063,16 +1301,54 @@ async def top_team2():
return await top_sorted_team(data)
-@app.get("/started_team1")
-async def started_team1():
- data = await team("team1")
- return await started_team(data)
+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":"", } for _ in range(n)]
+
+@app.get("/started_team1")
+async def started_team1(sort_by: str = None):
+ data = await team("team1")
+ 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 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 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 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 players
@app.get("/game")
@@ -1108,10 +1384,14 @@ 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 +1419,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 +1503,7 @@ async def status(request: Request):
League: {LEAGUE}
Team: {TEAM}
-Game ID: {GAME_ID}
+Game ID: {cached_game_id}{note}
Game Status: {gs_text}