diff --git a/__pycache__/get_data.cpython-312.pyc b/__pycache__/get_data.cpython-312.pyc index 90e2755..fc2870e 100644 Binary files a/__pycache__/get_data.cpython-312.pyc and b/__pycache__/get_data.cpython-312.pyc differ diff --git a/get_data.py b/get_data.py index b20029c..e28276a 100644 --- a/get_data.py +++ b/get_data.py @@ -1,7 +1,9 @@ from fastapi import FastAPI +from fastapi.responses import Response, HTMLResponse +from fastapi import HTTPException +from fastapi import Request from contextlib import asynccontextmanager import requests -from datetime import datetime import threading import time import queue @@ -9,6 +11,10 @@ import argparse import uvicorn from pprint import pprint import os +import pandas as pd +import json +from datetime import datetime, time as dtime, timedelta +from fastapi.responses import Response # передадим параметры через аргументы или глобальные переменные @@ -25,6 +31,12 @@ TEAM = args.team LANG = args.lang HOST = "https://ref.russiabasket.org" STATUS = False +GAME_ID = None +SEASON = None +GAME_START_DT = None # datetime начала матча (локальная из календаря) +GAME_TODAY = False # флаг: игра сегодня +GAME_SOON = False # флаг: игра сегодня и скоро (<1 часа) + URLS = { "seasons": "{host}/api/abc/comps/seasons?Tag={league}", "actual-standings": "{host}/api/abc/comps/actual-standings?tag={league}&season={season}&lang={lang}", @@ -81,43 +93,116 @@ def results_consumer(): msg = results_q.get(timeout=0.5) except queue.Empty: continue - if "play-by-play" in msg["source"]: - latest_data["game"]["data"]["result"]["plays"] = msg["data"]["result"] - elif "box-score" in msg["source"]: - if "game" in latest_data: - for team in latest_data["game"]["data"]["result"]["teams"]: - if team["teamNumber"] != 0: - box_team = [ - t - for t in msg["data"]["result"]["teams"] - if t["teamNumber"] == team["teamNumber"] - ] - if not box_team: - print("EROORRRRR") - next # log - box_team = box_team[0] - for player in team["starts"]: - box_player = [ - p - for p in box_team["starts"] - if p["startNum"] == player["startNum"] + + try: + source = msg.get("source") + payload = msg.get("data") or {} + + # универсальный статус (может не быть) + incoming_status = payload.get("status") # может быть None + + # 1) play-by-play + if "play-by-play" in source: + game = latest_data.get("game") + # если игра уже нормальная — приклеиваем плейи + if ( + game + and isinstance(game, dict) + and "data" in game + and "result" in game["data"] + ): + # у pbp тоже может не быть data/result + if "result" in payload: + game["data"]["result"]["plays"] = payload["result"] + + # а вот статус у play-by-play иногда просто "no-status" + latest_data[source] = { + "ts": msg["ts"], + "data": incoming_status if incoming_status is not None else payload, + } + + # 2) box-score + elif "box-score" in source: + game = latest_data.get("game") + if ( + game + and "data" in game + and "result" in game["data"] + and "teams" in game["data"]["result"] + and "result" in payload + and "teams" in payload["result"] + ): + # обновляем команды + for team in game["data"]["result"]["teams"]: + if team["teamNumber"] != 0: + box_team = [ + t + for t in payload["result"]["teams"] + if t["teamNumber"] == team["teamNumber"] ] - if box_player: - player["stats"] = box_player[0] + if not box_team: + print("ERROR: box-score team not found") + continue + box_team = box_team[0] - team["total"] = box_team["total"] - team["startTotal"] = box_team["startTotal"] - team["benchTotal"] = box_team["benchTotal"] - team["maxLeading"] = box_team["maxLeading"] - team["pointsInRow"] = box_team["pointsInRow"] - team["maxPointsInRow"] = box_team["maxPointsInRow"] + for player in team["starts"]: + box_player = [ + p + for p in box_team["starts"] + if p["startNum"] == player["startNum"] + ] + if box_player: + player["stats"] = box_player[0] - else: - latest_data[msg["source"]] = { - "ts": msg["ts"], - "data": msg["data"], - } + team["total"] = box_team["total"] + team["startTotal"] = box_team["startTotal"] + team["benchTotal"] = box_team["benchTotal"] + team["maxLeading"] = box_team["maxLeading"] + team["pointsInRow"] = box_team["pointsInRow"] + team["maxPointsInRow"] = box_team["maxPointsInRow"] + # в любом случае сохраняем сам факт, что box-score пришёл + latest_data[source] = { + "ts": msg["ts"], + "data": incoming_status if incoming_status is not None else payload, + } + + else: + if source == "game": + has_game_already = "game" in latest_data + + # есть ли в ответе ПОЛНАЯ структура + is_full = ( + "data" in payload + and isinstance(payload["data"], dict) + and "result" in payload["data"] + ) + + if is_full: + # полный game — всегда кладём + latest_data["game"] = { + "ts": msg["ts"], + "data": payload, + } + else: + # game неполный + if not has_game_already: + # 👉 раньше game вообще не было — лучше положить хоть что-то + latest_data["game"] = { + "ts": msg["ts"], + "data": payload, + } + else: + # 👉 уже есть какой-то game — неполным НЕ затираем + print("results_consumer: got partial game, keeping previous one") + + # и обязательно continue/return из этого elif/if + continue + + # ... остальная обработка ... + except Exception as e: + print("results_consumer error:", repr(e)) + continue def get_items(data: dict) -> list: """ @@ -132,56 +217,113 @@ def get_items(data: dict) -> list: return None -def get_game_id(data): +from datetime import datetime + +def pick_game_for_team(calendar_json): """ - получаем GAME_ID для домашней команды. Если матча сегодня нет, то берем последний. + Возвращает: + game_id: str | None + game_dt: datetime | None + is_today: bool + cal_status: str | None # Scheduled / Online / Result / ResultConfirmed + + Логика: + 1. если в календаре есть игра КОМАНДЫ на сегодня — берём ЕЁ и возвращаем её gameStatus + 2. иначе — берём последнюю прошедшую и тоже возвращаем её gameStatus """ - items = get_items(data) - for game in items[::-1]: - if game["team1"]["name"].lower() == TEAM.lower(): - game_date = datetime.strptime(game["game"]["localDate"], "%d.%m.%Y").date() - if game_date == datetime.now().date(): - game_id = game["game"]["id"] - print(f"Получили актуальный id: {game_id} для {TEAM}") - return game_id, True - elif game_date < datetime.now().date(): - game_id = game["game"]["id"] - print(f"Получили старый id: {game_id} для {TEAM}") - return game_id, False + items = get_items(calendar_json) + if not items: + return None, None, False, None + + today = datetime.now().date() + + # 1) сначала — сегодняшняя + for game in reversed(items): + if game["team1"]["name"].lower() != TEAM.lower(): + continue + + gdt = extract_game_datetime(game) + gdate = gdt.date() if gdt else datetime.strptime(game["game"]["localDate"], "%d.%m.%Y").date() + + if gdate == today: + cal_status = game["game"].get("gameStatus") + return game["game"]["id"], gdt, True, cal_status + + # 2) если на сегодня нет — берём последнюю прошедшую + last_id = None + last_dt = None + last_status = None + + for game in reversed(items): + if game["team1"]["name"].lower() != TEAM.lower(): + continue + + gdt = extract_game_datetime(game) + gdate = gdt.date() if gdt else datetime.strptime(game["game"]["localDate"], "%d.%m.%Y").date() + + if gdate <= today: + last_id = game["game"]["id"] + last_dt = gdt + last_status = game["game"].get("gameStatus") + break + + return last_id, last_dt, False, last_status + + + +def extract_game_datetime(game_item: dict) -> datetime | None: + """ + Из элемента календаря достаём datetime матча. + В календаре есть localDate и часто localTime. Если localTime нет — берём 00:00. + """ + try: + date_str = game_item["game"]["localDate"] # '31.10.2025' + dt_date = datetime.strptime(date_str, "%d.%m.%Y").date() + time_str = game_item["game"].get("localTime") # '19:30' + if time_str: + hh, mm = map(int, time_str.split(":")) + dt_time = dtime(hour=hh, minute=mm) else: - print(f"Не смогли найти игру для команды {TEAM}") # DEBUG + dt_time = dtime(hour=0, minute=0) + return datetime.combine(dt_date, dt_time) + except Exception: + return None @asynccontextmanager async def lifespan(app: FastAPI): - global STATUS - # -------- startup -------- - # 1. определим сезон (как у тебя) + global STATUS, GAME_ID, SEASON, GAME_START_DT, GAME_TODAY, GAME_SOON + + # 1. проверяем API: seasons try: - season = requests.get(URLS["seasons"].format(host=HOST, league=LEAGUE)).json()[ - "items" - ][0]["season"] + seasons_resp = requests.get(URLS["seasons"].format(host=HOST, league=LEAGUE)).json() + season = seasons_resp["items"][0]["season"] except Exception: - # WARNING now = datetime.now() if now.month > 9: season = now.year + 1 else: season = now.year - print("не удалось получить последний сезон.") # WARNING + print("не удалось получить последний сезон.") + SEASON = season + # 2. берём календарь try: calendar = requests.get( URLS["calendar"].format(host=HOST, league=LEAGUE, season=season, lang=LANG) ).json() except Exception as ex: - print(f"не получилось проверить работу API. код ошибки: {ex}") # ERROR - exit(1) + print(f"не получилось проверить работу API. код ошибки: {ex}") + # тут можно вообще не запускать сервер, но оставим как есть + calendar = None - game_id, STATUS = get_game_id(calendar) - - # 2. поднимем потоки + # 3. определяем игру + game_id, game_dt, is_today, cal_status = pick_game_for_team(calendar) if calendar else (None, None, False, None) + GAME_ID = game_id + GAME_START_DT = game_dt + GAME_TODAY = is_today + # 4. запускаем "длинные" потоки (они у тебя и так всегда) threads_long = [ threading.Thread( target=get_data_from_API, @@ -224,13 +366,17 @@ async def lifespan(app: FastAPI): daemon=True, ), ] + for t in threads_long: + t.start() + + # 5. Подготовим онлайн и офлайн наборы (как у тебя) threads_live = [ threading.Thread( target=get_data_from_API, args=( "game", URLS["game"].format(host=HOST, game_id=game_id, lang=LANG), - 0.0016667, + 5, stop_event, ), daemon=True, @@ -272,35 +418,76 @@ async def lifespan(app: FastAPI): args=( "game", URLS["game"].format(host=HOST, game_id=game_id, lang=LANG), - 1, + 1, # реже stop_event, ), daemon=True, ) ] - for t in threads_long: - t.start() - if STATUS: - for t in threads_live: - t.start() - else: + # 6. Решение: сегодня / не сегодня + if not is_today: + # ИГРЫ СЕГОДНЯ НЕТ → крутим только офлайн + STATUS = "no_game_today" for t in threads_offline: t.start() + else: + # игра сегодня + if cal_status is None: + # нет статуса в календаре — считаем, что ещё не началась + STATUS = "today_not_started" + for t in threads_offline: + t.start() + elif cal_status == "Scheduled": + # ещё не началась + # проверим, не меньше ли часа до начала + if game_dt: + delta = game_dt - datetime.now() + if delta <= timedelta(hours=1): + # уже скоро → можно запускать онлайн + STATUS = "live_soon" + GAME_SOON = True + for t in threads_live: + t.start() + else: + # ещё далеко → офлайн, но говорим что сегодня + STATUS = "today_not_started" + for t in threads_offline: + t.start() + # и можно повесить будильник, как раньше + else: + # нет времени — просто офлайн, но сегодня + STATUS = "today_not_started" + for t in threads_offline: + t.start() + elif cal_status == "Online": + # матч идёт → сразу онлайн + STATUS = "live" + GAME_SOON = False + for t in threads_live: + t.start() + elif cal_status in ["Result", "ResultConfirmed"]: + # матч уже сыгран, но дата всё ещё сегодня + STATUS = "finished_today" + for t in threads_offline: + t.start() + else: + # неизвестный статус — безопасный вариант + STATUS = "today_not_started" + for t in threads_offline: + t.start() - # отдаём управление FastAPI yield - + # -------- shutdown -------- stop_event.set() for t in threads_long: t.join(timeout=1) - if STATUS: - for t in threads_live: - t.join(timeout=1) - else: - for t in threads_offline: - t.join(timeout=1) + # офлайн/онлайн ты можешь не делить тут, но оставлю + for t in threads_offline: + t.join(timeout=1) + for t in threads_live: + t.join(timeout=1) app = FastAPI(lifespan=lifespan) @@ -324,11 +511,17 @@ def format_time(seconds: float | int) -> str: @app.get("/team1.json") async def team1(): + game = get_latest_game_safe() + if not game: + raise HTTPException(status_code=503, detail="game data not ready") return await team("team1") @app.get("/team2.json") async def team2(): + game = get_latest_game_safe() + if not game: + raise HTTPException(status_code=503, detail="game data not ready") return await team("team2") @@ -362,22 +555,140 @@ async def game(): @app.get("/status.json") -async def status(): - return [ - { - "name": item, - "status": latest_data[item]["data"]["status"], - "ts": latest_data[item]["ts"], - } - for item in latest_data - ] +async def status(request: Request): + data = { + "league": LEAGUE, + "team": TEAM, + "game_id": GAME_ID, + "game_status": STATUS, # <= сюда приходит твой индикатор состояния + "statuses": [ + { + "name": item, + "status": ( + latest_data[item]["data"]["status"] + if isinstance(latest_data[item]["data"], dict) and "status" in latest_data[item]["data"] + else latest_data[item]["data"] + ), + "ts": latest_data[item]["ts"], + "link": URLS[item].format( + host=HOST, + league=LEAGUE, + season=SEASON, + lang=LANG, + game_id=GAME_ID, + ), + } + for item in latest_data + ], + } + accept = request.headers.get("accept", "") + if "text/html" in accept: + status_raw = str(STATUS).lower() + if status_raw in ["live"]: + gs_class = "live" + gs_text = "🟢 LIVE" + elif status_raw in ["live_soon"]: + gs_class = "live" + gs_text = "🟢 GAME TODAY (soon)" + elif status_raw == "today_not_started": + gs_class = "upcoming" + gs_text = "🟡 Game today, not started" + elif status_raw in ["finished_today", "finished"]: + gs_class = "finished" + gs_text = "🔴 Game finished" + elif status_raw == "no_game_today": + gs_class = "unknown" + gs_text = "⚪ No game today" + else: + gs_class = "unknown" + gs_text = "⚪ UNKNOWN" + + html = f""" + +
+ + + + +League: {LEAGUE}
+Team: {TEAM}
+Game ID: {GAME_ID}
+Game Status: {gs_text}
+| Name | Status | Timestamp | Link |
|---|---|---|---|
| {s["name"]} | +{status_text} | +{s["ts"]} | +{s["link"]} | +