diff --git a/get_data.py b/get_data.py index a55850a..b6c7d9e 100644 --- a/get_data.py +++ b/get_data.py @@ -62,6 +62,7 @@ ALLOWED_LEAGUES = { "whl", # Высшая лига. Женщины "wcup", # Кубок России. Женщины "dubl-b", # Дюбл до 19 лет + # "pr-mezhreg-w13", # Межрегиональные соревнования до 14 лет # добавляй свои… } @@ -452,7 +453,7 @@ def format_time(seconds: float | int) -> str: return "0:00" -def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | None = None): +def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | None = None) -> None: """ Единая точка: принимает уже нормализованный merged, делает нужные вычисления (если надо) и сохраняет в JSON. @@ -1065,7 +1066,380 @@ def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | No atomic_write_json(out_path, started_team) logging.info("Сохранил payload: {out_path}") - +def time_outs_func(data_pbp: list[dict]) -> tuple[str, int, str, int]: + """ + Вычисляет количество оставшихся таймаутов для обеих команд + и формирует строку состояния. + + Args: + data_pbp: Список игровых событий (play-by-play). + + Returns: + Кортеж: (строка команды 1, остаток, строка команды 2, остаток) + """ + timeout1 = [] + timeout2 = [] + + for event in data_pbp: + if event.get("play") == 23: + if event.get("startNum") == 1: + timeout1.append(event) + elif event.get("startNum") == 2: + timeout2.append(event) + + def timeout_status(timeout_list: list[dict], last_event: dict) -> tuple[str, int]: + period = last_event.get("period", 0) + sec = last_event.get("sec", 0) + + if period < 3: + timeout_max = 2 + count = sum(1 for t in timeout_list if t.get("period", 0) <= period) + quarter = "1st half" + elif period < 5: + count = sum(1 for t in timeout_list if 3 <= t.get("period", 0) <= period) + quarter = "2nd half" + if period == 4 and sec >= 4800 and count in (0, 1): + timeout_max = 2 + else: + timeout_max = 3 + else: + timeout_max = 1 + count = sum(1 for t in timeout_list if t.get("period", 0) == period) + quarter = f"OverTime {period - 4}" + + left = max(0, timeout_max - count) + word = "Time-outs" if left != 1 else "Time-out" + text = f"{left if left != 0 else 'No'} {word} left in {quarter}" + return text, left + + if not data_pbp: + return "", 0, "", 0 + + last_event = data_pbp[-1] + t1_str, t1_left = timeout_status(timeout1, last_event) + t2_str, t2_left = timeout_status(timeout2, last_event) + + return t1_str, t1_left, t2_str, t2_left + +def add_data_for_teams(new_data: list[dict]) -> tuple[float, list, float]: + """ + Возвращает усреднённые статистики команды: + - средний возраст + - очки со старта и скамейки + их доли + - средний рост + + Args: + new_data (list[dict]): Список игроков с полями "startRole", "stats", "age", "height" + + Returns: + tuple: (avg_age: float, points: list, avg_height: float) + """ + players = [item for item in new_data if item.get("startRole") == "Player"] + + points_start = 0 + points_bench = 0 + total_age = 0 + total_height = 0 + player_count = len(players) + + for player in players: + stats = player.get("stats") + if stats: + is_start = stats.get("isStart") + + # Очки + if is_start is True: + points_start += stats.get("points", 0) + elif is_start is False: + points_bench += stats.get("points", 0) + + # Возраст и рост + total_age += player.get("age", 0) or 0 + total_height += player.get("height", 0) or 0 + + total_points = points_start + points_bench + points_start_pro = ( + f"{round(points_start * 100 / total_points)}%" if total_points else "0%" + ) + points_bench_pro = ( + f"{round(points_bench * 100 / total_points)}%" if total_points else "0%" + ) + + avg_age = round(total_age / player_count, 1) if player_count else 0 + avg_height = round(total_height / player_count, 1) if player_count else 0 + + points = [points_start, points_start_pro, points_bench, points_bench_pro] + return avg_age, points, avg_height + +def add_new_team_stat( + data: dict, + avg_age: float, + points: float, + avg_height: float, + timeout_str: str, + timeout_left: str, +) -> dict: + """ + Добавляет в словарь команды форматированную статистику. + Все значения приводятся к строкам. + + Args: + data: Исходная статистика команды. + avg_age: Средний возраст команды (строка). + points: Кортеж из 4 строк: ptsStart, ptsStart_pro, ptsBench, ptsBench_pro. + avg_height: Средний рост (в см). + timeout_str: Строка отображения таймаутов. + timeout_left: Остаток таймаутов. + + Returns: + Обновлённый словарь `data` с новыми ключами. + """ + + def safe_int(v): # Локальная защита от ValueError/TypeError + try: + return int(v) + except (ValueError, TypeError): + return 0 + + def format_percent(goal, shot): + goal, shot = safe_int(goal), safe_int(shot) + return f"{round(goal * 100 / shot)}%" if shot else "0%" + + goal1, shot1 = safe_int(data.get("goal1")), safe_int(data.get("shot1")) + goal2, shot2 = safe_int(data.get("goal2")), safe_int(data.get("shot2")) + goal3, shot3 = safe_int(data.get("goal3")), safe_int(data.get("shot3")) + + def_reb = safe_int(data.get("defReb")) + off_reb = safe_int(data.get("offReb")) + + data.update( + { + "pt-1": f"{goal1}/{shot1}", + "pt-2": f"{goal2}/{shot2}", + "pt-3": f"{goal3}/{shot3}", + "fg": f"{goal2 + goal3}/{shot2 + shot3}", + "pt-1_pro": format_percent(goal1, shot1), + "pt-2_pro": format_percent(goal2, shot2), + "pt-3_pro": format_percent(goal3, shot3), + "fg_pro": format_percent(goal2 + goal3, shot2 + shot3), + "Reb": str(def_reb + off_reb), + "avgAge": str(avg_age), + "ptsStart": str(points[0]), + "ptsStart_pro": str(points[1]), + "ptsBench": str(points[2]), + "ptsBench_pro": str(points[3]), + "avgHeight": f"{avg_height} cm", + "timeout_left": str(timeout_left), + "timeout_str": str(timeout_str), + } + ) + + # Приводим все значения к строкам, если нужно строго для сериализации + for k in data: + data[k] = str(data[k]) + + return data + +stat_name_list = [ + ("points", "Очки", "points"), + ("pt-1", "Штрафные", "free throws"), + ("pt-1_pro", "штрафные, процент", "free throws pro"), + ("pt-2", "2-очковые", "2-points"), + ("pt-2_pro", "2-очковые, процент", "2-points pro"), + ("pt-3", "3-очковые", "3-points"), + ("pt-3_pro", "3-очковые, процент", "3-points pro"), + ("fg", "очки с игры", "field goals"), + ("fg_pro", "Очки с игры, процент", "field goals pro"), + ("assist", "Передачи", "assists"), + ("pass", "", ""), + ("defReb", "подборы в защите", ""), + ("offReb", "подборы в нападении", ""), + ("Reb", "Подборы", "rebounds"), + ("steal", "Перехваты", "steals"), + ("block", "Блокшоты", "blocks"), + ("blocked", "", ""), + ("turnover", "Потери", "turnovers"), + ("foul", "Фолы", "fouls"), + ("foulsOn", "", ""), + ("foulT", "", ""), + ("foulD", "", ""), + ("foulC", "", ""), + ("foulB", "", ""), + ("second", "секунды", "seconds"), + ("dunk", "данки", "dunks"), + ("fastBreak", "", "fast breaks"), + ("plusMinus", "+/-", "+/-"), + ("avgAge", "", "avg Age"), + ("ptsBench", "", "Bench PTS"), + ("ptsBench_pro", "", "Bench PTS, %"), + ("ptsStart", "", "Start PTS"), + ("ptsStart_pro", "", "Start PTS, %"), + ("avgHeight", "", "avg height"), + ("timeout_left", "", "timeout left"), + ("timeout_str", "", "timeout str"), +] + + +def Team_Both_Stat(merged: dict, *, out_dir: str = "static") -> None: + """ + Обновляет файл team_stats.json, содержащий сравнение двух команд. + + Аргументы: + stop_event (threading.Event): Событие для остановки цикла. + """ + logger.info("START making json for team statistics") + + try: + teams = merged["result"]["teams"] + plays = merged["result"].get("plays", []) + + # Разделение команд + team_1 = next((t for t in teams if t["teamNumber"] == 1), None) + team_2 = next((t for t in teams if t["teamNumber"] == 2), None) + + if not team_1 or not team_2: + logger.warning("Не найдены обе команды в данных") + # time.sleep() + + # Таймауты + timeout_str1, timeout_left1, timeout_str2, timeout_left2 = time_outs_func( + plays + ) + + # Возраст, очки, рост + avg_age_1, points_1, avg_height_1 = add_data_for_teams( + team_1.get("starts", []) + ) + avg_age_2, points_2, avg_height_2 = add_data_for_teams( + team_2.get("starts", []) + ) + + if not team_1.get("total") or not team_2.get("total"): + logger.debug( + "Нет total у команд — пропускаю перезапись team_stats.json" + ) + + # Форматирование общей статистики (как и было) + total_1 = add_new_team_stat( + team_1["total"], + avg_age_1, + points_1, + avg_height_1, + timeout_str1, + timeout_left1, + ) + total_2 = add_new_team_stat( + team_2["total"], + avg_age_2, + points_2, + avg_height_2, + timeout_str2, + timeout_left2, + ) + + + # Финальный JSON + result_json = [] + for key in total_1: + val1 = ( + int(total_1[key]) + if isinstance(total_1[key], float) + else total_1[key] + ) + val2 = ( + int(total_2[key]) + if isinstance(total_2[key], float) + else total_2[key] + ) + stat_rus, stat_eng = "", "" + for s in stat_name_list: + if s[0] == key: + stat_rus, stat_eng = s[1], s[2] + break + + result_json.append( + { + "name": key, + "nameGFX_rus": stat_rus, + "nameGFX_eng": stat_eng, + "val1": val1, + "val2": val2, + } + ) + + out_path = Path(out_dir) / "team_stats.json" + atomic_write_json(out_path, result_json) + logging.info("Сохранил payload: {out_path}") + + logger.debug("Успешно записаны данные в team_stats.json") + except Exception as e: + logger.error( + f"Ошибка при обработке командной статистики: {e}", exc_info=True + ) + + +def Referee(merged: dict, *, out_dir: str = "static") -> None: + """ + Поток, создающий JSON-файл с информацией о судьях матча. + """ + logger.info("START making json for referee") + + desired_order = [ + "Crew chief", + "Referee 1", + "Referee 2", + "Commissioner", + "Ст.судья", + "Судья 1", + "Судья 2", + "Комиссар", + ] + + try: + # Найти судей (teamNumber == 0) + team_ref = next( + (t for t in merged["result"]["teams"] if t["teamNumber"] == 0), None + ) + if not team_ref: + logger.warning("Не найдена судейская бригада в данных.") + + referees_raw = team_ref.get("starts", []) + # print(referees_raw) + 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) + ), + ) + out_path = Path(out_dir) / "referee.json" + atomic_write_json(out_path, referees) + logging.info("Сохранил payload: {out_path}") + + except Exception as e: + logger.error(f"Ошибка в Referee потоке: {e}", exc_info=True) + + # ========================== # ---- ДОМЕННАЯ ЛОГИКА @@ -1164,6 +1538,8 @@ class PostProcessor: try: Json_Team_Generation(merged, out_dir="static", who="team1") Json_Team_Generation(merged, out_dir="static", who="team2") + Team_Both_Stat(merged, out_dir="static") + Referee(merged, out_dir="static") except Exception as e: logging.exception(f"Postproc failed: {e}") @@ -1333,7 +1709,6 @@ def daily_rollover_loop( while not stop_event.is_set(): now = datetime.now(APP_TZ) wakeup_at = next_midnight_local(now) - print(type(wakeup_at)) seconds = (wakeup_at - now).total_seconds() logger.info( # f"Ежедневка: проснусь {datetime.fromisoformat(wakeup_at.isoformat())} (через {int(seconds)} сек)." @@ -1467,6 +1842,8 @@ def main(): ) Json_Team_Generation(merged, out_dir="static", who="team1") Json_Team_Generation(merged, out_dir="static", who="team2") + Team_Both_Stat(merged, out_dir="static") + Referee(merged, out_dir="static") # print(merged) logger.info( f"Сегодня у {team} нет игры.\nПоследняя сыгранная: gameID={game_id}.\nМониторинг не запускаю."