diff --git a/get_data.py b/get_data.py index 1e661e2..44f3a8c 100644 --- a/get_data.py +++ b/get_data.py @@ -5001,6 +5001,1274 @@ async def game_history(): ) return history + +# Глобальное состояние выбранной сортировки +CURRENT_SORT = { + "sort_by": None, + "direction": "desc", +} + + +def get_nested_value(data, path: str, default=0): + if not path: + return default + + keys = path.split(".") + current = data + + for key in keys: + if isinstance(current, dict): + if key not in current: + return default + current = current[key] + elif isinstance(current, list): + try: + idx = int(key) + if idx < 0 or idx >= len(current): + return default + current = current[idx] + except: + return default + else: + return default + + return current if current not in (None, "", []) else default + + +def collect_paths(prefix, obj, out: set): + """ + Рекурсивно собирает все пути ключей: + lastName + season.points + carrier.points + season.0.points + """ + if isinstance(obj, dict): + for k, v in obj.items(): + new_prefix = f"{prefix}.{k}" if prefix else k + out.add(new_prefix) + collect_paths(new_prefix, v, out) + elif isinstance(obj, list): + for i, v in enumerate(obj): + new_prefix = f"{prefix}.{i}" if prefix else str(i) + out.add(new_prefix) + collect_paths(new_prefix, v, out) + + +def extract_all_sort_paths(items: list[dict]) -> list[str]: + """ + Собирает уникальные пути для сортировки из всех элементов milestones, + исключая нежелательные ключи. + """ + paths: set[str] = set() + for item in items: + collect_paths("", item, paths) + + # ключи, которые нужно исключить + EXCLUDE = { + "fastBreak", + "isStart", + "foulB", + "foulC", + "foulD", + "foulT", + "isDoubleDouble", + "isTripleDouble", + "pass", + "second", + "blockedOwnShot", + "playedTime", + } + + filtered = [] + for p in paths: + last_key = p.split(".")[-1] + if last_key not in EXCLUDE: + filtered.append(p) + + return sorted(filtered) + + +def _build_milestones() -> list[dict]: + """ + Собирает milestones по всем игрокам обеих команд из pregame-full-stats. + Возвращает список словарей с полями: + lastName, firstName, team, season, career + где season / career — это dict со статистикой (обычно Sum). + """ + players_stats_league = (latest_data.get("league_stats") or {}).get("data", {}).get( + "items" + ) or [] + if not players_stats_league: + return [] + players_stats_season = (latest_data.get("season_stats") or {}).get("data", {}).get( + "items" + ) or [] + if not players_stats_season: + return [] + game = (latest_data.get("game") or {}).get("data", {}).get("result", {}).get("teams", {}) + payload = {t["teamNumber"]: t for t in game if t.get("teamNumber") in (1, 2)} + + milestones: list[dict] = [] + excel_wrap = latest_data.get("excel_TEAMS_LEGEND").get("data") or [] + + for player in players_stats_league: + # все записи по этому игроку (обычно одна) + season_items = [ + item + for item in players_stats_season + if item.get("personId") == player.get("personId") + ] + team = "" + display_number = 0 + team_id = None + + if season_items: + season_item = season_items[0] + base_season_stats = season_item.get("stats") or {} + season_games = season_item.get("games", 0) + + season_stats = dict(base_season_stats) + season_stats["games"] = season_games + + team_id = season_item.get("team", {}).get("id") + display_number = season_item.get("displayNumber", 0) + else: + season_stats = {} + + # 🔍 Ищем название команды в excel_wrap + team_name = "" + if team_id: + for row in excel_wrap: + if int(row.get("Id")) == int(team_id): + team_name = row.get("Team", "") + break + + # карьера из league_stats + base_career_stats = player.get("stats") or {} + career_games = player.get("games", 0) + career_stats = dict(base_career_stats) + career_stats["games"] = career_games + + season_sum = season_stats # dict + career_sum = career_stats # dict + season_avg = compute_avg(season_sum) + career_avg = compute_avg(career_sum) + + milestones.append( + { + "lastName": (player.get("person") or {}).get("lastName", ""), + "firstName": (player.get("person") or {}).get("firstName", ""), + "team": ( + team_name if team_name != "" else player.get("team").get("name") + ), + "displayNumber": display_number, + "season": season_stats, + "career": career_stats, + "season_avg": season_avg, + "career_avg": career_avg, + } + ) + return milestones + + +def format_float(value): + """ + Возвращает строку float всегда с дробной частью: + 12 -> "12.0" + 12.0 -> "12.0" + 12.5 -> "12.5" + None -> "0.0" + """ + # пустые значения -> 0.0 + if value in (None, "", [], {}): + return "0.0" + + # если уже float или int + if isinstance(value, (int, float)): + val = float(value) + # всегда форматируем с дробной частью + return f"{val:.1f}" + + # если строка + if isinstance(value, str): + try: + val = float(value.replace(",", ".")) + return f"{val:.1f}" + except: + return "0.0" + + # fallback + try: + val = float(value) + return f"{val:.1f}" + except: + return "0.0" + + +def compress_milestone_for_vmix(m: dict) -> dict: + """ + Лёгкая версия игрока для vMix: + только имя, фамилия, команда, выбранная статистика, её значение и игры. + """ + data = latest_data["game"]["data"]["result"] + season_api = ( + f'{str(data["league"]["season"]-1)}-{str(data["league"]["season"])[2:]}' + ) + + return { + "firstName": m.get("firstName", ""), + "lastName": m.get("lastName", ""), + "displayNumber": m.get("displayNumber", ""), + "photo": m.get("photo", ""), + "team": m.get("team", ""), + "logo_xls": m.get("logo_xls", ""), + "statName1": ( + f"Regular season {season_api}" + if "season" in m.get("statName", "").split(".")[0] + else "VTB History" + ), + "statName2": ( + f"{ { + 'points':'point', + 'games':'game', + 'goal1':'free throw', + 'shot1':'free throw attempt', + 'goal2':'2-point', + 'shot2':'2-point attempt', + 'goal3':'3-point', + 'shot3':'3-point attempt', + 'goal23':'field goal', + 'shot23':'field goal attempt', + }.get(m.get('statName','').split('.')[-1], m.get('statName','').split('.')[-1]) }s" + if "shot" not in m.get("statName", "").split(".")[1] + else m.get("statName", "").split(".")[1] + ), + "statValue": m.get("statValue", ""), + "games": m.get("games", ""), + } + + +@app.get("/milestones") +async def milestones( + sort_by: str | None = None, + direction: str | None = None, + vmix: bool = False, # 👈 добавили флаг +): + global CURRENT_SORT + + if sort_by is not None: + CURRENT_SORT["sort_by"] = sort_by + + if direction is not None: + CURRENT_SORT["direction"] = direction + + sort_by = CURRENT_SORT.get("sort_by") + direction = CURRENT_SORT.get("direction") or "desc" + milestones = _build_milestones() + if not milestones: + return [{"message": "Данных об истории команд нет!"}] + + if sort_by: + reverse = direction != "asc" + + def sort_key(x): + raw = get_nested_value(x, sort_by, 0) + return normalize_number(raw) + + milestones = sorted(milestones, key=sort_key, reverse=reverse) + + for m in milestones: + row_team = get_excel_row_for_team(m["team"]) + team_logo_exl = row_team.get("TeamLogo", "") + m["statName"] = sort_by + + # значение для сортировки + raw_val = sort_key(m) + + # 🔹 Если сортировка по AVG — оставляем float с дробной частью + if sort_by.startswith("season_avg.") or sort_by.startswith("career_avg."): + m["statValue"] = format_float(raw_val) + else: + # 🔹 Не AVG — всегда целые числа, если это возможно + val = normalize_number(raw_val) + if isinstance(val, float) and val.is_integer(): + val = int(val) + m["statValue"] = val + + # 🆕 GAMES: + games_val = 0 + + if sort_by.startswith("season_avg."): + # сначала пробуем games из season_avg, если там нет — из season (Sum) + games_raw = get_nested_value(m, "season_avg.games", None) + if games_raw is None: + games_raw = get_nested_value(m, "season.games", 0) + games_val = normalize_number(games_raw) + + elif sort_by.startswith("season."): + # обычный сезонный Sum + games_raw = get_nested_value(m, "season.games", 0) + games_val = normalize_number(games_raw) + + elif sort_by.startswith("career_avg."): + # сначала career_avg.games, если пусто — career.games + games_raw = get_nested_value(m, "career_avg.games", None) + if games_raw is None: + games_raw = get_nested_value(m, "career.games", 0) + games_val = normalize_number(games_raw) + + elif sort_by.startswith("carrier.") or sort_by.startswith("career."): + # старый путь carrier/career (Sum) + games_raw = get_nested_value( + m, "carrier.games", None + ) or get_nested_value(m, "career.games", 0) + games_val = normalize_number(games_raw) + + # на всякий случай тоже дожмём до int, если вдруг float + if isinstance(games_val, float) and games_val.is_integer(): + games_val = int(games_val) + + m["games"] = games_val + m["logo_xls"] = team_logo_exl + m["photo"] = os.path.join( + "D:\\Photos", + "vtb", + m["team"], + f"{m.get('displayNumber')}.png", + ) + + else: + for m in milestones: + m["statName"] = None + m["statValue"] = None + m["games"] = None + m["logo_xls"] = None + m["photo"] = r"D:\Графика\БАСКЕТБОЛ\VTB League\ASSETS\EMPTY.png" + if vmix: + return [compress_milestone_for_vmix(m) for m in milestones[:20]] + + return milestones + + +def compute_avg(stats: dict) -> dict: + """ + Принимает суммарную статистику игрока вида: + { "points": 120, "rebounds": 50, "games": 6, ... } + + Возвращает словарь со средними значениями: + { "points": 20.0, "rebounds": 8.3, ... } + + Формат всегда float с .0 + """ + if not isinstance(stats, dict): + return {} + + games = stats.get("games", 0) + if not isinstance(games, (int, float)) or games == 0: + return {} + + avg = {} + for key, value in stats.items(): + if key == "games": + continue + if not isinstance(value, (int, float)): + continue + + avg_val = value / games + + # всегда форматируем как float с .0 + if isinstance(avg_val, float) and avg_val.is_integer(): + avg_val = float(f"{int(avg_val)}.0") + else: + avg_val = float(f"{avg_val:.1f}") + + avg[key] = avg_val + + return avg + + +def normalize_number(value): + """ + Превращает значение в корректное число: + - '12' -> 12 + - '12.0' -> 12 + - '12.5' -> 12.5 + - None/'' -> 0 + """ + if value in (None, "", [], {}): + return 0 + + # если уже int + if isinstance(value, int): + return value + + # если float + if isinstance(value, float): + # если выглядит как целое — сделать int + return int(value) if value.is_integer() else value + + # если строка + if isinstance(value, str): + try: + v = float(value.replace(",", ".")) + return int(v) if v.is_integer() else v + except: + return 0 + + # если что-то другое — неиспользуемый тип + return 0 + + +@app.get("/milestones/ui", response_class=HTMLResponse) +async def milestones_ui(): + milestones = _build_milestones() + keys = extract_all_sort_paths(milestones) + keys_js = json.dumps(keys, ensure_ascii=False) + + html = """ + + + + + Milestones + + + +
+
+
+
+
+ Milestones игроков + live +
+
+ Слева — список игроков, справа — подробная статистика по 4 блокам (Season / Season Avg / Career / Career Avg).
+ Сортировку меняем выбором блока и показателя. +
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ Выбери блок + показатель, чтобы отсортировать +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + +
+ Игрок + + + Команда + + + Статистика + + Значение + + Games + + Сезон (очки) + + + Карьера (очки) + + + Сырые данные +
+
+ + +
+ + +
+
+
+ + + + + """ + + html = html.replace("PLACEHOLDER_KEYS", keys_js) + return HTMLResponse(content=html) + + if __name__ == "__main__": uvicorn.run( "get_data:app", host="0.0.0.0", port=8000, reload=True, log_level="debug"