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""" + + + + + + +

📊 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)