diff --git a/PlayTypeID.json b/PlayTypeID.json new file mode 100644 index 0000000..3d2d516 --- /dev/null +++ b/PlayTypeID.json @@ -0,0 +1,302 @@ +[ + { + "PlayTypeID": 0, + "PlayInfoSite": "-", + "PlayInfo": "Пустое событие" + }, + { + "PlayTypeID": 1, + "PlayInfoSite": "1 очко", + "PlayInfo": "Штрафной бросок - попадание" + }, + { + "PlayTypeID": 2, + "PlayInfoSite": "2 очка", + "PlayInfo": "2х очковый бросок - попадание" + }, + { + "PlayTypeID": 3, + "PlayInfoSite": "3 очка", + "PlayInfo": "3х очковый бросок - попадание" + }, + { + "PlayTypeID": 4, + "PlayInfoSite": "Мимо 1 очко", + "PlayInfo": "Штрафной бросок - промах" + }, + { + "PlayTypeID": 5, + "PlayInfoSite": "Мимо 2 очка", + "PlayInfo": "2х очковый бросок - промах" + }, + { + "PlayTypeID": 6, + "PlayInfoSite": "Мимо 3 очка", + "PlayInfo": "3х очковый бросок - промах" + }, + { + "PlayTypeID": 7, + "PlayInfoSite": "Замена", + "PlayInfo": "Замена игроков" + }, + { + "PlayTypeID": 8, + "PlayInfoSite": "Выход", + "PlayInfo": "Выход на площадку" + }, + { + "PlayTypeID": 9, + "PlayInfoSite": "Уход", + "PlayInfo": "Ушел с площадки" + }, + { + "PlayTypeID": 10, + "PlayInfoSite": "Потеря", + "PlayInfo": "Потеря" + }, + { + "PlayTypeID": 11, + "PlayInfoSite": "Пас-потеря", + "PlayInfo": "Потеря мяча при передаче" + }, + { + "PlayTypeID": 12, + "PlayInfoSite": "Пробежка", + "PlayInfo": "Пробежка" + }, + { + "PlayTypeID": 13, + "PlayInfoSite": "Заступ в аут", + "PlayInfo": "Заступ в аут" + }, + { + "PlayTypeID": 14, + "PlayInfoSite": "Зона", + "PlayInfo": "Зона" + }, + { + "PlayTypeID": 15, + "PlayInfoSite": "Двойное ведение", + "PlayInfo": "Двойное ведение" + }, + { + "PlayTypeID": 16, + "PlayInfoSite": "3 секунды", + "PlayInfo": "3 секунды" + }, + { + "PlayTypeID": 17, + "PlayInfoSite": "5 секунд", + "PlayInfo": "5 секунд" + }, + { + "PlayTypeID": 18, + "PlayInfoSite": "8 секунд", + "PlayInfo": "8 секунд" + }, + { + "PlayTypeID": 19, + "PlayInfoSite": "24 секунды", + "PlayInfo": "24 секунды" + }, + { + "PlayTypeID": 20, + "PlayInfoSite": "Потеря мяча", + "PlayInfo": "Потеря мяча" + }, + { + "PlayTypeID": 21, + "PlayInfoSite": "Начало четверти", + "PlayInfo": "Начало четверти?" + }, + { + "PlayTypeID": 22, + "PlayInfoSite": "", + "PlayInfo": "Окончание четверти?" + }, + { + "PlayTypeID": 23, + "PlayInfoSite": "Тайм-аут", + "PlayInfo": "Тайм-аут" + }, + { + "PlayTypeID": 24, + "PlayInfoSite": "", + "PlayInfo": "неизвестно" + }, + { + "PlayTypeID": 25, + "PlayInfoSite": "Пас", + "PlayInfo": "Передача" + }, + { + "PlayTypeID": 26, + "PlayInfoSite": "Перехват", + "PlayInfo": "Перехват" + }, + { + "PlayTypeID": 27, + "PlayInfoSite": "Блокшот", + "PlayInfo": "Блокшот" + }, + { + "PlayTypeID": 28, + "PlayInfoSite": "Подбор", + "PlayInfo": "Подбор" + }, + { + "PlayTypeID": 29, + "PlayInfoSite": "Быстрый отрыв", + "PlayInfo": "Быстрый отрыв" + }, + { + "PlayTypeID": 30, + "PlayInfoSite": "Бросок сверху", + "PlayInfo": "Тип броска" + }, + { + "PlayTypeID": 31, + "PlayInfoSite": "В прыжке", + "PlayInfo": "Тип броска" + }, + { + "PlayTypeID": 32, + "PlayInfoSite": "В проходе", + "PlayInfo": "Тип броска" + }, + { + "PlayTypeID": 33, + "PlayInfoSite": "Добивание", + "PlayInfo": "Тип броска" + }, + { + "PlayTypeID": 34, + "PlayInfoSite": "Навес", + "PlayInfo": "Тип броска" + }, + { + "PlayTypeID": 35, + "PlayInfoSite": "Бросок крюком", + "PlayInfo": "Тип броска" + }, + { + "PlayTypeID": 40, + "PlayInfoSite": "Фол P", + "PlayInfo": "Фол персональный" + }, + { + "PlayTypeID": 41, + "PlayInfoSite": "Фол U", + "PlayInfo": "Фол неспортивный" + }, + { + "PlayTypeID": 42, + "PlayInfoSite": "Фол T", + "PlayInfo": "Фол технический" + }, + { + "PlayTypeID": 43, + "PlayInfoSite": "Фол D", + "PlayInfo": "Фол дисквалифицирующий" + }, + { + "PlayTypeID": 44, + "PlayInfoSite": "Фол C", + "PlayInfo": "Фол технический главному тренеру за его личное неспортивное поведение" + }, + { + "PlayTypeID": 45, + "PlayInfoSite": "Фол В", + "PlayInfo": "Фол технический главному тренеру по любой другой причине" + }, + { + "PlayTypeID": 47, + "PlayInfoSite": "В нападении", + "PlayInfo": "Тип фола" + }, + { + "PlayTypeID": 50, + "PlayInfoSite": "Вбрасывание", + "PlayInfo": "Тип фола" + }, + { + "PlayTypeID": 51, + "PlayInfoSite": "1 бросок", + "PlayInfo": "Назначение 1го штрафного броска" + }, + { + "PlayTypeID": 52, + "PlayInfoSite": "2 броска", + "PlayInfo": "Назначение 2х штрафных бросков" + }, + { + "PlayTypeID": 53, + "PlayInfoSite": "3 броска", + "PlayInfo": "Назначение 3х штрафных бросков" + }, + { + "PlayTypeID": 54, + "PlayInfoSite": "Компенсация", + "PlayInfo": "Обоюдное пробивание штрафных" + }, + { + "PlayTypeID": 59, + "PlayInfoSite": "", + "PlayInfo": "пробел для сайта" + }, + { + "PlayTypeID": 60, + "PlayInfoSite": "Разминка команд", + "PlayInfo": "Разминка команд" + }, + { + "PlayTypeID": 61, + "PlayInfoSite": "Представление команд", + "PlayInfo": "Представление команд" + }, + { + "PlayTypeID": 62, + "PlayInfoSite": "Менее 3 минут до начала игры", + "PlayInfo": "Менее 3 минут до начала игры" + }, + { + "PlayTypeID": 63, + "PlayInfoSite": "Игра сейчас начнется", + "PlayInfo": "Игра сейчас начнется" + }, + { + "PlayTypeID": 69, + "PlayInfoSite": "Видеопросмотр", + "PlayInfo": "Видеопросмотр?" + }, + { + "PlayTypeID": 70, + "PlayInfoSite": "", + "PlayInfo": "Возможно остановка игры?" + }, + { + "PlayTypeID": 71, + "PlayInfoSite": "", + "PlayInfo": "Возможно возобновление игры?" + }, + { + "PlayTypeID": 72, + "PlayInfoSite": "Видеопросмотр", + "PlayInfo": "Видеопросмотр?" + }, + { + "PlayTypeID": 73, + "PlayInfoSite": "Старт видеотрансляции", + "PlayInfo": "Старт видеотрансляции" + }, + { + "PlayTypeID": 74, + "PlayInfoSite": "Конец видеотрансляции", + "PlayInfo": "Конец видеотрансляции" + }, + { + "PlayTypeID": 100, + "PlayInfoSite": "Матч завершен", + "PlayInfo": "Матч завершен" + } +] diff --git a/get_data_new.py b/get_data_new.py index 5048558..ed9fbab 100644 --- a/get_data_new.py +++ b/get_data_new.py @@ -10,6 +10,7 @@ from datetime import datetime, timedelta, timezone from zoneinfo import ZoneInfo from typing import Any, Dict, List, Tuple, Optional import pandas as pd +import numpy as np import requests from requests.adapters import HTTPAdapter @@ -549,7 +550,7 @@ def poll_game_live( if game_finished: break - time.sleep(0.2) + time.sleep(1) # вторая точка выхода по stop_event после sleep if stop_event.is_set(): @@ -652,6 +653,8 @@ def render_loop(stop_event: threading.Event, out_name: str = "game") -> None: Json_Team_Generation(state, who="team1") Json_Team_Generation(state, who="team2") Scores_Quarter(state) + Referee(state) + Play_By_Play(state) # live_status отдельно, + общий state в .json atomic_write_json([state["result"]["live_status"]], "live_status") @@ -761,7 +764,7 @@ def Referee(merged: dict, *, out_dir: str = "static") -> None: else len(desired_order) ), ) - out_path = "referee.json" + out_path = "referee" atomic_write_json(referees, out_path) logging.info("Сохранил payload: {out_path}") @@ -769,6 +772,172 @@ def Referee(merged: dict, *, out_dir: str = "static") -> None: logger.error(f"Ошибка в Referee потоке: {e}", exc_info=True) +def Play_By_Play(data: dict) -> None: + """ + Поток, обновляющий JSON-файл с последовательностью бросков в матче. + """ + logger.info("START making json for play-by-play") + + try: + game_data = data["result"] if "result" in data else data + + if not game_data: + logger.debug("game_online_data отсутствует") + return + + teams = game_data["teams"] + team1_data = next((i for i in teams if i.get("teamNumber") == 1), None) + team2_data = next((i for i in teams if i.get("teamNumber") == 2), None) + + if not team1_data or not team2_data: + logger.warning("Не удалось получить команды из game_online_data") + return + team1_name = game_data["team1"]["name"] + team2_name = game_data["team2"]["name"] + team1_startnum = [ + p["startNum"] + for p in team1_data.get("starts", []) + if p.get("startRole") == "Player" + ] + team2_startnum = [ + p["startNum"] + for p in team2_data.get("starts", []) + if p.get("startRole") == "Player" + ] + + plays = game_data.get("plays", []) + if not plays: + logger.debug("нет данных в play-by-play") + return + + # Получение текущего времени игры + json_live_status = data["result"]["live_status"] if "result" in data and "live_status" in data["result"] else None + last_event = plays[-1] + + # if not json_live_status or json_live_status.get("message") == "Not Found": + if json_live_status is None: + period = last_event.get("period", 1) + second = 0 + else: + period = (json_live_status or {}).get("result", {}).get("period", 1) + second = (json_live_status or {}).get("result", {}).get("second", 0) + + # Создание DataFrame из событий + df = pd.DataFrame(plays[::-1]) + + # Преобразование для лиги 3x3 + # if "3x3" in LEAGUE: + # df["play"].replace({2: 1, 3: 2}, inplace=True) + + df_goals = df[df["play"].isin([1, 2, 3])].copy() + if df_goals.empty: + logger.debug("нет данных о голах в play-by-play") + return + + # Расчёты по очкам и времени + df_goals["score1"] = ( + df_goals["startNum"].isin(team1_startnum) * df_goals["play"] + ) + df_goals["score2"] = ( + df_goals["startNum"].isin(team2_startnum) * df_goals["play"] + ) + + df_goals["score_sum1"] = df_goals["score1"].fillna(0).cumsum() + df_goals["score_sum2"] = df_goals["score2"].fillna(0).cumsum() + + df_goals["new_sec"] = ( + pd.to_numeric(df_goals["sec"], errors="coerce").fillna(0).astype(int) + // 10 + ) + df_goals["time_now"] = (600 if period < 5 else 300) - second + df_goals["quar"] = period - df_goals["period"] + + df_goals["diff_time"] = np.where( + df_goals["quar"] == 0, + df_goals["time_now"] - df_goals["new_sec"], + (600 * df_goals["quar"] - df_goals["new_sec"]) + df_goals["time_now"], + ) + + df_goals["diff_time_str"] = df_goals["diff_time"].apply( + lambda x: ( + f"{x // 60}:{str(x % 60).zfill(2)}" if isinstance(x, int) else x + ) + ) + + # Текстовые поля + def generate_text(row, with_time=False, is_rus=False): + s1, s2 = int(row["score_sum1"]), int(row["score_sum2"]) + team = ( + team1_name + if not pd.isna(row["score1"]) and row["score1"] != 0 + else team2_name + ) + + # ✅ Правильный порядок счёта в зависимости от команды + if team == team1_name: + score = f"{s1}-{s2}" + else: + score = f"{s2}-{s1}" + + time_str = ( + f" за {row['diff_time_str']}" + if is_rus + else f" in last {row['diff_time_str']}" + ) + prefix = "рывок" if is_rus else "run" + + return f"{team} {score} {prefix}{time_str if with_time else ''}" + + df_goals["text_rus"] = df_goals.apply( + lambda r: generate_text(r, is_rus=True, with_time=False), axis=1 + ) + df_goals["text_time_rus"] = df_goals.apply( + lambda r: generate_text(r, is_rus=True, with_time=True), axis=1 + ) + df_goals["text"] = df_goals.apply( + lambda r: generate_text(r, is_rus=False, with_time=False), axis=1 + ) + df_goals["text_time"] = df_goals.apply( + lambda r: generate_text(r, is_rus=False, with_time=True), axis=1 + ) + + df_goals["team"] = df_goals["score1"].apply( + lambda x: team1_name if not pd.isna(x) and x != 0 else team2_name + ) + + # Удаление лишнего + drop_cols = [ + "children", + "start", + "stop", + "hl", + "sort", + "startNum", + "zone", + "x", + "y", + ] + df_goals.drop(columns=drop_cols, inplace=True, errors="ignore") + + # Порядок колонок + main_cols = ["text", "text_time"] + all_cols = main_cols + [ + col for col in df_goals.columns if col not in main_cols + ] + df_goals = df_goals[all_cols] + + # Сохранение JSON + directory = "static" + os.makedirs(directory, exist_ok=True) + filepath = os.path.join(directory, "play_by_play.json") + + df_goals.to_json(filepath, orient="records", force_ascii=False, indent=4) + logger.debug("Успешно положил данные об play-by-play в файл") + except Exception as e: + logger.error(f"Ошибка в Play_By_Play: {e}", exc_info=True) + + + def render_once_after_game( session: requests.Session, league: str, @@ -808,6 +977,7 @@ def render_once_after_game( Json_Team_Generation(state, who="team2") Scores_Quarter(state) Referee(state) + Play_By_Play(state) # === 4. live_status и общий state === atomic_write_json(state["result"], out_name) @@ -817,6 +987,7 @@ def render_once_after_game( except Exception as ex: logger.exception(f"[RENDER_ONCE] error while building final state: {ex}") + # ============================================================================ # 7. Постобработка статистики для вывода # ============================================================================ @@ -1446,7 +1617,9 @@ def Standing_func( # когда мы последний раз успешно обновили standings last_call_ts = 0 - json_seasons = fetch_api_data(session, "seasons", host=HOST, league=league, lang=lang) + json_seasons = fetch_api_data( + session, "seasons", host=HOST, league=league, lang=lang + ) season = json_seasons[0]["season"] # как часто вообще можно дёргать standings @@ -1549,7 +1722,7 @@ def Standing_func( filename = f"standings_{league}_{comp_name}" atomic_write_json(standings_payload, filename, out_dir) logger.info( - f"[STANDINGS_THREAD] saved {filename}.json ({len(standings_payload)} rows)" + f"[STANDINGS_THREAD] сохранил {filename}.json" ) # 2) плейофф-пары (playoffPairs) @@ -1757,8 +1930,8 @@ def main(): standings_thread.join() logger.info("[MAIN] standings thread stopped, shutdown complete") - # идём на новую итерацию while True - # (новая сессия / новый stop_event создаются в начале цикла) + # идём на новую итерацию while True + # (новая сессия / новый stop_event создаются в начале цикла) # ============================================================================ diff --git a/visual.py b/visual.py index 8755e1f..509550c 100644 --- a/visual.py +++ b/visual.py @@ -615,9 +615,7 @@ if isinstance(cached_game_online, dict): col5_2.metric("Fouls", foulsB) -if isinstance(cached_game_online, dict) and ( - cached_game_online.get("plays") or [] -): +if isinstance(cached_game_online, dict) and (cached_game_online.get("plays") or []): col_1_col = [f"col_1_{i}" for i in range(1, period_max + 1)] col_2_col = [f"col_2_{i}" for i in range(1, period_max + 1)] count_q = 0 @@ -1868,8 +1866,7 @@ try: except (FileNotFoundError, json.JSONDecodeError): play_type_id = [] -teams_section = ((cached_game_online or {}).get("result") or {}).get("teams") or {} - +teams_section = cached_game_online.get("teams") or {} # Если teams_section — список (например, [{"starts": [...]}, {...}]) if isinstance(teams_section, list): if len(teams_section) >= 2: @@ -1882,6 +1879,7 @@ if isinstance(teams_section, list): else: starts1 = [] starts2 = [] + # Если teams_section — словарь (обычно {"1": {...}, "2": {...}}) elif isinstance(teams_section, dict): starts1 = (teams_section.get(1) or teams_section.get("1") or {}).get("starts") or [] @@ -1927,8 +1925,43 @@ def get_event_time(row): return None +teams_section = cached_game_online.get("teams") or {} + +# Если teams_section — список (например, [{"starts": [...]}, {...}]) +if isinstance(teams_section, list): + if len(teams_section) >= 2: + starts1 = next( + (t.get("starts") for t in teams_section if t["teamNumber"] == 1), None + ) + starts2 = next( + (t.get("starts") for t in teams_section if t["teamNumber"] == 2), None + ) + else: + starts1 = [] + starts2 = [] +# Если teams_section — словарь (обычно {"1": {...}, "2": {...}}) +elif isinstance(teams_section, dict): + starts1 = (teams_section.get(1) or teams_section.get("1") or {}).get("starts") or [] + starts2 = (teams_section.get(2) or teams_section.get("2") or {}).get("starts") or [] +else: + starts1 = [] + starts2 = [] + +teams_temp = sorted( + [x for x in starts1 if isinstance(x, dict)], key=lambda x: x.get("playerNumber", 0) +) + sorted( + [x for x in starts2 if isinstance(x, dict)], key=lambda x: x.get("playerNumber", 0) +) + +list_fullname = [None] + [ + f"({x.get('displayNumber')}) {x.get('firstName','')} {x.get('lastName','')}".strip() + for x in teams_temp + if x.get("startRole") == "Player" +] + + with tab_pbp: - plays = ((cached_game_online or {}).get("result") or {}).get("plays") or [] + # plays = ((cached_game_online or {}).get("result") or {}).get("plays") or [] if plays: temp_data_pbp = pd.DataFrame(plays) col1_pbp, col2_pbp = tab_pbp.columns((3, 4))