{cgid or GAME_ID}")
lines.append(f"🕒 {GAME_START_DT}")
# сначала попробуем собрать нормальный game
game_wrap = latest_data.get("game")
has_game = False
if game_wrap:
raw = game_wrap.get("data") if isinstance(game_wrap, dict) else game_wrap
# raw может быть: dict (полный payload) | dict (уже result) | str ("ok"/"no-status")
result = {}
if isinstance(raw, dict):
# ваш нормальный полный ответ по game имеет структуру: {"data": {"result": {...}}}
# но на всякий случай поддержим и вариант, где сразу {"result": {...}} или уже {"game": ...}
result = (
raw.get("data", {}).get("result", {})
if "data" in raw
else (raw.get("result") or raw)
)
else:
result = {}
game_info = result.get("game") or {}
team1_name = (result.get("team1") or {}).get("name", "Team 1")
team2_name = (result.get("team2") or {}).get("name", "Team 2")
lines.append(f"👥 {team1_name} vs {team2_name}")
score_now = game_info.get("score") or ""
full_score = game_info.get("fullScore") or ""
if score_now:
lines.append(f"🔢 Score: {score_now}")
if isinstance(full_score, str) and full_score:
quarters = full_score.split(",")
q_text = " | ".join(f"Q{i+1} {q}" for i, q in enumerate(quarters) if q)
if q_text:
lines.append(f"🧱 By quarters: {q_text}")
has_game = bool(result)
# live-status отдельно
# ls = latest_data.get("live-status", {})
# ls_raw = ls.get("data") or {}
# ls_status = (
# ls_raw.get("status") or ls_raw.get("gameStatus") or ls_raw.get("state") or "—"
# )
# lines.append(f"🟢 LIVE status: {ls_status}")
ls_wrap = latest_data.get("live-status")
ls_status = "—"
if ls_wrap:
raw = ls_wrap.get("data")
if isinstance(raw, dict):
ls_dict = raw.get("result") or raw
ls_status = (
ls_dict.get("status")
or ls_dict.get("gameStatus")
or ls_dict.get("state")
or "—"
)
elif isinstance(raw, str):
# API/consumer могли положить просто строку статуса: "ok", "no-status", "error"
ls_status = raw
lines.append(f"🟢 LIVE status: {ls_status}")
# добавим блок по источникам — это как раз “состояние запросов”
sort_order = ["game", "live-status", "box-score", "play-by-play"]
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]
)
src_lines = []
for k in keys:
if "excel" not in k.lower():
d = latest_data.get(k) or {}
ts = d.get("ts", "—")
dat = d.get("data")
if isinstance(dat, dict) and "status" in dat:
st = str(dat["status"]).lower()
else:
st = str(dat).lower()
# Эмодзи-кружки для статусов
if any(x in st for x in ["ok", "success", "live", "online"]):
emoji = "🟢"
elif any(
x in st for x in ["error", "fail", "no-status", "none", "timeout"]
):
emoji = "🔴"
else:
emoji = "🟡"
src_lines.append(f"{emoji} {k}: {st} ({ts})")
if src_lines:
lines.append("📡 Sources:")
lines.extend(src_lines)
# даже если game не успел — мы всё равно что-то вернём
return "\n".join(lines)
def status_broadcaster():
"""
Если матч live — сразу шлём статус.
Потом — раз в 5 минут.
Если матч не live — ждём и проверяем снова.
"""
INTERVAL = 300 # 5 минут
last_text = None
first_live_sent = False
while not stop_event.is_set():
# если игра не идёт — спим по чуть-чуть и крутимся
if STATUS not in ("live", "live_soon"):
first_live_sent = False # чтобы при новом лайве снова сразу отправить
time.sleep(5)
continue
# сюда попадаем только если live
text = build_pretty_status_message()
if text and text != last_text:
logger.info(text)
last_text = text
first_live_sent = True
# после первого лайва ждём 5 минут, а до него — 10 секунд
wait_sec = INTERVAL if first_live_sent else 10
for _ in range(wait_sec):
if stop_event.is_set():
break
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),
150, # редкий опрос
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) Немедленно подгружаем ДАННЫЕ ПРОШЛОГО МАТЧА (один раз, оффлайн-поток 'game'),
чтобы программа имела данные до старта.
2) Ровно за 1:15 до начала — СБРАСЫВАЕМ эти данные (останавливаем оффлайн, чистим latest_data).
3) Ровно за 1:10 до начала — ВКЛЮЧАЕМ LIVE-треды.
"""
if not game_dt:
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
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()
if team_norm not in (t1):
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)
# print(prev_game_id, prev_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()
# если матч уже в другом конечном состоянии — выходим
if STATUS in ("live", "finished", "finished_wait", "finished_today"):
break
# Шаг 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() # на всякий
# 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"
globals()[
"CLEAR_OUTPUT_FOR_VMIX"
] = False # можно оставить пустоту до первых живых данных
stop_offline_threads() # на всякий случай
start_live_threads(SEASON, GAME_ID)
did_live = True
break
time.sleep(15)
t = threading.Thread(target=_runner, daemon=True)
t.start()
def get_excel():
return nasio.load_formatted(
user=SYNO_USERNAME,
password=SYNO_PASSWORD,
nas_ip=SYNO_URL,
nas_port="443",
path=SYNO_PATH_EXCEL,
# sheet="TEAMS LEGEND",
)
def excel_worker():
"""
Раз в минуту читает ВСЕ вкладки Excel
и сохраняет их в latest_data с префиксом excel_League: {LEAGUE}
Team: {TEAM}
Game ID: {cached_game_id}{note}
Game Status: {gs_text}
| Name | Status | Timestamp | Link |
|---|---|---|---|
| {s["name"]} | {status_text} | {s["ts"]} | {s["link"]} |
| # | Игрок | PTS | REB | AST | STL | BLK | MIN | FG | 2PT | 3PT | FT | TO | Foul | +/- | KPI |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {num} | {name} | {pts} | {reb} | {ast} | {stl} | {blk} | {time_played} | {fg} | {pt2} | {pt3} | {pt1} | {to} | {foul} | {plus_minus} | {kpi} |
| Период | {team1_name} | {team2_name} |
|---|---|---|
| {{q}} | {{s1}} | {{s2}} |
| # | PLAYER | PTS | FOULS |
|---|
| {team1_name} | {team2_name} | |
|---|---|---|
| {q} | {s1} | {s2} |
| # | PLAYER | PTS | FOULS |
|---|