коммит
This commit is contained in:
362
get_data.py
362
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"🏀 <b>{LEAGUE.upper()}</b> • {TEAM}")
|
||||
lines.append(f"📌 Game ID: <code>{GAME_ID}</code>")
|
||||
lines.append(f"📌 Game ID: <code>{cgid or GAME_ID}</code>")
|
||||
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' <span style="color:#ffb84d;">(предзагружены данные прошлой игры)</span>'
|
||||
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):
|
||||
<div class="header-info">
|
||||
<p><b>League:</b> {LEAGUE}</p>
|
||||
<p><b>Team:</b> {TEAM}</p>
|
||||
<p><b>Game ID:</b> {GAME_ID}</p>
|
||||
<p><b>Game ID:</b> {cached_game_id}{note}</p>
|
||||
<p><b>Game Status:</b> <span class="{gs_class}">{gs_text}</span></p>
|
||||
</div>
|
||||
|
||||
@@ -1851,15 +2131,15 @@ 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("isOnCourt") is True
|
||||
),
|
||||
key=lambda x: int(x.get("num") or 0),
|
||||
)
|
||||
return started_team
|
||||
# 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
|
||||
|
||||
|
||||
def add_new_team_stat(
|
||||
@@ -2353,6 +2633,8 @@ async def info():
|
||||
data = latest_data["game"]["data"]["result"]
|
||||
team1_name = data["team1"]["name"]
|
||||
team2_name = data["team2"]["name"]
|
||||
team1_name_short = data["team1"]["abcName"]
|
||||
team2_name_short = data["team2"]["abcName"]
|
||||
team1_logo = data["team1"]["logo"]
|
||||
team2_logo = data["team2"]["logo"]
|
||||
arena = data["arena"]["name"]
|
||||
@@ -2375,6 +2657,8 @@ async def info():
|
||||
{
|
||||
"team1": team1_name,
|
||||
"team2": team2_name,
|
||||
"team1_short": team1_name_short,
|
||||
"team2_short": team2_name_short,
|
||||
"logo1": team1_logo,
|
||||
"logo2": team2_logo,
|
||||
"arena": arena,
|
||||
|
||||
Reference in New Issue
Block a user