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
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():
@@ -126,6 +158,7 @@ def get_ip_address():
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
@@ -137,17 +170,22 @@ def read_match_id_json(path="match_id.json", attempts=10, delay=0.2):
return json.load(f)
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:
# EBUSY (errno=16) — подождём и ещё раз
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
# иные ошибки — пробрасываем дальше
raise
logger.error("Не удалось прочитать match_id.json после нескольких попыток; возвращаю {}")
logger.error(
"Не удалось прочитать match_id.json после нескольких попыток; возвращаю {}"
)
return {}
# === Аргументы командной строки ===
parser = argparse.ArgumentParser(description="VTB Data Fetcher")
parser.add_argument("--league", type=str, default="vtb", help="League tag")
@@ -206,7 +244,6 @@ def _ipcheck() -> str:
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")
@@ -231,7 +268,8 @@ def rewrite_file(filename: str, data: dict, directory: str = "JSON") -> None:
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 при желании
@@ -365,21 +403,22 @@ def Game_Online(game_id: int) -> dict | None:
if (
cached
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")
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"
live
and live.get("status") == "Ok"
and live.get("result", {}).get("gameStatus") == "Online"
)
@@ -391,7 +430,9 @@ def Game_Online(game_id: int) -> dict | None:
with _GAME_CACHE_LOCK:
_GAME_CACHE[cache_key] = {"mode": "old", "ts": now, "data": game}
else:
logger.warning(f"Не удалось получить данные старого матча: game_id={game_id}")
logger.warning(
f"Не удалось получить данные старого матча: game_id={game_id}"
)
return game
# 1. Получаем box score
@@ -412,18 +453,18 @@ def Game_Online(game_id: int) -> dict | None:
# 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
# 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"))
@@ -477,8 +518,8 @@ def game_online_loop(game_id: int, stop_event: threading.Event) -> None:
game_id (int): ID игры.
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():
try:
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:
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:
@@ -620,7 +666,6 @@ def calc_shot_percent_by_type(
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)
@@ -700,10 +745,56 @@ def sum_stat_with_online(
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
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:
@@ -732,7 +823,8 @@ def Json_Team_Generation(who, data, stop_event):
json_live_status = get_json(url)
online = (
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["result"]["gameStatus"] == "Online"
else False
@@ -756,38 +848,40 @@ def Json_Team_Generation(who, data, stop_event):
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}")
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]
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
initialized = True
# print(coach_stat)
# while not stop_event.is_set():
role_list = [
@@ -810,7 +904,8 @@ def Json_Team_Generation(who, data, stop_event):
row_player_season = next(
(
v
for row in player_season_stat if row
for row in player_season_stat
if row
for k, v in row.items()
if k == item["personId"]
),
@@ -819,7 +914,8 @@ def Json_Team_Generation(who, data, stop_event):
row_player_career = next(
(
v
for row in player_career_stat if row
for row in player_career_stat
if row
for k, v in row.items()
if k == item["personId"]
),
@@ -829,7 +925,8 @@ def Json_Team_Generation(who, data, stop_event):
row_coach_stat = next(
(
v
for row in coach_stat if row
for row in coach_stat
if row
for k, v in row.items()
if k == item["personId"]
),
@@ -1127,7 +1224,12 @@ def Json_Team_Generation(who, data, stop_event):
item["lastName"].strip() if item["lastName"] else ""
),
"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"
else ""
),
@@ -1386,8 +1488,92 @@ def Json_Team_Generation(who, data, stop_event):
"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")
@@ -1479,16 +1665,30 @@ def Json_Team_Generation(who, data, stop_event):
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}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)
if not player_stat_season:
@@ -1508,7 +1708,7 @@ def Player_Stat_Season(player_id: str, season: str) -> dict:
return {player_id: default_player_stats_season()}
def Player_all_game_in_season2(player_id: str, season:str) -> dict:
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 = {}
@@ -1538,25 +1738,23 @@ def Player_all_game2(player_id: str) -> dict:
]
for i in player_season_stat_temp:
print(i.result())
player_game += [
res.result() for res in player_season_stat_temp
]
player_game += [res.result() for res in player_season_stat_temp]
rewrite_file(player_id, player_game)
def Player_all_game_in_season(player_id: str, season: str) -> List[Dict[str, Any]]:
url = f"{URL}api/abc/players/stats?tag={LEAGUE}&season={season}&id={player_id}"
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 [
] # возвращаем пустой список, чтобы тип был стабилен
return [] # возвращаем пустой список, чтобы тип был стабилен
items = player_games.get("items") or []
# гарантируем список словарей
if not isinstance(items, list):
logger.warning(f"Неверный формат 'items' для {player_id}, сезон {season}: {type(items)}")
logger.warning(
f"Неверный формат 'items' для {player_id}, сезон {season}: {type(items)}"
)
return []
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]]:
# url = f"{URL}api/abc/players/info?tag={LEAGUE}&id={player_id}"
# player_seasons = get_json(url)
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 [] # последовательный тип
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},
]
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")]
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() # это уже список словарей
@@ -1696,7 +1898,7 @@ def default_player_stats() -> list:
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)
if not player_stat_career:
@@ -1954,7 +2156,7 @@ def Team_Both_Stat(stop_event: threading.Event) -> None:
stop_event (threading.Event): Событие для остановки цикла.
"""
logger.info("START making json for team statistics")
global game_online_data
global game_online_data, RUN_ONCE
while not stop_event.is_set():
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"):
logger.debug("Нет total у команд — пропускаю перезапись team_stats.json")
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,
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,
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)
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
@@ -2066,7 +2282,7 @@ def Referee(stop_event: threading.Event) -> None:
Поток, создающий JSON-файл с информацией о судьях матча.
"""
logger.info("START making json for referee")
global game_online_data
global game_online_data, RUN_ONCE
desired_order = [
"Crew chief",
@@ -2128,6 +2344,9 @@ def Referee(stop_event: threading.Event) -> None:
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)
@@ -2140,7 +2359,7 @@ def Scores_Quarter(stop_event: threading.Event) -> None:
Поток, обновляющий JSON со счётом по четвертям.
"""
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"]
@@ -2185,7 +2404,10 @@ def Scores_Quarter(stop_event: threading.Event) -> None:
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)
@@ -2201,13 +2423,15 @@ def status_online_func(data: dict) -> dict | None:
if json_live_status.get("status") != "Ok":
logger.warning(f"Live status API вернул не 'Ok': {json_live_status}")
return None
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)
@@ -2226,8 +2450,9 @@ def Status_Online(data: dict, stop_event: threading.Event) -> None:
Поток, обновляющий JSON-файл с онлайн-статусом матча.
"""
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():
try:
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 — пропуск записи.")
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)
@@ -2294,7 +2522,8 @@ def Play_By_Play(data: dict, stop_event: threading.Event) -> None:
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("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:
@@ -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)
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)
@@ -2428,16 +2659,22 @@ def schedule_daily_restart():
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)
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')}")
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)
logger.error(
f"Ошибка при автоматическом перезапуске: {e}", exc_info=True
)
def clean_np_ints(obj):
@@ -2462,14 +2699,19 @@ def get_season_and_schedule() -> dict | None:
"""
global URL, LEAGUE, LANG, TEAM
try:
# Получение активного сезона
season_url = f"{URL}api/abc/comps/seasons?Tag={LEAGUE}&Lang={LANG}"
season_data = get_json(season_url)
season = (
season_data.get("result", [{}])[0].get("season") if season_data else None
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:
@@ -2509,16 +2751,18 @@ def get_season_and_schedule() -> dict | 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"],
})
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)
@@ -2527,14 +2771,12 @@ def get_season_and_schedule() -> dict | None:
def Standing_func(data: dict, stop_event: threading.Event) -> None:
logger.info("START making json for standings")
global URL, LEAGUE, LANG
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}"
)
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"]:
@@ -2599,7 +2841,9 @@ def Standing_func(data: dict, stop_event: threading.Event) -> None:
indent=4,
)
logger.debug("Standings data saved successfully.")
if RUN_ONCE:
logger.info("<Standing_func>: однократный режим — выхожу из потока.")
return
except Exception as 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_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),
"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
global TEAM, LANG, URL, RUN_ONCE, CURRENT_GAME_ID
if TEAM is None:
logger.critical(f"{myhost}\nКоманда не указана")
@@ -2796,8 +3045,8 @@ def main():
URL = (
"https://basket.sportoteka.org/"
if "uba" in LEAGUE.lower()
# else "https://pro.russiabasket.org/"
else "https://vtb-league.org/"
else "https://pro.russiabasket.org/"
# else "https://vtb-league.org/"
)
stop_event = Event()
@@ -2808,11 +3057,24 @@ def main():
# === Ежедневный перезапуск функции на Linux/Mac ===
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(
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,
@@ -2835,27 +3097,27 @@ def main():
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"
# ),
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"
),
]
# Запуск всех потоков
@@ -2863,12 +3125,33 @@ def main():
t.start()
logger.debug(f"Поток {t.name} запущен.")
# How_To_Play_Quarter(data)
# pregame_data(data)
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()

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

322
visual.py
View File

@@ -181,7 +181,31 @@ def process_player_data(team_json, player_index):
"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 = {
@@ -190,24 +214,24 @@ config = {
"num": st.column_config.TextColumn("#", width=27),
"NameGFX": st.column_config.TextColumn(width=170),
"isOn": st.column_config.TextColumn("🏀", width=27),
"pts": st.column_config.NumberColumn("PTS", width=27),
"pt-2": st.column_config.TextColumn("2-PT", width=45),
"pt-3": st.column_config.TextColumn("3-PT", width=45),
"pt-1": st.column_config.TextColumn("FT", width=45),
"fg": st.column_config.TextColumn("FG", width=45),
"ast": st.column_config.NumberColumn("AS", width=27),
"stl": st.column_config.NumberColumn("ST", width=27),
"blk": st.column_config.NumberColumn("BL", width=27),
"blkVic": st.column_config.NumberColumn("BV", width=27),
"dreb": st.column_config.NumberColumn("DR", width=27),
"oreb": st.column_config.NumberColumn("OR", width=27),
"reb": st.column_config.NumberColumn("R", width=27),
"to": st.column_config.NumberColumn("TO", width=27),
"foul": st.column_config.NumberColumn("F", width=27),
"fouled": st.column_config.NumberColumn("Fed", width=27),
"plusMinus": st.column_config.NumberColumn("+/-", width=27),
"dunk": st.column_config.NumberColumn("DUNK", width=27),
"kpi": st.column_config.NumberColumn("KPI", width=27),
"pts": st.column_config.TextColumn("PTS", width="content", help="⭐ = Career High"),
"pt-2": st.column_config.TextColumn("2-PT", width="content"),
"pt-3": st.column_config.TextColumn("3-PT", width="content"),
"pt-1": st.column_config.TextColumn("FT", width="content"),
"fg": st.column_config.TextColumn("FG", width="content"),
"ast": st.column_config.TextColumn("AS", width="content"),
"stl": st.column_config.TextColumn("ST", width="content"),
"blk": st.column_config.TextColumn("BL", width="content"),
"blkVic": st.column_config.TextColumn("BV", width="content"),
"dreb": st.column_config.TextColumn("DR", width="content"),
"oreb": st.column_config.TextColumn("OR", width="content"),
"reb": st.column_config.TextColumn("R", width="content"),
"to": st.column_config.TextColumn("TO", width="content"),
"foul": st.column_config.TextColumn("F", width="content"),
"fouled": st.column_config.TextColumn("Fed", width="content"),
"plusMinus": st.column_config.TextColumn("+/-", width="content"),
"dunk": st.column_config.TextColumn("DUNK", width="content"),
"kpi": st.column_config.TextColumn("KPI", width="content"),
"time": st.column_config.TextColumn("TIME"),
"game_count": st.column_config.TextColumn("G", width=27),
"start_count": st.column_config.TextColumn("S", width=27),
@@ -813,19 +837,127 @@ columns_game = [
if cached_team1 and cached_team2:
team1_data = process_team_data(cached_team1, 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_data.style.apply(highlight_grey, axis=1)
.apply(highlight_foul, subset="foul")
.apply(highlight_max, subset="pts")
.apply(highlight_max, subset="kpi")
team1_data[columns_game].style
.apply(highlight_grey, axis=1)
.apply(highlight_foul, subset="foul")
.apply(lambda _: highlight_max_by_refcol(team1_data, "pts", "_pts_num"), axis=0, subset=["pts"])
.apply(lambda _: highlight_max_by_refcol(team1_data, "kpi", "_kpi_num"), axis=0, subset=["kpi"])
)
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_max, subset="pts")
.apply(highlight_max, subset="kpi")
.apply(lambda _: highlight_max_by_refcol(team2_data, "pts", "_pts_num"), axis=0, subset=["pts"])
.apply(lambda _: highlight_max_by_refcol(team2_data, "kpi", "_kpi_num"), axis=0, subset=["kpi"])
)
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)
# Указать нужные колонки для вывода
columns_to_show = [
"season",
"game.gameDate",
"game.team1Name",
"game.team2Name",
"game.score",
"stats.points",
"stats.shot2Percent",
"stats.shot3Percent",
"stats.shot23Percent",
"stats.shot1Percent",
"stats.assist",
"stats.steal",
"stats.blockShot",
"stats.defRebound",
"stats.offRebound",
"stats.rebound",
"stats.turnover",
"stats.foul",
"stats.playedTime",
"stats.plusMinus",
"Сезон",
"Дата",
"Команда 1",
"Команда 2",
"Счёт",
"PTS",
"2-PTS%",
"3-PTS%",
"FG%",
"FT%",
"AST",
"STL",
"BLK",
"DR",
"OR",
"REB",
"TO",
"F",
"TIME",
"+/-",
]
numeric_cols = [
"stats.points",
@@ -897,24 +1028,63 @@ if cached_team1 and cached_team2:
# df_filtered[numeric_cols] = df_filtered[numeric_cols].apply(
# 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 = df_filtered.rename(columns=rename_map)
df_filtered["Дата"] = df_filtered["Дата"].dt.strftime("%d.%m.%Y")
styled = (
df_filtered[columns_to_show]
.style
.apply(highlight_max, subset=["stats.points"])
.apply(highlight_max, subset=["stats.assist"])
.apply(highlight_max, subset=["stats.steal"])
.apply(highlight_max, subset=["stats.blockShot"])
.apply(highlight_max, subset=["stats.rebound"])
.style.apply(highlight_max, subset=["PTS"])
.apply(highlight_max, subset=["AST"])
.apply(highlight_max, subset=["STL"])
.apply(highlight_max, subset=["BLK"])
.apply(highlight_max, subset=["REB"])
.format(
{
"Дата": lambda x: x, # уже строка, просто оставляем как есть
"PTS": "{:,.0f}".format,
"AST": "{:,.0f}".format,
"STL": "{:,.0f}".format,
"BLK": "{:,.0f}".format,
"REB": "{:,.0f}".format,
}
)
)
return styled
# Вывод данных
col_player1, col_player2 = tab_temp_1.columns((5, 5))
config_copy = config.copy()
event1 = col_player1.dataframe(
team1_styled,
column_config=config,
column_config=config_copy,
hide_index=True,
height=460,
on_select="rerun",
@@ -960,7 +1130,8 @@ if cached_team1 and cached_team2:
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"):
selected_index2 = event2.selection["rows"][0]
@@ -988,7 +1159,8 @@ if cached_team1 and cached_team2:
column_config=config_season,
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))
if isinstance(cached_team_stats, list) and len(cached_team_stats) >= 34:
@@ -1085,12 +1257,31 @@ if cached_standings:
pass
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(
styled,
column_config={"logo": st.column_config.ImageColumn("logo")},
hide_index=True,
height=610,
width="content"
)
@@ -1534,23 +1725,22 @@ with tab_online:
live_data_map = {}
# Собираем live-данные по каждому game.id
for _, row in df_filtered.iterrows():
game_id = row["game.id"]
try:
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()
except Exception as ex:
# json_data = {
# "period": None,
# "timeToGo": 0.0,
# }
print(ex)
# Берём содержимое result (словарь с gameId и данными)
result = json_data.get("result", {})
if result and "gameId" in result:
live_data_map[result["gameId"]] = result
if json_data:
result = json_data.get("result", {})
if result and "gameId" in result:
live_data_map[result["gameId"]] = result
# Создаём колонки для live-данных
df_filtered["live_period"] = None

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 нет.")