Files
RFB/get_data.py

2883 lines
115 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: <b>{TEAM}</b>\nTAG: <b>{LEAGUE}</b> \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<b>{data['team1']}</b> VS {data['team2']}\n<i>{data['when']} {data['time']}</i>"
)
# 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()