diff --git a/.gitignore b/.gitignore index 6175d7a..150cfac 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /logs/* *.venv *.env -/shotmaps/* \ No newline at end of file +/shotmaps/* +get_data copy.py \ No newline at end of file diff --git a/get_data.py b/get_data.py index 9eefa49..1e661e2 100644 --- a/get_data.py +++ b/get_data.py @@ -1731,7 +1731,8 @@ async def status(request: Request): else latest_data[item]["data"] ), } - for item in sorted_keys if item not in ["league_stats", "season_stats"] + for item in sorted_keys + if item not in ["league_stats", "season_stats"] ], } @@ -5000,1260 +5001,6 @@ 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 [] - - 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'{m.get("statName", "").split(".")[1].replace("points", "point")}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"