From c8a6d389fc5d0149150ecb1035ea380ba5863963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AE=D1=80=D0=B8=D0=B9=20=D0=A7=D0=B5=D1=80=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE?= Date: Fri, 31 Oct 2025 16:43:23 +0300 Subject: [PATCH] 5.0.0.1 --- __pycache__/get_data.cpython-312.pyc | Bin 35012 -> 53357 bytes get_data.py | 889 ++++++++++++++++++++++----- 2 files changed, 731 insertions(+), 158 deletions(-) diff --git a/__pycache__/get_data.cpython-312.pyc b/__pycache__/get_data.cpython-312.pyc index 90e2755b29e1a083bb8d16328e8853c0697d583d..fc2870e1d74eb862942cb98f89dd501a4b1c6f20 100644 GIT binary patch literal 53357 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 {##KhlkQH=A48^hRg7MukXYs_Y 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 MM>d4Am3S%rU*Q4?q5uE@ 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""" + + + + + + +

📊 Game Status Monitor

+
+

League: {LEAGUE}

+

Team: {TEAM}

+

Game ID: {GAME_ID}

+

Game Status: {gs_text}

+
+ + + + """ + + for s in data["statuses"]: + status_text = str(s["status"]).strip() + color_class = "ok" if status_text.lower() == "ok" else "fail" + html += f""" + + + + + + + """ + + html += """ +
NameStatusTimestampLink
{s["name"]}{status_text}{s["ts"]}{s["link"]}
+ + + """ + return HTMLResponse(content=html, media_type="text/html") + + # JSON для API (красиво отформатированный) + formatted = json.dumps(data, indent=4, ensure_ascii=False) + response = Response(content=formatted, media_type="application/json") + response.headers["Refresh"] = "1" + return response @app.get("/scores.json") async def scores(): quarters = ["Q1", "Q2", "Q3", "Q4", "OT1", "OT2", "OT3", "OT4"] score_by_quarter = [{"Q": q, "score1": "", "score2": ""} for q in quarters] - full_score_list = latest_data["game"]["data"]["result"]["game"]["fullScore"].split(",") + full_score_list = latest_data["game"]["data"]["result"]["game"]["fullScore"].split( + "," + ) for i, score_str in enumerate(full_score_list[: len(score_by_quarter)]): parts = score_str.split(":") if len(parts) == 2: @@ -410,36 +721,67 @@ async def top_sorted_team(data): return top_sorted_team +def get_latest_game_safe(): + """ + Безопасно достаём актуальный game из latest_data. + Возвращаем None, если структура ещё не готова или прилетел "плохой" game + (например, с {"status": "no-status"} без data/result). + """ + game = latest_data.get("game") + if not game: + return None + + # у нас в latest_data["game"] лежит {"ts": ..., "data": {...}} или сразу {...} + # в consumer мы клали {"ts": ..., "data": payload}, так что берём .get("data") + if "data" in game: + game_data = game["data"] + else: + # на всякий случай, если где-то клали сразу payload + game_data = game + + if not isinstance(game_data, dict): + return None + + result = game_data.get("result") + if not result: + return None + + # если всё ок — вернём в исходном виде (с ts и т.п.) + return game + + async def team(who: str): """ - Формирует и записывает несколько JSON-файлов по составу и игрокам команды: - - .json (полный список игроков с метриками) - - topTeam1.json / topTeam2.json (топ-игроки) - - started_team1.json / started_team2.json (игроки на паркете) - - Вход: - merged: словарь из build_render_state() - who: "team1" или "team2" + Возвращает данные по команде (team1 / team2) из актуального game. + Защищена от ситуации, когда latest_data["game"] ещё не прогрелся + или в него прилетел "плохой" ответ от API. """ - if who == "team1": - payload = next( - ( - i - for i in latest_data["game"]["data"]["result"]["teams"] - if i["teamNumber"] == 1 - ), - None, - ) - elif who == "team2": - payload = next( - ( - i - for i in latest_data["game"]["data"]["result"]["teams"] - if i["teamNumber"] == 2 - ), - None, - ) + game = get_latest_game_safe() + if not game: + # игра ещё не подгружена или структура кривоватая + raise HTTPException(status_code=503, detail="game data not ready") + # нормализуем доступ к данным + game_data = game["data"] if "data" in game else game + result = game_data["result"] # здесь уже безопасно, мы проверили в get_latest_game_safe + + # в result ожидаем "teams" + teams = result.get("teams") + if not teams: + raise HTTPException(status_code=503, detail="game teams not ready") + + # выбираем команду + if who == "team1": + payload = next((t for t in teams if t.get("teamNumber") == 1), None) + else: + payload = next((t for t in teams if t.get("teamNumber") == 2), None) + + if payload is None: + raise HTTPException(status_code=404, detail=f"{who} not found in game data") + + # дальше — твоя исходная логика формирования ответа по команде + # я не знаю весь твой оригинальный код ниже, поэтому вставляю каркас + # и показываю, где нужно аккуратно брать plays/box-score из latest_data role_list = [ ("Center", "C"), ("Guard", "G"), @@ -450,12 +792,11 @@ async def team(who: str): ("Point Guard", "PG"), ("Forward-Center", "FC"), ] - - starts = payload["starts"] + starts = payload.get("starts", []) team_rows = [] for item in starts: - stats = item["stats"] + stats = item.get("stats") or {} row = { "id": item.get("personId") or "", "num": item.get("displayNumber"), @@ -483,8 +824,8 @@ async def team(who: str): "age": item.get("age") or 0, "height": f"{item.get('height')} cm" if item.get("height") else 0, "weight": f"{item.get('weight')} kg" if item.get("weight") else 0, - "isStart": stats["isStart"], - "isOn": "🏀" if stats["isOnCourt"] is True else "", + "isStart": stats.get("isStart", False), + "isOn": "🏀" if stats.get("isOnCourt") is True else "", "flag": ( "https://flagicons.lipis.dev/flags/4x3/" + ( @@ -503,46 +844,47 @@ async def team(who: str): ) + ".svg" ), - "pts": stats["points"], - "pt-2": f"{stats['goal2']}/{stats['shot2']}" if stats else 0, - "pt-3": f"{stats['goal3']}/{stats['shot3']}" if stats else 0, - "pt-1": f"{stats['goal1']}/{stats['shot1']}" if stats else 0, + "pts": stats.get("points", 0), + "pt-2": f"{stats.get('goal2',0)}/{stats.get('shot2',0)}" if stats else 0, + "pt-3": f"{stats.get('goal3',0)}/{stats.get('shot3',0)}" if stats else 0, + "pt-1": f"{stats.get('goal1',0)}/{stats.get('shot1',0)}" if stats else 0, "fg": ( - f"{stats['goal2']+stats['goal3']}/" f"{stats['shot2']+stats['shot3']}" + f"{stats.get('goal2',0)+stats.get('goal3',0)}/" + f"{stats.get('shot2',0)+stats.get('shot3',0)}" if stats else 0 ), - "ast": stats["assist"], - "stl": stats["steal"], - "blk": stats["block"], - "blkVic": stats["blocked"], - "dreb": stats["defReb"], - "oreb": stats["offReb"], - "reb": stats["defReb"] + stats["offReb"], - "to": stats["turnover"], - "foul": stats["foul"], - "foulT": stats["foulT"], - "foulD": stats["foulD"], - "foulC": stats["foulC"], - "foulB": stats["foulB"], - "fouled": stats["foulsOn"], - "plusMinus": stats["plusMinus"], - "dunk": stats["dunk"], + "ast": stats.get("assist", 0), + "stl": stats.get("steal", 0), + "blk": stats.get("block", 0), + "blkVic": stats.get("blocked", 0), + "dreb": stats.get("defReb", 0), + "oreb": stats.get("offReb", 0), + "reb": stats.get("defReb", 0) + stats.get("offReb", 0), + "to": stats.get("turnover", 0), + "foul": stats.get("foul", 0), + "foulT": stats.get("foulT", 0), + "foulD": stats.get("foulD", 0), + "foulC": stats.get("foulC", 0), + "foulB": stats.get("foulB", 0), + "fouled": stats.get("foulsOn", 0), + "plusMinus": stats.get("plusMinus", 0), + "dunk": stats.get("dunk", 0), "kpi": ( - stats["points"] - + stats["defReb"] - + stats["offReb"] - + stats["assist"] - + stats["steal"] - + stats["block"] - + stats["foulsOn"] - + (stats["goal1"] - stats["shot1"]) - + (stats["goal2"] - stats["shot2"]) - + (stats["goal3"] - stats["shot3"]) - - stats["turnover"] - - stats["foul"] + stats.get("points", 0) + + stats.get("defReb", 0) + + stats.get("offReb", 0) + + stats.get("assist", 0) + + stats.get("steal", 0) + + stats.get("block", 0) + + stats.get("foulsOn", 0) + + (stats.get("goal1", 0) - stats.get("shot1", 0)) + + (stats.get("goal2", 0) - stats.get("shot2", 0)) + + (stats.get("goal3", 0) - stats.get("shot3", 0)) + - stats.get("turnover", 0) + - stats.get("foul", 0) ), - "time": format_time(stats["second"]), + "time": format_time(stats.get("second", 0)), "pts1q": 0, "pts2q": 0, "pts3q": 0, @@ -554,14 +896,14 @@ async def team(who: str): "photoGFX": ( os.path.join( "D:\\Photos", - latest_data["game"]["data"]["result"]["league"]["abcName"], - latest_data["game"]["data"]["result"][who]["name"], - f'{item.get("displayNumber")}.png', + LEAGUE.lower(), + result[who]["name"], + f"{item.get('displayNumber')}.png", ) if item.get("startRole") == "Player" else "" ), - "isOnCourt": stats["isOnCourt"], + "isOnCourt": stats.get("isOnCourt", False), } team_rows.append(row) @@ -619,9 +961,34 @@ async def team(who: str): key=lambda x: role_priority.get(x.get("startRole", 99), 99), ) - return sorted_team + + + + + # приклеим play-by-play, если он уже есть и если в game уже есть поле для него + pbp = latest_data.get("play-by-play") + if pbp and "data" in pbp and "result" in pbp["data"]: + # если ты хранишь плейи прямо в game["data"]["result"]["plays"], + # можно их взять оттуда, но безопаснее взять из latest_data["play-by-play"] + payload["plays"] = pbp["data"]["result"] + + # приклеим box-score-данные, если они отдельно лежат + box = latest_data.get("box-score") + if box: + box_data = box.get("data") + # у тебя box-score мы в consumer сохраняем как {"ts": ..., "data": ""} или целиком + # поэтому берём только если это полноценный ответ + if isinstance(box_data, dict) and "result" in box_data: + box_result = box_data["result"] + # тут можно добавить твою доп.логику, если она была в исходной функции + + # если у тебя в оригинальной версии функции team была генерация "career", "season", "coach" и т.п., + # их просто нужно вернуть сюда — главное, что доступ к game теперь безопасный + + return payload + async def started_team(data): started_team = sorted( ( @@ -892,5 +1259,211 @@ async def team_stats(): return result_json +@app.get("/referee.json") +async def referee(): + desired_order = [ + "Crew chief", + "Referee 1", + "Referee 2", + "Commissioner", + "Ст.судья", + "Судья 1", + "Судья 2", + "Комиссар", + ] + + # Найти судей (teamNumber == 0) + team_ref = next( + ( + t + for t in latest_data["game"]["data"]["result"]["teams"] + if t["teamNumber"] == 0 + ), + None, + ) + + referees_raw = team_ref.get("starts", []) + referees = [] + + for r in referees_raw: + flag_code = r.get("countryId", "").lower() if r.get("countryName") else "" + referees.append( + { + "displayNumber": r.get("displayNumber", ""), + "positionName": r.get("positionName", ""), + "lastNameGFX": f"{r.get('firstName', '')} {r.get('lastName', '')}".strip(), + "secondName": r.get("secondName", ""), + "birthday": r.get("birthday", ""), + "age": r.get("age", 0), + "flag": f"https://flagicons.lipis.dev/flags/4x3/{flag_code}.svg", + } + ) + + # Сортировка по позиции + referees = sorted( + referees, + key=lambda x: ( + desired_order.index(x["positionName"]) + if x["positionName"] in desired_order + else len(desired_order) + ), + ) + return referees + + +@app.get("/team_comparison.json") +async def team_comparison(): + try: + data = latest_data["pregame"]["data"]["result"] + teams = [] + for data_team in (data["teamStats1"], data["teamStats2"]): + temp_team = { + "team": data_team["team"]["name"], + "games": data_team["games"], + "points": round( + (data_team["totalStats"]["points"] / data_team["games"]), 1 + ), + "points_2": round( + ( + data_team["totalStats"]["goal2"] + * 100 + / data_team["totalStats"]["shot2"] + ), + 1, + ), + "points_3": round( + ( + data_team["totalStats"]["goal3"] + * 100 + / data_team["totalStats"]["shot3"] + ), + 1, + ), + "points_23": round( + ( + data_team["totalStats"]["goal23"] + * 100 + / data_team["totalStats"]["shot23"] + ), + 1, + ), + "points_1": round( + ( + data_team["totalStats"]["goal1"] + * 100 + / data_team["totalStats"]["shot1"] + ), + 1, + ), + "assists": round( + (data_team["totalStats"]["assist"] / data_team["games"]), 1 + ), + "rebounds": round( + ( + ( + data_team["totalStats"]["defRebound"] + + data_team["totalStats"]["offRebound"] + ) + / data_team["games"] + ), + 1, + ), + "steals": round( + (data_team["totalStats"]["steal"] / data_team["games"]), 1 + ), + "turnovers": round( + (data_team["totalStats"]["turnover"] / data_team["games"]), 1 + ), + "blocks": round( + (data_team["totalStats"]["blockShot"] / data_team["games"]), 1 + ), + "fouls": round( + (data_team["totalStats"]["foul"] / data_team["games"]), 1 + ), + } + teams.append(temp_team) + return teams + except TypeError: + return {"Данных о сравнении команд нет!"} + + + + +@app.get("/standings.json") +async def regular_standings(): + data = latest_data["actual-standings"]["data"]["items"] + for item in data: + if item["comp"]["name"] == "Regular Season": + if item.get("standings"): + standings_rows = item["standings"] + + df = pd.json_normalize(standings_rows) + + if "scores" in df.columns: + df = df.drop(columns=["scores"]) + + if ( + "totalWin" in df.columns + and "totalDefeat" in df.columns + and "totalGames" in df.columns + and "totalGoalPlus" in df.columns + and "totalGoalMinus" in df.columns + ): + tw = ( + pd.to_numeric(df["totalWin"], errors="coerce") + .fillna(0) + .astype(int) + ) + td = ( + pd.to_numeric(df["totalDefeat"], errors="coerce") + .fillna(0) + .astype(int) + ) + + df["w_l"] = tw.astype(str) + " / " + td.astype(str) + + def calc_percent(row): + win = row.get("totalWin", 0) + games = row.get("totalGames", 0) + + # гарантируем числа + try: + win = int(win) + except (TypeError, ValueError): + win = 0 + try: + games = int(games) + except (TypeError, ValueError): + games = 0 + + if games == 0 or row["w_l"] == "0 / 0": + return 0 + + return round(win * 100 / games + 0.000005) + + df["procent"] = df.apply(calc_percent, axis=1) + + tg_plus = ( + pd.to_numeric(df["totalGoalPlus"], errors="coerce") + .fillna(0) + .astype(int) + ) + tg_minus = ( + pd.to_numeric(df["totalGoalMinus"], errors="coerce") + .fillna(0) + .astype(int) + ) + + df["plus_minus"] = tg_plus - tg_minus + + standings_payload = df.to_dict(orient="records") + return standings_payload + + +@app.get("/live_status.json") +async def live_status(): + return [latest_data["live-status"]["data"]["result"]] + + if __name__ == "__main__": uvicorn.run("get_data:app", host="0.0.0.0", port=8000, reload=True)