This commit is contained in:
2025-10-22 10:40:32 +00:00
24 changed files with 720 additions and 3691605 deletions

View File

@@ -111,6 +111,38 @@ _GAME_CACHE_LOCK = Lock()
THROTTLE_OLD_MINUTES = 30 THROTTLE_OLD_MINUTES = 30
FOLDER_JSON = "JSON" if sys.platform.startswith("win") else "static" 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(): def get_ip_address():
@@ -126,6 +158,7 @@ def get_ip_address():
ip_address = socket.gethostbyname(socket.gethostname()) ip_address = socket.gethostbyname(socket.gethostname())
return ip_address return ip_address
def read_match_id_json(path="match_id.json", attempts=10, delay=0.2): def read_match_id_json(path="match_id.json", attempts=10, delay=0.2):
"""Надёжное чтение match_id.json с ретраями при EBUSY/битом JSON.""" """Надёжное чтение match_id.json с ретраями при EBUSY/битом JSON."""
d = delay d = delay
@@ -137,17 +170,22 @@ def read_match_id_json(path="match_id.json", attempts=10, delay=0.2):
return json.load(f) return json.load(f)
except json.JSONDecodeError: except json.JSONDecodeError:
# файл переписывают — подождём и попробуем снова # файл переписывают — подождём и попробуем снова
time.sleep(d); d = min(d*1.6, 2.0) time.sleep(d)
d = min(d * 1.6, 2.0)
except OSError as e: except OSError as e:
# EBUSY (errno=16) — подождём и ещё раз # EBUSY (errno=16) — подождём и ещё раз
if getattr(e, "errno", None) == errno.EBUSY: if getattr(e, "errno", None) == errno.EBUSY:
time.sleep(d); d = min(d*1.6, 2.0) time.sleep(d)
d = min(d * 1.6, 2.0)
continue continue
# иные ошибки — пробрасываем дальше # иные ошибки — пробрасываем дальше
raise raise
logger.error("Не удалось прочитать match_id.json после нескольких попыток; возвращаю {}") logger.error(
"Не удалось прочитать match_id.json после нескольких попыток; возвращаю {}"
)
return {} return {}
# === Аргументы командной строки === # === Аргументы командной строки ===
parser = argparse.ArgumentParser(description="VTB Data Fetcher") parser = argparse.ArgumentParser(description="VTB Data Fetcher")
parser.add_argument("--league", type=str, default="vtb", help="League tag") parser.add_argument("--league", type=str, default="vtb", help="League tag")
@@ -206,7 +244,6 @@ def _ipcheck() -> str:
ip_str = get_ip_address() ip_str = get_ip_address()
except Exception: except Exception:
ip_str = None ip_str = None
ip_map = globals().get("ip_check") or {} ip_map = globals().get("ip_check") or {}
if ip_str and isinstance(ip_map, dict): if ip_str and isinstance(ip_map, dict):
host = (ip_map.get(ip_str) or {}).get("host") host = (ip_map.get(ip_str) or {}).get("host")
@@ -231,7 +268,8 @@ def rewrite_file(filename: str, data: dict, directory: str = "JSON") -> None:
os.makedirs(directory, exist_ok=True) os.makedirs(directory, exist_ok=True)
host_prefix = _ipcheck() host_prefix = _ipcheck()
# print(host_prefix)
# host_prefix = "spb_"
filepath = os.path.join(directory, f"{host_prefix}{filename}.json") filepath = os.path.join(directory, f"{host_prefix}{filename}.json")
# print(filepath) # оставил как у тебя; можно заменить на logger.debug при желании # print(filepath) # оставил как у тебя; можно заменить на logger.debug при желании
@@ -365,21 +403,22 @@ def Game_Online(game_id: int) -> dict | None:
if ( if (
cached cached
and cached.get("mode") == "old" and cached.get("mode") == "old"
and (now - cached.get("ts", now)) < timedelta(minutes=OLD_GAME_THROTTLE_MINUTES) and (now - cached.get("ts", now))
< timedelta(minutes=OLD_GAME_THROTTLE_MINUTES)
): ):
return cached.get("data") return cached.get("data")
def build_url(endpoint: str) -> str: def build_url(endpoint: str) -> str:
return f"{URL}api/abc/games/{endpoint}?Id={game_id}&Lang={LANG}" return f"{URL}api/abc/games/{endpoint}?Id={game_id}&Lang={LANG}"
box_score = get_json(build_url("box-score")) box_score = get_json(build_url("box-score"))
print(box_score) print(box_score)
if not box_score or box_score.get("status") != "Ok": if not box_score or box_score.get("status") != "Ok":
# Проверим — матч сейчас online? # Проверим — матч сейчас online?
live = get_json(f"{URL}api/abc/games/live-status?id={game_id}") live = get_json(f"{URL}api/abc/games/live-status?id={game_id}")
is_online = bool( is_online = bool(
live and live.get("status") == "Ok" live
and live.get("status") == "Ok"
and live.get("result", {}).get("gameStatus") == "Online" and live.get("result", {}).get("gameStatus") == "Online"
) )
@@ -391,7 +430,9 @@ def Game_Online(game_id: int) -> dict | None:
with _GAME_CACHE_LOCK: with _GAME_CACHE_LOCK:
_GAME_CACHE[cache_key] = {"mode": "old", "ts": now, "data": game} _GAME_CACHE[cache_key] = {"mode": "old", "ts": now, "data": game}
else: else:
logger.warning(f"Не удалось получить данные старого матча: game_id={game_id}") logger.warning(
f"Не удалось получить данные старого матча: game_id={game_id}"
)
return game return game
# 1. Получаем box score # 1. Получаем box score
@@ -477,8 +518,8 @@ def game_online_loop(game_id: int, stop_event: threading.Event) -> None:
game_id (int): ID игры. game_id (int): ID игры.
stop_event (threading.Event): Событие для остановки цикла. stop_event (threading.Event): Событие для остановки цикла.
""" """
global game_online_data global game_online_data, RUN_ONCE, CURRENT_GAME_ID
single_run = RUN_ONCE
while not stop_event.is_set(): while not stop_event.is_set():
try: try:
data = Game_Online(game_id) data = Game_Online(game_id)
@@ -490,7 +531,12 @@ def game_online_loop(game_id: int, stop_event: threading.Event) -> None:
except Exception as e: except Exception as e:
logger.error(f"Ошибка в game_online_loop: {e}", exc_info=True) logger.error(f"Ошибка в game_online_loop: {e}", exc_info=True)
stop_event.wait(TIMEOUT_ONLINE) # Лучше чем time.sleep — можно остановить сразу # --- однократный режим для старого матча ---
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: def coach_team_stat(data: list[dict], team_id: int) -> dict:
@@ -620,7 +666,6 @@ def calc_shot_percent_by_type(
goal_key = f"goal{t}" goal_key = f"goal{t}"
shot_key = f"shot{t}" shot_key = f"shot{t}"
sum_goal_raw = sum_stat.get(goal_key) sum_goal_raw = sum_stat.get(goal_key)
sum_shot_raw = sum_stat.get(shot_key) sum_shot_raw = sum_stat.get(shot_key)
item_goal_raw = item_stats.get(goal_key) item_goal_raw = item_stats.get(goal_key)
@@ -700,10 +745,56 @@ def sum_stat_with_online(
return base + online_val 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): def Json_Team_Generation(who, data, stop_event):
logger.info(f"START making json for {data[who]}, {data[f'{who}_id']}") logger.info(f"START making json for {data[who]}, {data[f'{who}_id']}")
global game_online_data global game_online_data, RUN_ONCE
initialized = False 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(): while not stop_event.is_set():
try: try:
@@ -732,7 +823,8 @@ def Json_Team_Generation(who, data, stop_event):
json_live_status = get_json(url) json_live_status = get_json(url)
online = ( online = (
True True
if json_live_status and "status" in json_live_status if json_live_status
and "status" in json_live_status
and json_live_status["status"] == "Ok" and json_live_status["status"] == "Ok"
and json_live_status["result"]["gameStatus"] == "Online" and json_live_status["result"]["gameStatus"] == "Online"
else False else False
@@ -756,38 +848,40 @@ def Json_Team_Generation(who, data, stop_event):
player_season_stat = [] player_season_stat = []
player_career_stat = [] player_career_stat = []
coach_stat = [] coach_stat = []
# with ThreadPoolExecutor() as pool: with ThreadPoolExecutor() as pool:
# player_season_stat_temp = [ player_season_stat_temp = [
# pool.submit(Player_Stat_Season, player_id, data["season"]) pool.submit(Player_Stat_Season, player_id, data["season"])
# for player_id in player_ids for player_id in player_ids
# ] ]
# player_career_stat_temp = [ player_career_stat_temp = [
# pool.submit(Player_Stat_Career, player_id) pool.submit(Player_Stat_Career, player_id)
# for player_id in player_ids for player_id in player_ids
# ] ]
# coach_stat_temp = [ coach_stat_temp = [
# pool.submit( pool.submit(
# Coach_Stat, coach_id, data["season"], data[f"{who}_id"] Coach_Stat, coach_id, data["season"], data[f"{who}_id"]
# ) )
# for coach_id in coach_ids for coach_id in coach_ids
# ] ]
# player_futures = [pool.submit(Player_all_game, pid) for pid in player_ids] player_futures = [
# all_players_games = [] pool.submit(Player_all_game, pid) for pid in player_ids
# for fut in as_completed(player_futures): ]
# try: all_players_games = []
# all_players_games.append(fut.result()) for fut in as_completed(player_futures):
# except Exception as e: try:
# logger.exception(f"Ошибка при обработке игрока: {e}") all_players_games.append(fut.result())
except Exception as e:
logger.exception(f"Ошибка при обработке игрока: {e}")
# player_season_stat += [ player_season_stat += [
# res.result() for res in player_season_stat_temp res.result() for res in player_season_stat_temp
# ] ]
# player_career_stat += [ player_career_stat += [
# res.result() for res in player_career_stat_temp res.result() for res in player_career_stat_temp
# ] ]
# coach_stat += [res.result() for res in coach_stat_temp] coach_stat += [res.result() for res in coach_stat_temp]
# initialized = True initialized = True
# print(coach_stat) # print(coach_stat)
# while not stop_event.is_set(): # while not stop_event.is_set():
role_list = [ role_list = [
@@ -810,7 +904,8 @@ def Json_Team_Generation(who, data, stop_event):
row_player_season = next( row_player_season = next(
( (
v v
for row in player_season_stat if row for row in player_season_stat
if row
for k, v in row.items() for k, v in row.items()
if k == item["personId"] if k == item["personId"]
), ),
@@ -819,7 +914,8 @@ def Json_Team_Generation(who, data, stop_event):
row_player_career = next( row_player_career = next(
( (
v v
for row in player_career_stat if row for row in player_career_stat
if row
for k, v in row.items() for k, v in row.items()
if k == item["personId"] if k == item["personId"]
), ),
@@ -829,7 +925,8 @@ def Json_Team_Generation(who, data, stop_event):
row_coach_stat = next( row_coach_stat = next(
( (
v v
for row in coach_stat if row for row in coach_stat
if row
for k, v in row.items() for k, v in row.items()
if k == item["personId"] if k == item["personId"]
), ),
@@ -1127,7 +1224,12 @@ def Json_Team_Generation(who, data, stop_event):
item["lastName"].strip() if item["lastName"] else "" item["lastName"].strip() if item["lastName"] else ""
), ),
"photoGFX": ( "photoGFX": (
os.path.join("D:\\Photos", LEAGUE, data[who], f"{item['displayNumber']}.png") os.path.join(
"D:\\Photos",
LEAGUE,
data[who],
f"{item['displayNumber']}.png",
)
if item["startRole"] == "Player" if item["startRole"] == "Player"
else "" else ""
), ),
@@ -1386,8 +1488,92 @@ def Json_Team_Generation(who, data, stop_event):
"CareerTStartCount": sum_stat_with_online( "CareerTStartCount": sum_stat_with_online(
"isStarts", row_player_career_sum, item["stats"], online "isStarts", row_player_career_sum, item["stats"], online
), # если нужно, можно +1 при старте ), # если нужно, можно +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, "HeadCoachStatsCareer": HeadCoachStatsCareer,
"HeadCoachStatsTeam": HeadCoachStatsTeam, "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) team.append(player)
count_player = sum(1 for x in team if x["startRole"] == "Player") count_player = sum(1 for x in team if x["startRole"] == "Player")
@@ -1479,16 +1665,30 @@ def Json_Team_Generation(who, data, stop_event):
reverse=False, reverse=False,
) )
rewrite_file(f"started_{who}", started_team) 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) time.sleep(TIMEOUT_ONLINE)
else: else:
print(f"{who} НЕ ПОЛУЧАЕТСЯ ПРОЧИТАТЬ") print(f"{who} НЕ ПОЛУЧАЕТСЯ ПРОЧИТАТЬ")
# --- однократный режим для старого матча ---
except Exception as e: except Exception as e:
print(f"[{who}] Ошибка: {e}, {e.with_traceback()}") 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) time.sleep(TIMEOUT_ONLINE)
def Player_Stat_Season(player_id: str, season: str) -> dict: def Player_Stat_Season(player_id: str, season: str) -> dict:
url = f"{URL}api/abc/players/stats?teamId=0&Tag={LEAGUE}&season={season}&Id={player_id}" 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) player_stat_season = get_json(url)
if not player_stat_season: if not player_stat_season:
@@ -1538,25 +1738,23 @@ def Player_all_game2(player_id: str) -> dict:
] ]
for i in player_season_stat_temp: for i in player_season_stat_temp:
print(i.result()) print(i.result())
player_game += [ player_game += [res.result() for res in player_season_stat_temp]
res.result() for res in player_season_stat_temp
]
rewrite_file(player_id, player_game) rewrite_file(player_id, player_game)
def Player_all_game_in_season(player_id: str, season: str) -> List[Dict[str, Any]]: def Player_all_game_in_season(player_id: str, season: str) -> List[Dict[str, Any]]:
url = f"{URL}api/abc/players/stats?tag={LEAGUE}&season={season}&id={player_id}" 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) player_games = get_json(url)
if not player_games: if not player_games:
logger.debug(f"Пустой ответ от API для игрока {player_id}, сезон {season}") logger.debug(f"Пустой ответ от API для игрока {player_id}, сезон {season}")
return [ return [] # возвращаем пустой список, чтобы тип был стабилен
] # возвращаем пустой список, чтобы тип был стабилен
items = player_games.get("items") or [] items = player_games.get("items") or []
# гарантируем список словарей # гарантируем список словарей
if not isinstance(items, list): if not isinstance(items, list):
logger.warning(f"Неверный формат 'items' для {player_id}, сезон {season}: {type(items)}") logger.warning(
f"Неверный формат 'items' для {player_id}, сезон {season}: {type(items)}"
)
return [] return []
for it in items: for it in items:
@@ -1566,41 +1764,45 @@ def Player_all_game_in_season(player_id: str, season: str) -> List[Dict[str, Any
def Player_all_game(player_id: str) -> List[Dict[str, Any]]: def Player_all_game(player_id: str) -> List[Dict[str, Any]]:
# url = f"{URL}api/abc/players/info?tag={LEAGUE}&id={player_id}" 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) player_seasons = get_json(url)
# if not player_seasons: if not player_seasons:
# logger.debug(f"Пустой ответ от API для игрока {player_id}") logger.debug(f"Пустой ответ от API для игрока {player_id}")
# return [] # последовательный тип return [] # последовательный тип
# result = player_seasons.get("result") or {} result = player_seasons.get("result") or {}
# seasons = result.get("seasons") or [] seasons = result.get("seasons") or []
seasons = [ # seasons = [
{"id": 2026}, # {"id": 2026},
{"id": 2025}, # {"id": 2025},
{"id": 2024}, # {"id": 2024},
{"id": 2023}, # {"id": 2023},
{"id": 2022}, # {"id": 2022},
{"id": 2021}, # {"id": 2021},
{"id": 2020}, # {"id": 2020},
{"id": 2019}, # {"id": 2019},
{"id": 2018}, # {"id": 2018},
{"id": 2017}, # {"id": 2017},
{"id": 2016}, # {"id": 2016},
{"id": 2015}, # {"id": 2015},
{"id": 2014}, # {"id": 2014},
{"id": 2013}, # {"id": 2013},
{"id": 2012}, # {"id": 2012},
{"id": 2011}, # {"id": 2011},
{"id": 2010}, # {"id": 2010},
] # ]
if not isinstance(seasons, list) or not seasons: if not isinstance(seasons, list) or not seasons:
logger.debug(f"Нет сезонов для игрока {player_id}") logger.debug(f"Нет сезонов для игрока {player_id}")
return [] return []
all_games: List[Dict[str, Any]] = [] all_games: List[Dict[str, Any]] = []
with ThreadPoolExecutor() as pool: 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")] 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): for fut in as_completed(futures):
try: try:
items = fut.result() # это уже список словарей items = fut.result() # это уже список словарей
@@ -1696,7 +1898,7 @@ def default_player_stats() -> list:
def Player_Stat_Career(player_id: str) -> dict: def Player_Stat_Career(player_id: str) -> dict:
url = f"{URL}api/abc/players/career?teamId=0&Tag={LEAGUE}&Id={player_id}" 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) player_stat_career = get_json(url)
if not player_stat_career: if not player_stat_career:
@@ -1954,7 +2156,7 @@ def Team_Both_Stat(stop_event: threading.Event) -> None:
stop_event (threading.Event): Событие для остановки цикла. stop_event (threading.Event): Событие для остановки цикла.
""" """
logger.info("START making json for team statistics") logger.info("START making json for team statistics")
global game_online_data global game_online_data, RUN_ONCE
while not stop_event.is_set(): while not stop_event.is_set():
with game_online_lock: with game_online_lock:
@@ -1991,16 +2193,28 @@ def Team_Both_Stat(stop_event: threading.Event) -> None:
) )
if not team_1.get("total") or not team_2.get("total"): if not team_1.get("total") or not team_2.get("total"):
logger.debug("Нет total у команд — пропускаю перезапись team_stats.json") logger.debug(
"Нет total у команд — пропускаю перезапись team_stats.json"
)
stop_event.wait(TIMEOUT_ONLINE) stop_event.wait(TIMEOUT_ONLINE)
continue continue
# Форматирование общей статистики (как и было) # Форматирование общей статистики (как и было)
total_1 = add_new_team_stat( total_1 = add_new_team_stat(
team_1["total"], avg_age_1, points_1, avg_height_1, timeout_str1, timeout_left1, team_1["total"],
avg_age_1,
points_1,
avg_height_1,
timeout_str1,
timeout_left1,
) )
total_2 = add_new_team_stat( total_2 = add_new_team_stat(
team_2["total"], avg_age_2, points_2, avg_height_2, timeout_str2, timeout_left2, team_2["total"],
avg_age_2,
points_2,
avg_height_2,
timeout_str2,
timeout_left2,
) )
# # Форматирование общей статистики # # Форматирование общей статистики
@@ -2052,7 +2266,9 @@ def Team_Both_Stat(stop_event: threading.Event) -> None:
rewrite_file("team_stats", result_json) rewrite_file("team_stats", result_json)
logger.debug("Успешно записаны данные в team_stats.json") logger.debug("Успешно записаны данные в team_stats.json")
if RUN_ONCE:
logger.info("<Team_Both_Stat>: однократный режим — выхожу из потока.")
return
except Exception as e: except Exception as e:
logger.error( logger.error(
f"Ошибка при обработке командной статистики: {e}", exc_info=True f"Ошибка при обработке командной статистики: {e}", exc_info=True
@@ -2066,7 +2282,7 @@ def Referee(stop_event: threading.Event) -> None:
Поток, создающий JSON-файл с информацией о судьях матча. Поток, создающий JSON-файл с информацией о судьях матча.
""" """
logger.info("START making json for referee") logger.info("START making json for referee")
global game_online_data global game_online_data, RUN_ONCE
desired_order = [ desired_order = [
"Crew chief", "Crew chief",
@@ -2128,6 +2344,9 @@ def Referee(stop_event: threading.Event) -> None:
rewrite_file("referee", referees) rewrite_file("referee", referees)
logger.debug("Успешно записаны судьи в файл") logger.debug("Успешно записаны судьи в файл")
if RUN_ONCE:
logger.info("<Referee>: однократный режим — выхожу из потока.")
return
except Exception as e: except Exception as e:
logger.error(f"Ошибка в Referee потоке: {e}", exc_info=True) logger.error(f"Ошибка в Referee потоке: {e}", exc_info=True)
@@ -2140,7 +2359,7 @@ def Scores_Quarter(stop_event: threading.Event) -> None:
Поток, обновляющий JSON со счётом по четвертям. Поток, обновляющий JSON со счётом по четвертям.
""" """
logger.info("START making json for scores quarter") logger.info("START making json for scores quarter")
global game_online_data global game_online_data, RUN_ONCE
quarters = ["Q1", "Q2", "Q3", "Q4", "OT1", "OT2", "OT3", "OT4"] quarters = ["Q1", "Q2", "Q3", "Q4", "OT1", "OT2", "OT3", "OT4"]
@@ -2185,7 +2404,10 @@ def Scores_Quarter(stop_event: threading.Event) -> None:
except Exception as e: except Exception as e:
logger.error(f"Ошибка в Scores_Quarter: {e}", exc_info=True) logger.error(f"Ошибка в Scores_Quarter: {e}", exc_info=True)
# --- однократный режим для старого матча ---
if RUN_ONCE:
logger.info("Scores_Quarter: однократный режим — выхожу из потока.")
return
stop_event.wait(TIMEOUT_ONLINE) stop_event.wait(TIMEOUT_ONLINE)
@@ -2201,13 +2423,15 @@ def status_online_func(data: dict) -> dict | None:
if json_live_status.get("status") != "Ok": if json_live_status.get("status") != "Ok":
logger.warning(f"Live status API вернул не 'Ok': {json_live_status}") logger.warning(f"Live status API вернул не 'Ok': {json_live_status}")
return None return {
"foulsA": 0,
"foulsB": 0,
}
status_data = json_live_status["result"] status_data = json_live_status["result"]
path_to_png = ( path_to_png = (
r"D:\ГРАФИКА\БАСКЕТБОЛ\ЕДИНАЯ ЛИГА ВТБ 2022-2023\Scorebug Indicators" r"D:\ГРАФИКА\БАСКЕТБОЛ\ЕДИНАЯ ЛИГА ВТБ 2022-2023\Scorebug Indicators"
) )
fouls_a = min(status_data.get("foulsA", 0), 5) fouls_a = min(status_data.get("foulsA", 0), 5)
fouls_b = min(status_data.get("foulsB", 0), 5) fouls_b = min(status_data.get("foulsB", 0), 5)
@@ -2226,8 +2450,9 @@ def Status_Online(data: dict, stop_event: threading.Event) -> None:
Поток, обновляющий JSON-файл с онлайн-статусом матча. Поток, обновляющий JSON-файл с онлайн-статусом матча.
""" """
logger.info("START making json for status online") logger.info("START making json for status online")
global game_status_data global game_status_data, RUN_ONCE
# «Снимок» режима на момент старта потока
single_run = RUN_ONCE
while not stop_event.is_set(): while not stop_event.is_set():
try: try:
result = status_online_func(data) result = status_online_func(data)
@@ -2240,7 +2465,10 @@ def Status_Online(data: dict, stop_event: threading.Event) -> None:
logger.warning("status_online_func вернула None — пропуск записи.") logger.warning("status_online_func вернула None — пропуск записи.")
except Exception as e: except Exception as e:
logger.error(f"Ошибка в Status_Online: {e}", exc_info=True) logger.error(f"Ошибка в Status_Online: {e}", exc_info=True)
# --- однократный режим для старого матча ---
if single_run:
logger.info("Status_Online: однократный режим — выхожу из потока.")
return
stop_event.wait(TIMEOUT_ONLINE) stop_event.wait(TIMEOUT_ONLINE)
@@ -2294,7 +2522,8 @@ def Play_By_Play(data: dict, stop_event: threading.Event) -> None:
json_live_status = get_json(url) json_live_status = get_json(url)
last_event = plays[-1] 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("message") == "Not Found":
if not json_live_status or json_live_status.get("status") == "Not Found":
period = last_event.get("period", 1) period = last_event.get("period", 1)
second = 0 second = 0
else: else:
@@ -2416,7 +2645,9 @@ def Play_By_Play(data: dict, stop_event: threading.Event) -> None:
df_goals.to_json(filepath, orient="records", force_ascii=False, indent=4) df_goals.to_json(filepath, orient="records", force_ascii=False, indent=4)
logger.debug("Успешно положил данные об play-by-play в файл") logger.debug("Успешно положил данные об play-by-play в файл")
if RUN_ONCE:
logger.info("<Play_By_Play>: однократный режим — выхожу из потока.")
return
except Exception as e: except Exception as e:
logger.error(f"Ошибка в Play_By_Play: {e}", exc_info=True) logger.error(f"Ошибка в Play_By_Play: {e}", exc_info=True)
@@ -2428,16 +2659,22 @@ def schedule_daily_restart():
if not sys.platform.startswith("win"): if not sys.platform.startswith("win"):
while True: while True:
now = datetime.now() now = datetime.now()
next_run = (now + timedelta(days=1)).replace(hour=0, minute=5, second=0, microsecond=0) next_run = (now + timedelta(days=1)).replace(
hour=0, minute=5, second=0, microsecond=0
)
sleep_time = (next_run - now).total_seconds() sleep_time = (next_run - now).total_seconds()
logger.info(f"Следующий перезапуск get_season_and_schedule запланирован на {next_run.strftime('%Y-%m-%d %H:%M')}") logger.info(
f"Следующий перезапуск get_season_and_schedule запланирован на {next_run.strftime('%Y-%m-%d %H:%M')}"
)
time.sleep(sleep_time) time.sleep(sleep_time)
try: try:
logger.info("⏰ Автоматический перезапуск get_season_and_schedule()") logger.info("⏰ Автоматический перезапуск get_season_and_schedule()")
get_season_and_schedule() get_season_and_schedule()
except Exception as e: except Exception as e:
logger.error(f"Ошибка при автоматическом перезапуске: {e}", exc_info=True) logger.error(
f"Ошибка при автоматическом перезапуске: {e}", exc_info=True
)
def clean_np_ints(obj): def clean_np_ints(obj):
@@ -2462,14 +2699,19 @@ def get_season_and_schedule() -> dict | None:
""" """
global URL, LEAGUE, LANG, TEAM global URL, LEAGUE, LANG, TEAM
try: try:
# Получение активного сезона # Получение активного сезона
season_url = f"{URL}api/abc/comps/seasons?Tag={LEAGUE}&Lang={LANG}" season_url = f"{URL}api/abc/comps/seasons?Tag={LEAGUE}&Lang={LANG}"
season_data = get_json(season_url) season_data = get_json(season_url)
season = ( season = (
season_data.get("result", [{}])[0].get("season") if season_data else None 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) print(season)
if not season: if not season:
@@ -2509,7 +2751,8 @@ def get_season_and_schedule() -> dict | None:
last_game = df_filtered.iloc[-1] last_game = df_filtered.iloc[-1]
return clean_np_ints({ return clean_np_ints(
{
"season": season, "season": season,
"game_id": last_game["game.id"], "game_id": last_game["game.id"],
"team1_id": last_game["team1.teamId"], "team1_id": last_game["team1.teamId"],
@@ -2518,7 +2761,8 @@ def get_season_and_schedule() -> dict | None:
"team2": last_game["team2.name"], "team2": last_game["team2.name"],
"when": last_game["game.DateStr"], "when": last_game["game.DateStr"],
"time": last_game["game.localTime"], "time": last_game["game.localTime"],
}) }
)
except Exception as e: except Exception as e:
logger.error(f"Ошибка при получении сезона или расписания: {e}", exc_info=True) logger.error(f"Ошибка при получении сезона или расписания: {e}", exc_info=True)
@@ -2527,14 +2771,12 @@ def get_season_and_schedule() -> dict | None:
def Standing_func(data: dict, stop_event: threading.Event) -> None: def Standing_func(data: dict, stop_event: threading.Event) -> None:
logger.info("START making json for standings") logger.info("START making json for standings")
global URL, LEAGUE, LANG global URL, LEAGUE, LANG, RUN_ONCE
while not stop_event.is_set(): while not stop_event.is_set():
try: try:
season = data["season"] season = data["season"]
url = ( url = f"{URL}api/abc/comps/actual-standings?tag={LEAGUE}&season={season}&lang={LANG}"
f"{URL}api/abc/comps/actual-standings?tag={LEAGUE}&season={season}&lang={LANG}"
)
data_standings = get_json(url) data_standings = get_json(url)
if data_standings and "items" in data_standings and data_standings["items"]: if data_standings and "items" in data_standings and data_standings["items"]:
@@ -2599,7 +2841,9 @@ def Standing_func(data: dict, stop_event: threading.Event) -> None:
indent=4, indent=4,
) )
logger.debug("Standings data saved successfully.") logger.debug("Standings data saved successfully.")
if RUN_ONCE:
logger.info("<Standing_func>: однократный режим — выхожу из потока.")
return
except Exception as e: except Exception as e:
logger.warning(f"Ошибка в турнирном положении: {e}") logger.warning(f"Ошибка в турнирном положении: {e}")
@@ -2765,19 +3009,24 @@ def pregame_data(data: dict) -> None:
"points_23": round((data_team["goal23"] * 100 / data_team["shot23"]), 1), "points_23": round((data_team["goal23"] * 100 / data_team["shot23"]), 1),
"points_1": round((data_team["goal1"] * 100 / data_team["shot1"]), 1), "points_1": round((data_team["goal1"] * 100 / data_team["shot1"]), 1),
"assists": round((data_team["assist"] / data_team["games"]), 1), "assists": round((data_team["assist"] / data_team["games"]), 1),
"rebounds": round(((data_team["defRebound"] + data_team["offRebound"]) / 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), "steals": round((data_team["steal"] / data_team["games"]), 1),
"turnovers": round((data_team["turnover"] / data_team["games"]), 1), "turnovers": round((data_team["turnover"] / data_team["games"]), 1),
"blocks": round((data_team["blockShot"] / data_team["games"]), 1), "blocks": round((data_team["blockShot"] / data_team["games"]), 1),
"fouls": round((data_team["foul"] / data_team["games"]), 1), "fouls": round((data_team["foul"] / data_team["games"]), 1),
} }
teams.append(temp_team) teams.append(temp_team)
rewrite_file("team_comparison", teams) rewrite_file("team_comparison", teams)
def main(): def main():
global TEAM, LANG, URL global TEAM, LANG, URL, RUN_ONCE, CURRENT_GAME_ID
if TEAM is None: if TEAM is None:
logger.critical(f"{myhost}\nКоманда не указана") logger.critical(f"{myhost}\nКоманда не указана")
@@ -2796,8 +3045,8 @@ def main():
URL = ( URL = (
"https://basket.sportoteka.org/" "https://basket.sportoteka.org/"
if "uba" in LEAGUE.lower() if "uba" in LEAGUE.lower()
# else "https://pro.russiabasket.org/" else "https://pro.russiabasket.org/"
else "https://vtb-league.org/" # else "https://vtb-league.org/"
) )
stop_event = Event() stop_event = Event()
@@ -2808,11 +3057,24 @@ def main():
# === Ежедневный перезапуск функции на Linux/Mac === # === Ежедневный перезапуск функции на Linux/Mac ===
if not sys.platform.startswith("win"): if not sys.platform.startswith("win"):
threading.Thread(target=schedule_daily_restart, daemon=True, name="ScheduleRestart").start() threading.Thread(
target=schedule_daily_restart, daemon=True, name="ScheduleRestart"
).start()
logger.info( logger.info(
f"{myhost}\n<b>{data['team1']}</b> VS {data['team2']}\n<i>{data['when']} {data['time']}</i>" 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) # logger.debug(data)
# data = { # data = {
# "season": 2026, # "season": 2026,
@@ -2835,27 +3097,27 @@ def main():
args=("team1", data, stop_event), args=("team1", data, stop_event),
name="Team1JSON", name="Team1JSON",
), ),
# threading.Thread( threading.Thread(
# target=Json_Team_Generation, target=Json_Team_Generation,
# args=("team2", data, stop_event), args=("team2", data, stop_event),
# name="Team2JSON", name="Team2JSON",
# ), ),
# threading.Thread( threading.Thread(
# target=Team_Both_Stat, args=(stop_event,), name="BothTeamsStat" target=Team_Both_Stat, args=(stop_event,), name="BothTeamsStat"
# ), ),
# threading.Thread(target=Referee, args=(stop_event,), name="Referee"), threading.Thread(target=Referee, args=(stop_event,), name="Referee"),
# threading.Thread( threading.Thread(
# target=Scores_Quarter, args=(stop_event,), name="QuarterScore" target=Scores_Quarter, args=(stop_event,), name="QuarterScore"
# ), ),
# threading.Thread( threading.Thread(
# target=Status_Online, args=(data, stop_event), name="StatusOnline" target=Status_Online, args=(data, stop_event), name="StatusOnline"
# ), ),
# threading.Thread( threading.Thread(
# target=Play_By_Play, args=(data, stop_event), name="PlayByPlay" target=Play_By_Play, args=(data, stop_event), name="PlayByPlay"
# ), ),
# threading.Thread( threading.Thread(
# target=Standing_func, args=(data, stop_event), name="Standings" target=Standing_func, args=(data, stop_event), name="Standings"
# ), ),
] ]
# Запуск всех потоков # Запуск всех потоков
@@ -2863,12 +3125,33 @@ def main():
t.start() t.start()
logger.debug(f"Поток {t.name} запущен.") logger.debug(f"Поток {t.name} запущен.")
# How_To_Play_Quarter(data) How_To_Play_Quarter(data)
# pregame_data(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: try:
if RUN_ONCE:
# 🔚 Однократный режим: ждём завершения и выходим
for t in threads:
t.join()
logger.debug(f"Поток {t.name} завершён.")
logger.info("Однократный режим: всё готово, выхожу.")
return
# Онлайн-режим: держим процесс живым
while True: while True:
time.sleep(1) time.sleep(1)
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info("Остановка по Ctrl+C... Завершение потоков.") logger.info("Остановка по Ctrl+C... Завершение потоков.")
stop_event.set() stop_event.set()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

13
match_id.json Normal file
View File

@@ -0,0 +1,13 @@
{
"10.0.85.2111": {"host": "", "tag": "vtb", "root": 1, "team": ""},
"10.10.35.21": {"host": "gfx", "tag": "vtb", "root": 1, "team": "Lokomotiv Kuban"},
"10.10.35.22": {"host": "krd", "tag": "vtb", "root": 1, "team": "Lokomotiv Kuban1"},
"10.10.35.23": {"host": "ekb", "tag": "vtb", "root": 1, "team": "uralmash1"},
"10.10.35.24": {"host": "per", "tag": "vtb", "root": 1, "team": "betcity parma1"},
"10.10.35.25": {"host": "sar", "tag": "vtb", "root": 1, "team": "avtodor1"},
"10.10.35.26": {"host": "spb", "tag": "vtb", "root": 1, "team": "zenit1"},
"10.10.35.27": {"host": "sam", "tag": "vtb", "root": 1, "team": "samara1"},
"10.10.35.28": {"host": "msk1", "tag": "vtb", "root": 1, "team": "mba-mai1"},
"10.10.35.29": {"host": "msk2", "tag": "vtb", "root": 1, "team": "Pari Nizhny Novgorod1"},
"10.10.35.30": {"host": "kaz", "tag": "vtb", "root": 1, "team": "unics1"}
}

View File

@@ -1,97 +0,0 @@
Dim url As String = "https://ekb.tvstart.ru/app/static/ekb_team1.json"
Dim json As String = ""
' Чтение URL
Try
Dim wc As New System.Net.WebClient
wc.Encoding = System.Text.Encoding.UTF8
json = wc.DownloadString(url)
Catch ex As Exception
Console.WriteLine("Ошибка загрузки JSON: " & ex.Message)
Return
End Try
' --- Парсинг первых 12 num + NameGFX ---
Dim nums(11) As String
Dim names(11) As String
Dim count As Integer = 0
Dim pos As Integer = 0
While count < 12
' ищем "num"
Dim k As Integer = json.IndexOf("""num""", pos)
If k = -1 Then Exit While
Dim c As Integer = json.IndexOf(":", k)
If c = -1 Then Exit While
Dim j As Integer = c + 1
While j < json.Length AndAlso Char.IsWhiteSpace(json(j))
j += 1
End While
Dim numVal As String = ""
If j < json.Length AndAlso json(j) = """"c Then
j += 1
Dim startQ As Integer = j
While j < json.Length AndAlso json(j) <> """"c
j += 1
End While
numVal = json.Substring(startQ, j - startQ)
j += 1
Else
Dim startN As Integer = j
While j < json.Length AndAlso (Char.IsDigit(json(j)) OrElse json(j) = "-"c OrElse json(j) = "."c)
j += 1
End While
numVal = json.Substring(startN, j - startN)
End If
' ищем "NameGFX"
pos = j
Dim kn As Integer = json.IndexOf("""NameGFX""", pos)
If kn = -1 Then Exit While
Dim cn As Integer = json.IndexOf(":", kn)
If cn = -1 Then Exit While
Dim jn As Integer = cn + 1
While jn < json.Length AndAlso Char.IsWhiteSpace(json(jn))
jn += 1
End While
Dim nameVal As String = ""
If jn < json.Length AndAlso json(jn) = """"c Then
jn += 1
Dim startGN As Integer = jn
While jn < json.Length AndAlso json(jn) <> """"c
jn += 1
End While
nameVal = json.Substring(startGN, jn - startGN)
jn += 1
End If
nums(count) = numVal
names(count) = nameVal
count += 1
pos = jn
End While
' --- Выводим результат ---
Console.WriteLine("=== Первые " & count.ToString() & " игроков ===")
For i As Integer = 0 To count - 1
Console.WriteLine(nums(i) & "_" & names(i))
Next
' --- Выводим результат ---
Console.WriteLine("=== Первые " & count.ToString() & " игроков ===")
For i As Integer = 0 To count - 1
Console.WriteLine(nums(i) & "_" & names(i))
' === Отправляем в титр TeamRoster.gtzip ===
' Номер
API.Function("SetText", Input:="TeamRoster.gtzip", SelectedName:="PlayerNamber" & (i + 1).ToString() & ".Text", Value:=nums(i))
' Имя
API.Function("SetText", Input:="TeamRoster.gtzip", SelectedName:="PlayerName" & (i + 1).ToString() & ".Text", Value:=names(i))
Next

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,128 +0,0 @@
[
{
"displayNumber": "",
"positionName": "Crew chief",
"lastNameGFX": "Aleksey Davydov",
"secondName": "Mikhailovich",
"birthday": "1984-10-05T00:00:00",
"age": 41,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Referee 1",
"lastNameGFX": "Sergey Mikhailov",
"secondName": "Alexandrovich",
"birthday": "1977-05-20T00:00:00",
"age": 48,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Referee 2",
"lastNameGFX": "Maksim Zhitlukhin",
"secondName": "Sergeevich",
"birthday": "1986-12-19T00:00:00",
"age": 38,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Commissioner",
"lastNameGFX": "Igor Lebedev",
"secondName": "Anatolevich",
"birthday": "1965-05-27T00:00:00",
"age": 60,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Scorer",
"lastNameGFX": "Viktoriya Isaeva",
"secondName": "Dmitrievna",
"birthday": "1996-01-09T00:00:00",
"age": 29,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Assistant Scorer",
"lastNameGFX": "Dmitriy Kibenko",
"secondName": "Andreevich",
"birthday": "1983-12-25T00:00:00",
"age": 41,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Timekeeper",
"lastNameGFX": "Olga Prosneva",
"secondName": "Nikolaevna",
"birthday": "1971-06-15T00:00:00",
"age": 54,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Operator 24 sec",
"lastNameGFX": "Aleksey Nagibin",
"secondName": "Vitalevich",
"birthday": "1982-08-21T00:00:00",
"age": 43,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Dictor",
"lastNameGFX": "Адель Халимов",
"secondName": "Рашидович",
"birthday": "1996-07-31T00:00:00",
"age": 29,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Statistic",
"lastNameGFX": "Veronika Shuvagina",
"secondName": "Vladimirovna",
"birthday": "1968-05-08T00:00:00",
"age": 57,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "IS Operator",
"lastNameGFX": "Rashid Khabibullin",
"secondName": "Rinatovich",
"birthday": "1987-07-26T00:00:00",
"age": 38,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Statistic",
"lastNameGFX": "Mariya Shuvagina",
"secondName": "Dmitrievna",
"birthday": "2002-08-04T00:00:00",
"age": 23,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "Video reviewer",
"lastNameGFX": "Kamil Habibullin",
"secondName": "Ildarovich",
"birthday": "1975-12-08T00:00:00",
"age": 49,
"flag": "https://flagicons.lipis.dev/flags/4x3/ru.svg"
},
{
"displayNumber": "",
"positionName": "-",
"lastNameGFX": "None None",
"secondName": null,
"birthday": null,
"age": null,
"flag": "https://flagicons.lipis.dev/flags/4x3/.svg"
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +0,0 @@
[
{
"Q": "Q1",
"score1": "33",
"score2": "20"
},
{
"Q": "Q2",
"score1": "26",
"score2": "21"
},
{
"Q": "Q3",
"score1": "26",
"score2": "20"
},
{
"Q": "Q4",
"score1": "18",
"score2": "22"
},
{
"Q": "OT1",
"score1": "",
"score2": ""
},
{
"Q": "OT2",
"score1": "",
"score2": ""
},
{
"Q": "OT3",
"score1": "",
"score2": ""
},
{
"Q": "OT4",
"score1": "",
"score2": ""
}
]

View File

@@ -1,88 +0,0 @@
[
{
"team": "UNICS",
"winQ1": 0,
"loseQ1": 0,
"drawQ1": 0,
"scoreQ1": 0,
"score_avgQ1": null,
"winQ2": 0,
"loseQ2": 0,
"drawQ2": 0,
"scoreQ2": 0,
"score_avgQ2": null,
"winQ3": 0,
"loseQ3": 0,
"drawQ3": 0,
"scoreQ3": 0,
"score_avgQ3": null,
"winQ4": 0,
"loseQ4": 0,
"drawQ4": 0,
"scoreQ4": 0,
"score_avgQ4": null,
"winOT1": 0,
"loseOT1": 0,
"drawOT1": 0,
"scoreOT1": 0,
"score_avgOT1": null,
"winOT2": 0,
"loseOT2": 0,
"drawOT2": 0,
"scoreOT2": 0,
"score_avgOT2": null,
"winOT3": 0,
"loseOT3": 0,
"drawOT3": 0,
"scoreOT3": 0,
"score_avgOT3": null,
"winOT4": 0,
"loseOT4": 0,
"drawOT4": 0,
"scoreOT4": 0,
"score_avgOT4": null
},
{
"team": "BETCITY PARMA",
"winQ1": 0,
"loseQ1": 0,
"drawQ1": 0,
"scoreQ1": 0,
"score_avgQ1": null,
"winQ2": 0,
"loseQ2": 0,
"drawQ2": 0,
"scoreQ2": 0,
"score_avgQ2": null,
"winQ3": 0,
"loseQ3": 0,
"drawQ3": 0,
"scoreQ3": 0,
"score_avgQ3": null,
"winQ4": 0,
"loseQ4": 0,
"drawQ4": 0,
"scoreQ4": 0,
"score_avgQ4": null,
"winOT1": 0,
"loseOT1": 0,
"drawOT1": 0,
"scoreOT1": 0,
"score_avgOT1": null,
"winOT2": 0,
"loseOT2": 0,
"drawOT2": 0,
"scoreOT2": 0,
"score_avgOT2": null,
"winOT3": 0,
"loseOT3": 0,
"drawOT3": 0,
"scoreOT3": 0,
"score_avgOT3": null,
"winOT4": 0,
"loseOT4": 0,
"drawOT4": 0,
"scoreOT4": 0,
"score_avgOT4": null
}
]

View File

@@ -1 +0,0 @@
[]

View File

@@ -1 +0,0 @@
[]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,296 +0,0 @@
[
{
"name": "points",
"nameGFX_rus": "Очки",
"nameGFX_eng": "points",
"val1": "103",
"val2": "83"
},
{
"name": "goal2",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "28",
"val2": "25"
},
{
"name": "shot2",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "42",
"val2": "43"
},
{
"name": "goal3",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "9",
"val2": "7"
},
{
"name": "shot3",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "24",
"val2": "17"
},
{
"name": "goal1",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "20",
"val2": "12"
},
{
"name": "shot1",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "21",
"val2": "16"
},
{
"name": "assist",
"nameGFX_rus": "Передачи",
"nameGFX_eng": "assists",
"val1": "29",
"val2": "24"
},
{
"name": "pass",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "32",
"val2": "24"
},
{
"name": "steal",
"nameGFX_rus": "Перехваты",
"nameGFX_eng": "steals",
"val1": "10",
"val2": "9"
},
{
"name": "block",
"nameGFX_rus": "Блокшоты",
"nameGFX_eng": "blocks",
"val1": "3",
"val2": "0"
},
{
"name": "blocked",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "0",
"val2": "3"
},
{
"name": "defReb",
"nameGFX_rus": "подборы в защите",
"nameGFX_eng": "",
"val1": "19",
"val2": "15"
},
{
"name": "offReb",
"nameGFX_rus": "подборы в нападении",
"nameGFX_eng": "",
"val1": "14",
"val2": "8"
},
{
"name": "foulsOn",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "24",
"val2": "23"
},
{
"name": "turnover",
"nameGFX_rus": "Потери",
"nameGFX_eng": "turnovers",
"val1": "16",
"val2": "19"
},
{
"name": "foul",
"nameGFX_rus": "Фолы",
"nameGFX_eng": "fouls",
"val1": "23",
"val2": "24"
},
{
"name": "foulT",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "0",
"val2": "0"
},
{
"name": "foulD",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "0",
"val2": "0"
},
{
"name": "foulC",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "0",
"val2": "0"
},
{
"name": "foulB",
"nameGFX_rus": "",
"nameGFX_eng": "",
"val1": "0",
"val2": "0"
},
{
"name": "second",
"nameGFX_rus": "секунды",
"nameGFX_eng": "seconds",
"val1": "12000",
"val2": "12000"
},
{
"name": "dunk",
"nameGFX_rus": "данки",
"nameGFX_eng": "dunks",
"val1": "7",
"val2": "4"
},
{
"name": "fastBreak",
"nameGFX_rus": "",
"nameGFX_eng": "fast breaks",
"val1": "6",
"val2": "4"
},
{
"name": "plusMinus",
"nameGFX_rus": "+/-",
"nameGFX_eng": "+/-",
"val1": "100",
"val2": "-100"
},
{
"name": "pt-1",
"nameGFX_rus": "Штрафные",
"nameGFX_eng": "free throws",
"val1": "20/21",
"val2": "12/16"
},
{
"name": "pt-2",
"nameGFX_rus": "2-очковые",
"nameGFX_eng": "2-points",
"val1": "28/42",
"val2": "25/43"
},
{
"name": "pt-3",
"nameGFX_rus": "3-очковые",
"nameGFX_eng": "3-points",
"val1": "9/24",
"val2": "7/17"
},
{
"name": "fg",
"nameGFX_rus": "очки с игры",
"nameGFX_eng": "field goals",
"val1": "37/66",
"val2": "32/60"
},
{
"name": "pt-1_pro",
"nameGFX_rus": "штрафные, процент",
"nameGFX_eng": "free throws pro",
"val1": "95%",
"val2": "75%"
},
{
"name": "pt-2_pro",
"nameGFX_rus": "2-очковые, процент",
"nameGFX_eng": "2-points pro",
"val1": "67%",
"val2": "58%"
},
{
"name": "pt-3_pro",
"nameGFX_rus": "3-очковые, процент",
"nameGFX_eng": "3-points pro",
"val1": "38%",
"val2": "41%"
},
{
"name": "fg_pro",
"nameGFX_rus": "Очки с игры, процент",
"nameGFX_eng": "field goals pro",
"val1": "56%",
"val2": "53%"
},
{
"name": "Reb",
"nameGFX_rus": "Подборы",
"nameGFX_eng": "rebounds",
"val1": "33",
"val2": "23"
},
{
"name": "avgAge",
"nameGFX_rus": "",
"nameGFX_eng": "avg Age",
"val1": "26.8",
"val2": "25.4"
},
{
"name": "ptsStart",
"nameGFX_rus": "",
"nameGFX_eng": "Start PTS",
"val1": "74",
"val2": "51"
},
{
"name": "ptsStart_pro",
"nameGFX_rus": "",
"nameGFX_eng": "Start PTS, %",
"val1": "72%",
"val2": "61%"
},
{
"name": "ptsBench",
"nameGFX_rus": "",
"nameGFX_eng": "Bench PTS",
"val1": "29",
"val2": "32"
},
{
"name": "ptsBench_pro",
"nameGFX_rus": "",
"nameGFX_eng": "Bench PTS, %",
"val1": "28%",
"val2": "39%"
},
{
"name": "avgHeight",
"nameGFX_rus": "",
"nameGFX_eng": "avg height",
"val1": "198.8 cm",
"val2": "201.1 cm"
},
{
"name": "timeout_left",
"nameGFX_rus": "",
"nameGFX_eng": "timeout left",
"val1": "2",
"val2": "1"
},
{
"name": "timeout_str",
"nameGFX_rus": "",
"nameGFX_eng": "timeout str",
"val1": "2 Time-outs left in 2nd half",
"val2": "1 Time-out left in 2nd half"
}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
import requests
import json
url = "https://org.infobasket.su/Widget/GetOnline/921412?format=json&lang=ru"
response = requests.get(url)
response.raise_for_status()
data = response.json()
# print(data)
team1 = data["GameTeams"][0]
team2 = data["GameTeams"][1]
print(team1)

11252
temp.json

File diff suppressed because it is too large Load Diff

314
visual.py
View File

@@ -181,7 +181,31 @@ def process_player_data(team_json, player_index):
"time": str(player_data["CareerTPlayedTime"]), "time": str(player_data["CareerTPlayedTime"]),
} }
return [season_total, season_avg, career_total], player_data career_avg = {
"name": "Career Average",
"game_count": "",
"start_count": "",
"pts": str(player_data["AvgCarPoints"]),
"pt-2": str(player_data["CareerTShot2Percent"]),
"pt-3": str(player_data["CareerTShot3Percent"]),
"pt-1": str(player_data["CareerTShot1Percent"]),
"fg": str(player_data["CareerTShot23Percent"]),
"ast": str(player_data["AvgCarAssist"]),
"stl": str(player_data["AvgCarSteal"]),
"blk": str(player_data["AvgCarBlocks"]),
"dreb": str(player_data["AvgCarDefRebound"]),
"oreb": str(player_data["AvgCarOffRebound"]),
"reb": str(player_data["AvgCarRebound"]),
# "to": str(player_data["AvgTurnover"]),
# "foul": str(player_data["AvgFoul"]),
"fouled": str(player_data["AvgCarOpponentFoul"]),
"dunk": str(player_data["AvgCarDunk"]),
"time": str(player_data["AvgCarPlayedTime"]),
}
return [season_total, season_avg, career_total, career_avg], player_data
config = { config = {
@@ -190,24 +214,24 @@ config = {
"num": st.column_config.TextColumn("#", width=27), "num": st.column_config.TextColumn("#", width=27),
"NameGFX": st.column_config.TextColumn(width=170), "NameGFX": st.column_config.TextColumn(width=170),
"isOn": st.column_config.TextColumn("🏀", width=27), "isOn": st.column_config.TextColumn("🏀", width=27),
"pts": st.column_config.NumberColumn("PTS", width=27), "pts": st.column_config.TextColumn("PTS", width="content", help="⭐ = Career High"),
"pt-2": st.column_config.TextColumn("2-PT", width=45), "pt-2": st.column_config.TextColumn("2-PT", width="content"),
"pt-3": st.column_config.TextColumn("3-PT", width=45), "pt-3": st.column_config.TextColumn("3-PT", width="content"),
"pt-1": st.column_config.TextColumn("FT", width=45), "pt-1": st.column_config.TextColumn("FT", width="content"),
"fg": st.column_config.TextColumn("FG", width=45), "fg": st.column_config.TextColumn("FG", width="content"),
"ast": st.column_config.NumberColumn("AS", width=27), "ast": st.column_config.TextColumn("AS", width="content"),
"stl": st.column_config.NumberColumn("ST", width=27), "stl": st.column_config.TextColumn("ST", width="content"),
"blk": st.column_config.NumberColumn("BL", width=27), "blk": st.column_config.TextColumn("BL", width="content"),
"blkVic": st.column_config.NumberColumn("BV", width=27), "blkVic": st.column_config.TextColumn("BV", width="content"),
"dreb": st.column_config.NumberColumn("DR", width=27), "dreb": st.column_config.TextColumn("DR", width="content"),
"oreb": st.column_config.NumberColumn("OR", width=27), "oreb": st.column_config.TextColumn("OR", width="content"),
"reb": st.column_config.NumberColumn("R", width=27), "reb": st.column_config.TextColumn("R", width="content"),
"to": st.column_config.NumberColumn("TO", width=27), "to": st.column_config.TextColumn("TO", width="content"),
"foul": st.column_config.NumberColumn("F", width=27), "foul": st.column_config.TextColumn("F", width="content"),
"fouled": st.column_config.NumberColumn("Fed", width=27), "fouled": st.column_config.TextColumn("Fed", width="content"),
"plusMinus": st.column_config.NumberColumn("+/-", width=27), "plusMinus": st.column_config.TextColumn("+/-", width="content"),
"dunk": st.column_config.NumberColumn("DUNK", width=27), "dunk": st.column_config.TextColumn("DUNK", width="content"),
"kpi": st.column_config.NumberColumn("KPI", width=27), "kpi": st.column_config.TextColumn("KPI", width="content"),
"time": st.column_config.TextColumn("TIME"), "time": st.column_config.TextColumn("TIME"),
"game_count": st.column_config.TextColumn("G", width=27), "game_count": st.column_config.TextColumn("G", width=27),
"start_count": st.column_config.TextColumn("S", width=27), "start_count": st.column_config.TextColumn("S", width=27),
@@ -813,19 +837,127 @@ columns_game = [
if cached_team1 and cached_team2: if cached_team1 and cached_team2:
team1_data = process_team_data(cached_team1, columns_game) team1_data = process_team_data(cached_team1, columns_game)
team2_data = process_team_data(cached_team2, columns_game) team2_data = process_team_data(cached_team2, columns_game)
# Добавляем звездочку, если pts > PTS_Career_High
def _get_first_number(x):
"""Безопасно вытащить число из строки/значения (например '12 (60%)' -> 12)."""
try:
if x is None:
return None
s = str(x)
# заберём ведущие число/знак (поддержим +/-)
import re
m = re.search(r"[-+]?\d+(\.\d+)?", s)
return float(m.group(0)) if m else None
except Exception:
return None
CAREER_HIGH_KEYS = {
"pts": ["PTS_Career_High", "CareerHighPoints", "career_high_pts"],
"ast": ["AST_Career_High", "CareerHighAssist", "career_high_ast"],
"stl": ["STL_Career_High", "CareerHighSteal", "career_high_stl"],
"blk": ["BLK_Career_High", "CareerHighBlocks", "career_high_blk"],
"reb": ["REB_Career_High", "CareerHighRebound", "career_high_reb"],
# если нужно — добавь ещё пары "df_column": ["possible_key1","possible_key2"...]
}
def _build_career_high_map(cached_team_list):
"""Вернёт словарь: player_id -> {stat_key: value} для всех доступных максимумов."""
out = {}
if not isinstance(cached_team_list, list):
return out
for p in cached_team_list:
if not isinstance(p, dict):
continue
pid = p.get("id")
if pid is None:
continue
out[pid] = {}
for stat_col, aliases in CAREER_HIGH_KEYS.items():
for k in aliases:
if k in p and p[k] not in (None, ""):
out[pid][stat_col] = _get_first_number(p[k])
break
return out
def _ensure_id_column(df, cached_team_list):
"""Присвоить игрокам id в том же порядке, что и в списке cached_team."""
try:
ids = [p.get("id") if isinstance(p, dict) else None for p in cached_team_list][:len(df)]
if "id" not in df.columns:
df["id"] = ids
else:
# не затираем, только заполняем пустые
df["id"] = df["id"].fillna(pd.Series(ids, index=df.index))
except Exception:
pass
def _mark_star_for_columns(df, cached_team_list, columns):
"""
Для каждого col в columns: если текущее значение > career high — добавляем ' ⭐️'.
Преобразуем колонки в текст (для отображения эмодзи).
"""
_ensure_id_column(df, cached_team_list)
ch_map = _build_career_high_map(cached_team_list)
def format_with_star(val, career_max):
v = _get_first_number(val)
cm = _get_first_number(career_max)
if v is not None and cm is not None and v >= cm and val > 0:
# сохраняем исходное текстовое представление + ⭐️
return f"{val} ⭐️"
return f"{val}" if val is not None else ""
for col in columns:
if col not in df.columns:
continue
new_vals = []
for idx, row in df.iterrows():
pid = row.get("id")
career_max = (ch_map.get(pid, {}) or {}).get(col)
new_vals.append(format_with_star(row[col], career_max))
df[col] = new_vals # теперь это текст для отображения
STAR_COLUMNS = [
"pts", "ast", "stl", "blk", "reb",
]
team1_data["_pts_num"] = pd.to_numeric(team1_data["pts"], errors="coerce")
team1_data["_kpi_num"] = pd.to_numeric(team1_data["kpi"], errors="coerce")
team2_data["_pts_num"] = pd.to_numeric(team2_data["pts"], errors="coerce")
team2_data["_kpi_num"] = pd.to_numeric(team2_data["kpi"], errors="coerce")
def highlight_max_by_refcol(df, view_col, ref_col):
ref = pd.to_numeric(df[ref_col], errors="coerce")
mx = ref.max()
return [("background-color: green" if (pd.notna(v) and v == mx and v > 0) else "")
for v in ref]
_mark_star_for_columns(team1_data, cached_team1, STAR_COLUMNS)
_mark_star_for_columns(team2_data, cached_team2, STAR_COLUMNS)
# Стилизация данных # Стилизация данных
# team1_styled = (
# team1_data.style.apply(highlight_grey, axis=1)
# .apply(highlight_foul, subset="foul")
# .apply(highlight_max, subset="pts")
# .apply(highlight_max, subset="kpi")
# )
# team2_styled = (
# team2_data.style.apply(highlight_grey, axis=1)
# .apply(highlight_foul, subset="foul")
# .apply(highlight_max, subset="pts")
# .apply(highlight_max, subset="kpi")
# )
team1_styled = ( team1_styled = (
team1_data.style.apply(highlight_grey, axis=1) team1_data[columns_game].style
.apply(highlight_grey, axis=1)
.apply(highlight_foul, subset="foul") .apply(highlight_foul, subset="foul")
.apply(highlight_max, subset="pts") .apply(lambda _: highlight_max_by_refcol(team1_data, "pts", "_pts_num"), axis=0, subset=["pts"])
.apply(highlight_max, subset="kpi") .apply(lambda _: highlight_max_by_refcol(team1_data, "kpi", "_kpi_num"), axis=0, subset=["kpi"])
) )
team2_styled = ( team2_styled = (
team2_data.style.apply(highlight_grey, axis=1) team2_data[columns_game].style
.apply(highlight_grey, axis=1)
.apply(highlight_foul, subset="foul") .apply(highlight_foul, subset="foul")
.apply(highlight_max, subset="pts") .apply(lambda _: highlight_max_by_refcol(team2_data, "pts", "_pts_num"), axis=0, subset=["pts"])
.apply(highlight_max, subset="kpi") .apply(lambda _: highlight_max_by_refcol(team2_data, "kpi", "_kpi_num"), axis=0, subset=["kpi"])
) )
def get_player_all_game(player_data_1): def get_player_all_game(player_data_1):
@@ -863,29 +995,28 @@ if cached_team1 and cached_team2:
# Сортировка от последнего матча к первому # Сортировка от последнего матча к первому
df_filtered = df_filtered.sort_values(by="game.gameDate", ascending=False) df_filtered = df_filtered.sort_values(by="game.gameDate", ascending=False)
# Указать нужные колонки для вывода # Указать нужные колонки для вывода
columns_to_show = [ columns_to_show = [
"season", "Сезон",
"game.gameDate", "Дата",
"game.team1Name", "Команда 1",
"game.team2Name", "Команда 2",
"game.score", "Счёт",
"stats.points", "PTS",
"stats.shot2Percent", "2-PTS%",
"stats.shot3Percent", "3-PTS%",
"stats.shot23Percent", "FG%",
"stats.shot1Percent", "FT%",
"stats.assist", "AST",
"stats.steal", "STL",
"stats.blockShot", "BLK",
"stats.defRebound", "DR",
"stats.offRebound", "OR",
"stats.rebound", "REB",
"stats.turnover", "TO",
"stats.foul", "F",
"stats.playedTime", "TIME",
"stats.plusMinus", "+/-",
] ]
numeric_cols = [ numeric_cols = [
"stats.points", "stats.points",
@@ -897,24 +1028,63 @@ if cached_team1 and cached_team2:
# df_filtered[numeric_cols] = df_filtered[numeric_cols].apply( # df_filtered[numeric_cols] = df_filtered[numeric_cols].apply(
# pd.to_numeric, errors="coerce" # pd.to_numeric, errors="coerce"
# ) # )
df_filtered[numeric_cols] = df_filtered[numeric_cols].apply(pd.to_numeric, errors="coerce")
# 🟢 Переименовываем колонки для отображения в Streamlit
rename_map = {
"season": "Сезон",
"game.gameDate": "Дата",
"game.team1Name": "Команда 1",
"game.team2Name": "Команда 2",
"game.score": "Счёт",
"stats.points": "PTS",
"stats.shot2Percent": "2-PTS%",
"stats.shot3Percent": "3-PTS%",
"stats.shot23Percent": "FG%",
"stats.shot1Percent": "FT%",
"stats.assist": "AST",
"stats.steal": "STL",
"stats.blockShot": "BLK",
"stats.defRebound": "DR",
"stats.offRebound": "OR",
"stats.rebound": "REB",
"stats.turnover": "TO",
"stats.foul": "F",
"stats.playedTime": "TIME",
"stats.plusMinus": "+/-",
}
df_filtered[numeric_cols] = df_filtered[numeric_cols].apply(
pd.to_numeric, errors="coerce"
)
df_filtered[numeric_cols] = df_filtered[numeric_cols].round(0).astype("Int64") df_filtered[numeric_cols] = df_filtered[numeric_cols].round(0).astype("Int64")
df_filtered = df_filtered.rename(columns=rename_map)
df_filtered["Дата"] = df_filtered["Дата"].dt.strftime("%d.%m.%Y")
styled = ( styled = (
df_filtered[columns_to_show] df_filtered[columns_to_show]
.style .style.apply(highlight_max, subset=["PTS"])
.apply(highlight_max, subset=["stats.points"]) .apply(highlight_max, subset=["AST"])
.apply(highlight_max, subset=["stats.assist"]) .apply(highlight_max, subset=["STL"])
.apply(highlight_max, subset=["stats.steal"]) .apply(highlight_max, subset=["BLK"])
.apply(highlight_max, subset=["stats.blockShot"]) .apply(highlight_max, subset=["REB"])
.apply(highlight_max, subset=["stats.rebound"]) .format(
{
"Дата": lambda x: x, # уже строка, просто оставляем как есть
"PTS": "{:,.0f}".format,
"AST": "{:,.0f}".format,
"STL": "{:,.0f}".format,
"BLK": "{:,.0f}".format,
"REB": "{:,.0f}".format,
}
)
) )
return styled return styled
# Вывод данных # Вывод данных
col_player1, col_player2 = tab_temp_1.columns((5, 5)) col_player1, col_player2 = tab_temp_1.columns((5, 5))
config_copy = config.copy()
event1 = col_player1.dataframe( event1 = col_player1.dataframe(
team1_styled, team1_styled,
column_config=config, column_config=config_copy,
hide_index=True, hide_index=True,
height=460, height=460,
on_select="rerun", on_select="rerun",
@@ -960,7 +1130,8 @@ if cached_team1 and cached_team2:
hide_index=True, hide_index=True,
) )
col_player1.dataframe(get_player_all_game(player_data_1)) col_player1.title("Статистика каждой игры")
col_player1.dataframe(get_player_all_game(player_data_1), hide_index=True)
if event2.selection and event2.selection.get("rows"): if event2.selection and event2.selection.get("rows"):
selected_index2 = event2.selection["rows"][0] selected_index2 = event2.selection["rows"][0]
@@ -988,7 +1159,8 @@ if cached_team1 and cached_team2:
column_config=config_season, column_config=config_season,
hide_index=True, hide_index=True,
) )
col_player2.dataframe(get_player_all_game(player_data_2)) col_player2.title("Статистика каждой игры")
col_player2.dataframe(get_player_all_game(player_data_2), hide_index=True)
team_col1, team_col2 = tab_temp_2.columns((5, 5)) team_col1, team_col2 = tab_temp_2.columns((5, 5))
if isinstance(cached_team_stats, list) and len(cached_team_stats) >= 34: if isinstance(cached_team_stats, list) and len(cached_team_stats) >= 34:
@@ -1085,12 +1257,31 @@ if cached_standings:
pass pass
return [""] * len(s) return [""] * len(s)
styled = df_st.style.apply(highlight_teams, axis=1) styled = df_st[
[
"teamId",
"start",
"place",
"name",
"regionName",
"totalGames",
"totalWin",
"totalDefeat",
"totalPoints",
"totalGoalPlus",
"totalGoalMinus",
"logo",
"w_l",
"procent",
"plus_minus",
]
].style.apply(highlight_teams, axis=1)
tab_temp_4.dataframe( tab_temp_4.dataframe(
styled, styled,
column_config={"logo": st.column_config.ImageColumn("logo")}, column_config={"logo": st.column_config.ImageColumn("logo")},
hide_index=True, hide_index=True,
height=610, height=610,
width="content"
) )
@@ -1534,20 +1725,19 @@ with tab_online:
live_data_map = {} live_data_map = {}
# Собираем live-данные по каждому game.id # Собираем live-данные по каждому game.id
for _, row in df_filtered.iterrows(): for _, row in df_filtered.iterrows():
game_id = row["game.id"] game_id = row["game.id"]
try: try:
json_data = requests.get( json_data = requests.get(
f"https://pro.russiabasket.org/api/abc/games/live-status?Id={game_id}&Lang=en", f"https://vtb-league.org/api/abc/games/live-status?Id={game_id}&Lang=en",
).json() ).json()
except Exception as ex: except Exception as ex:
# json_data = {
# "period": None,
# "timeToGo": 0.0,
# }
print(ex) print(ex)
# Берём содержимое result (словарь с gameId и данными) # Берём содержимое result (словарь с gameId и данными)
if json_data:
result = json_data.get("result", {}) result = json_data.get("result", {})
if result and "gameId" in result: if result and "gameId" in result:
live_data_map[result["gameId"]] = result live_data_map[result["gameId"]] = result

View File

@@ -1,421 +0,0 @@
*** visual.py 2025-01-22 00:00:00.000000000 +0000
--- visual.py 2025-10-07 00:00:00.000000000 +0000
***************
*** 1,10 ****
import os
import json
import socket
import platform
import numpy as np
import pandas as pd
import streamlit as st
import sys
from streamlit_autorefresh import st_autorefresh
-
st.set_page_config(
page_title="Баскетбол",
page_icon="🏀",
layout="wide",
--- 1,10 ----
import os
import json
import socket
import platform
import numpy as np
import pandas as pd
import streamlit as st
import sys
from streamlit_autorefresh import st_autorefresh
+
st.set_page_config(
page_title="Баскетбол",
page_icon="🏀",
layout="wide",
***************
*** 164,169 ****
--- 164,216 ----
def ensure_state(key: str, default=None):
# Инициализирует ключ один раз и возвращает значение
return st.session_state.setdefault(key, default)
+
+ # ======== UNIVERSAL SAFE RENDER WRAPPER ========
+ def _is_empty_like(x) -> bool:
+ if x is None:
+ return True
+ # DataFrame
+ if isinstance(x, pd.DataFrame):
+ return x.empty
+ # Pandas Styler
+ try:
+ # импортируем лениво, чтобы не ломаться, если нет pandas.io.formats.style в рантайме
+ from pandas.io.formats.style import Styler # type: ignore
+ if isinstance(x, Styler):
+ # у Styler нет __len__, но есть .data
+ return getattr(x, "data", pd.DataFrame()).empty
+ except Exception:
+ pass
+ # пустые коллекции
+ if isinstance(x, (list, tuple, dict, set)):
+ return len(x) == 0
+ return False
+
+ def safe_show(func, *args, **kwargs):
+ """
+ Безопасно вызывает функции отображения Streamlit (dataframe, table, metric, image, markdown, ...).
+ - Ничего не рендерит, если основной аргумент данных пуст/None.
+ - Нормализует height (убирает None, <0; приводит к int).
+ - Возвращает результат вызова func (нужно для dataframe-selection).
+ - Перехватывает исключения и показывает предупреждение.
+ """
+ # Если среди позиционных/именованных аргументов есть пустые/None-данные — не показываем
+ for a in args:
+ if _is_empty_like(a):
+ return None
+ for k, v in kwargs.items():
+ # пропускаем не-данные параметры (типа width/unsafe_allow_html)
+ if k.lower() in ("height", "width", "use_container_width", "unsafe_allow_html", "on_select", "selection_mode", "column_config", "hide_index", "border", "delta_color", "key"):
+ continue
+ if _is_empty_like(v):
+ return None
+
+ # height -> валидный int, иначе уберём
+ if "height" in kwargs:
+ h = kwargs.get("height")
+ if h is None:
+ kwargs.pop("height")
+ else:
+ try:
+ h = int(h)
+ if h < 0:
+ kwargs.pop("height")
+ else:
+ kwargs["height"] = h
+ except Exception:
+ kwargs.pop("height")
+
+ try:
+ return func(*args, **kwargs)
+ except Exception as e:
+ st.warning(f"⚠️ Ошибка при отображении: {e}")
+ return None
+ # ======== /SAFE WRAPPER ========
+
***************
*** 221,231 ****
col1, col4, col2, col5, col3 = st.columns([1, 5, 3, 5, 1])
t1 = (result.get("team1") or {})
t2 = (result.get("team2") or {})
- if t1.get("logo"):
- col1.image(t1["logo"], width=100)
team1_name = t1.get("name") or ""
team2_name = t2.get("name") or ""
- if team1_name or team2_name:
- col2.markdown(
- f"<h2 style='text-align: center'>{team1_name} — {team2_name}</h2>",
- unsafe_allow_html=True,
- )
- if t2.get("logo"):
- col3.image(t2["logo"], width=100)
+ safe_show(col1.image, t1.get("logo"), width=100)
+ if team1_name or team2_name:
+ safe_show(
+ col2.markdown,
+ f"<h2 style='text-align: center'>{team1_name} — {team2_name}</h2>",
+ unsafe_allow_html=True,
+ )
+ safe_show(col3.image, t2.get("logo"), width=100)
col4_1, col4_2, col4_3 = col4.columns((1, 1, 1))
col5_1, col5_2, col5_3 = col5.columns((1, 1, 1))
***************
*** 237,253 ****
if isinstance(cached_team_stats, list) and len(cached_team_stats) > 0:
v1 = cached_team_stats[0].get("val1")
v2 = cached_team_stats[0].get("val2")
if v1 is not None and v2 is not None:
val1, val2 = int(v1), int(v2)
delta_color_1 = "off" if val1 == val2 else "normal"
- col4_1.metric("Points", v1, val1 - val2, delta_color_1)
- col5_3.metric("Points", v2, val2 - val1, delta_color_1)
+ safe_show(col4_1.metric, "Points", v1, val1 - val2, delta_color_1)
+ safe_show(col5_3.metric, "Points", v2, val2 - val1, delta_color_1)
- col4_3.metric("TimeOuts", len(timeout1))
- col5_1.metric("TimeOuts", len(timeout2))
+ safe_show(col4_3.metric, "TimeOuts", len(timeout1))
+ safe_show(col5_1.metric, "TimeOuts", len(timeout2))
if isinstance(cached_live_status, list) and cached_live_status:
foulsA = (cached_live_status[0] or {}).get("foulsA")
foulsB = (cached_live_status[0] or {}).get("foulsB")
if foulsA is not None:
- col4_2.metric("Fouls", foulsA)
+ safe_show(col4_2.metric, "Fouls", foulsA)
if foulsB is not None:
- col5_2.metric("Fouls", foulsB)
+ safe_show(col5_2.metric, "Fouls", foulsB)
***************
*** 270,280 ****
for q1, q2, col1_i, col2_i in zip(score_by_quarter_1, score_by_quarter_2, col_1_col, col_2_col):
count_q += 1
name_q = f"OT{count_q-4}" if count_q > 4 else f"Q{count_q}"
try:
delta_color = "off" if int(q1) == int(q2) else "normal"
- col1_i.metric(name_q, q1, int(q1) - int(q2), delta_color, border=True)
- col2_i.metric(name_q, q2, int(q2) - int(q1), delta_color, border=True)
+ safe_show(col1_i.metric, name_q, q1, int(q1) - int(q2), delta_color, border=True)
+ safe_show(col2_i.metric, name_q, q2, int(q2) - int(q1), delta_color, border=True)
except (ValueError, TypeError):
# если кривые данные в JSON, просто пропустим
pass
***************
*** 403,424 ****
team1_styled = (
team1_data.style.apply(highlight_grey, axis=1)
.apply(highlight_foul, subset="foul")
.apply(highlight_max, subset="pts")
)
team2_styled = (
team2_data.style.apply(highlight_grey, axis=1)
.apply(highlight_foul, subset="foul")
.apply(highlight_max, subset="pts")
)
# Вывод данных
col_player1, col_player2 = tab_temp_1.columns((5, 5))
- event1 = col_player1.dataframe(
- team1_styled,
- column_config=config,
- hide_index=True,
- height=460,
- on_select="rerun",
- selection_mode=[
- "single-row",
- ],
- )
- event2 = col_player2.dataframe(
- team2_styled,
- column_config=config,
- hide_index=True,
- height=460,
- on_select="rerun",
- selection_mode=[
- "single-row",
- ],
- )
+ event1 = safe_show(
+ col_player1.dataframe,
+ team1_styled,
+ column_config=config,
+ hide_index=True,
+ height=460,
+ on_select="rerun",
+ selection_mode=["single-row"],
+ )
+ event2 = safe_show(
+ col_player2.dataframe,
+ team2_styled,
+ column_config=config,
+ hide_index=True,
+ height=460,
+ on_select="rerun",
+ selection_mode=["single-row"],
+ )
if event1 and getattr(event1, "selection", None) and event1.selection.get("rows"):
selected_index1 = event1.selection["rows"][0]
st.session_state["player1"] = (
selected_index1 # Сохранение состояния в session_state
***************
*** 433,441 ****
if player_data_1["num"]:
z, a, b, c, d, e = col_player1.columns((1, 6, 1, 1, 1, 1))
- z.metric("Номер", player_data_1["num"], border=False)
- a.metric("Игрок", player_data_1["NameGFX"], border=False)
- b.metric("Амплуа", player_data_1["roleShort"], border=False)
- c.metric("Возраст", player_data_1["age"], border=False)
- d.metric("Рост", player_data_1["height"].split()[0], border=False)
- e.metric("Вес", player_data_1["weight"].split()[0], border=False)
-
- col_player1.dataframe(
- selected_player_1,
- column_config=config_season,
- hide_index=True,
- )
+ safe_show(z.metric, "Номер", player_data_1["num"], border=False)
+ safe_show(a.metric, "Игрок", player_data_1["NameGFX"], border=False)
+ safe_show(b.metric, "Амплуа", player_data_1["roleShort"], border=False)
+ safe_show(c.metric, "Возраст", player_data_1["age"], border=False)
+ safe_show(d.metric, "Рост", player_data_1["height"].split()[0], border=False)
+ safe_show(e.metric, "Вес", player_data_1["weight"].split()[0], border=False)
+
+ safe_show(
+ col_player1.dataframe,
+ selected_player_1,
+ column_config=config_season,
+ hide_index=True,
+ )
***************
*** 446,454 ****
if player_data_2["num"]:
z, a, b, c, d, e = col_player2.columns((1, 6, 1, 1, 1, 1))
- z.metric("Номер", player_data_2["num"], border=False)
- a.metric("Игрок", player_data_2["NameGFX"], border=False)
- b.metric("Амплуа", player_data_2["roleShort"], border=False)
- c.metric("Возраст", player_data_2["age"], border=False)
- d.metric("Рост", player_data_2["height"].split()[0], border=False)
- e.metric("Вес", player_data_2["weight"].split()[0], border=False)
-
- col_player2.dataframe(
- selected_player_2,
- column_config=config_season,
- hide_index=True,
- )
+ safe_show(z.metric, "Номер", player_data_2["num"], border=False)
+ safe_show(a.metric, "Игрок", player_data_2["NameGFX"], border=False)
+ safe_show(b.metric, "Амплуа", player_data_2["roleShort"], border=False)
+ safe_show(c.metric, "Возраст", player_data_2["age"], border=False)
+ safe_show(d.metric, "Рост", player_data_2["height"].split()[0], border=False)
+ safe_show(e.metric, "Вес", player_data_2["weight"].split()[0], border=False)
+
+ safe_show(
+ col_player2.dataframe,
+ selected_player_2,
+ column_config=config_season,
+ hide_index=True,
+ )
***************
*** 459,468 ****
if isinstance(cached_team_stats, list) and len(cached_team_stats) >= 34:
cached_team_stats_new = [
cached_team_stats[0],
*cached_team_stats[25:29],
cached_team_stats[7],
cached_team_stats[33],
*cached_team_stats[9:11],
*cached_team_stats[15:17],
]
- tab_temp_2.table(cached_team_stats_new)
+ safe_show(tab_temp_2.table, cached_team_stats_new)
***************
*** 470,484 ****
- if isinstance(cached_referee, (list, pd.DataFrame)):
- tab_temp_3.dataframe(cached_referee, height=600, column_config={"flag": st.column_config.ImageColumn("flag")})
-
-
column_config_ref = {
"flag": st.column_config.ImageColumn(
"flag",
),
}
if cached_referee:
- tab_temp_3.dataframe(cached_referee, height=600, column_config=column_config_ref)
+ safe_show(tab_temp_3.dataframe, cached_referee, height=600, column_config=column_config_ref)
***************
*** 503,511 ****
styled = df_st.style.apply(highlight_teams, axis=1)
- tab_temp_4.dataframe(
- styled,
- column_config={"logo": st.column_config.ImageColumn("logo")},
- hide_index=True,
- height=610,
- )
+ safe_show(
+ tab_temp_4.dataframe,
+ styled,
+ column_config={"logo": st.column_config.ImageColumn("logo")},
+ hide_index=True,
+ height=610,
+ )
***************
*** 552,558 ****
]
col.write(q)
- col.dataframe(df_col)
+ safe_show(col.dataframe, df_col)
# Овертаймы
for index, col in enumerate(columns_quarters):
q = columns_quarters_name_ot[index]
df_col = [
--- 564,570 ----
***************
*** 572,578 ****
]
col.write(q)
- col.dataframe(df_col)
+ safe_show(col.dataframe, df_col)
***************
*** 582,586 ****
if isinstance(cached_play_by_play, list) and isinstance(cached_game_online, dict):
plays = (cached_game_online.get("result") or {}).get("plays") or []
if plays:
- tab_temp_6.table(cached_play_by_play)
+ safe_show(tab_temp_6.table, cached_play_by_play)
***************
*** 703,724 ****
height1 = 38 * max(count_game_1, 10)
height2 = 38 * max(count_game_2, 10)
- col1_schedule.dataframe(
- team1_data,
- hide_index=True,
- height=int(min(height1, 1200)),
- column_config=column_config,
- )
- col2_schedule.dataframe(
- team2_data,
- hide_index=True,
- height=int(min(height2, 1200)),
- column_config=column_config,
- )
+ safe_show(
+ col1_schedule.dataframe,
+ team1_data,
+ hide_index=True,
+ height=int(min(height1, 1200)),
+ column_config=column_config,
+ )
+ safe_show(
+ col2_schedule.dataframe,
+ team2_data,
+ hide_index=True,
+ height=int(min(height2, 1200)),
+ column_config=column_config,
+ )
***************
*** 836,845 ****
filtered_data_pbp = temp_data_pbp[mask1]
count_pbp = len(filtered_data_pbp)
column_pbp = ["num", "info", "who", "period", "time"]
column_config_pbp = {
"info": st.column_config.TextColumn(width="medium"),
"who": st.column_config.TextColumn(width="large"),
}
- col2_pbp.dataframe(
- filtered_data_pbp[column_pbp],
- column_config=column_config_pbp,
- hide_index=True,
- height=(38 * count_pbp if count_pbp > 10 else None),
- )
+ safe_show(
+ col2_pbp.dataframe,
+ filtered_data_pbp[column_pbp],
+ column_config=column_config_pbp,
+ hide_index=True,
+ height=(38 * count_pbp if count_pbp > 10 else None),
+ )
else:
st.info("Данных play-by-play нет.")