1982 lines
74 KiB
Python
1982 lines
74 KiB
Python
import os
|
||
import json
|
||
import socket
|
||
import platform
|
||
import numpy as np
|
||
import pandas as pd
|
||
import streamlit as st
|
||
import sys
|
||
import requests
|
||
from streamlit_autorefresh import st_autorefresh
|
||
import errno
|
||
import time
|
||
import re
|
||
from datetime import datetime, timedelta, timezone
|
||
|
||
|
||
st.set_page_config(
|
||
page_title="Баскетбол",
|
||
page_icon="🏀",
|
||
layout="wide",
|
||
initial_sidebar_state="expanded",
|
||
menu_items={"About": "версия 3.2 от 23.10.2025"},
|
||
)
|
||
REMOVE_PADDING_FROM_SIDES = """
|
||
<style>
|
||
.block-container {
|
||
padding-top: 0rem;
|
||
padding-bottom: 0rem;
|
||
}
|
||
</style>
|
||
"""
|
||
|
||
st.markdown(REMOVE_PADDING_FROM_SIDES, unsafe_allow_html=True)
|
||
st_autorefresh()
|
||
|
||
|
||
def schedule_daily_restart():
|
||
"""Перезапуск get_season_and_schedule каждый день в 00:05 (только не на Windows)."""
|
||
if not sys.platform.startswith("win"):
|
||
while True:
|
||
now = datetime.now()
|
||
next_run = (now + timedelta(days=1)).replace(
|
||
hour=0, minute=5, second=0, microsecond=0
|
||
)
|
||
sleep_time = (next_run - now).total_seconds()
|
||
time.sleep(sleep_time)
|
||
|
||
st.cache_data.clear()
|
||
|
||
|
||
# Функции для стилизации
|
||
def highlight_max(data):
|
||
# Преобразуем данные к числовому типу, заменяя некорректные значения на NaN
|
||
numeric_data = pd.to_numeric(data, errors="coerce")
|
||
max_value = numeric_data.max() if pd.notna(numeric_data.max()) else None
|
||
return [
|
||
"background-color: green" if pd.notna(v) and v == max_value and v > 0 else ""
|
||
for v in numeric_data
|
||
]
|
||
|
||
|
||
def color_win(s):
|
||
return [
|
||
(
|
||
"background-color: ForestGreen"
|
||
if v == True
|
||
else "background-color: #FF4B4B" if v == False else None
|
||
)
|
||
for v in s
|
||
]
|
||
|
||
|
||
def highlight_grey(s):
|
||
return ["background-color: grey"] * len(s) if s.foul == 5 else [""] * len(s)
|
||
|
||
|
||
def highlight_foul(s):
|
||
return [
|
||
(
|
||
"background-color: orange"
|
||
if v == 4
|
||
else "background-color: red" if v == 5 else ""
|
||
)
|
||
for v in s
|
||
]
|
||
|
||
|
||
def load_json_data(filepath):
|
||
"""
|
||
Загружает данные из JSON файла и кэширует их.
|
||
Возвращает None, если файл не удается прочитать.
|
||
"""
|
||
try:
|
||
with open(filepath, "r", encoding="utf-8") as file:
|
||
return json.load(file)
|
||
except (json.JSONDecodeError, FileNotFoundError):
|
||
return None
|
||
|
||
|
||
# Функция для обработки данных одной команды
|
||
def process_team_data(team_json, columns_to_include):
|
||
team_data = pd.json_normalize(team_json)
|
||
|
||
# Оставляем только нужные колонки
|
||
team_data = team_data[:12][columns_to_include]
|
||
|
||
# Обработка height и weight
|
||
for column in ["height", "weight"]:
|
||
if column in team_data.columns:
|
||
team_data[column] = team_data[column].apply(
|
||
lambda value: "" if value == 0 else value
|
||
)
|
||
|
||
return team_data
|
||
|
||
|
||
def process_player_data(team_json, player_index):
|
||
team_data = pd.json_normalize(team_json)
|
||
player_data = team_data.iloc[player_index]
|
||
season_total = {
|
||
"name": "Season Total",
|
||
"game_count": str(player_data["TGameCount"]),
|
||
"start_count": str(player_data["TStartCount"]),
|
||
"pts": str(player_data["TPoints"]),
|
||
"pt-2": str(player_data["TShots2"]),
|
||
"pt-3": str(player_data["TShots3"]),
|
||
"pt-1": str(player_data["TShots1"]),
|
||
"fg": str(player_data["TShots23"]),
|
||
"ast": str(player_data["TAssist"]),
|
||
"stl": str(player_data["TSteal"]),
|
||
"blk": str(player_data["TBlocks"]),
|
||
"dreb": str(player_data["TDefRebound"]),
|
||
"oreb": str(player_data["TOffRebound"]),
|
||
"reb": str(player_data["TRebound"]),
|
||
# "to": str(player_data["TTurnover"]),
|
||
# "foul": str(player_data["TFoul"]),
|
||
"fouled": str(player_data["TOpponentFoul"]),
|
||
"dunk": str(player_data["TDunk"]),
|
||
"time": str(player_data["TPlayedTime"]),
|
||
}
|
||
season_avg = {
|
||
"name": "Season Average",
|
||
"game_count": "",
|
||
"start_count": "",
|
||
"pts": str(player_data["AvgPoints"]),
|
||
"pt-2": str(player_data["Shot2Percent"]),
|
||
"pt-3": str(player_data["Shot3Percent"]),
|
||
"pt-1": str(player_data["Shot1Percent"]),
|
||
"fg": str(player_data["Shot23Percent"]),
|
||
"ast": str(player_data["AvgAssist"]),
|
||
"stl": str(player_data["AvgSteal"]),
|
||
"blk": str(player_data["AvgBlocks"]),
|
||
"dreb": str(player_data["AvgDefRebound"]),
|
||
"oreb": str(player_data["AvgOffRebound"]),
|
||
"reb": str(player_data["AvgRebound"]),
|
||
# "to": str(player_data["AvgTurnover"]),
|
||
# "foul": str(player_data["AvgFoul"]),
|
||
"fouled": str(player_data["AvgOpponentFoul"]),
|
||
"dunk": str(player_data["AvgDunk"]),
|
||
"time": str(player_data["AvgPlayedTime"]),
|
||
}
|
||
career_total = {
|
||
"name": "Career Total",
|
||
"game_count": str(player_data["CareerTGameCount"]),
|
||
"start_count": str(player_data["CareerTStartCount"]),
|
||
"pts": str(player_data["CareerTPoints"]),
|
||
"pt-2": str(player_data["CareerTShots2"]),
|
||
"pt-3": str(player_data["CareerTShots3"]),
|
||
"pt-1": str(player_data["CareerTShots1"]),
|
||
"fg": str(player_data["CareerTShots23"]),
|
||
"ast": str(player_data["CareerTAssist"]),
|
||
"stl": str(player_data["CareerTSteal"]),
|
||
"blk": str(player_data["CareerTBlocks"]),
|
||
"dreb": str(player_data["CareerTDefRebound"]),
|
||
"oreb": str(player_data["CareerTOffRebound"]),
|
||
"reb": str(player_data["CareerTRebound"]),
|
||
# "to": str(player_data["CareerTTurnover"]),
|
||
# "foul": str(player_data["CareerTFoul"]),
|
||
"fouled": str(player_data["CareerTOpponentFoul"]),
|
||
"dunk": str(player_data["CareerTDunk"]),
|
||
"time": str(player_data["CareerTPlayedTime"]),
|
||
}
|
||
|
||
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 = {
|
||
"flag": st.column_config.ImageColumn("flag"),
|
||
"roleShort": st.column_config.TextColumn("R", width=27),
|
||
"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.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),
|
||
"q_pts": st.column_config.TextColumn("PTS", width=27),
|
||
"q_ast": st.column_config.TextColumn("AS", width=27),
|
||
"q_stl": st.column_config.TextColumn("ST", width=27),
|
||
"q_blk": st.column_config.TextColumn("BL", width=27),
|
||
"q_reb": st.column_config.TextColumn("R", width=27),
|
||
"q_rnk": st.column_config.TextColumn("KPI", width=27),
|
||
"q_f": st.column_config.TextColumn("F", width=27),
|
||
"q_f_on": st.column_config.TextColumn("Fed", width=27),
|
||
"q_to": st.column_config.TextColumn("TO", width=27),
|
||
"q_time": st.column_config.TextColumn("TIME"),
|
||
"q_pt2": st.column_config.TextColumn("2-PT", width=45),
|
||
"q_pt3": st.column_config.TextColumn("3-PT", width=45),
|
||
"q_pt23": st.column_config.TextColumn("FG", width=45),
|
||
"q_ft": st.column_config.TextColumn("FT", width=45),
|
||
}
|
||
config_season = {
|
||
"flag": st.column_config.ImageColumn("flag"),
|
||
"roleShort": st.column_config.TextColumn("R", width=27),
|
||
"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.TextColumn("PTS", width=40),
|
||
"pt-2": st.column_config.TextColumn("2-PT", width=60),
|
||
"pt-3": st.column_config.TextColumn("3-PT", width=60),
|
||
"pt-1": st.column_config.TextColumn("FT", width=60),
|
||
"fg": st.column_config.TextColumn("FG", width=60),
|
||
"ast": st.column_config.TextColumn("AS", width=40),
|
||
"stl": st.column_config.TextColumn("ST", width=40),
|
||
"blk": st.column_config.TextColumn("BL", width=40),
|
||
"blkVic": st.column_config.TextColumn("BV", width=40),
|
||
"dreb": st.column_config.TextColumn("DR", width=40),
|
||
"oreb": st.column_config.TextColumn("OR", width=40),
|
||
"reb": st.column_config.TextColumn("R", width=40),
|
||
"to": st.column_config.TextColumn("TO", width=40),
|
||
"foul": st.column_config.TextColumn("F", width=40),
|
||
"fouled": st.column_config.TextColumn("Fed", width=40),
|
||
"plusMinus": st.column_config.TextColumn("+/-", width=40),
|
||
"dunk": st.column_config.TextColumn("DUNK", width=40),
|
||
"kpi": st.column_config.TextColumn("KPI", width=40),
|
||
"time": st.column_config.TextColumn("TIME"),
|
||
"game_count": st.column_config.TextColumn("G", width=40),
|
||
"start_count": st.column_config.TextColumn("S", width=40),
|
||
}
|
||
|
||
if "player1" not in st.session_state:
|
||
st.session_state.player1 = None
|
||
if "player2" not in st.session_state:
|
||
st.session_state.player2 = None
|
||
|
||
|
||
FOLDER_JSON = "static"
|
||
|
||
|
||
def get_ip_address():
|
||
try:
|
||
# Попытка получить IP-адрес с использованием внешнего сервиса
|
||
# Может потребоваться подключение к интернету
|
||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||
s.connect(("8.8.8.8", 80))
|
||
ip_address = s.getsockname()[0]
|
||
except socket.error:
|
||
# Если не удалось получить IP-адрес через внешний сервис,
|
||
# используем метод для локального получения IP
|
||
ip_address = socket.gethostbyname(socket.gethostname())
|
||
return ip_address
|
||
|
||
|
||
def _is_meaningful_payload(obj) -> bool:
|
||
# Нулевые/битые/пустые — не считаем осмысленными
|
||
if obj is None:
|
||
return False
|
||
# Пустая коллекция — не осмысленно
|
||
if isinstance(obj, (list, dict, set, tuple)):
|
||
return len(obj) > 0
|
||
# Любой другой тип (str/числа) считаем осмысленным только если не пустая строка
|
||
if isinstance(obj, str):
|
||
return obj.strip() != ""
|
||
return True
|
||
|
||
|
||
def load_data_from_json(filepath):
|
||
"""
|
||
Читает JSON и обновляет session_state ТОЛЬКО если данные осмысленные.
|
||
Иначе оставляет предыдущий кэш нетронутым (stale-if-error/stale-if-empty).
|
||
"""
|
||
directory = FOLDER_JSON
|
||
os.makedirs(directory, exist_ok=True)
|
||
filepath_full = os.path.join(directory, f"{filepath}.json")
|
||
print(filepath_full)
|
||
print(filepath)
|
||
# вычисление ключа
|
||
# ip = get_ip_address()
|
||
# host = ip_check.get(ip, {}).get("host") or ""
|
||
host = _ipcheck()
|
||
base = os.path.basename(filepath_full).replace(".json", "")
|
||
key = base.replace(f"{host}", "", 1)
|
||
new_payload = load_json_data(filepath_full) # может быть None/битое/пустое
|
||
# if "team1_" in filepath_full:
|
||
# print(filepath_full)
|
||
# print(new_payload)
|
||
|
||
# Если новый payload осмысленный — обновляем кэш и "last_good_*"
|
||
if _is_meaningful_payload(new_payload):
|
||
st.session_state[key] = new_payload
|
||
st.session_state.setdefault("_last_good", {})
|
||
st.session_state["_last_good"][key] = new_payload
|
||
return
|
||
|
||
# Иначе: если раньше уже был хороший — восстанавливаем его и НЕ трогаем
|
||
if "_last_good" in st.session_state and key in st.session_state["_last_good"]:
|
||
# ничего не делаем — оставляем текущее значение как есть
|
||
return
|
||
|
||
# Иначе (нет ни нового, ни прошлого хорошего) — вообще не создаём ключ
|
||
# чтобы ниже по коду .get(...) вернул None и UI ничего не нарисовал
|
||
# (при желании можно явно "очищать": st.session_state.pop(key, None))
|
||
|
||
|
||
def _ipcheck() -> str:
|
||
"""Возвращает префикс для имени файла по IP.
|
||
Если IP локальный или host не найден — возвращает пустую строку.
|
||
"""
|
||
try:
|
||
ip_str = get_ip_address()
|
||
except Exception:
|
||
ip_str = None
|
||
|
||
ip_map = globals().get("ip_check") or {}
|
||
if ip_str and isinstance(ip_map, dict):
|
||
host = (ip_map.get(ip_str) or {}).get("host")
|
||
if host:
|
||
return f"{host}_"
|
||
return ""
|
||
|
||
|
||
def read_match_id_json(path="match_id.json", attempts=10, delay=0.2):
|
||
"""Надёжное чтение match_id.json с ретраями при EBUSY/битом JSON."""
|
||
d = delay
|
||
for i in range(attempts):
|
||
try:
|
||
if not os.path.isfile(path):
|
||
return {}
|
||
with open(path, "r", encoding="utf-8") as f:
|
||
return json.load(f)
|
||
except json.JSONDecodeError:
|
||
# файл переписывают — подождём и попробуем снова
|
||
time.sleep(d)
|
||
d = min(d * 1.6, 2.0)
|
||
except OSError as e:
|
||
# EBUSY (errno=16) — подождём и ещё раз
|
||
if getattr(e, "errno", None) == errno.EBUSY:
|
||
time.sleep(d)
|
||
d = min(d * 1.6, 2.0)
|
||
continue
|
||
# иные ошибки — пробрасываем дальше
|
||
raise
|
||
print("Не удалось прочитать match_id.json после нескольких попыток; возвращаю {}")
|
||
return {}
|
||
|
||
|
||
def rewrite_file(filename: str, data: dict, directory: str = "JSON") -> None:
|
||
"""
|
||
Перезаписывает JSON-файл с заданными данными.
|
||
Если запуск локальный (локальный IP), префикс host в имени файла не используется.
|
||
Если IP не локальный и есть словарь ip_check с хостами — добавим префикс host_.
|
||
"""
|
||
# Если глобальная константа задана — используем её
|
||
try:
|
||
directory = FOLDER_JSON # type: ignore[name-defined]
|
||
except NameError:
|
||
# иначе используем аргумент по умолчанию/переданный
|
||
pass
|
||
|
||
os.makedirs(directory, exist_ok=True)
|
||
|
||
filepath = os.path.join(directory, f"{filename}.json")
|
||
# print(filepath) # оставил как у тебя; можно заменить на logger.debug при желании
|
||
|
||
try:
|
||
with open(filepath, "w", encoding="utf-8") as f:
|
||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||
except Exception as e:
|
||
print(f"Ошибка при записи файла {filepath}: {e}")
|
||
|
||
|
||
ip_check = read_match_id_json("match_id.json") or {}
|
||
|
||
|
||
prefix = _ipcheck()
|
||
|
||
load_data_from_json("game")
|
||
cached_game_online = st.session_state.get("game")
|
||
|
||
load_data_from_json("team1")
|
||
cached_team1 = st.session_state.get("team1")
|
||
|
||
load_data_from_json("team2")
|
||
cached_team2 = st.session_state.get("team2")
|
||
|
||
load_data_from_json("referee")
|
||
cached_referee = st.session_state.get("referee")
|
||
|
||
# standings — может не быть тега/файла
|
||
league_tag = None
|
||
if isinstance(cached_game_online, dict):
|
||
league_tag = (cached_game_online.get("league") or {}).get(
|
||
"tag"
|
||
)
|
||
print(cached_game_online.get("comp").get("name"))
|
||
comp_name = (
|
||
(cached_game_online.get("comp") or {})
|
||
.get("name")
|
||
.replace(" ", "_")
|
||
)
|
||
if league_tag:
|
||
load_data_from_json(f"standings_{league_tag}_{comp_name}")
|
||
cached_standings = (
|
||
st.session_state.get(f"standings_{league_tag}_{comp_name}") if league_tag else None
|
||
)
|
||
load_data_from_json("scores_quarter")
|
||
cached_scores_quarter = st.session_state.get("scores_quarter")
|
||
|
||
load_data_from_json("play_by_play")
|
||
cached_play_by_play = st.session_state.get("play_by_play")
|
||
|
||
load_data_from_json("team_stats")
|
||
cached_team_stats = st.session_state.get("team_stats")
|
||
|
||
load_data_from_json("scores")
|
||
cached_scores = st.session_state.get("scores") or [] # важно!
|
||
|
||
load_data_from_json("api_live-status")
|
||
cached_live_status = st.session_state.get("api_live-status")
|
||
|
||
load_data_from_json("schedule")
|
||
cached_schedule = st.session_state.get("schedule")
|
||
|
||
load_data_from_json("team_comparison")
|
||
cached_team_comparison = st.session_state.get("team_comparison")
|
||
|
||
|
||
def _is_empty_like(x) -> bool:
|
||
if x is None:
|
||
return True
|
||
if isinstance(x, pd.DataFrame):
|
||
return x.empty
|
||
try:
|
||
from pandas.io.formats.style import Styler
|
||
|
||
if isinstance(x, Styler):
|
||
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):
|
||
for a in args:
|
||
if _is_empty_like(a):
|
||
return None
|
||
for k, v in kwargs.items():
|
||
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
|
||
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 ========
|
||
def ensure_state(key: str, default=None):
|
||
# Инициализирует ключ один раз и возвращает значение
|
||
return st.session_state.setdefault(key, default)
|
||
|
||
|
||
period_max = 0
|
||
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:
|
||
df_data_pbp = pd.DataFrame(cached_play_by_play)
|
||
if not df_data_pbp.empty and "period" in df_data_pbp.columns:
|
||
period_max = int(df_data_pbp.iloc[0]["period"])
|
||
count_quarter = [
|
||
f"Четверть {i}" if i < 5 else f"Овертайм {i-4}"
|
||
for i in range(1, period_max + 1)
|
||
]
|
||
|
||
for i in range(1, period_max + 1):
|
||
key_team1 = f"team1_{i}"
|
||
key_team2 = f"team2_{i}"
|
||
load_data_from_json(key_team1)
|
||
load_data_from_json(key_team2)
|
||
_q1 = st.session_state.get(key_team1) # может быть None — это ок
|
||
_q2 = st.session_state.get(key_team2)
|
||
|
||
|
||
timeout1 = []
|
||
timeout2 = []
|
||
|
||
if isinstance(cached_game_online, dict):
|
||
result = cached_game_online
|
||
plays = result.get("plays") or []
|
||
|
||
timeout1, timeout2 = [], []
|
||
for event in plays:
|
||
if isinstance(event, dict) and event.get("play") == 23:
|
||
if event.get("startNum") == 1:
|
||
timeout1.append(event)
|
||
elif event.get("startNum") == 2:
|
||
timeout2.append(event)
|
||
|
||
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,
|
||
)
|
||
col2.markdown(
|
||
f"<h3 style='text-align: center'>{result['game']['localDate']} {result['game']['defaultZoneTime']}</h3>",
|
||
unsafe_allow_html=True,
|
||
)
|
||
if t2.get("logo"):
|
||
col3.image(t2["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))
|
||
|
||
# Points метрики безопасно
|
||
val1 = val2 = None
|
||
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)
|
||
|
||
col4_3.metric("TimeOuts", len(timeout1))
|
||
col5_1.metric("TimeOuts", len(timeout2))
|
||
|
||
if isinstance([cached_live_status], list) and cached_live_status:
|
||
foulsA = (cached_live_status["result"] or {}).get("foulsA")
|
||
foulsB = (cached_live_status["result"] or {}).get("foulsB")
|
||
if foulsA is not None:
|
||
col4_2.metric("Fouls", foulsA)
|
||
if foulsB is not None:
|
||
col5_2.metric("Fouls", foulsB)
|
||
|
||
|
||
if isinstance(cached_game_online, dict) and (
|
||
(cached_game_online.get("result") or {}).get("plays") or []
|
||
):
|
||
col_1_col = [f"col_1_{i}" for i in range(1, period_max + 1)]
|
||
col_2_col = [f"col_2_{i}" for i in range(1, period_max + 1)]
|
||
count_q = 0
|
||
|
||
score_by_quarter_1 = [
|
||
x.get("score1")
|
||
for x in cached_scores
|
||
if isinstance(x, dict) and x.get("score1") not in ("", None)
|
||
]
|
||
score_by_quarter_2 = [
|
||
x.get("score2")
|
||
for x in cached_scores
|
||
if isinstance(x, dict) and x.get("score2") not in ("", None)
|
||
]
|
||
|
||
if score_by_quarter_1:
|
||
col_1_col = col4.columns([1 for _ in range(len(score_by_quarter_1))])
|
||
col_2_col = col5.columns([1 for _ in range(len(score_by_quarter_2))])
|
||
|
||
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)
|
||
except (ValueError, TypeError):
|
||
# если кривые данные в JSON, просто пропустим
|
||
pass
|
||
|
||
|
||
(
|
||
tab_temp_1,
|
||
tab_temp_2,
|
||
tab_temp_3,
|
||
tab_temp_4,
|
||
tab_temp_5,
|
||
tab_temp_6,
|
||
tab_pbp,
|
||
tab_temp_7,
|
||
tab_temp_8,
|
||
tab_schedule,
|
||
tab_online,
|
||
) = st.tabs(
|
||
[
|
||
"Игроки",
|
||
"Команды",
|
||
"Судьи",
|
||
"Турнирная таблица",
|
||
"Статистика четвертей",
|
||
"Ход игры",
|
||
"События игры",
|
||
"Статистика по четвертям",
|
||
"Milestones",
|
||
"Прошедшие/будущие матчи",
|
||
"Сегодня",
|
||
]
|
||
)
|
||
|
||
|
||
def check_milestone(value, milestone_type, name, num, where):
|
||
milestone_checks = {
|
||
"PTS": "point",
|
||
"AST": "assist",
|
||
"BLK": "block",
|
||
"REB": "rebound",
|
||
"DREB": "defensive rebound",
|
||
"OREB": "offensive rebound",
|
||
"STL": "steal",
|
||
"GAMES": "game",
|
||
}
|
||
if milestone_type == "GAMES":
|
||
list_data = [*range(50, 5100, 50)]
|
||
if int(value) % 100 in [49, 99]:
|
||
diff = [l - int(value) for l in list_data]
|
||
positive_numbers = [num for num in diff if num > -1]
|
||
count = 0
|
||
for i in diff:
|
||
count += 1
|
||
if i == min(positive_numbers):
|
||
break
|
||
full_word = [
|
||
word
|
||
for w, word in milestone_checks.items()
|
||
if w.lower() == milestone_type.lower()
|
||
][0]
|
||
# print(positive_numbers)
|
||
if min(positive_numbers) != 0:
|
||
word = full_word if min(positive_numbers) == 1 else f"{full_word}s"
|
||
if where == "season":
|
||
where = "in this season"
|
||
elif where == "league":
|
||
where = "in career VTB"
|
||
string_value = f"{name} needs {min(positive_numbers)} {word} to reach {list_data[count-1]} {full_word}s {where}"
|
||
else:
|
||
string_value = ""
|
||
return {
|
||
"NameGFX": f"{name} ({num})",
|
||
"type": milestone_type.upper(),
|
||
"value": value,
|
||
"string_value": string_value,
|
||
}
|
||
else:
|
||
list_data = [*range(100, 5100, 100)]
|
||
if (int(value) % 100) >= 90 or (int(value) % 1000) in list_data:
|
||
diff = [l - int(value) for l in list_data]
|
||
positive_numbers = [num for num in diff if num > -1]
|
||
count = 0
|
||
for i in diff:
|
||
count += 1
|
||
if i == min(positive_numbers):
|
||
break
|
||
# print(positive_numbers)
|
||
full_word = [
|
||
word
|
||
for w, word in milestone_checks.items()
|
||
if w.lower() == milestone_type.lower()
|
||
][0]
|
||
# print(positive_numbers)
|
||
if min(positive_numbers) != 0:
|
||
word = full_word if min(positive_numbers) == 1 else f"{full_word}s"
|
||
if where == "season":
|
||
where = "in this season"
|
||
elif where == "league":
|
||
where = "in career VTB"
|
||
string_value = f"{name} needs {min(positive_numbers)} {word} to reach {list_data[count-1]} {full_word}s {where}"
|
||
else:
|
||
string_value = ""
|
||
return {
|
||
"NameGFX": f"{name} ({num})",
|
||
"type": milestone_type.upper(),
|
||
"value": value,
|
||
"string_value": string_value,
|
||
}
|
||
return None
|
||
|
||
|
||
def milestones(data):
|
||
new_data_season = []
|
||
new_data_career = []
|
||
for d in data:
|
||
if d["startRole"] == "Player":
|
||
milestone_checks = {
|
||
"PTS": d["TPoints"],
|
||
"AST": d["TAssist"],
|
||
"BLK": d["TBlocks"],
|
||
"REB": d["TRebound"],
|
||
"DREB": d["TDefRebound"],
|
||
"OREB": d["TOffRebound"],
|
||
"STL": d["TSteal"],
|
||
"GAMES": d["TGameCount"],
|
||
}
|
||
milestone_career_checks = {
|
||
"PTS": d["CareerTPoints"],
|
||
"AST": d["CareerTAssist"],
|
||
"BLK": d["CareerTBlocks"],
|
||
"REB": d["CareerTRebound"],
|
||
"DREB": d["CareerTDefRebound"],
|
||
"OREB": d["CareerTOffRebound"],
|
||
"STL": d["CareerTSteal"],
|
||
"GAMES": d["CareerTGameCount"],
|
||
}
|
||
for milestone_type, value in milestone_checks.items():
|
||
milestone_data = check_milestone(
|
||
value, milestone_type, d["NameGFX"], d["num"], "season"
|
||
)
|
||
if milestone_data:
|
||
new_data_season.append(milestone_data)
|
||
for milestone_type_car, value_car in milestone_career_checks.items():
|
||
milestone_data_car = check_milestone(
|
||
value_car, milestone_type_car, d["NameGFX"], d["num"], "league"
|
||
)
|
||
if milestone_data_car:
|
||
new_data_career.append(milestone_data_car)
|
||
return new_data_season, new_data_career
|
||
|
||
|
||
columns_game = [
|
||
"num",
|
||
# "roleShort",
|
||
"NameGFX",
|
||
"isOn",
|
||
# "flag",
|
||
"pts",
|
||
"pt-2",
|
||
"pt-3",
|
||
"pt-1",
|
||
"fg",
|
||
"ast",
|
||
"stl",
|
||
"blk",
|
||
# "blkVic",
|
||
"dreb",
|
||
"oreb",
|
||
"reb",
|
||
"to",
|
||
"foul",
|
||
# "fouled",
|
||
"plusMinus",
|
||
"dunk",
|
||
"kpi",
|
||
"time",
|
||
]
|
||
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[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[columns_game].style
|
||
.apply(highlight_grey, axis=1)
|
||
.apply(highlight_foul, subset="foul")
|
||
.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):
|
||
|
||
try:
|
||
directory = FOLDER_JSON # type: ignore[name-defined]
|
||
except NameError:
|
||
# иначе используем аргумент по умолчанию/переданный
|
||
pass
|
||
|
||
os.makedirs(directory, exist_ok=True)
|
||
|
||
host_prefix = _ipcheck()
|
||
filename = player_data_1["id"]
|
||
filepath = os.path.join(directory, f"{host_prefix}{filename}.json")
|
||
# print(filepath) # оставил как у тебя; можно заменить на logger.debug при желании
|
||
|
||
try:
|
||
with open(filepath, "r", encoding="utf-8") as f:
|
||
players_game = json.load(f)
|
||
except Exception:
|
||
pass
|
||
df_player_game = pd.json_normalize(players_game)
|
||
# Отфильтровать строки, где class == "Normal"
|
||
# print(player_data_1)
|
||
df_filtered = df_player_game[df_player_game["class"] == "Normal"].copy()
|
||
|
||
# Преобразуем game.gameDate в datetime
|
||
df_filtered["game.gameDate"] = pd.to_datetime(
|
||
df_filtered["game.gameDate"], errors="coerce", dayfirst=True
|
||
)
|
||
|
||
# Удалим строки без корректной даты (если такие есть)
|
||
# df_filtered = df_filtered.dropna(subset=["game.gameDate"])
|
||
|
||
# Сортировка от последнего матча к первому
|
||
df_filtered = df_filtered.sort_values(by="game.gameDate", ascending=False)
|
||
# Указать нужные колонки для вывода
|
||
columns_to_show = [
|
||
"Сезон",
|
||
"Дата",
|
||
"Команда 1",
|
||
"Команда 2",
|
||
"Счёт",
|
||
"PTS",
|
||
"2-PTS%",
|
||
"3-PTS%",
|
||
"FG%",
|
||
"FT%",
|
||
"AST",
|
||
"STL",
|
||
"BLK",
|
||
"DR",
|
||
"OR",
|
||
"REB",
|
||
"TO",
|
||
"F",
|
||
"TIME",
|
||
"+/-",
|
||
]
|
||
numeric_cols = [
|
||
"stats.points",
|
||
"stats.assist",
|
||
"stats.steal",
|
||
"stats.blockShot",
|
||
"stats.rebound",
|
||
]
|
||
# 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=["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_copy,
|
||
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",
|
||
],
|
||
)
|
||
|
||
if event1.selection and event1.selection.get("rows"):
|
||
selected_index1 = event1.selection["rows"][0]
|
||
st.session_state["player1"] = (
|
||
selected_index1 # Сохранение состояния в session_state
|
||
)
|
||
else:
|
||
st.session_state["player1"] = None
|
||
if st.session_state["player1"] is not None:
|
||
selected_player_1, player_data_1 = process_player_data(
|
||
cached_team1, st.session_state["player1"]
|
||
)
|
||
if player_data_1["num"]:
|
||
z, a, q, b, c, d, e = col_player1.columns((1, 6, 1, 1, 1, 1, 1))
|
||
z.metric("Номер", player_data_1["num"], border=False)
|
||
a.metric("Игрок", player_data_1["NameGFX"], border=False)
|
||
q.image(player_data_1["flag"])
|
||
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,
|
||
)
|
||
|
||
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]
|
||
st.session_state["player2"] = (
|
||
selected_index2 # Сохранение состояния в session_state
|
||
)
|
||
else:
|
||
st.session_state["player2"] = None
|
||
if st.session_state["player2"] is not None:
|
||
selected_player_2, player_data_2 = process_player_data(
|
||
cached_team2, st.session_state["player2"]
|
||
)
|
||
if player_data_2["num"]:
|
||
z, a, q, b, c, d, e = col_player2.columns((1, 6, 1, 1, 1, 1, 1))
|
||
z.metric("Номер", player_data_2["num"], border=False)
|
||
a.metric("Игрок", player_data_2["NameGFX"], border=False)
|
||
q.image(player_data_2["flag"])
|
||
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,
|
||
)
|
||
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:
|
||
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],
|
||
]
|
||
team_col1.title("Статистика за матч")
|
||
team_col1.dataframe(cached_team_stats_new, height=500)
|
||
|
||
if isinstance(cached_team_comparison, list):
|
||
team_col2.title("Статистика за сезон")
|
||
|
||
# задаём желаемый порядок ключей
|
||
order = [
|
||
"points",
|
||
"points_1",
|
||
"points_2",
|
||
"points_3",
|
||
"points_23",
|
||
"assists",
|
||
"rebounds",
|
||
"steals",
|
||
"blocks",
|
||
"turnovers",
|
||
"fouls",
|
||
"games",
|
||
# "team",
|
||
]
|
||
|
||
# формируем результат с учётом порядка
|
||
result = [
|
||
{
|
||
"name": key,
|
||
"val1": cached_team_comparison[0][key],
|
||
"val2": cached_team_comparison[1][key],
|
||
}
|
||
for key in order
|
||
if key in cached_team_comparison[0]
|
||
]
|
||
team_col2.dataframe(result, width="stretch", height=500)
|
||
|
||
if isinstance(cached_referee, (list, pd.DataFrame)):
|
||
tab_temp_3.dataframe(
|
||
cached_referee,
|
||
height=600,
|
||
width="content",
|
||
column_config={"flag": st.column_config.ImageColumn("flag")},
|
||
)
|
||
|
||
|
||
column_config_ref = {
|
||
"flag": st.column_config.ImageColumn(
|
||
"flag",
|
||
),
|
||
}
|
||
|
||
|
||
def highlight_teams(s):
|
||
try:
|
||
if s.iloc[0] in (
|
||
cached_game_online["result"]["team1"]["teamId"],
|
||
cached_game_online["result"]["team2"]["teamId"],
|
||
):
|
||
return ["background-color: #FF4B4B"] * len(s)
|
||
else:
|
||
return [""] * len(s)
|
||
except NameError:
|
||
return [""] * len(s)
|
||
|
||
|
||
if cached_standings:
|
||
df_st = pd.json_normalize(cached_standings)
|
||
|
||
def highlight_teams(s):
|
||
try:
|
||
t1 = (
|
||
((cached_game_online or {}).get("result") or {})
|
||
.get("team1", {})
|
||
.get("teamId")
|
||
)
|
||
t2 = (
|
||
((cached_game_online or {}).get("result") or {})
|
||
.get("team2", {})
|
||
.get("teamId")
|
||
)
|
||
if s.iloc[0] in (t1, t2):
|
||
return ["background-color: #FF4B4B"] * len(s)
|
||
except Exception:
|
||
pass
|
||
return [""] * len(s)
|
||
|
||
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"
|
||
)
|
||
|
||
|
||
if isinstance(cached_scores_quarter, list) and len(cached_scores_quarter) >= 2:
|
||
column_config = {}
|
||
for quarter in ["Q1", "Q2", "Q3", "Q4", "OT1", "OT2", "OT3", "OT4"]:
|
||
column_name = f"score_avg{quarter}"
|
||
column_config[column_name] = st.column_config.NumberColumn(
|
||
column_name, format="%.1f"
|
||
)
|
||
|
||
columns_quarters_name = ["Q1", "Q2", "Q3", "Q4"]
|
||
columns_quarters_name_ot = ["OT1", "OT2", "OT3", "OT4"]
|
||
columns_quarters = tab_temp_5.columns((1, 1, 1, 1))
|
||
|
||
# Основные четверти
|
||
for index, col in enumerate(columns_quarters):
|
||
q = columns_quarters_name[index]
|
||
df_col = [
|
||
{
|
||
"team": cached_scores_quarter[0].get("team"),
|
||
"W": cached_scores_quarter[0].get(f"win{q}"),
|
||
"L": cached_scores_quarter[0].get(f"lose{q}"),
|
||
"D": cached_scores_quarter[0].get(f"draw{q}"),
|
||
"PTS": cached_scores_quarter[0].get(f"score{q}"),
|
||
"AVG": cached_scores_quarter[0].get(f"score_avg{q}"),
|
||
},
|
||
{
|
||
"team": cached_scores_quarter[1].get("team"),
|
||
"W": cached_scores_quarter[1].get(f"win{q}"),
|
||
"L": cached_scores_quarter[1].get(f"lose{q}"),
|
||
"D": cached_scores_quarter[1].get(f"draw{q}"),
|
||
"PTS": cached_scores_quarter[1].get(f"score{q}"),
|
||
"AVG": cached_scores_quarter[1].get(f"score_avg{q}"),
|
||
},
|
||
]
|
||
col.write(q)
|
||
col.dataframe(df_col)
|
||
|
||
# Овертаймы
|
||
for index, col in enumerate(columns_quarters):
|
||
q = columns_quarters_name_ot[index]
|
||
df_col = [
|
||
{
|
||
"team": cached_scores_quarter[0].get("team"),
|
||
"W": cached_scores_quarter[0].get(f"win{q}"),
|
||
"L": cached_scores_quarter[0].get(f"lose{q}"),
|
||
"D": cached_scores_quarter[0].get(f"draw{q}"),
|
||
"PTS": cached_scores_quarter[0].get(f"score{q}"),
|
||
"AVG": cached_scores_quarter[0].get(f"score_avg{q}"),
|
||
},
|
||
{
|
||
"team": cached_scores_quarter[1].get("team"),
|
||
"W": cached_scores_quarter[1].get(f"win{q}"),
|
||
"L": cached_scores_quarter[1].get(f"lose{q}"),
|
||
"D": cached_scores_quarter[1].get(f"draw{q}"),
|
||
"PTS": cached_scores_quarter[1].get(f"score{q}"),
|
||
"AVG": cached_scores_quarter[1].get(f"score_avg{q}"),
|
||
},
|
||
]
|
||
col.write(q)
|
||
col.dataframe(df_col)
|
||
|
||
|
||
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)
|
||
|
||
|
||
def _ensure_columns(df: pd.DataFrame, cols: list[str]) -> pd.DataFrame:
|
||
"""Гарантирует наличие всех столбцов в df, отсутствующие заполняет NaN/0 по типу."""
|
||
for c in cols:
|
||
if c not in df.columns:
|
||
# для числовых метрик по четверти логично ставить 0
|
||
df[c] = 0
|
||
# Переупорядочим столбцы
|
||
return df[cols]
|
||
|
||
|
||
def _safe_dataframe(
|
||
container, df: pd.DataFrame, cols_for_st: list[str], height_per_row: int = 38
|
||
):
|
||
df = _ensure_columns(df, cols_for_st)
|
||
rows = len(df)
|
||
|
||
# Стилизуем безопасно (если вдруг highlight_max упадёт — показываем без стиля)
|
||
try:
|
||
styled = (
|
||
df.style.apply(highlight_max, subset="q_pts") if not df.empty else df.style
|
||
)
|
||
except Exception:
|
||
styled = df.style
|
||
|
||
kwargs = dict(column_config=config, hide_index=True)
|
||
|
||
# height только если он действительно нужен
|
||
if rows > 10:
|
||
kwargs["height"] = height_per_row * rows
|
||
# или: kwargs["height"] = "stretch" # если хотите растягивать блок
|
||
|
||
container.dataframe(styled, **kwargs)
|
||
|
||
|
||
# 1) Безопасная проверка наличия плей-данных
|
||
has_plays = bool(
|
||
cached_game_online and cached_game_online.get("result", {}).get("plays")
|
||
)
|
||
if has_plays:
|
||
|
||
# 2) Готовим вкладки по количеству периодов
|
||
# Если у вас уже есть count_quarter — используйте его. Иначе берём из period_max.
|
||
|
||
# === REPLACE: безопасное определение количества периодов и создание вкладок ===
|
||
def _safe_int(x, default=None):
|
||
try:
|
||
v = int(x)
|
||
return v if v is not None else default
|
||
except Exception:
|
||
return default
|
||
|
||
def _max_period_from_plays(cached_game_online) -> int:
|
||
plays = (cached_game_online or {}).get("result", {}).get("plays") or []
|
||
periods = []
|
||
for item in plays:
|
||
try:
|
||
v = int(item.get("period"))
|
||
periods.append(v)
|
||
except Exception:
|
||
# пропускаем пустые/неконвертируемые значения
|
||
continue
|
||
return max(periods) if periods else 0
|
||
|
||
count_quarter = _max_period_from_plays(cached_game_online)
|
||
|
||
# 3) попытка по ключам team1_/team2_ в session_state (например, team1_1..team1_4)
|
||
if count_quarter is None or count_quarter <= 0:
|
||
import re
|
||
|
||
mx = 0
|
||
for k in st.session_state.keys():
|
||
m1 = re.fullmatch(r"team1_(\d+)", k)
|
||
m2 = re.fullmatch(r"team2_(\d+)", k)
|
||
if m1:
|
||
mx = max(mx, int(m1.group(1)))
|
||
if m2:
|
||
mx = max(mx, int(m2.group(1)))
|
||
count_quarter = mx if mx > 0 else None
|
||
|
||
# 4) дефолт, если ничего не получилось
|
||
if count_quarter is None or count_quarter <= 0:
|
||
# если совсем нет данных — не создаём вкладки и показываем инфо
|
||
tab_temp_7.info(
|
||
"Нет корректного числа периодов для отображения таблиц по четвертям."
|
||
)
|
||
count_quarter = 0
|
||
|
||
# 5) создаём вкладки, только если их хотя бы одна
|
||
if count_quarter > 0:
|
||
tab_labels = [f"{i+1}Q" for i in range(count_quarter)]
|
||
columns_quarter = tab_temp_7.tabs(tab_labels)
|
||
else:
|
||
columns_quarter = [] # чтобы ниже цикл просто не выполнялся
|
||
|
||
columns_quarter_for_st = [
|
||
"num",
|
||
"NameGFX",
|
||
"q_pts",
|
||
"q_pt2",
|
||
"q_pt3",
|
||
"q_ft",
|
||
"q_pt23",
|
||
"q_ast",
|
||
"q_stl",
|
||
"q_blk",
|
||
"q_reb",
|
||
"q_to",
|
||
"q_f",
|
||
"q_f_on",
|
||
"q_rnk",
|
||
"q_time",
|
||
]
|
||
|
||
# 3) Рендер таблиц по четвертям
|
||
for i in range(count_quarter):
|
||
# Каждая вкладка -> 2 колонки
|
||
try:
|
||
col_quarter1, col_quarter2 = columns_quarter[i].columns((5, 5))
|
||
except IndexError:
|
||
# На случай несоответствия размеров
|
||
st.warning(f"Недостаточно вкладок для периода #{i+1}. Пропуск.")
|
||
continue
|
||
|
||
# --- Team 1 ---
|
||
key_team1 = f"team1_{i+1}"
|
||
try:
|
||
load_data_from_json(key_team1)
|
||
except Exception as e:
|
||
tab_temp_7.warning(f"Не удалось загрузить данные для {key_team1}: {e}")
|
||
continue
|
||
|
||
data_team1 = st.session_state.get(key_team1, [])
|
||
# print(data_team1)
|
||
df_team1 = (
|
||
pd.DataFrame(data_team1)
|
||
if isinstance(data_team1, (list, tuple, dict))
|
||
else pd.DataFrame()
|
||
)
|
||
|
||
# _safe_dataframe(col_quarter1, df_team1, columns_quarter_for_st)
|
||
|
||
# --- Team 2 ---
|
||
key_team2 = f"team2_{i+1}"
|
||
try:
|
||
load_data_from_json(key_team2)
|
||
except Exception as e:
|
||
tab_temp_7.warning(f"Не удалось загрузить данные для {key_team2}: {e}")
|
||
continue
|
||
|
||
data_team2 = st.session_state.get(key_team2, [])
|
||
df_team2 = (
|
||
pd.DataFrame(data_team2)
|
||
if isinstance(data_team2, (list, tuple, dict))
|
||
else pd.DataFrame()
|
||
)
|
||
# _safe_dataframe(col_quarter2, df_team2, columns_quarter_for_st)
|
||
tab_temp_7.warning("В процессе разработки!!!!!!")
|
||
|
||
# 4) Блок с milestones по командам (если есть кэш)
|
||
if cached_team1 and cached_team2:
|
||
try:
|
||
data_team_season_1, data_team_career_1 = milestones(cached_team1)
|
||
data_team_season_2, data_team_career_2 = milestones(cached_team2)
|
||
except Exception as e:
|
||
st.warning(f"Не удалось получить milestones: {e}")
|
||
else:
|
||
# На случай, если функции вернули не DataFrame
|
||
if not isinstance(data_team_season_1, pd.DataFrame):
|
||
data_team_season_1 = pd.DataFrame(data_team_season_1 or {})
|
||
if not isinstance(data_team_career_1, pd.DataFrame):
|
||
data_team_career_1 = pd.DataFrame(data_team_career_1 or {})
|
||
if not isinstance(data_team_season_2, pd.DataFrame):
|
||
data_team_season_2 = pd.DataFrame(data_team_season_2 or {})
|
||
if not isinstance(data_team_career_2, pd.DataFrame):
|
||
data_team_career_2 = pd.DataFrame(data_team_career_2 or {})
|
||
|
||
tab7_col1, tab7_col2 = tab_temp_8.columns((5, 5))
|
||
|
||
tab7_col1.title("Сезон")
|
||
tab7_col1.dataframe(data_team_season_1)
|
||
tab7_col2.title("Сезон")
|
||
tab7_col2.dataframe(data_team_season_2)
|
||
tab7_col1.title("Карьера")
|
||
tab7_col1.dataframe(data_team_career_1)
|
||
tab7_col2.title("Карьера")
|
||
tab7_col2.dataframe(data_team_career_2)
|
||
else:
|
||
tab_temp_7.info("Нет онлайн-плей-данных для отображения.")
|
||
|
||
|
||
def schedule_selected_team(team_id, data, game_id, selected, away_team_id):
|
||
columns = [
|
||
"game.localDate",
|
||
"team1.name",
|
||
"team1.logo",
|
||
"game.score",
|
||
"team2.logo",
|
||
"team2.name",
|
||
"game.fullScore",
|
||
"win",
|
||
"team1.teamId",
|
||
"team2.teamId",
|
||
]
|
||
df_schedule_new = data.loc[
|
||
# (data["game.id"] < game_id)
|
||
# &
|
||
(
|
||
(data["team1.teamId"].isin([team_id]))
|
||
| (data["team2.teamId"].isin([team_id]))
|
||
)
|
||
]
|
||
df_schedule_new.loc[:, "game.fullScore"] = df_schedule_new[
|
||
"game.fullScore"
|
||
].str.split(",")
|
||
conditions = [
|
||
(df_schedule_new["team1.teamId"] == team_id)
|
||
& (df_schedule_new["game.score1"] > df_schedule_new["game.score2"]),
|
||
(df_schedule_new["team1.teamId"] == team_id)
|
||
& (df_schedule_new["game.score1"] < df_schedule_new["game.score2"]),
|
||
(df_schedule_new["team2.teamId"] == team_id)
|
||
& (df_schedule_new["game.score2"] > df_schedule_new["game.score1"]),
|
||
(df_schedule_new["team2.teamId"] == team_id)
|
||
& (df_schedule_new["game.score2"] < df_schedule_new["game.score1"]),
|
||
]
|
||
values = [True, False, True, False]
|
||
|
||
df_schedule_new = df_schedule_new.copy()
|
||
df_schedule_new.loc[:, "win"] = np.select(conditions, values, default=None)
|
||
mask = pd.Series(True, index=df_schedule_new.index)
|
||
|
||
# Проверяем каждое выбранное условие и объединяем с маской
|
||
if selected:
|
||
if "Дома" in selected:
|
||
mask &= df_schedule_new["team1.teamId"] == team_id
|
||
if "В гостях" in selected:
|
||
mask &= df_schedule_new["team2.teamId"] == team_id
|
||
if "Выигрыши" in selected:
|
||
mask &= df_schedule_new["win"] == True
|
||
if "Поражения" in selected:
|
||
mask &= df_schedule_new["win"] == False
|
||
if "Друг с другом" in selected:
|
||
mask &= df_schedule_new["team1.teamId"].isin(
|
||
[away_team_id, team_id]
|
||
) & df_schedule_new["team2.teamId"].isin([away_team_id, team_id])
|
||
return df_schedule_new[columns].loc[mask]
|
||
|
||
|
||
def get_in(d, path, default=None):
|
||
cur = d
|
||
for key in path:
|
||
if not isinstance(cur, dict):
|
||
return default
|
||
cur = cur.get(key, default)
|
||
if cur is default:
|
||
return default
|
||
return cur
|
||
|
||
|
||
if tab_schedule:
|
||
if cached_schedule and "items" in cached_schedule:
|
||
cached_schedule = cached_schedule["items"]
|
||
pd_schedule = pd.json_normalize(cached_schedule)
|
||
|
||
game_online = st.session_state.get("game_online")
|
||
# print(game_online)
|
||
team1_id = get_in(game_online, ["result", "team1", "teamId"])
|
||
team1_name = get_in(game_online, ["result", "team1", "name"])
|
||
team2_id = get_in(game_online, ["result", "team2", "teamId"])
|
||
team2_name = get_in(game_online, ["result", "team2", "name"])
|
||
game_id = get_in(game_online, ["result", "game", "id"])
|
||
|
||
col1_schedule, col2_schedule = tab_schedule.columns((5, 5))
|
||
options = ["Дома", "В гостях", "Выигрыши", "Поражения", "Друг с другом"]
|
||
selection1 = col1_schedule.segmented_control(
|
||
"Фильтр", options, selection_mode="multi", key="1"
|
||
)
|
||
selection2 = col2_schedule.segmented_control(
|
||
"Фильтр", options, selection_mode="multi", key="2"
|
||
)
|
||
|
||
team1_data = schedule_selected_team(
|
||
team1_id, pd_schedule, game_id, selection1, team2_id
|
||
)
|
||
# print(team1_data)
|
||
team2_data = schedule_selected_team(
|
||
team2_id, pd_schedule, game_id, selection2, team1_id
|
||
)
|
||
|
||
def highlight_two_teams(s):
|
||
# print(s)
|
||
try:
|
||
if s.loc["team1.teamId"] in (
|
||
team1_id,
|
||
team2_id,
|
||
) and s.loc["team2.teamId"] in (
|
||
team1_id,
|
||
team2_id,
|
||
):
|
||
return ["background-color: #FF4B4B"] * len(s)
|
||
else:
|
||
return [""] * len(s)
|
||
except NameError:
|
||
return [""] * len(s)
|
||
|
||
column_config = {
|
||
"team1.name": st.column_config.TextColumn("Команда1", width=150),
|
||
"team2.name": st.column_config.TextColumn("Команда2", width=150),
|
||
"game.score": st.column_config.TextColumn(
|
||
"Счёт",
|
||
),
|
||
"game.localDate": st.column_config.TextColumn(
|
||
"Дата",
|
||
),
|
||
"game.fullScore": st.column_config.Column(
|
||
"Счёт по четвертям", width="medium"
|
||
),
|
||
"team1.logo": st.column_config.ImageColumn("Лого1", width=50),
|
||
"team2.logo": st.column_config.ImageColumn("Лого2", width=50),
|
||
}
|
||
count_game_1 = len(team1_data)
|
||
count_game_2 = len(team2_data)
|
||
team1_data = team1_data.style.apply(highlight_two_teams, axis=1).apply(
|
||
color_win, subset="win"
|
||
)
|
||
team2_data = team2_data.style.apply(highlight_two_teams, axis=1).apply(
|
||
color_win, subset="win"
|
||
)
|
||
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,
|
||
)
|
||
|
||
|
||
with tab_online:
|
||
if isinstance(cached_schedule, list):
|
||
current_date = pd.to_datetime(datetime.now().date())
|
||
df_online = pd.json_normalize(cached_schedule)
|
||
df_online["game.localDate"] = pd.to_datetime(
|
||
df_online["game.localDate"], format="%d.%m.%Y", errors="coerce"
|
||
)
|
||
df_filtered = df_online[df_online["game.localDate"] == current_date].copy()
|
||
|
||
names = {
|
||
"Avtodor": r"D:\Графика\БАСКЕТБОЛ\VTB League\TEAM LOGOS\LOGOS 2025-2026\AVTODOR.png",
|
||
"BETCITY PARMA": r"D:\Графика\БАСКЕТБОЛ\VTB League\TEAM LOGOS\LOGOS 2025-2026\BETCITY_PARMA_ENG.png",
|
||
"CSKA": r"D:\Графика\БАСКЕТБОЛ\VTB League\TEAM LOGOS\LOGOS 2025-2026\CSKA_BLACK_BACK_ENG.png",
|
||
"Enisey": r"D:\Графика\БАСКЕТБОЛ\VTB League\TEAM LOGOS\LOGOS 2025-2026\ENISEY_ENG.png",
|
||
"Lokomotiv Kuban": r"D:\Графика\БАСКЕТБОЛ\VTB League\TEAM LOGOS\LOGOS 2025-2026\LOKOMOTIV KUBAN.png",
|
||
"MBA-MAI": r"D:\Графика\БАСКЕТБОЛ\VTB League\TEAM LOGOS\LOGOS 2025-2026\MBA-MAI.png",
|
||
"Pari Nizhny Novgorod": r"D:\Графика\БАСКЕТБОЛ\VTB League\TEAM LOGOS\LOGOS 2025-2026\PARI_NN_ENG.png",
|
||
"Samara": r"D:\Графика\БАСКЕТБОЛ\VTB League\TEAM LOGOS\LOGOS 2025-2026\SAMARA_BLACK_BACK.png",
|
||
"UNICS": r"D:\Графика\БАСКЕТБОЛ\VTB League\TEAM LOGOS\LOGOS 2025-2026\UNICS_BLACK_BACK.png",
|
||
"Uralmash": r"D:\Графика\БАСКЕТБОЛ\VTB League\TEAM LOGOS\LOGOS 2025-2026\URALMASH_ENG.png",
|
||
"Zenit": r"D:\Графика\БАСКЕТБОЛ\VTB League\TEAM LOGOS\LOGOS 2025-2026\ZENIT.png",
|
||
}
|
||
|
||
df_filtered["logo1"] = df_filtered["team1.name"].map(names)
|
||
df_filtered["logo2"] = df_filtered["team2.name"].map(names)
|
||
|
||
# Словарь для быстрого поиска по gameId
|
||
live_data_map = {}
|
||
|
||
# Собираем live-данные по каждому game.id
|
||
|
||
for _, row in df_filtered.iterrows():
|
||
game_id = row["game.id"]
|
||
try:
|
||
json_data = requests.get(
|
||
f"https://vtb-league.org/api/abc/games/live-status?Id={game_id}&Lang=en",
|
||
).json()
|
||
except Exception as ex:
|
||
|
||
print(ex)
|
||
|
||
# Берём содержимое result (словарь с gameId и данными)
|
||
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
|
||
df_filtered["live_timeToGo"] = None
|
||
df_filtered["live_scoreA"] = None
|
||
df_filtered["live_scoreB"] = None
|
||
df_filtered["live_status"] = None
|
||
df_filtered["period"] = None
|
||
# Заполняем колонки, где game.id совпадает с gameId
|
||
for idx, row in df_filtered.iterrows():
|
||
game_id = row["game.id"]
|
||
if game_id in live_data_map:
|
||
live_info = live_data_map[game_id]
|
||
df_filtered.at[idx, "live_period"] = live_info.get("period")
|
||
df_filtered.at[idx, "live_timeToGo"] = live_info.get("timeToGo")
|
||
df_filtered.at[idx, "live_scoreA"] = live_info.get("scoreA")
|
||
df_filtered.at[idx, "live_scoreB"] = live_info.get("scoreB")
|
||
df_filtered.at[idx, "live_status"] = live_info.get("gameStatus")
|
||
try:
|
||
if live_info.get("timeToGo") != "":
|
||
if float(live_info.get("timeToGo")) == 0.0:
|
||
if live_info.get("period") == 2:
|
||
df_filtered.at[idx, "period"] = "HT"
|
||
elif live_info.get("period") in [1, 3]:
|
||
df_filtered.at[idx, "period"] = (
|
||
f"END {live_info.get('period')}Q"
|
||
)
|
||
elif float(live_info.get("timeToGo")) > 0.0:
|
||
df_filtered.at[idx, "period"] = (
|
||
f"{live_info.get('period')}Q"
|
||
)
|
||
else:
|
||
df_filtered.at[idx, "period"] = ""
|
||
except Exception as ex:
|
||
df_filtered.at[idx, "period"] = ""
|
||
print(ex)
|
||
|
||
df_filtered["live_scoreA"] = pd.to_numeric(
|
||
df_filtered["live_scoreA"], errors="coerce"
|
||
).fillna(pd.to_numeric(df_filtered["game.score1"], errors="coerce"))
|
||
|
||
df_filtered["live_scoreB"] = pd.to_numeric(
|
||
df_filtered["live_scoreB"], errors="coerce"
|
||
).fillna(pd.to_numeric(df_filtered["game.score2"], errors="coerce"))
|
||
|
||
# Вывод нужных колонок
|
||
columns = [
|
||
"game.id",
|
||
"game.gameStatus",
|
||
"status.displayName",
|
||
"game.defaultZoneTime",
|
||
"team1.name",
|
||
"team2.name",
|
||
"game.score1",
|
||
"game.score2",
|
||
"live_scoreA",
|
||
"live_scoreB",
|
||
"live_period",
|
||
"live_timeToGo",
|
||
"live_status",
|
||
"period",
|
||
"team1.logo",
|
||
"team2.logo",
|
||
"logo1",
|
||
"logo2",
|
||
]
|
||
|
||
existing_columns = [c for c in columns if c in df_filtered.columns]
|
||
df_sel = df_filtered.loc[:, existing_columns].copy()
|
||
data = df_sel.to_dict(orient="records") # список словарей
|
||
rewrite_file("online", data)
|
||
|
||
st.dataframe(
|
||
df_filtered[existing_columns],
|
||
column_config={
|
||
"team1.logo": st.column_config.ImageColumn("team1.logo"),
|
||
"team2.logo": st.column_config.ImageColumn("team2.logo"),
|
||
},
|
||
width="content",
|
||
)
|
||
|
||
|
||
def get_play_info(play):
|
||
# Ищем в списке play_type_id элемент, у которого PlayTypeID совпадает с play
|
||
for item in play_type_id:
|
||
if item["PlayTypeID"] == play:
|
||
return item["PlayInfoSite"]
|
||
return None # Если совпадение не найдено
|
||
|
||
|
||
def get_player_name(start_num):
|
||
# Ищем в списке teams_temp элемент, у которого startNum совпадает с temp_data_pbp["startNum"]
|
||
for player in teams_temp:
|
||
if player["startNum"] == start_num:
|
||
return f"{player['firstName']} {player['lastName']}"
|
||
return None # Если совпадение не найдено
|
||
|
||
|
||
def get_event_time(row):
|
||
if row != 0:
|
||
time_str = 6000 - row
|
||
if time_str == 0:
|
||
time_str = "0:00"
|
||
else:
|
||
time_str = time_str // 10
|
||
time_str = f"{time_str // 60}:{str(time_str % 60).zfill(2)}"
|
||
return time_str
|
||
|
||
|
||
# Безопасная загрузка PlayTypeID
|
||
try:
|
||
with open("PlayTypeID.json", "r", encoding="utf-8") as f:
|
||
play_type_id = json.load(f)
|
||
except (FileNotFoundError, json.JSONDecodeError):
|
||
play_type_id = []
|
||
|
||
teams_section = ((cached_game_online or {}).get("result") or {}).get("teams") or {}
|
||
|
||
# Если teams_section — список (например, [{"starts": [...]}, {...}])
|
||
if isinstance(teams_section, list):
|
||
if len(teams_section) >= 2:
|
||
starts1 = next(
|
||
(t.get("starts") for t in teams_section if t["teamNumber"] == 1), None
|
||
)
|
||
starts2 = next(
|
||
(t.get("starts") for t in teams_section if t["teamNumber"] == 2), None
|
||
)
|
||
else:
|
||
starts1 = []
|
||
starts2 = []
|
||
# Если teams_section — словарь (обычно {"1": {...}, "2": {...}})
|
||
elif isinstance(teams_section, dict):
|
||
starts1 = (teams_section.get(1) or teams_section.get("1") or {}).get("starts") or []
|
||
starts2 = (teams_section.get(2) or teams_section.get("2") or {}).get("starts") or []
|
||
else:
|
||
starts1 = []
|
||
starts2 = []
|
||
|
||
teams_temp = sorted(
|
||
[x for x in starts1 if isinstance(x, dict)], key=lambda x: x.get("playerNumber", 0)
|
||
) + sorted(
|
||
[x for x in starts2 if isinstance(x, dict)], key=lambda x: x.get("playerNumber", 0)
|
||
)
|
||
|
||
list_fullname = [None] + [
|
||
f"({x.get('displayNumber')}) {x.get('firstName','')} {x.get('lastName','')}".strip()
|
||
for x in teams_temp
|
||
if x.get("startRole") == "Player"
|
||
]
|
||
|
||
|
||
def get_play_info(play):
|
||
for item in play_type_id:
|
||
if isinstance(item, dict) and item.get("PlayTypeID") == play:
|
||
return item.get("PlayInfoSite")
|
||
return None
|
||
|
||
|
||
def get_player_name(start_num):
|
||
for player in teams_temp:
|
||
if player.get("startNum") == start_num:
|
||
return f"{player.get('firstName','')} {player.get('lastName','')}".strip()
|
||
return None
|
||
|
||
|
||
def get_event_time(row):
|
||
if isinstance(row, (int, float)) and row != 0:
|
||
time_val = 6000 - int(row)
|
||
if time_val <= 0:
|
||
return "0:00"
|
||
time_val //= 10
|
||
return f"{time_val // 60}:{str(time_val % 60).zfill(2)}"
|
||
return None
|
||
|
||
|
||
with tab_pbp:
|
||
plays = ((cached_game_online or {}).get("result") or {}).get("plays") or []
|
||
if plays:
|
||
temp_data_pbp = pd.DataFrame(plays)
|
||
col1_pbp, col2_pbp = tab_pbp.columns((3, 4))
|
||
option_player = col1_pbp.selectbox("Выбрать игрока", list_fullname)
|
||
|
||
options_pbp = ["1 очко", "2 очка", "3 очка"]
|
||
selection_pbp = col1_pbp.segmented_control(
|
||
"Фильтр", options_pbp, selection_mode="multi", key=3
|
||
)
|
||
|
||
if not temp_data_pbp.empty and "period" in temp_data_pbp.columns:
|
||
options_quarter = [
|
||
(f"{i+1} четверть" if i + 1 < 5 else f"{i-3} овертайм")
|
||
for i in range(int(temp_data_pbp["period"].max()))
|
||
]
|
||
selection_quarter = col1_pbp.segmented_control(
|
||
"Выбор четверти", options_quarter, selection_mode="multi", key=4
|
||
)
|
||
|
||
temp_data_pbp["info"] = temp_data_pbp["play"].map(get_play_info)
|
||
temp_data_pbp["who"] = temp_data_pbp["startNum"].map(get_player_name)
|
||
temp_data_pbp["time"] = temp_data_pbp["sec"].map(get_event_time)
|
||
|
||
mask1 = pd.Series(True, index=temp_data_pbp.index)
|
||
|
||
if option_player:
|
||
# безопасный поиск startNum
|
||
for x in teams_temp:
|
||
display = f"({x.get('displayNumber')}) {x.get('firstName','')} {x.get('lastName','')}".strip()
|
||
if display == option_player:
|
||
mask1 &= temp_data_pbp["startNum"] == x.get("startNum")
|
||
break
|
||
|
||
if selection_pbp:
|
||
plays_mapping = {"1 очко": 1, "2 очка": 2, "3 очка": 3}
|
||
selected_plays = [
|
||
plays_mapping[p] for p in selection_pbp if p in plays_mapping
|
||
]
|
||
mask1 &= temp_data_pbp["play"].isin(selected_plays)
|
||
|
||
if selection_quarter:
|
||
select_quart = [
|
||
i + 1
|
||
for i, q in enumerate(options_quarter)
|
||
if q in selection_quarter
|
||
]
|
||
mask1 &= temp_data_pbp["period"].isin(select_quart)
|
||
|
||
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 "auto"),
|
||
)
|
||
else:
|
||
tab_pbp.info("Данных play-by-play нет.")
|