commit 59e892193b35eb3fbfcc86a8ebd1f15cda2e5903 Author: Юрий Черненко Date: Mon Oct 20 12:32:16 2025 +0300 first commit diff --git a/get_data.py b/get_data.py new file mode 100644 index 0000000..270bb12 --- /dev/null +++ b/get_data.py @@ -0,0 +1,2882 @@ +from __future__ import annotations +import os +import sys +import json +import time +import socket +import urllib3 +import logging +import logging.config +import argparse +import platform +import requests +import threading +import numpy as np +import pandas as pd +from threading import Event, Lock +from concurrent.futures import ThreadPoolExecutor, as_completed +from typing import List, Dict, Any +from datetime import datetime, timedelta, timezone +from ipaddress import ip_address as _ip_parse +import errno + +urllib3.disable_warnings() + + +# === Настройки и логгер === +myhost = platform.node() +VERSION = "v.2.1 от 08.10.2025" +TAGS = [ + {"tag": "vtb", "name": "Единая Лига ВТБ", "lang": "en"}, + {"tag": "vtbyouth", "name": "Молодежка ВТБ", "lang": ""}, + {"tag": "rfb-deti", "name": "Дети", "lang": ""}, + {"tag": "rfb-silent", "name": "Тихий!баскетбол", "lang": ""}, + {"tag": "orgRoot", "name": "Все соревнования", "lang": ""}, + {"tag": "vtb-supercup", "name": "Супер-Кубок ЕЛ ВТБ", "lang": ""}, + {"tag": "uba-leto", "name": "UBA лето", "lang": "ru"}, + {"tag": "phygital-russia-cup-m", "name": "Фиджитал кубок", "lang": "ru"}, + {"tag": "phygital", "name": "Фиджитал", "lang": "ru"}, + {"tag": "3x3Root", "name": "3х3", "lang": "ru"}, + {"tag": "LS3x3", "name": "Лига Сильных 3х3", "lang": "ru"}, + {"tag": "uba", "name": "ЮБА", "lang": "ru"}, + {"tag": "uba-main", "name": "ЮБА-маин", "lang": "ru"}, + {"tag": "msl", "name": "Суперлига. Мужчины", "lang": ""}, + {"tag": "mhl", "name": "Высшая лига. Мужчины", "lang": ""}, + {"tag": "mcup", "name": "Кубок России. Мужчины", "lang": ""}, + {"tag": "wpremier", "name": "Премьер-Лига. Женщины", "lang": ""}, + {"tag": "wsl", "name": "Суперлига. Женщины", "lang": ""}, + {"tag": "whl", "name": "Высшая лига. Женщины", "lang": ""}, + {"tag": "wcup", "name": "Кубок России. Женщины", "lang": ""}, + {"tag": "uba-summer", "name": "UBA Leto", "lang": "ru"}, + {"tag": "unics", "name": "UNICS", "lang": "ru"}, +] + +TOKEN = "7639240596:AAH0YtdQoWZSC-_R_EW4wKAHHNLIA0F_ARY" +GROUP_CHAT = 228977654 + +if not os.path.exists("logs"): + os.makedirs("logs") +if not os.path.exists("JSON"): + os.makedirs("JSON") + +LOG_CONFIG = { + "version": 1, + "handlers": { + "telegram": { + "class": "telegram_handler.TelegramHandler", + "level": "INFO", + "token": TOKEN, + "chat_id": GROUP_CHAT, + "formatter": "telegram", + }, + "console": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "simple", + "stream": "ext://sys.stdout", + }, + "file": { + "class": "logging.FileHandler", + "level": "DEBUG", + "formatter": "simple", + "filename": f"logs/GFX_{myhost}.log", + "encoding": "utf-8", + }, + }, + "loggers": { + __name__: {"handlers": ["console", "file", "telegram"], "level": "DEBUG"}, + }, + "formatters": { + "telegram": { + "class": "telegram_handler.HtmlFormatter", + "format": "%(levelname)s %(message)s", + "use_emoji": "True", + }, + "simple": { + "class": "logging.Formatter", + "format": "%(asctime)s %(levelname)-8s %(funcName)s() - %(message)s", + "datefmt": "%d.%m.%Y %H:%M:%S", + }, + }, +} + +logging.config.dictConfig(LOG_CONFIG) +logger = logging.getLogger(__name__) +logger.handlers[2].formatter.use_emoji = True + +# Глобальный кэш и лок для потокобезопасности +_GAME_CACHE: dict[tuple[int, str], dict] = {} +_GAME_CACHE_LOCK = Lock() + +THROTTLE_OLD_MINUTES = 30 + +FOLDER_JSON = "JSON" if sys.platform.startswith("win") else "static" + + +def get_ip_address(): + try: + # Попытка получить IP-адрес с использованием внешнего сервиса + # Может потребоваться подключение к интернету + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip_address = s.getsockname()[0] + except socket.error: + # Если не удалось получить IP-адрес через внешний сервис, + # используем метод для локального получения IP + ip_address = socket.gethostbyname(socket.gethostname()) + return ip_address + +def read_match_id_json(path="match_id.json", attempts=10, delay=0.2): + """Надёжное чтение match_id.json с ретраями при EBUSY/битом JSON.""" + d = delay + for i in range(attempts): + try: + if not os.path.isfile(path): + return {} + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + except json.JSONDecodeError: + # файл переписывают — подождём и попробуем снова + time.sleep(d); d = min(d*1.6, 2.0) + except OSError as e: + # EBUSY (errno=16) — подождём и ещё раз + if getattr(e, "errno", None) == errno.EBUSY: + time.sleep(d); d = min(d*1.6, 2.0) + continue + # иные ошибки — пробрасываем дальше + raise + logger.error("Не удалось прочитать match_id.json после нескольких попыток; возвращаю {}") + return {} + +# === Аргументы командной строки === +parser = argparse.ArgumentParser(description="VTB Data Fetcher") +parser.add_argument("--league", type=str, default="vtb", help="League tag") +parser.add_argument("--lang", type=str, default="en", help="Language") +# parser.add_argument("--team", type=str, required=True, help="Team name") +# parser.add_argument("--region", type=int, default=0, help="for tvstart.ru") + +group = parser.add_mutually_exclusive_group(required=True) +group.add_argument("--team", type=str, help="Team name") +group.add_argument("--region", action="store_true", help="for tvstart.ru") + +args = parser.parse_args() + +LEAGUE = args.league +LANG = args.lang + +if args.team: + TEAM = args.team +else: # значит указан --region + ip_check = read_match_id_json("match_id.json") or {} + + ip_address = get_ip_address() + TEAM = ip_check.get(ip_address, {}).get("team") + + if TEAM is None: + parser.error("Не удалось определить команду по IP. Укажите --team явно.") + + +# === Глобальные настройки === +TIMEOUT_ONLINE = 1 +FETCH_INTERVAL = 2 +TIMEOUT_DATA_OFF = 60 + + +game_online_data = None +game_online_lock = threading.Lock() +game_status_data = None +game_status_lock = threading.Lock() + + +def _is_local_ip(ip: str) -> bool: + """True, если IP локальный/loopback/link-local или не распарсился.""" + try: + ipobj = _ip_parse(ip) + return ipobj.is_private or ipobj.is_loopback or ipobj.is_link_local + except Exception: + # Если get_ip_address() вернул что-то странное — считаем локальным, чтобы не префиксовать. + return True + + +def _ipcheck() -> str: + """Возвращает префикс для имени файла по IP. + Если IP локальный или host не найден — возвращает пустую строку. + """ + try: + ip_str = get_ip_address() + except Exception: + ip_str = None + + ip_map = globals().get("ip_check") or {} + if ip_str and isinstance(ip_map, dict): + host = (ip_map.get(ip_str) or {}).get("host") + if host: + return f"{host}_" + return "" + + +def rewrite_file(filename: str, data: dict, directory: str = "JSON") -> None: + """ + Перезаписывает JSON-файл с заданными данными. + Если запуск локальный (локальный IP), префикс host в имени файла не используется. + Если IP не локальный и есть словарь ip_check с хостами — добавим префикс host_. + """ + # Если глобальная константа задана — используем её + try: + directory = FOLDER_JSON # type: ignore[name-defined] + except NameError: + # иначе используем аргумент по умолчанию/переданный + pass + + os.makedirs(directory, exist_ok=True) + + host_prefix = _ipcheck() + + filepath = os.path.join(directory, f"{host_prefix}{filename}.json") + # print(filepath) # оставил как у тебя; можно заменить на logger.debug при желании + + try: + with open(filepath, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + logger.debug(f"Файл {filepath} перезаписан.") + except Exception as e: + logger.error(f"Ошибка при записи файла {filepath}: {e}") + + +def get_json(url: str, timeout: int = 10, verify_ssl: bool = True) -> dict | None: + """ + Получает JSON-ответ по указанному URL. + + Args: + url (str): URL для запроса. + timeout (int): Таймаут запроса в секундах. + verify_ssl (bool): Проверять ли SSL-сертификат. + + Returns: + dict | None: Распарсенный JSON или None при ошибке. + """ + try: + logger.debug(f"Пытаюсь получить данные с {url}") + response = requests.get(url, timeout=timeout, verify=verify_ssl) + response.raise_for_status() # выбросит исключение, если статус != 2xx + return response.json() + except requests.RequestException as e: + logger.warning(f"Ошибка при запросе: {url}\n{e}") + return None + except ValueError as e: + logger.warning(f"Некорректный JSON-ответ от {url}:\n{e}") + return None + + +def Game_Online2(game_id: int) -> dict | None: + """ + Получает и объединяет данные об игре, включая онлайн-информацию. + + Использует глобальные переменные: URL и LANG. + + Args: + game_id (int): ID матча. + + Returns: + dict | None: Объект игры или None при ошибке. + """ + global URL, LANG + + def build_url(endpoint: str) -> str: + return f"{URL}api/abc/games/{endpoint}?Id={game_id}&Lang={LANG}" + + # 1. Получаем box score + box_score = get_json(build_url("box-score")) + + if not box_score or box_score.get("status") != "Ok": + # Получаем данные старого матча + game = get_json(build_url("game")) + if game: + logger.debug("У нас получилось получить данные со старого матча") + else: + logger.warning( + f"Не удалось получить данные старого матча: game_id={game_id}" + ) + return game + + # 2. Получаем онлайн-данные + game = get_json(build_url("game")) + play_by_play = get_json(build_url("play-by-play")) + + if not game or not play_by_play: + logger.warning(f"Ошибка при получении онлайн-данных для матча {game_id}") + return None + + try: + # 3. Совмещаем статистику + for index_team, team in enumerate(game["result"]["teams"][1:]): + box_team = box_score["result"]["teams"][index_team] + for player in team.get("starts", []): + stat = next( + ( + s + for s in box_team.get("starts", []) + if s.get("startNum") == player.get("startNum") + ), + None, + ) + if stat: + player["stats"] = stat + + team["total"] = box_team.get("total", {}) + + # 4. Добавляем дополнительные данные + game["result"]["plays"] = play_by_play.get("result", []) + game["result"]["scoreByPeriods"] = box_score["result"].get("scoreByPeriods", []) + game["result"]["fullScore"] = box_score["result"].get("fullScore", {}) + + logger.debug("Склеил данные по онлайн матчу") + return game + + except Exception as e: + logger.error(f"Ошибка при обработке данных игры {game_id}: {e}") + return None + + +def Game_Online(game_id: int) -> dict | None: + """ + Получает и объединяет данные об игре, включая онлайн-информацию, + с троттлингом обновления "старых" матчей (не чаще, чем раз в 30 минут). + + Использует глобальные переменные: URL и LANG. + + Args: + game_id (int): ID матча. + + Returns: + dict | None: Объект игры или None при ошибке. + """ + global URL, LANG, _GAME_CACHE, _GAME_CACHE_LOCK + + now = datetime.now(timezone.utc) + cache_key = (game_id, str(LANG)) + + # Время троттлинга для старых матчей + OLD_GAME_THROTTLE_MINUTES = 10 + + # 0. Предварительная проверка: если недавно получали "старый" матч — отдаем кэш + with _GAME_CACHE_LOCK: + cached = _GAME_CACHE.get(cache_key) + if ( + cached + and cached.get("mode") == "old" + and (now - cached.get("ts", now)) < timedelta(minutes=OLD_GAME_THROTTLE_MINUTES) + ): + return cached.get("data") + + def build_url(endpoint: str) -> str: + return f"{URL}api/abc/games/{endpoint}?Id={game_id}&Lang={LANG}" + + + box_score = get_json(build_url("box-score")) + print(box_score) + if not box_score or box_score.get("status") != "Ok": + # Проверим — матч сейчас online? + live = get_json(f"{URL}api/abc/games/live-status?id={game_id}") + is_online = bool( + live and live.get("status") == "Ok" + and live.get("result", {}).get("gameStatus") == "Online" + ) + + game = get_json(build_url("game")) + if game: + logger.debug("У нас получилось получить данные со старого матча") + # Только если матч не в онлайн-режиме — кладём в кэш как 'old' + if not is_online: + with _GAME_CACHE_LOCK: + _GAME_CACHE[cache_key] = {"mode": "old", "ts": now, "data": game} + else: + logger.warning(f"Не удалось получить данные старого матча: game_id={game_id}") + return game + + # 1. Получаем box score + # box_score = get_json(build_url("box-score")) + + # if not box_score or box_score.get("status") != "Ok": + # # Получаем данные старого матча (и кэшируем их с режимом 'old') + # live = get_json(f"{URL}api/abc/games/live-status?id={game_id}") + # is_online = bool(live and live.get("status") == "Ok" and live.get("result", {}).get("gameStatus") == "Online") + + # game = get_json(build_url("game")) + # if game: + # logger.debug("У нас получилось получить данные со старого матча") + # if not is_online: + # with _GAME_CACHE_LOCK: + # _GAME_CACHE[cache_key] = {"mode": "old", "ts": now, "data": game} + # else: + # logger.warning(f"Не удалось получить данные старого матча: game_id={game_id}") + # return game + + # game = get_json(build_url("game")) + # if game: + # logger.debug("У нас получилось получить данные со старого матча") + # with _GAME_CACHE_LOCK: + # _GAME_CACHE[cache_key] = {"mode": "old", "ts": now, "data": game} + # else: + # logger.warning( + # f"Не удалось получить данные старого матча: game_id={game_id}" + # ) + # # Даже неудачный ответ имеет смысл закэшировать как 'old' на короткое время, + # # но чтобы не скрыть будущий успех, кэшируем только при наличии данных. + # return game + + # 2. Получаем онлайн-данные + game = get_json(build_url("game")) + play_by_play = get_json(build_url("play-by-play")) + + if not game or not play_by_play: + logger.warning(f"Ошибка при получении онлайн-данных для матча {game_id}") + return None + + try: + # 3. Совмещаем статистику + for index_team, team in enumerate(game["result"]["teams"][1:]): + box_team = box_score["result"]["teams"][index_team] + for player in team.get("starts", []): + stat = next( + ( + s + for s in box_team.get("starts", []) + if s.get("startNum") == player.get("startNum") + ), + None, + ) + if stat: + player["stats"] = stat + + team["total"] = box_team.get("total", {}) + + # 4. Добавляем дополнительные данные + game["result"]["plays"] = play_by_play.get("result", []) + game["result"]["scoreByPeriods"] = box_score["result"].get("scoreByPeriods", []) + game["result"]["fullScore"] = box_score["result"].get("fullScore", {}) + + logger.debug("Склеил данные по онлайн матчу") + + # Обновляем кэш и снимаем режим 'old' + with _GAME_CACHE_LOCK: + _GAME_CACHE[cache_key] = {"mode": "online", "ts": now, "data": game} + + return game + + except Exception as e: + logger.error(f"Ошибка при обработке данных игры {game_id}: {e}") + return None + + +def game_online_loop(game_id: int, stop_event: threading.Event) -> None: + """ + Цикл обновления онлайн-данных игры с заданным интервалом. + + Args: + game_id (int): ID игры. + stop_event (threading.Event): Событие для остановки цикла. + """ + global game_online_data + + while not stop_event.is_set(): + try: + data = Game_Online(game_id) + if data: + with game_online_lock: + game_online_data = data + else: + logger.warning(f"Game_Online вернул None для game_id={game_id}") + except Exception as e: + logger.error(f"Ошибка в game_online_loop: {e}", exc_info=True) + + stop_event.wait(TIMEOUT_ONLINE) # Лучше чем time.sleep — можно остановить сразу + + +def coach_team_stat(data: list[dict], team_id: int) -> dict: + """ + Считает статистику тренера по матчам и сезонам для конкретной команды. + + Args: + data (list[dict]): Список сезонов с данными. + team_id (int): ID команды. + + Returns: + dict: Статистика тренера. + """ + total_games = total_wins = total_loses = seasons = 0 + + for d in data or []: + team = d.get("team") + if team and team.get("id") == team_id: + seasons += 1 + total_games += d.get("games", 0) + total_wins += d.get("wins", 0) + total_loses += d.get("loses", 0) + + return { + "games": total_games, + "wins": total_wins, + "loses": total_loses, + "gamesAsCoach": total_games, + "winsAsCoach": total_wins, + "losesAsCoach": total_loses, + "season": seasons, + } + + +def safe_int(value: any, default: int = 0) -> int: + """ + Безопасное преобразование значения в int. Возвращает default при ошибке. + + Args: + value: Значение для преобразования (строка, число и т.п.). + default: Значение по умолчанию при ошибке. + + Returns: + Целое число или default. + """ + try: + if isinstance(value, str): + value = value.strip() + return int(float(value)) + except (ValueError, TypeError): + return default + + +def format_time(seconds: float | int) -> str: + """ + Форматирует время в секундах в строку "M:SS". + + Args: + seconds (float | int): Количество секунд. + + Returns: + str: Время в формате "M:SS". + """ + try: + total_seconds = int(float(seconds)) + minutes = total_seconds // 60 + sec = total_seconds % 60 + return f"{minutes}:{sec:02}" + except (ValueError, TypeError): + return "0:00" + + +def safe_percent(numerator: float | int | str, denominator: float | int | str) -> str: + """ + Безопасно вычисляет процент (numerator / denominator * 100), округляя до 1 знака. + + Args: + numerator: Числитель (может быть числом или строкой). + denominator: Знаменатель (может быть числом или строкой). + + Returns: + Строка с процентом, например "75.0%" или "0.0%" при ошибке. + """ + try: + num = float(str(numerator).strip()) + den = float(str(denominator).strip()) + if den == 0: + return "0.0%" + return f"{round(num * 100 / den, 1)}%" + except (ValueError, TypeError): + return "0.0%" + + +def calc_shot_percent_by_type( + sum_stat: dict | None, + item_stats: dict, + online: bool, + shot_types: int | list[int] = 1, + digits: int = 1, + empty: str = "0.0%", +) -> str: + """ + Возвращает процент реализации бросков по одному или нескольким типам (goalX / shotX). + + Args: + sum_stat: Сезонная статистика игрока или None. + item_stats: Онлайн-статистика (например, item["stats"]). + online: Учитывать ли онлайн-данные. + shot_types: Целое число или список номеров бросков (1, 2, 3, или [2, 3]). + digits: Кол-во знаков после запятой. + empty: Что возвращать при отсутствии данных или делении на 0. + + Returns: + Строка процента, например '68.5%' или '0.0%'. + """ + if isinstance(shot_types, int): + shot_types = [shot_types] + + if not sum_stat or item_stats is None: + return empty + + total_goal = 0 + total_shot = 0 + # print(item_stats) + for t in shot_types: + # print(t) + goal_key = f"goal{t}" + shot_key = f"shot{t}" + + + sum_goal_raw = sum_stat.get(goal_key) + sum_shot_raw = sum_stat.get(shot_key) + item_goal_raw = item_stats.get(goal_key) + item_shot_raw = item_stats.get(shot_key) + + # Если какие-либо данные отсутствуют, или являются "", считаем 0 + sum_goal = safe_int(sum_goal_raw if sum_goal_raw != "" else 0) + sum_shot = safe_int(sum_shot_raw if sum_shot_raw != "" else 0) + + item_goal = safe_int(item_goal_raw if item_goal_raw != "" else 0) + item_shot = safe_int(item_shot_raw if item_shot_raw != "" else 0) + + total_goal += sum_goal + (item_goal if online else 0) + total_shot += sum_shot + (item_shot if online else 0) + + if total_shot == 0: + return empty + + percent = round(total_goal * 100 / total_shot, digits) + return f"{percent}%" + + +def calc_total_shots_str( + sum_stat: dict | None, + item_stats: dict, + online: bool, + shot_types: int | list[int], + empty: str = "0/0", +) -> str: + """ + Формирует строку вида 'goalX/shotX' или сумму по нескольким типам бросков. + + Args: + sum_stat: Сезонная статистика игрока (может быть None). + item_stats: Онлайн-статистика из item["stats"]. + online: Добавлять ли онлайн-значения. + shot_types: Один номер броска (int) или список (например, [2, 3] для TShots23). + empty: Что возвращать при отсутствии данных. + + Returns: + Строка вида '5/8' или '0/0' при ошибке/отсутствии данных. + """ + if isinstance(shot_types, int): + shot_types = [shot_types] + + if not sum_stat or item_stats is None: + return empty + + total_goal = 0 + total_shot = 0 + + for t in shot_types: + goal_key = f"goal{t}" + shot_key = f"shot{t}" + + if sum_stat.get(shot_key) == "" or item_stats.get(shot_key) == "": + return empty + + goal = safe_int(sum_stat.get(goal_key)) + shot = safe_int(sum_stat.get(shot_key)) + + if online: + goal += safe_int(item_stats.get(goal_key) or 0) + shot += safe_int(item_stats.get(shot_key) or 0) + + total_goal += goal + total_shot += shot + + return f"{total_goal}/{total_shot}" + + +def sum_stat_with_online( + stat_name: str, base_stat: dict | None, online_stat: dict | None, online: bool +) -> int: + base = safe_int((base_stat or {}).get(stat_name)) + online_val = safe_int((online_stat or {}).get(stat_name)) if online else 0 + return base + online_val + + +def Json_Team_Generation(who, data, stop_event): + logger.info(f"START making json for {data[who]}, {data[f'{who}_id']}") + global game_online_data + initialized = False + + while not stop_event.is_set(): + try: + with game_online_lock: + game_online_data_copy = game_online_data + if game_online_data_copy is not None: + if who == "team1": + logger.debug( + f"send {game_online_data_copy['result']['team1']['name']}" + ) + for i in game_online_data_copy["result"]["teams"]: + if i["teamNumber"] == 1: + payload = i + elif who == "team2": + logger.debug( + f"send {game_online_data_copy['result']['team2']['name']}" + ) + for i in game_online_data_copy["result"]["teams"]: + if i["teamNumber"] == 2: + payload = i + + # print(payload) + # получаю ID игроков + + url = f"{URL}api/abc/games/live-status?id={data['game_id']}" + json_live_status = get_json(url) + online = ( + True + if json_live_status and "status" in json_live_status + and json_live_status["status"] == "Ok" + and json_live_status["result"]["gameStatus"] == "Online" + else False + ) + # online = True + + # получаю статистику на игроков по сезону и карьере + if not initialized: + player_ids = [ + i["personId"] + for i in payload["starts"] + if i["startRole"] == "Player" + ] + print(player_ids) + coach_ids = [ + i["personId"] + for i in payload["starts"] + if i["startRole"] == "Coach" and i["personId"] is not None + ] + print(coach_ids) + player_season_stat = [] + player_career_stat = [] + coach_stat = [] + # with ThreadPoolExecutor() as pool: + # player_season_stat_temp = [ + # pool.submit(Player_Stat_Season, player_id, data["season"]) + # for player_id in player_ids + # ] + # player_career_stat_temp = [ + # pool.submit(Player_Stat_Career, player_id) + # for player_id in player_ids + # ] + # coach_stat_temp = [ + # pool.submit( + # Coach_Stat, coach_id, data["season"], data[f"{who}_id"] + # ) + # for coach_id in coach_ids + # ] + # player_futures = [pool.submit(Player_all_game, pid) for pid in player_ids] + # all_players_games = [] + # for fut in as_completed(player_futures): + # try: + # all_players_games.append(fut.result()) + # except Exception as e: + # logger.exception(f"Ошибка при обработке игрока: {e}") + + # player_season_stat += [ + # res.result() for res in player_season_stat_temp + # ] + # player_career_stat += [ + # res.result() for res in player_career_stat_temp + # ] + # coach_stat += [res.result() for res in coach_stat_temp] + + # initialized = True + # print(coach_stat) + # while not stop_event.is_set(): + role_list = [ + ("Center", "C"), + ("Guard", "G"), + ("Forward", "F"), + ("Power Forward", "PF"), + ("Small Forward", "SF"), + ("Shooting Guard", "SG"), + ("Point Guard", "PG"), + ("Forward-Center", "FC"), + ] + starts = payload["starts"] + HeadCoachStatsCareer, HeadCoachStatsTeam = "", "" + team = [] + # print(starts) + for item in starts: + # print(player_season_stat) + + row_player_season = next( + ( + v + for row in player_season_stat if row + for k, v in row.items() + if k == item["personId"] + ), + None, + ) + row_player_career = next( + ( + v + for row in player_career_stat if row + for k, v in row.items() + if k == item["personId"] + ), + None, + ) + # print(item) + row_coach_stat = next( + ( + v + for row in coach_stat if row + for k, v in row.items() + if k == item["personId"] + ), + None, + ) + row_player_season_avg, sum_stat = ( + ( + next( + ( + r["stats"] + for r in row_player_season + if r["class"] == "Avg" + ), + None, + ), + next( + ( + r["stats"] + for r in row_player_season + if r["class"] == "Sum" + ), + None, + ), + ) + if row_player_season + else (None, None) + ) + row_player_career_avg, row_player_career_sum = ( + ( + next( + ( + r["stats"] + for r in row_player_career + if r["class"] == "Avg" + ), + None, + ), + next( + ( + r["stats"] + for r in row_player_career + if r["class"] == "Sum" + ), + None, + ), + ) + if row_player_career + else (None, None) + ) + if row_coach_stat: + games_word = ( + "game" if row_coach_stat[-1]["games"] in [0, 1] else "games" + ) + total_season = len(row_coach_stat) - 1 + season_word = "season" if total_season == 1 else "seasons" + procent = ( + round( + (row_coach_stat[-1]["wins"] * 100) + / row_coach_stat[-1]["games"], + 1, + ) + if row_coach_stat[-1]["games"] != 0 + else "" + ) + # HeadCoachStatsCareer = ( + # f'{row_s["total_seasons"]} {season_word} in The VTB United League career' + # ) + HeadCoachStatsCareer = "in The VTB United League career" + if total_season == 0: + HeadCoachStatsCareer = f'{row_coach_stat[-1]["games"] if row_coach_stat[-1]["games"] != 0 else "first"} {games_word} as {data[who]} head coach' + HeadCoachStatsCareer += ( + f'\n{row_coach_stat[-1]["games"] if row_coach_stat[-1]["games"] != 0 else "first"} {games_word}: {row_coach_stat[-1]["wins"]}-{row_coach_stat[-1]["loses"]} ({procent}% wins)' + if row_coach_stat[-1]["games"] != 0 + else "" + ) + coach_team_stat_temp = coach_team_stat( + row_coach_stat, data[f"{who}_id"] + ) + games_word_team = ( + "game" + if coach_team_stat_temp["games"] in [0, 1] + else "games" + ) + season_word_team = ( + "season" + if coach_team_stat_temp["season"] == 1 + else "seasons" + ) + procent_team = ( + round( + (coach_team_stat_temp["wins"] * 100) + / coach_team_stat_temp["games"], + 1, + ) + if coach_team_stat_temp["games"] != 0 + else "" + ) + # HeadCoachStatsTeam = ( + # f'{coach_team_stat_temp["season"]} {season_word_team} as {data[f"{who}"]} head coach' + # ) + HeadCoachStatsTeam = f"{data[f'{who}']} head coach" + if coach_team_stat_temp["season"] == 0: + HeadCoachStatsTeam = f'{coach_team_stat_temp["games"] if coach_team_stat_temp["games"] != 0 else "first"} {games_word} as {data[f"{who}"]} head coach' + HeadCoachStatsTeam += ( + f'\n{coach_team_stat_temp["games"] if coach_team_stat_temp["games"] != 0 else "first"} {games_word}: {coach_team_stat_temp["wins"]}-{coach_team_stat_temp["loses"]} ({procent_team}% wins)' + if coach_team_stat_temp["games"] != 0 + else "" + ) + + text = "" + if row_player_season_avg: + if LANG == "en": + text = f"GAMES: {row_player_season_avg['games']} MINUTES: {row_player_season_avg['playedTime']} " + text += ( + f"PTS: {row_player_season_avg['points']} " + if row_player_season_avg["points"] != "" + else "" + ) + text += ( + f"REB: {row_player_season_avg['rebound']} " + if row_player_season_avg["rebound"] != "" + and float(row_player_season_avg["rebound"]) >= 1.0 + else "" + ) + text += ( + f"AST: {row_player_season_avg['assist']} " + if row_player_season_avg["assist"] != "" + and float(row_player_season_avg["assist"]) >= 1.0 + else "" + ) + text += ( + f"STL: {row_player_season_avg['steal']} " + if row_player_season_avg["steal"] != "" + and float(row_player_season_avg["steal"]) >= 1.0 + else "" + ) + text += ( + f"BLK: {row_player_season_avg['blockShot']} " + if row_player_season_avg["blockShot"] != "" + and float(row_player_season_avg["blockShot"]) >= 1.0 + else "" + ) + else: + text = f"ИГРЫ: {row_player_season_avg['games']} ВРЕМЯ: {row_player_season_avg['playedTime']} " + text += ( + f"ОЧКИ: {row_player_season_avg['points']} " + if row_player_season_avg["points"] != "" + else "" + ) + text += ( + f"ПОДБОРЫ: {row_player_season_avg['rebound']} " + if row_player_season_avg["rebound"] != "" + and float(row_player_season_avg["rebound"]) >= 1.0 + else "" + ) + text += ( + f"ПЕРЕДАЧИ: {row_player_season_avg['assist']} " + if row_player_season_avg["assist"] != "" + and float(row_player_season_avg["assist"]) >= 1.0 + else "" + ) + text += ( + f"ПЕРЕХВАТЫ: {row_player_season_avg['steal']} " + if row_player_season_avg["steal"] != "" + and float(row_player_season_avg["steal"]) >= 1.0 + else "" + ) + text += ( + f"БЛОКШОТЫ: {row_player_season_avg['blockShot']} " + if row_player_season_avg["blockShot"] != "" + and float(row_player_season_avg["blockShot"]) >= 1.0 + else "" + ) + text = text.strip() + # print(item["personId"] if item["startRole"] != "Team" else , item["startRole"]) + # if not item["personId"]: + # item["personId"] = data[f'{who}_id'] + # print(item) + # item["personId"] = item["personId"] if item["personId"] else data[f'{who}_id'] + # print(item) + # print(data[f'{who}_id'], type(data[f'{who}_id']), data) + # print(item) + player = { + "id": ( + item["personId"] + if item["personId"] + else int(data[f"{who}_id"]) + ), + "num": item["displayNumber"], + "startRole": item["startRole"], + "role": item["positionName"], + "roleShort": ( + [ + r[1] + for r in role_list + if r[0].lower() == item["positionName"].lower() + ][0] + if any( + r[0].lower() == item["positionName"].lower() + for r in role_list + ) + else "" + ), + "NameGFX": ( + f"{item['firstName'].strip()} {item['lastName'].strip()}" + if item["firstName"] is not None + and item["lastName"] is not None + else "Команда" + ), + "captain": item["isCapitan"], + "age": item["age"] if item["age"] is not None else 0, + "height": f'{item["height"]} cm' if item["height"] else 0, + "weight": f'{item["weight"]} kg' if item["weight"] else 0, + "isStart": ( + item["stats"]["isStart"] if item["stats"] else False + ), + "isOn": ( + "🏀" + if item["stats"] and item["stats"]["isOnCourt"] is True + else "" + ), + "flag": f"https://flagicons.lipis.dev/flags/4x3/{'ru' if item['countryId'] is None and item['countryName'] == 'Russia' else '' if item['countryId'] is None else item['countryId'].lower() if item['countryName'] is not None else ''}.svg", + "pts": item["stats"]["points"] if item["stats"] else 0, + "pt-2": ( + f"{item['stats']['goal2']}/{item['stats']['shot2']}" + if item["stats"] + else 0 + ), + "pt-3": ( + f"{item['stats']['goal3']}/{item['stats']['shot3']}" + if item["stats"] + else 0 + ), + "pt-1": ( + f"{item['stats']['goal1']}/{item['stats']['shot1']}" + if item["stats"] + else 0 + ), + "fg": ( + f"{item['stats']['goal2'] + item['stats']['goal3']}/{item['stats']['shot2'] + item['stats']['shot3']}" + if item["stats"] + else 0 + ), + "ast": item["stats"]["assist"] if item["stats"] else 0, + "stl": item["stats"]["steal"] if item["stats"] else 0, + "blk": item["stats"]["block"] if item["stats"] else 0, + "blkVic": item["stats"]["blocked"] if item["stats"] else 0, + "dreb": item["stats"]["defReb"] if item["stats"] else 0, + "oreb": item["stats"]["offReb"] if item["stats"] else 0, + "reb": ( + item["stats"]["defReb"] + item["stats"]["offReb"] + if item["stats"] + else 0 + ), + "to": item["stats"]["turnover"] if item["stats"] else 0, + "foul": item["stats"]["foul"] if item["stats"] else 0, + "foulT": item["stats"]["foulT"] if item["stats"] else 0, + "foulD": item["stats"]["foulD"] if item["stats"] else 0, + "foulC": item["stats"]["foulC"] if item["stats"] else 0, + "foulB": item["stats"]["foulB"] if item["stats"] else 0, + "fouled": item["stats"]["foulsOn"] if item["stats"] else 0, + "plusMinus": item["stats"]["plusMinus"] if item["stats"] else 0, + "dunk": item["stats"]["dunk"] if item["stats"] else 0, + "kpi": ( + item["stats"]["points"] + + item["stats"]["defReb"] + + item["stats"]["offReb"] + + item["stats"]["assist"] + + item["stats"]["steal"] + + item["stats"]["block"] + + item["stats"]["foulsOn"] + + (item["stats"]["goal1"] - item["stats"]["shot1"]) + + (item["stats"]["goal2"] - item["stats"]["shot2"]) + + (item["stats"]["goal3"] - item["stats"]["shot3"]) + - item["stats"]["turnover"] + - item["stats"]["foul"] + if item["stats"] + else 0 + ), + "time": ( + format_time(item["stats"]["second"]) + if item["stats"] + else "0:00" + ), + "pts1q": 0, + "pts2q": 0, + "pts3q": 0, + "pts4q": 0, + "pts1h": 0, + "pts2h": 0, + "Name1GFX": ( + item["firstName"].strip() if item["firstName"] else "" + ), + "Name2GFX": ( + item["lastName"].strip() if item["lastName"] else "" + ), + "photoGFX": ( + os.path.join("D:\\Photos", LEAGUE, data[who], f"{item['displayNumber']}.png") + if item["startRole"] == "Player" + else "" + ), + "season": text, + "isOnCourt": ( + item["stats"]["isOnCourt"] if item["stats"] else False + ), + "AvgPoints": ( + row_player_season_avg["points"] + if row_player_season_avg + and row_player_season_avg["points"] != "" + else "0.0" + ), + "AvgAssist": ( + row_player_season_avg["assist"] + if row_player_season_avg + and row_player_season_avg["assist"] != "" + else "0.0" + ), + "AvgBlocks": ( + row_player_season_avg["blockShot"] + if row_player_season_avg + and row_player_season_avg["blockShot"] != "" + else "0.0" + ), + "AvgDefRebound": ( + row_player_season_avg["defRebound"] + if row_player_season_avg + and row_player_season_avg["defRebound"] != "" + else "0.0" + ), + "AvgOffRebound": ( + row_player_season_avg["offRebound"] + if row_player_season_avg + and row_player_season_avg["offRebound"] != "" + else "0.0" + ), + "AvgRebound": ( + row_player_season_avg["rebound"] + if row_player_season_avg + and row_player_season_avg["rebound"] != "" + else "0.0" + ), + "AvgSteal": ( + row_player_season_avg["steal"] + if row_player_season_avg + and row_player_season_avg["steal"] != "" + else "0.0" + ), + "AvgTurnover": ( + row_player_season_avg["turnover"] + if row_player_season_avg + and row_player_season_avg["turnover"] != "" + else "0.0" + ), + "AvgFoul": ( + row_player_season_avg["foul"] + if row_player_season_avg + and row_player_season_avg["foul"] != "" + else "0.0" + ), + "AvgOpponentFoul": ( + row_player_season_avg["foulsOnPlayer"] + if row_player_season_avg + and row_player_season_avg["foulsOnPlayer"] != "" + else "0.0" + ), + "AvgPlusMinus": ( + row_player_season_avg["plusMinus"] + if row_player_season_avg + and row_player_season_avg["plusMinus"] != "" + else "0.0" + ), + "AvgDunk": ( + row_player_season_avg["dunk"] + if row_player_season_avg + and row_player_season_avg["dunk"] != "" + else "0.0" + ), + "AvgKPI": "0.0", + "AvgPlayedTime": ( + row_player_season_avg["playedTime"] + if row_player_season_avg + and row_player_season_avg["playedTime"] != "" + else "0:00" + ), + "Shot1Percent": calc_shot_percent_by_type( + sum_stat, item["stats"], online, shot_types=1 + ), + "Shot2Percent": calc_shot_percent_by_type( + sum_stat, item["stats"], online, shot_types=2 + ), + "Shot3Percent": calc_shot_percent_by_type( + sum_stat, item["stats"], online, shot_types=3 + ), + "Shot23Percent": calc_shot_percent_by_type( + sum_stat, item["stats"], online, shot_types=[2, 3] + ), + "TPoints": sum_stat_with_online( + "points", sum_stat, item["stats"], online + ), + "TShots1": calc_total_shots_str( + sum_stat, item["stats"], online, 1 + ), + "TShots2": calc_total_shots_str( + sum_stat, item["stats"], online, 2 + ), + "TShots3": calc_total_shots_str( + sum_stat, item["stats"], online, 3 + ), + "TShots23": calc_total_shots_str( + sum_stat, item["stats"], online, [2, 3] + ), + "TShot1Percent": calc_shot_percent_by_type( + sum_stat, item["stats"], online, shot_types=1 + ), + "TShot2Percent": calc_shot_percent_by_type( + sum_stat, item["stats"], online, shot_types=2 + ), + "TShot3Percent": calc_shot_percent_by_type( + sum_stat, item["stats"], online, shot_types=3 + ), + "TShot23Percent": calc_shot_percent_by_type( + sum_stat, item["stats"], online, shot_types=[2, 3] + ), + "TAssist": sum_stat_with_online( + "assist", sum_stat, item["stats"], online + ), + "TBlocks": sum_stat_with_online( + "blockShot", sum_stat, item["stats"], online + ), + "TDefRebound": sum_stat_with_online( + "defRebound", sum_stat, item["stats"], online + ), + "TOffRebound": sum_stat_with_online( + "offRebound", sum_stat, item["stats"], online + ), + "TRebound": ( + sum_stat_with_online( + "defRebound", sum_stat, item["stats"], online + ) + + sum_stat_with_online( + "offRebound", sum_stat, item["stats"], online + ) + ), + "TSteal": sum_stat_with_online( + "steal", sum_stat, item["stats"], online + ), + "TTurnover": sum_stat_with_online( + "turnover", sum_stat, item["stats"], online + ), + "TFoul": sum_stat_with_online( + "foul", sum_stat, item["stats"], online + ), + "TOpponentFoul": sum_stat_with_online( + "foulsOnPlayer", sum_stat, item["stats"], online + ), + "TPlusMinus": 0, + "TDunk": sum_stat_with_online( + "dunk", sum_stat, item["stats"], online + ), + "TKPI": 0, + "TPlayedTime": sum_stat["playedTime"] if sum_stat else "0:00", + "TGameCount": ( + safe_int(sum_stat["games"]) + if sum_stat and sum_stat.get("games") != "" + else 0 + ) + + (1 if online else 0), + "TStartCount": ( + safe_int(sum_stat["isStarts"]) + if sum_stat and sum_stat.get("isStarts", 0) != "" + else 0 + ), + "CareerTShots1": calc_total_shots_str( + row_player_career_sum, item["stats"], online, 1 + ), + "CareerTShots2": calc_total_shots_str( + row_player_career_sum, item["stats"], online, 2 + ), + "CareerTShots3": calc_total_shots_str( + row_player_career_sum, item["stats"], online, 3 + ), + "CareerTShots23": calc_total_shots_str( + row_player_career_sum, item["stats"], online, [2, 3] + ), + "CareerTShot1Percent": calc_shot_percent_by_type( + row_player_career_sum, item["stats"], online, 1 + ), + "CareerTShot2Percent": calc_shot_percent_by_type( + row_player_career_sum, item["stats"], online, 2 + ), + "CareerTShot3Percent": calc_shot_percent_by_type( + row_player_career_sum, item["stats"], online, 3 + ), + "CareerTShot23Percent": calc_shot_percent_by_type( + row_player_career_sum, item["stats"], online, [2, 3] + ), + "CareerTPoints": sum_stat_with_online( + "points", row_player_career_sum, item["stats"], online + ), + "CareerTAssist": sum_stat_with_online( + "assist", row_player_career_sum, item["stats"], online + ), + "CareerTBlocks": sum_stat_with_online( + "blockShot", row_player_career_sum, item["stats"], online + ), + "CareerTDefRebound": sum_stat_with_online( + "defRebound", row_player_career_sum, item["stats"], online + ), + "CareerTOffRebound": sum_stat_with_online( + "offRebound", row_player_career_sum, item["stats"], online + ), + "CareerTRebound": ( + sum_stat_with_online( + "defRebound", + row_player_career_sum, + item["stats"], + online, + ) + + sum_stat_with_online( + "offRebound", + row_player_career_sum, + item["stats"], + online, + ) + ), + "CareerTSteal": sum_stat_with_online( + "steal", row_player_career_sum, item["stats"], online + ), + "CareerTTurnover": sum_stat_with_online( + "turnover", row_player_career_sum, item["stats"], online + ), + "CareerTFoul": sum_stat_with_online( + "foul", row_player_career_sum, item["stats"], online + ), + "CareerTOpponentFoul": sum_stat_with_online( + "foulsOnPlayer", + row_player_career_sum, + item["stats"], + online, + ), + "CareerTPlusMinus": 0, # оставить как есть + "CareerTDunk": sum_stat_with_online( + "dunk", row_player_career_sum, item["stats"], online + ), + "CareerTPlayedTime": ( + row_player_career_sum["playedTime"] + if row_player_career_sum + else "0:00" + ), + "CareerTGameCount": sum_stat_with_online( + "games", row_player_career_sum, item["stats"], online + ) + + (1 if online else 0), + "CareerTStartCount": sum_stat_with_online( + "isStarts", row_player_career_sum, item["stats"], online + ), # если нужно, можно +1 при старте + "HeadCoachStatsCareer": HeadCoachStatsCareer, + "HeadCoachStatsTeam": HeadCoachStatsTeam, + } + team.append(player) + count_player = sum(1 for x in team if x["startRole"] == "Player") + # print(count_player) + if count_player < 12: + if team: # Check if team is not empty + empty_rows = [ + { + key: ( + False + if key in ["captain", "isStart", "isOnCourt"] + else ( + 0 + if key + in [ + "id", + "pts", + "weight", + "height", + "age", + "ast", + "stl", + "blk", + "blkVic", + "dreb", + "oreb", + "reb", + "to", + "foul", + "foulT", + "foulD", + "foulC", + "foulB", + "fouled", + "plusMinus", + "dunk", + "kpi", + ] + else "" + ) + ) + for key in team[0].keys() + } + for _ in range( + (4 if count_player <= 4 else 12) - count_player + ) + ] + team.extend(empty_rows) + role_priority = { + "Player": 0, + "": 1, + "Coach": 2, + "Team": 3, + None: 4, + "Other": 5, # на случай неизвестных + } + # print(team) + sorted_team = sorted( + team, + key=lambda x: role_priority.get( + x.get("startRole", 99), 99 + ), # 99 — по умолчанию + ) + rewrite_file(who, sorted_team) + rewrite_file(f"{who}_copy", sorted_team) + # print(sorted_team) + top_sorted_team = sorted( + filter(lambda x: x["startRole"] in ["Player", ""], sorted_team), + key=lambda x: ( + x["pts"], + x["dreb"] + x["oreb"], + x["ast"], + x["stl"], + x["blk"], + x["time"], + ), + reverse=True, + ) + for item in top_sorted_team: + item["pts"] = "" if item["num"] == "" else item["pts"] + item["foul"] = "" if item["num"] == "" else item["foul"] + rewrite_file(f"top{who.replace('t','T')}", top_sorted_team) + started_team = sorted( + filter( + lambda x: x["startRole"] == "Player" and x["isOnCourt"] is True, + sorted_team, + ), + key=lambda x: int(x["num"]), + reverse=False, + ) + rewrite_file(f"started_{who}", started_team) + time.sleep(TIMEOUT_ONLINE) + else: + print(f"{who} НЕ ПОЛУЧАЕТСЯ ПРОЧИТАТЬ") + except Exception as e: + print(f"[{who}] Ошибка: {e}, {e.with_traceback()}") + time.sleep(TIMEOUT_ONLINE) + + +def Player_Stat_Season(player_id: str, season: str) -> dict: + url = f"{URL}api/abc/players/stats?teamId=0&Tag={LEAGUE}&season={season}&Id={player_id}" + player_stat_season = get_json(url) + + if not player_stat_season: + logger.debug(f"Пустой ответ от API для игрока {player_id} за сезон {season}") + return {player_id: default_player_stats_season()} + + items = player_stat_season.get("items") + if items: + logger.debug( + f"Данные за сезон {season} для игрока {player_id} успешно получены." + ) + return {player_id: items[-2:]} # последние две записи: Sum и Avg + + logger.debug( + f"Нет данных на игрока {player_id} за сезон {season}. Вероятно, еще не играл." + ) + return {player_id: default_player_stats_season()} + + +def Player_all_game_in_season2(player_id: str, season:str) -> dict: + url = f"{URL}api/abc/players/stats?tag={LEAGUE}&season={season}&id={player_id}" + player_games = get_json(url) + # games = {} + if not player_games: + logger.debug(f"Пустой ответ от API для игрока {player_id}") + return {player_id: default_player_stats_season()} + + for i in player_games.get("items"): + i["season"] = season + return {player_games.get("items")} + + +def Player_all_game2(player_id: str) -> dict: + url = f"{URL}api/abc/players/info?tag={LEAGUE}&id={player_id}" + player_seasons = get_json(url) + + if not player_seasons: + logger.debug(f"Пустой ответ от API для игрока {player_id}") + return {player_id: default_player_stats_season()} + + seasons = player_seasons.get("result").get("seasons") + player_game = [] + with ThreadPoolExecutor() as pool: + player_season_stat_temp = [ + pool.submit(Player_all_game_in_season, player_id, season["id"]) + for season in seasons + ] + for i in player_season_stat_temp: + print(i.result()) + player_game += [ + res.result() for res in player_season_stat_temp + ] + rewrite_file(player_id, player_game) + + + +def Player_all_game_in_season(player_id: str, season: str) -> List[Dict[str, Any]]: + url = f"{URL}api/abc/players/stats?tag={LEAGUE}&season={season}&id={player_id}" + player_games = get_json(url) + if not player_games: + logger.debug(f"Пустой ответ от API для игрока {player_id}, сезон {season}") + return [ + ] # возвращаем пустой список, чтобы тип был стабилен + + items = player_games.get("items") or [] + # гарантируем список словарей + if not isinstance(items, list): + logger.warning(f"Неверный формат 'items' для {player_id}, сезон {season}: {type(items)}") + return [] + + for it in items: + if isinstance(it, dict): + it["season"] = season + return items + + +def Player_all_game(player_id: str) -> List[Dict[str, Any]]: + # url = f"{URL}api/abc/players/info?tag={LEAGUE}&id={player_id}" + # player_seasons = get_json(url) + + # if not player_seasons: + # logger.debug(f"Пустой ответ от API для игрока {player_id}") + # return [] # последовательный тип + + # result = player_seasons.get("result") or {} + # seasons = result.get("seasons") or [] + seasons = [ + {"id": 2026}, + {"id": 2025}, + {"id": 2024}, + {"id": 2023}, + {"id": 2022}, + {"id": 2021}, + {"id": 2020}, + {"id": 2019}, + {"id": 2018}, + {"id": 2017}, + {"id": 2016}, + {"id": 2015}, + {"id": 2014}, + {"id": 2013}, + {"id": 2012}, + {"id": 2011}, + {"id": 2010}, + ] + if not isinstance(seasons, list) or not seasons: + logger.debug(f"Нет сезонов для игрока {player_id}") + return [] + + all_games: List[Dict[str, Any]] = [] + with ThreadPoolExecutor() as pool: + futures = [pool.submit(Player_all_game_in_season, player_id, s.get("id")) for s in seasons if s.get("id")] + for fut in as_completed(futures): + try: + items = fut.result() # это уже список словарей + all_games.extend(items) + except Exception as e: + logger.exception(f"Ошибка при сборе игр игрока {player_id}: {e}") + + # если нужно писать на диск — пишем уже готовый JSON-совместимый список + rewrite_file(player_id, all_games) + return all_games + + +def default_player_stats_season() -> list: + empty_stats = { + "games": 0, + "isStarts": "", + "points": "", + "goal2": "", + "shot2": "", + "goal3": "", + "shot3": "", + "goal1": "", + "shot1": "", + "goal23": "", + "shot23": "", + "shot2Percent": "", + "shot3Percent": "", + "shot23Percent": "", + "shot1Percent": "", + "assist": "", + "pass": "", + "steal": "", + "blockShot": "", + "blockedOwnShot": "", + "defRebound": "", + "offRebound": "", + "rebound": "", + "foulsOnPlayer": "", + "turnover": "", + "foul": "", + "second": 0, + "playedTime": "", + "dunk": "", + "fastBreak": "", + "plusMinus": None, + } + + return [ + {"team": None, "game": None, "stats": empty_stats.copy(), "class": "Sum"}, + {"team": None, "game": None, "stats": empty_stats.copy(), "class": "Avg"}, + ] + + +def default_player_stats() -> list: + empty_stats = { + "games": 0, + "isStarts": "", + "points": "", + "goal2": "", + "shot2": "", + "goal3": "", + "shot3": "", + "goal1": "", + "shot1": "", + "goal23": "", + "shot23": "", + "shot2Percent": "", + "shot3Percent": "", + "shot23Percent": "", + "shot1Percent": "", + "assist": "", + "pass": "", + "steal": "", + "blockShot": "", + "blockedOwnShot": "", + "defRebound": "", + "offRebound": "", + "rebound": "", + "foulsOnPlayer": "", + "turnover": "", + "foul": "", + "second": 0, + "playedTime": "", + "dunk": "", + "fastBreak": "", + "plusMinus": None, + } + + return [ + {"season": None, "team": None, "stats": empty_stats.copy(), "class": "Sum"}, + {"season": None, "team": None, "stats": empty_stats.copy(), "class": "Avg"}, + ] + + +def Player_Stat_Career(player_id: str) -> dict: + url = f"{URL}api/abc/players/career?teamId=0&Tag={LEAGUE}&Id={player_id}" + player_stat_career = get_json(url) + + if not player_stat_career: + logger.debug(f"Пустой ответ от API для игрока {player_id}") + return {player_id: default_player_stats()} + + items = player_stat_career.get("items") + if items: + logger.debug(f"Данные за карьеру игрока {player_id} успешно получены.") + return {player_id: items[-2:]} # последние два сезона (Sum и Avg) + + logger.debug(f"Данные на игрока {player_id} не найдены. Вероятно, новичок.") + return {player_id: default_player_stats()} + + +def Coach_Stat(coach_id: str, season: str, team_id: str) -> dict | None: + url = f"{URL}api/abc/coaches/career?teamId={team_id}&tag={LEAGUE}&season={season}&Id={coach_id}" + coach_stat = get_json(url) + + if not coach_stat: + logger.debug(f"Пустой ответ от API для тренера {coach_id}") + return None + + items = coach_stat.get("items") + if items: + logger.debug(f"Данные за карьеру тренера {coach_id} успешно получены.") + return {coach_id: items} + + logger.debug(f"Данные для тренера {coach_id} не найдены. Возможно, он новичок.") + return None + + +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 + + +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 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 + + +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(stop_event: threading.Event) -> None: + """ + Обновляет файл team_stats.json, содержащий сравнение двух команд. + + Аргументы: + stop_event (threading.Event): Событие для остановки цикла. + """ + logger.info("START making json for team statistics") + global game_online_data + + while not stop_event.is_set(): + with game_online_lock: + game_data = game_online_data + + if not game_data: + time.sleep(1) + continue + + try: + teams = game_data["result"]["teams"] + plays = game_data["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_ONLINE) + continue + + # Таймауты + 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") + stop_event.wait(TIMEOUT_ONLINE) + continue + + # Форматирование общей статистики (как и было) + 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, + ) + + # # Форматирование общей статистики + # 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, + } + ) + + rewrite_file("team_stats", result_json) + logger.debug("Успешно записаны данные в team_stats.json") + + except Exception as e: + logger.error( + f"Ошибка при обработке командной статистики: {e}", exc_info=True + ) + + stop_event.wait(TIMEOUT_ONLINE) + + +def Referee(stop_event: threading.Event) -> None: + """ + Поток, создающий JSON-файл с информацией о судьях матча. + """ + logger.info("START making json for referee") + global game_online_data + + desired_order = [ + "Crew chief", + "Referee 1", + "Referee 2", + "Commissioner", + "Ст.судья", + "Судья 1", + "Судья 2", + "Комиссар", + ] + + while not stop_event.is_set(): + with game_online_lock: + game_data = game_online_data + + if not game_data: + stop_event.wait(TIMEOUT_ONLINE) + continue + try: + # Найти судей (teamNumber == 0) + team_ref = next( + (t for t in game_data["result"]["teams"] if t["teamNumber"] == 0), None + ) + if not team_ref: + logger.warning("Не найдена судейская бригада в данных.") + stop_event.wait(TIMEOUT_DATA_OFF) + continue + + 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) + ), + ) + + rewrite_file("referee", referees) + logger.debug("Успешно записаны судьи в файл") + + except Exception as e: + logger.error(f"Ошибка в Referee потоке: {e}", exc_info=True) + + stop_event.wait(TIMEOUT_DATA_OFF) + + +def Scores_Quarter(stop_event: threading.Event) -> None: + """ + Поток, обновляющий JSON со счётом по четвертям. + """ + logger.info("START making json for scores quarter") + global game_online_data + + quarters = ["Q1", "Q2", "Q3", "Q4", "OT1", "OT2", "OT3", "OT4"] + + while not stop_event.is_set(): + try: + with game_online_lock: + game_data = game_online_data + + if not game_data: + stop_event.wait(FETCH_INTERVAL) + continue + + rewrite_file("game_online", game_data) + + score_by_quarter = [{"Q": q, "score1": "", "score2": ""} for q in quarters] + + # Сначала пробуем fullScore + full_score_str = ( + game_data.get("result", {}).get("game", {}).get("fullScore", "") + ) + if full_score_str: + full_score_list = full_score_str.split(",") + for i, score_str in enumerate(full_score_list[: len(score_by_quarter)]): + parts = score_str.split(":") + if len(parts) == 2: + score_by_quarter[i]["score1"] = parts[0] + score_by_quarter[i]["score2"] = parts[1] + logger.debug("Счёт по четвертям получен из fullScore.") + + # Если нет fullScore, пробуем scoreByPeriods + elif "scoreByPeriods" in game_data.get("result", {}): + periods = game_data["result"]["scoreByPeriods"] + for i, score in enumerate(periods[: len(score_by_quarter)]): + score_by_quarter[i]["score1"] = str(score.get("score1", "")) + score_by_quarter[i]["score2"] = str(score.get("score2", "")) + logger.debug("Счёт по четвертям получен из scoreByPeriods.") + + else: + logger.debug("Нет данных по счёту, сохраняем пустые значения.") + + rewrite_file("scores", score_by_quarter) + + except Exception as e: + logger.error(f"Ошибка в Scores_Quarter: {e}", exc_info=True) + + stop_event.wait(TIMEOUT_ONLINE) + + +def status_online_func(data: dict) -> dict | None: + """ + Получает онлайн-статус игры и возвращает данные + путь к PNG-фолам. + """ + global URL + try: + game_id = data["game_id"] + url = f"{URL}api/abc/games/live-status?id={game_id}" + json_live_status = get_json(url) + + if json_live_status.get("status") != "Ok": + logger.warning(f"Live status API вернул не 'Ok': {json_live_status}") + return None + + status_data = json_live_status["result"] + path_to_png = ( + r"D:\ГРАФИКА\БАСКЕТБОЛ\ЕДИНАЯ ЛИГА ВТБ 2022-2023\Scorebug Indicators" + ) + + fouls_a = min(status_data.get("foulsA", 0), 5) + fouls_b = min(status_data.get("foulsB", 0), 5) + + status_data["foulsA_png"] = f"{path_to_png}\\Home_{fouls_a}.png" + status_data["foulsB_png"] = f"{path_to_png}\\Away_{fouls_b}.png" + + return status_data + + except Exception as e: + logger.error(f"Ошибка в status_online_func: {e}", exc_info=True) + return None + + +def Status_Online(data: dict, stop_event: threading.Event) -> None: + """ + Поток, обновляющий JSON-файл с онлайн-статусом матча. + """ + logger.info("START making json for status online") + global game_status_data + + while not stop_event.is_set(): + try: + result = status_online_func(data) + if result: + with game_status_lock: + game_status_data = result + rewrite_file("live_status", [game_status_data]) + logger.debug("Успешно записан онлайн-статус в файл.") + else: + logger.warning("status_online_func вернула None — пропуск записи.") + except Exception as e: + logger.error(f"Ошибка в Status_Online: {e}", exc_info=True) + + stop_event.wait(TIMEOUT_ONLINE) + + +def Play_By_Play(data: dict, stop_event: threading.Event) -> None: + """ + Поток, обновляющий JSON-файл с последовательностью бросков в матче. + """ + logger.info("START making json for play-by-play") + global game_online_data, LEAGUE + + while not stop_event.is_set(): + try: + with game_online_lock: + game_data = game_online_data + + if not game_data: + logger.debug("game_online_data отсутствует") + stop_event.wait(TIMEOUT_DATA_OFF) + continue + + teams = game_data.get("result", {}).get("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") + stop_event.wait(TIMEOUT_DATA_OFF) + continue + + team1_name = data["team1"] + team2_name = data["team2"] + 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("result", {}).get("plays", []) + if not plays: + logger.debug("нет данных в play-by-play") + stop_event.wait(TIMEOUT_DATA_OFF) + continue + + # Получение текущего времени игры + url = f"{URL}api/abc/games/live-status?id={data['game_id']}" + json_live_status = get_json(url) + last_event = plays[-1] + + if not json_live_status or json_live_status.get("message") == "Not Found": + 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") + stop_event.wait(TIMEOUT_DATA_OFF) + continue + + # Расчёты по очкам и времени + 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 = FOLDER_JSON + os.makedirs(directory, exist_ok=True) + # ip_address = get_ip_address() + # host = ip_check.get(ip_address, {}).get("host") + host_prefix = _ipcheck() + filepath = os.path.join(directory, f"{host_prefix}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) + + stop_event.wait(TIMEOUT_ONLINE) + + +def schedule_daily_restart(): + """Перезапуск get_season_and_schedule каждый день в 00:05 (только не на Windows).""" + if not sys.platform.startswith("win"): + while True: + now = datetime.now() + next_run = (now + timedelta(days=1)).replace(hour=0, minute=5, second=0, microsecond=0) + sleep_time = (next_run - now).total_seconds() + logger.info(f"Следующий перезапуск get_season_and_schedule запланирован на {next_run.strftime('%Y-%m-%d %H:%M')}") + time.sleep(sleep_time) + + try: + logger.info("⏰ Автоматический перезапуск get_season_and_schedule()") + get_season_and_schedule() + except Exception as e: + logger.error(f"Ошибка при автоматическом перезапуске: {e}", exc_info=True) + + +def clean_np_ints(obj): + if isinstance(obj, dict): + return {k: clean_np_ints(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [clean_np_ints(v) for v in obj] + elif isinstance(obj, np.integer): + return int(obj) + else: + return obj + + +def get_season_and_schedule() -> dict | None: + """ + Получает текущий сезон и ближайший сыгранный матч для команды. + + Использует глобальные переменные: URL, LEAGUE, LANG, TEAM. + + Returns: + dict | None: Словарь с данными матча или None при ошибке. + """ + global URL, LEAGUE, LANG, TEAM + + + try: + # Получение активного сезона + season_url = f"{URL}api/abc/comps/seasons?Tag={LEAGUE}&Lang={LANG}" + season_data = get_json(season_url) + + season = ( + season_data.get("result", [{}])[0].get("season") if season_data else None + ) + print(season) + if not season: + logger.warning("Сезон не найден в данных.") + return None + + # season = 2025 + # Получение расписания + schedule_url = f"{URL}api/abc/comps/calendar?Tag={LEAGUE}&Season={season}&Lang={LANG}&MaxResultCount=1000" + schedule_data = get_json(schedule_url) + rewrite_file("schedule", schedule_data) + + items = schedule_data.get("items") if schedule_data else None + if not items: + logger.warning("Расписание не содержит игр.") + return None + + df = pd.json_normalize(items) + + # Преобразование и фильтрация + df["game.localDate"] = pd.to_datetime( + df["game.localDate"], format="%d.%m.%Y", errors="coerce" + ) + df["game.DateStr"] = df["game.localDate"].dt.strftime("%d.%m.%Y") + df["team1.name"] = df["team1.name"].str.strip() + + current_date = pd.to_datetime(datetime.now().date()) + + df_filtered = df[ + (df["game.localDate"] <= current_date) + & (df["team1.name"].str.lower() == TEAM.lower()) + ] + + if df_filtered.empty: + logger.warning("Нет подходящих матчей для команды на сегодня.") + return None + + last_game = df_filtered.iloc[-1] + + return clean_np_ints({ + "season": season, + "game_id": last_game["game.id"], + "team1_id": last_game["team1.teamId"], + "team2_id": last_game["team2.teamId"], + "team1": last_game["team1.name"], + "team2": last_game["team2.name"], + "when": last_game["game.DateStr"], + "time": last_game["game.localTime"], +}) + + except Exception as e: + logger.error(f"Ошибка при получении сезона или расписания: {e}", exc_info=True) + return None + + +def Standing_func(data: dict, stop_event: threading.Event) -> None: + logger.info("START making json for standings") + global URL, LEAGUE, LANG + + while not stop_event.is_set(): + try: + season = data["season"] + url = ( + f"{URL}api/abc/comps/actual-standings?tag={LEAGUE}&season={season}&lang={LANG}" + ) + data_standings = get_json(url) + + if data_standings and "items" in data_standings and data_standings["items"]: + standings_temp = data_standings["items"] + for item in standings_temp: + if "standings" in item and item["standings"] != []: + standings_temp = item["standings"] + df = pd.json_normalize(standings_temp) + del df["scores"] + if not df["totalWin"].isna().all(): + df["w_l"] = ( + df["totalWin"].astype(str) + + " / " + + df["totalDefeat"].astype(str) + ) + df["procent"] = df.apply( + lambda row: ( + 0 + if row["w_l"] == "0 / 0" + or row["totalGames"] == 0 + or pd.isna(row["totalWin"]) + else round( + row["totalWin"] * 100 / row["totalGames"] + + 0.000005 + ) + ), + axis=1, + ) + df["plus_minus"] = ( + df["totalGoalPlus"] - df["totalGoalMinus"] + ) + df["name"] = df["name"].replace("PBC Uralmash", "Uralmash") + directory = FOLDER_JSON + os.makedirs(directory, exist_ok=True) + host = _ipcheck() + filepath = os.path.join( + directory, + f"{host}standings_{LEAGUE}_{item['comp']['name'].replace(' ', '_')}.json", + ) + + df.to_json( + filepath, + orient="records", + force_ascii=False, + indent=4, + ) + logger.debug("Standings data saved successfully.") + elif "playoffPairs" in item and item["playoffPairs"] != []: + standings_temp = item["playoffPairs"] + df = pd.json_normalize(standings_temp) + directory = FOLDER_JSON + os.makedirs(directory, exist_ok=True) + host = _ipcheck() + filepath = os.path.join( + directory, + f"{host}standings_{LEAGUE}_{item['comp']['name'].replace(' ', '_')}.json", + ) + df.to_json( + filepath, + orient="records", + force_ascii=False, + indent=4, + ) + logger.debug("Standings data saved successfully.") + + except Exception as e: + logger.warning(f"Ошибка в турнирном положении: {e}") + + stop_event.wait(TIMEOUT_DATA_OFF) + + +def convert_numpy(obj): + if isinstance(obj, dict): + return {k: convert_numpy(v) for k, v in obj.items()} + elif isinstance(obj, list): + return [convert_numpy(v) for v in obj] + elif isinstance(obj, (np.integer, np.int64, np.int32)): + return int(obj) + elif isinstance(obj, (np.floating, np.float64, np.float32)): + return float(obj) + elif isinstance(obj, (np.bool_)): + return bool(obj) + elif isinstance(obj, (np.ndarray,)): + return obj.tolist() + return obj + + +def How_To_Play_Quarter(data: dict) -> None: + logger.info("START making json for How_To_Play_Quarter") + + global LEAGUE, LANG + game_id = data["game_id"] + team1_id, team2_id = data["team1_id"], data["team2_id"] + team1_name, team2_name = data["team1"], data["team2"] + season = data["season"] + + url = f"{URL}api/abc/comps/calendar?Tag={LEAGUE}&Season={season}&Lang={LANG}&MaxResultCount=1000" + schedule_data = get_json(url) + df_schedule = pd.json_normalize(schedule_data["items"]) + + df_schedule = df_schedule[ + [ + "game.id", + "ot", + "game.gameStatus", + "game.score1", + "game.score2", + "game.fullScore", + "team1.teamId", + "team1.name", + "team2.teamId", + "team2.name", + ] + ] + + df_schedule = df_schedule[ + (df_schedule["game.id"] < game_id) + & ( + df_schedule["team1.teamId"].isin([team1_id, team2_id]) + | df_schedule["team2.teamId"].isin([team1_id, team2_id]) + ) + ] + + QUARTERS = ["Q1", "Q2", "Q3", "Q4", "OT1", "OT2", "OT3", "OT4"] + + def parse_quarters(df): + temp_score_quarter = df["game.fullScore"].str.split(",") + df = df.copy() + for i in range(1, 9): + col = f"OT{i - 4}" if i > 4 else f"Q{i}" + df[col] = temp_score_quarter.apply( + lambda x: x[i - 1] if x and len(x) >= i else None + ) + return df + + def compute_results(df, team_id): + df = parse_quarters(df) + for q in QUARTERS: + + def result(row): + if pd.notna(row[q]) and ":" in row[q]: + score1, score2 = map(int, row[q].split(":")) + if row["team1.teamId"] == team_id: + return ( + "win" + if score1 > score2 + else "lose" if score1 < score2 else "draw" + ) + elif row["team2.teamId"] == team_id: + return ( + "win" + if score2 > score1 + else "lose" if score2 < score1 else "draw" + ) + return "" + + df[f"wld{q}"] = df.apply(result, axis=1) + df[f"win{q}"] = (df[f"wld{q}"] == "win").astype(int) + df[f"lose{q}"] = (df[f"wld{q}"] == "lose").astype(int) + df[f"draw{q}"] = (df[f"wld{q}"] == "draw").astype(int) + return df + + def compute_scores(df, team_id): + df = parse_quarters(df) + for q in QUARTERS: + + def score(row): + if pd.notna(row[q]) and ":" in row[q]: + score1, score2 = map(int, row[q].split(":")) + if row["team1.teamId"] == team_id: + return score1 + elif row["team2.teamId"] == team_id: + return score2 + return None + + df[f"score{q}"] = df.apply(score, axis=1) + return df + + df_team1 = compute_results(df_schedule.copy(), team1_id) + df_team2 = compute_results(df_schedule.copy(), team2_id) + df_scores1 = compute_scores(df_schedule.copy(), team1_id) + df_scores2 = compute_scores(df_schedule.copy(), team2_id) + + def aggregate_data(team_name, df_result, df_score): + team_stats = {"team": team_name} + for q in QUARTERS: + team_stats[f"win{q}"] = df_result[f"win{q}"].sum() + team_stats[f"lose{q}"] = df_result[f"lose{q}"].sum() + team_stats[f"draw{q}"] = df_result[f"draw{q}"].sum() + team_stats[f"score{q}"] = int(df_score[f"score{q}"].sum()) + team_stats[f"score_avg{q}"] = ( + round(df_score[f"score{q}"].mean(), 1) + if not df_score[f"score{q}"].isna().all() + else None + ) + return team_stats + + schedule_json_quarter = [ + aggregate_data(team1_name, df_team1, df_scores1), + aggregate_data(team2_name, df_team2, df_scores2), + ] + json_ready_data = convert_numpy(schedule_json_quarter) + rewrite_file("scores_quarter", json_ready_data) + + +def pregame_data(data: dict) -> None: + logger.info("START making json for pregame_data") + + global LEAGUE, LANG + game_id = data["game_id"] + team1_id, team2_id = data["team1_id"], data["team2_id"] + team1_name, team2_name = data["team1"], data["team2"] + season = data["season"] + + teams = [] + for team_id in (team1_id, team2_id): + url = f"{URL}api/abc/teams/stats?Tag={LEAGUE}&Season={season}&Id={team_id}" + team_stat = get_json(url) + data_team = team_stat["result"]["totalStats"] + data_team["team"] = team_stat["result"]["team"]["name"] + data_team["games"] = team_stat["result"]["games"] + temp_team = { + "team": data_team["team"], + "games": data_team["games"], + "points": round((data_team["points"] / data_team["games"]), 1), + "points_2": round((data_team["goal2"] * 100 / data_team["shot2"]), 1), + "points_3": round((data_team["goal3"] * 100 / data_team["shot3"]), 1), + "points_23": round((data_team["goal23"] * 100 / data_team["shot23"]), 1), + "points_1": round((data_team["goal1"] * 100 / data_team["shot1"]), 1), + "assists": round((data_team["assist"] / data_team["games"]), 1), + "rebounds": round(((data_team["defRebound"] + data_team["offRebound"]) / data_team["games"]), 1), + "steals": round((data_team["steal"] / data_team["games"]), 1), + "turnovers": round((data_team["turnover"] / data_team["games"]), 1), + "blocks": round((data_team["blockShot"] / data_team["games"]), 1), + "fouls": round((data_team["foul"] / data_team["games"]), 1), + + } + teams.append(temp_team) + rewrite_file("team_comparison", teams) + + +def main(): + global TEAM, LANG, URL + + if TEAM is None: + logger.critical(f"{myhost}\nКоманда не указана") + time.sleep(1) + sys.exit(1) + + logger.info( + f"{myhost}\n!!!!!!!!СТАРТ!!!!!!!!! \nHOST: {TEAM}\nTAG: {LEAGUE} \nВерсия кода: {VERSION}" + ) + + if LANG is None: + LANG = next( + (tag["lang"] for tag in TAGS if tag["tag"].lower() == LEAGUE.lower()), "" + ) + + URL = ( + "https://basket.sportoteka.org/" + if "uba" in LEAGUE.lower() + # else "https://pro.russiabasket.org/" + else "https://vtb-league.org/" + ) + + stop_event = Event() + data = get_season_and_schedule() + if not data: + logger.critical("Не удалось получить данные сезона/матча.") + sys.exit(1) + + # === Ежедневный перезапуск функции на Linux/Mac === + if not sys.platform.startswith("win"): + threading.Thread(target=schedule_daily_restart, daemon=True, name="ScheduleRestart").start() + + logger.info( + f"{myhost}\n{data['team1']} VS {data['team2']}\n{data['when']} {data['time']}" + ) + # logger.debug(data) + # data = { + # "season": 2026, + # "game_id": 921412, + # "team1_id":3059, + # "team2_id":682, + # "team1":"BETCITY PARMA", + # "team2":"Avtodor", + # } + + # Список потоков и их задач + threads = [ + threading.Thread( + target=game_online_loop, + args=(data["game_id"], stop_event), + name="OnlineLoop", + ), + threading.Thread( + target=Json_Team_Generation, + args=("team1", data, stop_event), + name="Team1JSON", + ), + # threading.Thread( + # target=Json_Team_Generation, + # args=("team2", data, stop_event), + # name="Team2JSON", + # ), + # threading.Thread( + # target=Team_Both_Stat, args=(stop_event,), name="BothTeamsStat" + # ), + # threading.Thread(target=Referee, args=(stop_event,), name="Referee"), + # threading.Thread( + # target=Scores_Quarter, args=(stop_event,), name="QuarterScore" + # ), + # threading.Thread( + # target=Status_Online, args=(data, stop_event), name="StatusOnline" + # ), + # threading.Thread( + # target=Play_By_Play, args=(data, stop_event), name="PlayByPlay" + # ), + # threading.Thread( + # target=Standing_func, args=(data, stop_event), name="Standings" + # ), + ] + + # Запуск всех потоков + for t in threads: + t.start() + logger.debug(f"Поток {t.name} запущен.") + + # How_To_Play_Quarter(data) + # pregame_data(data) + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + logger.info("Остановка по Ctrl+C... Завершение потоков.") + stop_event.set() + for t in threads: + t.join() + logger.debug(f"Поток {t.name} завершён.") + logger.info("Все потоки завершены.") + + +if __name__ == "__main__": + main() diff --git a/match_id.json b/match_id.json new file mode 100644 index 0000000..664d14f --- /dev/null +++ b/match_id.json @@ -0,0 +1,13 @@ +{ + "10.10.1.180": {"host": "", "tag": "vtb", "root": 1, "team": "cska"}, + "10.10.35.21": {"host": "gfx", "tag": "vtb", "root": 1, "team": "Lokomotiv Kuban"}, + "10.10.35.22": {"host": "krd", "tag": "vtb", "root": 1, "team": "Lokomotiv Kuban1"}, + "10.10.35.23": {"host": "ekb", "tag": "vtb", "root": 1, "team": "uralmash1"}, + "10.10.35.24": {"host": "per", "tag": "vtb", "root": 1, "team": "betcity parma1"}, + "10.10.35.25": {"host": "sar", "tag": "vtb", "root": 1, "team": "avtodor1"}, + "10.10.35.26": {"host": "spb", "tag": "vtb", "root": 1, "team": "zenit1"}, + "10.10.35.27": {"host": "sam", "tag": "vtb", "root": 1, "team": "samara1"}, + "10.10.35.28": {"host": "msk1", "tag": "vtb", "root": 1, "team": "mba-mai1"}, + "10.10.35.29": {"host": "msk2", "tag": "vtb", "root": 1, "team": "Pari Nizhny Novgorod1"}, + "10.10.35.30": {"host": "kaz", "tag": "vtb", "root": 1, "team": "unics1"} +} \ No newline at end of file