3171 lines
128 KiB
Python
3171 lines
128 KiB
Python
GBPLF GBP:F
|
||
|
||
|
||
|
||
|
||
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"
|
||
# --- режим однократного прогона для старого матча ---
|
||
RUN_ONCE = False # включится, если матч не Online
|
||
CURRENT_GAME_ID: int | None = None
|
||
|
||
|
||
def is_game_online(game_id: int) -> bool:
|
||
"""True, если матч в статусе Online (live-status). Возвращает False при любом нештатном ответе."""
|
||
global URL
|
||
try:
|
||
live = get_json(f"{URL}api/abc/games/live-status?id={game_id}")
|
||
|
||
if not isinstance(live, dict):
|
||
logger.warning(f"live-status: неожиданный тип ответа: {type(live).__name__}")
|
||
return False
|
||
|
||
status = live.get("status")
|
||
result = live.get("result") or {}
|
||
if not isinstance(result, dict):
|
||
logger.warning(f"live-status: 'result' не dict: {type(result).__name__}")
|
||
return False
|
||
print(status)
|
||
if status == "Not Found":
|
||
return False
|
||
|
||
game_status = result.get("gameStatus")
|
||
logger.debug(f"live-status raw: status={status}, gameStatus={game_status}")
|
||
|
||
return (status == "Ok") and (game_status == "Online")
|
||
|
||
except Exception as e:
|
||
logger.error(f"is_game_online: ошибка при запросе live-status для game_id={game_id}: {e}", exc_info=True)
|
||
return False
|
||
|
||
|
||
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()
|
||
# print(host_prefix)
|
||
# host_prefix = "spb_"
|
||
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, RUN_ONCE, CURRENT_GAME_ID
|
||
single_run = RUN_ONCE
|
||
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)
|
||
|
||
# --- однократный режим для старого матча ---
|
||
if single_run:
|
||
logger.info("game_online_loop: однократный режим — выхожу из потока.")
|
||
return
|
||
|
||
stop_event.wait(TIMEOUT_ONLINE)
|
||
|
||
|
||
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 get_carrer_high(player_id, name_stat):
|
||
try:
|
||
directory = FOLDER_JSON # type: ignore[name-defined]
|
||
except NameError:
|
||
# иначе используем аргумент по умолчанию/переданный
|
||
pass
|
||
|
||
os.makedirs(directory, exist_ok=True)
|
||
|
||
host_prefix = _ipcheck()
|
||
# print(host_prefix)
|
||
# host_prefix = "spb_"
|
||
filepath = os.path.join(directory, f"{host_prefix}{player_id}.json")
|
||
# print(filepath) # оставил как у тебя; можно заменить на logger.debug при желании
|
||
|
||
try:
|
||
with open(filepath, "r", encoding="utf-8") as f:
|
||
player_data = json.load(f)
|
||
max_points = 0
|
||
|
||
for item in player_data:
|
||
if item.get("class") == "Normal":
|
||
points = item["stats"].get(name_stat)
|
||
if points and points.isdigit(): # Проверяем, что значение числовое
|
||
points = int(points)
|
||
if points > max_points:
|
||
max_points = points
|
||
return max_points
|
||
except Exception as ex:
|
||
logger.warning(f"[{player_id}] {ex}")
|
||
return None
|
||
|
||
|
||
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, RUN_ONCE
|
||
initialized = False
|
||
did_write = False # 👈 флаг: была ли хотя бы одна успешная запись файлов
|
||
|
||
# 👇 В однократном режиме дождёмся, пока OnlineLoop положит данные (до 5 сек)
|
||
if RUN_ONCE:
|
||
t0 = time.time()
|
||
while not stop_event.is_set():
|
||
with game_online_lock:
|
||
if game_online_data is not None:
|
||
break
|
||
if time.time() - t0 > 5: # таймаут ожидания
|
||
logger.warning(f"Json_Team_Generation[{who}]: нет game_online_data >5с, попробую всё равно.")
|
||
break
|
||
time.sleep(0.05)
|
||
|
||
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 при старте
|
||
"AvgCarPoints": (
|
||
row_player_career_avg["points"]
|
||
if row_player_career_avg
|
||
and row_player_career_avg["points"] != ""
|
||
else "0.0"
|
||
),
|
||
"AvgCarAssist": (
|
||
row_player_career_avg["assist"]
|
||
if row_player_career_avg
|
||
and row_player_career_avg["assist"] != ""
|
||
else "0.0"
|
||
),
|
||
"AvgCarBlocks": (
|
||
row_player_career_avg["blockShot"]
|
||
if row_player_career_avg
|
||
and row_player_career_avg["blockShot"] != ""
|
||
else "0.0"
|
||
),
|
||
"AvgCarDefRebound": (
|
||
row_player_career_avg["defRebound"]
|
||
if row_player_career_avg
|
||
and row_player_career_avg["defRebound"] != ""
|
||
else "0.0"
|
||
),
|
||
"AvgCarOffRebound": (
|
||
row_player_career_avg["offRebound"]
|
||
if row_player_career_avg
|
||
and row_player_career_avg["offRebound"] != ""
|
||
else "0.0"
|
||
),
|
||
"AvgCarRebound": (
|
||
row_player_career_avg["rebound"]
|
||
if row_player_career_avg
|
||
and row_player_career_avg["rebound"] != ""
|
||
else "0.0"
|
||
),
|
||
"AvgCarSteal": (
|
||
row_player_career_avg["steal"]
|
||
if row_player_career_avg
|
||
and row_player_career_avg["steal"] != ""
|
||
else "0.0"
|
||
),
|
||
"AvgCarTurnover": (
|
||
row_player_career_avg["turnover"]
|
||
if row_player_career_avg
|
||
and row_player_career_avg["turnover"] != ""
|
||
else "0.0"
|
||
),
|
||
"AvgCarFoul": (
|
||
row_player_career_avg["foul"]
|
||
if row_player_career_avg
|
||
and row_player_career_avg["foul"] != ""
|
||
else "0.0"
|
||
),
|
||
"AvgCarOpponentFoul": (
|
||
row_player_career_avg["foulsOnPlayer"]
|
||
if row_player_career_avg
|
||
and row_player_career_avg["foulsOnPlayer"] != ""
|
||
else "0.0"
|
||
),
|
||
"AvgCarPlusMinus": (
|
||
row_player_career_avg["plusMinus"]
|
||
if row_player_career_avg
|
||
and row_player_career_avg["plusMinus"] != ""
|
||
else "0.0"
|
||
),
|
||
"AvgCarDunk": (
|
||
row_player_career_avg["dunk"]
|
||
if row_player_career_avg
|
||
and row_player_career_avg["dunk"] != ""
|
||
else "0.0"
|
||
),
|
||
"AvgCarKPI": "0.0",
|
||
"AvgCarPlayedTime": (
|
||
row_player_career_avg["playedTime"]
|
||
if row_player_career_avg
|
||
and row_player_career_avg["playedTime"] != ""
|
||
else "0:00"
|
||
),
|
||
"HeadCoachStatsCareer": HeadCoachStatsCareer,
|
||
"HeadCoachStatsTeam": HeadCoachStatsTeam,
|
||
"PTS_Career_High": get_carrer_high(item["personId"], "points"),
|
||
"AST_Career_High": get_carrer_high(item["personId"], "assist"),
|
||
"REB_Career_High": get_carrer_high(item["personId"], "rebound"),
|
||
"STL_Career_High": get_carrer_high(item["personId"], "steal"),
|
||
"BLK_Career_High": get_carrer_high(item["personId"], "blockShot"),
|
||
}
|
||
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)
|
||
# обычный цикл / однократный выход:
|
||
if RUN_ONCE:
|
||
logger.info(f"Json_Team_Generation[{who}]: однократный режим — файлы записаны, выхожу.")
|
||
return
|
||
did_write = True # 👈 отметили успешную запись
|
||
time.sleep(TIMEOUT_ONLINE)
|
||
else:
|
||
print(f"{who} НЕ ПОЛУЧАЕТСЯ ПРОЧИТАТЬ")
|
||
# --- однократный режим для старого матча ---
|
||
except Exception as e:
|
||
print(f"[{who}] Ошибка: {e}, {e.with_traceback()}")
|
||
if RUN_ONCE:
|
||
if did_write:
|
||
logger.info(f"Json_Team_Generation[{who}]: один успешный проход выполнен — выхожу.")
|
||
return
|
||
else:
|
||
# данных ещё не было — короткая задержка и ещё одна попытка
|
||
stop_event.wait(0.5)
|
||
continue
|
||
time.sleep(TIMEOUT_ONLINE)
|
||
|
||
|
||
def Player_Stat_Season(player_id: str, season: str) -> dict:
|
||
url = f"{URL.replace('pro.russiabasket.org', 'vtb-league.org')}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.replace('pro.russiabasket.org', 'vtb-league.org')}api/abc/players/stats?tag={LEAGUE}&season={season}&id={player_id}&Lang={LANG}"
|
||
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.replace('pro.russiabasket.org', 'vtb-league.org')}api/abc/players/info?tag={LEAGUE}&id={player_id}&Lang={LANG}"
|
||
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.replace('pro.russiabasket.org', 'vtb-league.org')}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, RUN_ONCE
|
||
|
||
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")
|
||
if RUN_ONCE:
|
||
logger.info("<Team_Both_Stat>: однократный режим — выхожу из потока.")
|
||
return
|
||
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, RUN_ONCE
|
||
|
||
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("Успешно записаны судьи в файл")
|
||
if RUN_ONCE:
|
||
logger.info("<Referee>: однократный режим — выхожу из потока.")
|
||
return
|
||
|
||
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, RUN_ONCE
|
||
|
||
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)
|
||
# --- однократный режим для старого матча ---
|
||
if RUN_ONCE:
|
||
logger.info("Scores_Quarter: однократный режим — выхожу из потока.")
|
||
return
|
||
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 {
|
||
"foulsA": 0,
|
||
"foulsB": 0,
|
||
}
|
||
|
||
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, RUN_ONCE
|
||
# «Снимок» режима на момент старта потока
|
||
single_run = RUN_ONCE
|
||
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)
|
||
# --- однократный режим для старого матча ---
|
||
if single_run:
|
||
logger.info("Status_Online: однократный режим — выхожу из потока.")
|
||
return
|
||
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":
|
||
if not json_live_status or json_live_status.get("status") == "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 в файл")
|
||
if RUN_ONCE:
|
||
logger.info("<Play_By_Play>: однократный режим — выхожу из потока.")
|
||
return
|
||
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 and "vtb" in URL
|
||
else (
|
||
season_data.get("items", [{}])[0].get("season")
|
||
if season_data and "pro.russiabasket" in URL
|
||
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, RUN_ONCE
|
||
|
||
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.")
|
||
if RUN_ONCE:
|
||
logger.info("<Standing_func>: однократный режим — выхожу из потока.")
|
||
return
|
||
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, RUN_ONCE, CURRENT_GAME_ID
|
||
|
||
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>"
|
||
)
|
||
CURRENT_GAME_ID = data["game_id"] # запомним глобально id матча
|
||
# если матч НЕ онлайн — включаем однократный прогон
|
||
# RUN_ONCE = is_game_online(CURRENT_GAME_ID)
|
||
when_str = data.get("when", "").strip()
|
||
game_dt = datetime.strptime(when_str, "%d.%m.%Y")
|
||
now_date = datetime.now().date()
|
||
RUN_ONCE = True if game_dt.date() < now_date else False
|
||
if RUN_ONCE:
|
||
logger.info("Матч не Online/уже завершён — запускаю потоки в режиме ОДНОГО прохода.")
|
||
else:
|
||
logger.info("Матч в Online — работаем в циклах обновления.")
|
||
# 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("Все потоки завершены.")
|
||
|
||
try:
|
||
if RUN_ONCE:
|
||
# 🔚 Однократный режим: ждём завершения и выходим
|
||
for t in threads:
|
||
t.join()
|
||
logger.debug(f"Поток {t.name} завершён.")
|
||
logger.info("Однократный режим: всё готово, выхожу.")
|
||
return
|
||
|
||
# Онлайн-режим: держим процесс живым
|
||
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()
|