добавил сезонную статистику и карьерную статистику на игроков
This commit is contained in:
436
get_data.py
436
get_data.py
@@ -310,6 +310,18 @@ def stop_offline_threads():
|
|||||||
logger.info("[threads] OFFLINE threads stopped")
|
logger.info("[threads] OFFLINE threads stopped")
|
||||||
|
|
||||||
|
|
||||||
|
def has_full_game_ready() -> bool:
|
||||||
|
game = latest_data.get("game")
|
||||||
|
if not game:
|
||||||
|
return False
|
||||||
|
payload = game.get("data", game)
|
||||||
|
return (
|
||||||
|
isinstance(payload, dict)
|
||||||
|
and isinstance(payload.get("data"), dict)
|
||||||
|
and isinstance(payload["data"].get("result"), dict)
|
||||||
|
and "teams" in payload["data"]["result"]
|
||||||
|
)
|
||||||
|
|
||||||
# Функция запускаемая в потоках
|
# Функция запускаемая в потоках
|
||||||
def get_data_from_API(
|
def get_data_from_API(
|
||||||
name: str,
|
name: str,
|
||||||
@@ -321,12 +333,10 @@ def get_data_from_API(
|
|||||||
did_first_fetch = False
|
did_first_fetch = False
|
||||||
while not stop_event.is_set():
|
while not stop_event.is_set():
|
||||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||||
if stop_when_live and globals().get("STATUS") == "live" and did_first_fetch:
|
if stop_when_live and globals().get("STATUS") == "live" and has_full_game_ready():
|
||||||
logger.info(
|
logger.info(f"{[{current_time}]} [{name}] stopping because STATUS='live' and full game is ready")
|
||||||
f"{[{current_time}]} [{name}] stopping because STATUS='live' and first fetch done"
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
start = time.time()
|
|
||||||
try:
|
try:
|
||||||
value = requests.get(url, timeout=5).json()
|
value = requests.get(url, timeout=5).json()
|
||||||
did_first_fetch = True # помечаем, что один заход сделали
|
did_first_fetch = True # помечаем, что один заход сделали
|
||||||
@@ -370,11 +380,10 @@ def get_data_from_API(
|
|||||||
while slept < sleep_time:
|
while slept < sleep_time:
|
||||||
if stop_event.is_set():
|
if stop_event.is_set():
|
||||||
break
|
break
|
||||||
if stop_when_live and globals().get("STATUS") == "live" and did_first_fetch:
|
if stop_when_live and globals().get("STATUS") == "live" and has_full_game_ready():
|
||||||
logger.info(
|
logger.info(f"[{name}] stopping during sleep because STATUS='live' and full game is ready")
|
||||||
f"[{name}] stopping during sleep because STATUS='live' and first fetch done"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
slept += 1
|
slept += 1
|
||||||
# если запрос занял дольше — просто сразу следующую итерацию
|
# если запрос занял дольше — просто сразу следующую итерацию
|
||||||
@@ -575,19 +584,19 @@ def results_consumer():
|
|||||||
# чтобы /status видел "живость" раз в 5 минут независимо от полноты JSON.
|
# чтобы /status видел "живость" раз в 5 минут независимо от полноты JSON.
|
||||||
if globals().get("STATUS") != "live":
|
if globals().get("STATUS") != "live":
|
||||||
latest_data["game"] = {"ts": msg["ts"], "data": payload}
|
latest_data["game"] = {"ts": msg["ts"], "data": payload}
|
||||||
# можно залогировать для отладки:
|
|
||||||
logger.debug("results_consumer: pre-live game → updated (full=%s)", is_full)
|
logger.debug("results_consumer: pre-live game → updated (full=%s)", is_full)
|
||||||
|
else:
|
||||||
|
# ✅ если игры ещё НЕТ в кэше — примем ПЕРВЫЙ game даже неполный,
|
||||||
|
# чтобы box-score/play-by-play могли его дорастить
|
||||||
|
if is_full or not has_game_already:
|
||||||
|
latest_data["game"] = {"ts": msg["ts"], "data": payload}
|
||||||
|
logger.debug("results_consumer: LIVE → stored (full=%s, had=%s)", is_full, has_game_already)
|
||||||
|
else:
|
||||||
|
logger.debug("results_consumer: LIVE & partial game → keep previous one")
|
||||||
|
|
||||||
# 2) Когда матч УЖЕ online (STATUS == 'live'):
|
# 2) Когда матч УЖЕ online (STATUS == 'live'):
|
||||||
# - поток 'game' в live-режиме погаснет сам (stop_when_live=True),
|
# - поток 'game' в live-режиме погаснет сам (stop_when_live=True),
|
||||||
# но если вдруг что-то долетит, кладём только полный JSON.
|
# но если вдруг что-то долетит, кладём только полный JSON.
|
||||||
else:
|
|
||||||
if is_full:
|
|
||||||
latest_data["game"] = {"ts": msg["ts"], "data": payload}
|
|
||||||
logger.debug("results_consumer: LIVE & full game → replaced")
|
|
||||||
else:
|
|
||||||
# Ничего не трогаем, чтобы не затереть полноценные данные пустотой.
|
|
||||||
logger.debug("results_consumer: LIVE & partial game → keep previous one")
|
|
||||||
continue
|
continue
|
||||||
# # game неполный
|
# # game неполный
|
||||||
# if not has_game_already:
|
# if not has_game_already:
|
||||||
@@ -1010,7 +1019,7 @@ def format_time(seconds: float | int) -> str:
|
|||||||
|
|
||||||
@app.get("/team1")
|
@app.get("/team1")
|
||||||
async def team1():
|
async def team1():
|
||||||
game = get_latest_game_safe()
|
game = get_latest_game_safe("game")
|
||||||
if not game:
|
if not game:
|
||||||
raise HTTPException(status_code=503, detail="game data not ready")
|
raise HTTPException(status_code=503, detail="game data not ready")
|
||||||
return await team("team1")
|
return await team("team1")
|
||||||
@@ -1018,7 +1027,7 @@ async def team1():
|
|||||||
|
|
||||||
@app.get("/team2")
|
@app.get("/team2")
|
||||||
async def team2():
|
async def team2():
|
||||||
game = get_latest_game_safe()
|
game = get_latest_game_safe("game")
|
||||||
if not game:
|
if not game:
|
||||||
raise HTTPException(status_code=503, detail="game data not ready")
|
raise HTTPException(status_code=503, detail="game data not ready")
|
||||||
return await team("team2")
|
return await team("team2")
|
||||||
@@ -1247,7 +1256,7 @@ async def status(request: Request):
|
|||||||
|
|
||||||
@app.get("/scores")
|
@app.get("/scores")
|
||||||
async def scores():
|
async def scores():
|
||||||
game = get_latest_game_safe()
|
game = get_latest_game_safe("game")
|
||||||
if not game:
|
if not game:
|
||||||
# игры ещё нет или пришёл только частичный ответ
|
# игры ещё нет или пришёл только частичный ответ
|
||||||
# отдаём пустую структуру, чтобы фронт не падал
|
# отдаём пустую структуру, чтобы фронт не падал
|
||||||
@@ -1309,13 +1318,13 @@ async def top_sorted_team(data):
|
|||||||
return top_sorted_team
|
return top_sorted_team
|
||||||
|
|
||||||
|
|
||||||
def get_latest_game_safe():
|
def get_latest_game_safe(name:str):
|
||||||
"""
|
"""
|
||||||
Безопасно достаём актуальный game из latest_data.
|
Безопасно достаём актуальный game из latest_data.
|
||||||
Возвращаем None, если структура ещё не готова или прилетел "плохой" game
|
Возвращаем None, если структура ещё не готова или прилетел "плохой" game
|
||||||
(например, с {"status": "no-status"} без data/result).
|
(например, с {"status": "no-status"} без data/result).
|
||||||
"""
|
"""
|
||||||
game = latest_data.get("game")
|
game = latest_data.get(name)
|
||||||
if not game:
|
if not game:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -1338,33 +1347,118 @@ def get_latest_game_safe():
|
|||||||
return game
|
return game
|
||||||
|
|
||||||
|
|
||||||
|
def format_time(seconds: float | int) -> str:
|
||||||
|
"""
|
||||||
|
Форматирует время в секундах в строку "M:SS".
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seconds (float | int): Количество секунд.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Время в формате "M:SS".
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
total_seconds = int(float(seconds))
|
||||||
|
minutes = total_seconds // 60
|
||||||
|
sec = total_seconds % 60
|
||||||
|
return f"{minutes}:{sec:02}"
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return "0:00"
|
||||||
|
|
||||||
|
|
||||||
|
def _pick_last_avg_and_sum(stats_list: list) -> tuple[dict, dict]:
|
||||||
|
"""Возвращает (season_sum, season_avg) из seasonStats. Безопасно при пустых данных."""
|
||||||
|
if not isinstance(stats_list, list) or len(stats_list) == 0:
|
||||||
|
return {}, {}
|
||||||
|
# В JSON конец массива: ... {"class":"Sum"}, {"class":"Avg"}
|
||||||
|
last = stats_list[-1] if stats_list else None
|
||||||
|
prev = stats_list[-2] if len(stats_list) >= 2 else None
|
||||||
|
|
||||||
|
season_avg = last.get("stats", {}) if isinstance(last, dict) and str(last.get("class")).lower() == "avg" else {}
|
||||||
|
season_sum = prev.get("stats", {}) if isinstance(prev, dict) and str(prev.get("class")).lower() == "sum" else {}
|
||||||
|
|
||||||
|
# Бывают инверсии порядка (на всякий случай): попробуем найти явно
|
||||||
|
if not season_avg or not season_sum:
|
||||||
|
for x in reversed(stats_list):
|
||||||
|
if isinstance(x, dict) and str(x.get("class")).lower() == "avg" and not season_avg:
|
||||||
|
season_avg = x.get("stats", {}) or {}
|
||||||
|
if isinstance(x, dict) and str(x.get("class")).lower() == "sum" and not season_sum:
|
||||||
|
season_sum = x.get("stats", {}) or {}
|
||||||
|
if season_avg and season_sum:
|
||||||
|
break
|
||||||
|
return season_sum, season_avg
|
||||||
|
|
||||||
|
|
||||||
|
def _pick_career_sum_and_avg(carrier_list: list) -> tuple[dict, dict]:
|
||||||
|
"""Возвращает (career_sum, career_avg) из carrier. В API встречаются блоки с class: Normal/Sum/Avg."""
|
||||||
|
if not isinstance(carrier_list, list) or len(carrier_list) == 0:
|
||||||
|
return {}, {}
|
||||||
|
career_sum, career_avg = {}, {}
|
||||||
|
# Ищем явные «Sum» и «Avg»
|
||||||
|
for x in reversed(carrier_list):
|
||||||
|
if isinstance(x, dict):
|
||||||
|
cls = str(x.get("class", "")).lower()
|
||||||
|
stats = x.get("stats", {}) or {}
|
||||||
|
if cls == "sum" and not career_sum:
|
||||||
|
career_sum = stats
|
||||||
|
elif cls == "avg" and not career_avg:
|
||||||
|
career_avg = stats
|
||||||
|
if career_sum and career_avg:
|
||||||
|
break
|
||||||
|
# Если «Avg» нет (часто для карьеры бывает только Normal/Sum) — ок, оставим пустым
|
||||||
|
return career_sum, career_avg
|
||||||
|
|
||||||
|
|
||||||
|
def _as_int(v, default=0):
|
||||||
|
try:
|
||||||
|
# в JSON часто строки; пустые строки -> 0
|
||||||
|
if v in ("", None):
|
||||||
|
return default
|
||||||
|
return int(float(v))
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _safe(d: dict) -> dict:
|
||||||
|
return d if isinstance(d, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
async def team(who: str):
|
async def team(who: str):
|
||||||
"""
|
"""
|
||||||
Возвращает данные по команде (team1 / team2) из актуального game.
|
Возвращает данные по команде (team1 / team2) из актуального game.
|
||||||
Защищена от ситуации, когда latest_data["game"] ещё не прогрелся
|
Защищена от ситуации, когда latest_data["game"] ещё не прогрелся
|
||||||
или в него прилетел "плохой" ответ от API.
|
или в него прилетел "плохой" ответ от API.
|
||||||
"""
|
"""
|
||||||
game = get_latest_game_safe()
|
game = get_latest_game_safe("game")
|
||||||
if not game:
|
if not game:
|
||||||
# игра ещё не подгружена или структура кривоватая
|
# игра ещё не подгружена или структура кривоватая
|
||||||
raise HTTPException(status_code=503, detail="game data not ready")
|
raise HTTPException(status_code=503, detail="game data not ready")
|
||||||
|
|
||||||
|
full_stat = get_latest_game_safe("pregame-full-stats")
|
||||||
|
if not full_stat:
|
||||||
|
raise HTTPException(status_code=503, detail="pregame-full-stats data not ready")
|
||||||
|
|
||||||
# нормализуем доступ к данным
|
# нормализуем доступ к данным
|
||||||
game_data = game["data"] if "data" in game else game
|
game_data = game["data"] if "data" in game else game
|
||||||
|
full_stat_data = full_stat["data"] if "data" in full_stat else full_stat
|
||||||
result = game_data[
|
result = game_data[
|
||||||
"result"
|
"result"
|
||||||
] # здесь уже безопасно, мы проверили в get_latest_game_safe
|
] # здесь уже безопасно, мы проверили в get_latest_game_safe
|
||||||
|
result_full = full_stat_data["result"]
|
||||||
|
|
||||||
# в result ожидаем "teams"
|
# в result ожидаем "teams"
|
||||||
teams = result.get("teams")
|
teams = result.get("teams")
|
||||||
|
|
||||||
if not teams:
|
if not teams:
|
||||||
raise HTTPException(status_code=503, detail="game teams not ready")
|
raise HTTPException(status_code=503, detail="game teams not ready")
|
||||||
|
|
||||||
# выбираем команду
|
# выбираем команду
|
||||||
if who == "team1":
|
if who == "team1":
|
||||||
payload = next((t for t in teams if t.get("teamNumber") == 1), None)
|
payload = next((t for t in teams if t.get("teamNumber") == 1), None)
|
||||||
|
payload_full = result_full.get("team1PlayersStats")
|
||||||
else:
|
else:
|
||||||
payload = next((t for t in teams if t.get("teamNumber") == 2), None)
|
payload = next((t for t in teams if t.get("teamNumber") == 2), None)
|
||||||
|
payload_full = result_full.get("team2PlayersStats")
|
||||||
|
|
||||||
if payload is None:
|
if payload is None:
|
||||||
raise HTTPException(status_code=404, detail=f"{who} not found in game data")
|
raise HTTPException(status_code=404, detail=f"{who} not found in game data")
|
||||||
@@ -1383,10 +1477,126 @@ async def team(who: str):
|
|||||||
("Forward-Center", "FC"),
|
("Forward-Center", "FC"),
|
||||||
]
|
]
|
||||||
starts = payload.get("starts", [])
|
starts = payload.get("starts", [])
|
||||||
|
|
||||||
team_rows = []
|
team_rows = []
|
||||||
|
|
||||||
for item in starts:
|
for item in starts:
|
||||||
stats = item.get("stats") or {}
|
stats = item.get("stats") or {}
|
||||||
|
pid = str(item.get("personId"))
|
||||||
|
full_obj = next((p for p in (payload_full or []) if str(p.get("personId")) == pid), None)
|
||||||
|
|
||||||
|
season_sum = season_avg = career_sum = career_avg = {}
|
||||||
|
if full_obj:
|
||||||
|
# сезон
|
||||||
|
season_sum, season_avg = _pick_last_avg_and_sum(full_obj.get("seasonStats") or [])
|
||||||
|
# карьера
|
||||||
|
career_sum, career_avg = _pick_career_sum_and_avg(full_obj.get("carrier") or [])
|
||||||
|
|
||||||
|
season_sum = _safe(season_sum)
|
||||||
|
season_avg = _safe(season_avg)
|
||||||
|
career_sum = _safe(career_sum)
|
||||||
|
career_avg = _safe(career_avg)
|
||||||
|
|
||||||
|
# Полезные числа для Totals+Live
|
||||||
|
# live-поля в box-score называются goal1/2/3, shot1/2/3, defReb/offReb и т.п.
|
||||||
|
g1 = _as_int(stats.get("goal1"))
|
||||||
|
s1 = _as_int(stats.get("shot1"))
|
||||||
|
g2 = _as_int(stats.get("goal2"))
|
||||||
|
s2 = _as_int(stats.get("shot2"))
|
||||||
|
g3 = _as_int(stats.get("goal3"))
|
||||||
|
s3 = _as_int(stats.get("shot3"))
|
||||||
|
|
||||||
|
# Сезонные суммы из pregame-full-stats
|
||||||
|
ss_pts = _as_int(season_sum.get("points"))
|
||||||
|
ss_ast = _as_int(season_sum.get("assist"))
|
||||||
|
ss_blk = _as_int(season_sum.get("blockShot"))
|
||||||
|
ss_dreb = _as_int(season_sum.get("defRebound"))
|
||||||
|
ss_oreb = _as_int(season_sum.get("offRebound"))
|
||||||
|
ss_reb = _as_int(season_sum.get("rebound"))
|
||||||
|
ss_stl = _as_int(season_sum.get("steal"))
|
||||||
|
ss_to = _as_int(season_sum.get("turnover"))
|
||||||
|
ss_foul = _as_int(season_sum.get("foul"))
|
||||||
|
ss_sec = _as_int(season_sum.get("second"))
|
||||||
|
ss_gms = _as_int(season_sum.get("games"))
|
||||||
|
ss_st = _as_int(season_sum.get("isStarts"))
|
||||||
|
ss_g1 = _as_int(season_sum.get("goal1"))
|
||||||
|
ss_s1 = _as_int(season_sum.get("shot1"))
|
||||||
|
ss_g2 = _as_int(season_sum.get("goal2"))
|
||||||
|
ss_s2 = _as_int(season_sum.get("shot2"))
|
||||||
|
ss_g3 = _as_int(season_sum.get("goal3"))
|
||||||
|
ss_s3 = _as_int(season_sum.get("shot3"))
|
||||||
|
|
||||||
|
# Карьерные суммы из pregame-full-stats
|
||||||
|
car_ss_pts = _as_int(career_sum.get("points"))
|
||||||
|
car_ss_ast = _as_int(career_sum.get("assist"))
|
||||||
|
car_ss_blk = _as_int(career_sum.get("blockShot"))
|
||||||
|
car_ss_dreb = _as_int(career_sum.get("defRebound"))
|
||||||
|
car_ss_oreb = _as_int(career_sum.get("offRebound"))
|
||||||
|
car_ss_reb = _as_int(career_sum.get("rebound"))
|
||||||
|
car_ss_stl = _as_int(career_sum.get("steal"))
|
||||||
|
car_ss_to = _as_int(career_sum.get("turnover"))
|
||||||
|
car_ss_foul = _as_int(career_sum.get("foul"))
|
||||||
|
car_ss_sec = _as_int(career_sum.get("second"))
|
||||||
|
car_ss_gms = _as_int(career_sum.get("games"))
|
||||||
|
car_ss_st = _as_int(career_sum.get("isStarts"))
|
||||||
|
car_ss_g1 = _as_int(career_sum.get("goal1"))
|
||||||
|
car_ss_s1 = _as_int(career_sum.get("shot1"))
|
||||||
|
car_ss_g2 = _as_int(career_sum.get("goal2"))
|
||||||
|
car_ss_s2 = _as_int(career_sum.get("shot2"))
|
||||||
|
car_ss_g3 = _as_int(career_sum.get("goal3"))
|
||||||
|
car_ss_s3 = _as_int(career_sum.get("shot3"))
|
||||||
|
|
||||||
|
# Totals по сезону, «с учётом текущего матча»:
|
||||||
|
T_points = ss_pts + _as_int(stats.get("points"))
|
||||||
|
T_assist = ss_ast + _as_int(stats.get("assist"))
|
||||||
|
T_block = ss_blk + _as_int(stats.get("block"))
|
||||||
|
T_dreb = ss_dreb + _as_int(stats.get("defReb"))
|
||||||
|
T_oreb = ss_oreb + _as_int(stats.get("offReb"))
|
||||||
|
T_reb = ss_reb + (_as_int(stats.get("defReb")) + _as_int(stats.get("offReb")))
|
||||||
|
T_steal = ss_stl + _as_int(stats.get("steal"))
|
||||||
|
T_turn = ss_to + _as_int(stats.get("turnover"))
|
||||||
|
T_foul = ss_foul + _as_int(stats.get("foul"))
|
||||||
|
T_sec = ss_sec + _as_int(stats.get("second"))
|
||||||
|
T_gms = ss_gms + (1 if _as_int(stats.get("second")) > 0 else 0)
|
||||||
|
T_starts = ss_st + (1 if bool(stats.get("isStart")) else 0)
|
||||||
|
|
||||||
|
T_g1 = ss_g1 + g1
|
||||||
|
T_s1 = ss_s1 + s1
|
||||||
|
T_g2 = ss_g2 + g2
|
||||||
|
T_s2 = ss_s2 + s2
|
||||||
|
T_g3 = ss_g3 + g3
|
||||||
|
T_s3 = ss_s3 + s3
|
||||||
|
|
||||||
|
# Totals по карьере, «с учётом текущего матча»:
|
||||||
|
car_T_points = car_ss_pts + _as_int(stats.get("points"))
|
||||||
|
car_T_assist = car_ss_ast + _as_int(stats.get("assist"))
|
||||||
|
car_T_block = car_ss_blk + _as_int(stats.get("block"))
|
||||||
|
car_T_dreb = car_ss_dreb + _as_int(stats.get("defReb"))
|
||||||
|
car_T_oreb = car_ss_oreb + _as_int(stats.get("offReb"))
|
||||||
|
car_T_reb = car_ss_reb + (_as_int(stats.get("defReb")) + _as_int(stats.get("offReb")))
|
||||||
|
car_T_steal = car_ss_stl + _as_int(stats.get("steal"))
|
||||||
|
car_T_turn = car_ss_to + _as_int(stats.get("turnover"))
|
||||||
|
car_T_foul = car_ss_foul + _as_int(stats.get("foul"))
|
||||||
|
car_T_sec = car_ss_sec + _as_int(stats.get("second"))
|
||||||
|
car_T_gms = car_ss_gms + (1 if _as_int(stats.get("second")) > 0 else 0)
|
||||||
|
car_T_starts = car_ss_st + (1 if bool(stats.get("isStart")) else 0)
|
||||||
|
|
||||||
|
car_T_g1 = car_ss_g1 + g1
|
||||||
|
car_T_s1 = car_ss_s1 + s1
|
||||||
|
car_T_g2 = car_ss_g2 + g2
|
||||||
|
car_T_s2 = car_ss_s2 + s2
|
||||||
|
car_T_g3 = car_ss_g3 + g3
|
||||||
|
car_T_s3 = car_ss_s3 + s3
|
||||||
|
|
||||||
|
# Проценты (без деления на 0)
|
||||||
|
def _pct(goal, shot):
|
||||||
|
return f"{round(goal*100/shot, 1)}%" if shot else "0.0%"
|
||||||
|
|
||||||
|
# Для «23» используем сумму 2-х и 3-х
|
||||||
|
T_g23 = T_g2 + T_g3
|
||||||
|
T_s23 = T_s2 + T_s3
|
||||||
|
car_T_g23 = car_T_g2 + car_T_g3
|
||||||
|
car_T_s23 = car_T_s2 + car_T_s3
|
||||||
|
# print(avg_season, total_season)
|
||||||
row = {
|
row = {
|
||||||
"id": item.get("personId") or "",
|
"id": item.get("personId") or "",
|
||||||
"num": item.get("displayNumber"),
|
"num": item.get("displayNumber"),
|
||||||
@@ -1434,69 +1644,121 @@ async def team(who: str):
|
|||||||
)
|
)
|
||||||
+ ".svg"
|
+ ".svg"
|
||||||
),
|
),
|
||||||
"pts": stats.get("points", 0),
|
# live-стата
|
||||||
"pt-2": f"{stats.get('goal2',0)}/{stats.get('shot2',0)}" if stats else 0,
|
"pts": _as_int(stats.get("points")),
|
||||||
"pt-3": f"{stats.get('goal3',0)}/{stats.get('shot3',0)}" if stats else 0,
|
"pt-2": f"{g2}/{s2}",
|
||||||
"pt-1": f"{stats.get('goal1',0)}/{stats.get('shot1',0)}" if stats else 0,
|
"pt-3": f"{g3}/{s3}",
|
||||||
"fg": (
|
"pt-1": f"{g1}/{s1}",
|
||||||
f"{stats.get('goal2',0)+stats.get('goal3',0)}/"
|
"fg": f"{g2+g3}/{s2+s3}",
|
||||||
f"{stats.get('shot2',0)+stats.get('shot3',0)}"
|
"ast": _as_int(stats.get("assist")),
|
||||||
if stats
|
"stl": _as_int(stats.get("steal")),
|
||||||
else 0
|
"blk": _as_int(stats.get("block")),
|
||||||
),
|
"blkVic": _as_int(stats.get("blocked")),
|
||||||
"ast": stats.get("assist", 0),
|
"dreb": _as_int(stats.get("defReb")),
|
||||||
"stl": stats.get("steal", 0),
|
"oreb": _as_int(stats.get("offReb")),
|
||||||
"blk": stats.get("block", 0),
|
"reb": _as_int(stats.get("defReb")) + _as_int(stats.get("offReb")),
|
||||||
"blkVic": stats.get("blocked", 0),
|
"to": _as_int(stats.get("turnover")),
|
||||||
"dreb": stats.get("defReb", 0),
|
"foul": _as_int(stats.get("foul")),
|
||||||
"oreb": stats.get("offReb", 0),
|
"foulT": _as_int(stats.get("foulT")),
|
||||||
"reb": stats.get("defReb", 0) + stats.get("offReb", 0),
|
"foulD": _as_int(stats.get("foulD")),
|
||||||
"to": stats.get("turnover", 0),
|
"foulC": _as_int(stats.get("foulC")),
|
||||||
"foul": stats.get("foul", 0),
|
"foulB": _as_int(stats.get("foulB")),
|
||||||
"foulT": stats.get("foulT", 0),
|
"fouled": _as_int(stats.get("foulsOn")),
|
||||||
"foulD": stats.get("foulD", 0),
|
"plusMinus": _as_int(stats.get("plusMinus")),
|
||||||
"foulC": stats.get("foulC", 0),
|
"dunk": _as_int(stats.get("dunk")),
|
||||||
"foulB": stats.get("foulB", 0),
|
"kpi": (
|
||||||
"fouled": stats.get("foulsOn", 0),
|
_as_int(stats.get("points"))
|
||||||
"plusMinus": stats.get("plusMinus", 0),
|
+ _as_int(stats.get("defReb")) + _as_int(stats.get("offReb"))
|
||||||
"dunk": stats.get("dunk", 0),
|
+ _as_int(stats.get("assist")) + _as_int(stats.get("steal")) + _as_int(stats.get("block"))
|
||||||
"kpi": (
|
+ _as_int(stats.get("foulsOn"))
|
||||||
stats.get("points", 0)
|
+ (g1 - s1) + (g2 - s2) + (g3 - s3)
|
||||||
+ stats.get("defReb", 0)
|
- _as_int(stats.get("turnover")) - _as_int(stats.get("foul"))
|
||||||
+ stats.get("offReb", 0)
|
),
|
||||||
+ stats.get("assist", 0)
|
"time": format_time(_as_int(stats.get("second"))),
|
||||||
+ stats.get("steal", 0)
|
|
||||||
+ stats.get("block", 0)
|
|
||||||
+ stats.get("foulsOn", 0)
|
|
||||||
+ (stats.get("goal1", 0) - stats.get("shot1", 0))
|
|
||||||
+ (stats.get("goal2", 0) - stats.get("shot2", 0))
|
|
||||||
+ (stats.get("goal3", 0) - stats.get("shot3", 0))
|
|
||||||
- stats.get("turnover", 0)
|
|
||||||
- stats.get("foul", 0)
|
|
||||||
),
|
|
||||||
"time": format_time(stats.get("second", 0)),
|
|
||||||
"pts1q": 0,
|
|
||||||
"pts2q": 0,
|
|
||||||
"pts3q": 0,
|
|
||||||
"pts4q": 0,
|
|
||||||
"pts1h": 0,
|
|
||||||
"pts2h": 0,
|
|
||||||
"Name1GFX": (item.get("firstName") or "").strip(),
|
|
||||||
"Name2GFX": (item.get("lastName") or "").strip(),
|
|
||||||
"photoGFX": (
|
|
||||||
os.path.join(
|
|
||||||
"D:\\Photos",
|
|
||||||
LEAGUE.lower(),
|
|
||||||
result[who]["name"],
|
|
||||||
f"{item.get('displayNumber')}.png",
|
|
||||||
)
|
|
||||||
if item.get("startRole") == "Player"
|
|
||||||
else ""
|
|
||||||
),
|
|
||||||
"isOnCourt": stats.get("isOnCourt", False),
|
|
||||||
}
|
|
||||||
team_rows.append(row)
|
|
||||||
|
|
||||||
|
# сезон — средние (из последнего Avg)
|
||||||
|
"AvgPoints": season_avg.get("points") or "0.0",
|
||||||
|
"AvgAssist": season_avg.get("assist") or "0.0",
|
||||||
|
"AvgBlocks": season_avg.get("blockShot") or "0.0",
|
||||||
|
"AvgDefRebound": season_avg.get("defRebound") or "0.0",
|
||||||
|
"AvgOffRebound": season_avg.get("offRebound") or "0.0",
|
||||||
|
"AvgRebound": season_avg.get("rebound") or "0.0",
|
||||||
|
"AvgSteal": season_avg.get("steal") or "0.0",
|
||||||
|
"AvgTurnover": season_avg.get("turnover") or "0.0",
|
||||||
|
"AvgFoul": season_avg.get("foul") or "0.0",
|
||||||
|
"AvgOpponentFoul": season_avg.get("foulsOnPlayer") or "0.0",
|
||||||
|
"AvgDunk": season_avg.get("dunk") or "0.0",
|
||||||
|
"AvgPlayedTime": season_avg.get("playedTime") or "0:00",
|
||||||
|
"Shot1Percent": season_avg.get("shot1Percent") or "0.0%",
|
||||||
|
"Shot2Percent": season_avg.get("shot2Percent") or "0.0%",
|
||||||
|
"Shot3Percent": season_avg.get("shot3Percent") or "0.0%",
|
||||||
|
"Shot23Percent": season_avg.get("shot23Percent") or "0.0%",
|
||||||
|
|
||||||
|
# сезон — Totals (суммы из Sum + live)
|
||||||
|
"TPoints": T_points,
|
||||||
|
"TShots1": f"{T_g1}/{T_s1}",
|
||||||
|
"TShots2": f"{T_g2}/{T_s2}",
|
||||||
|
"TShots3": f"{T_g3}/{T_s3}",
|
||||||
|
"TShots23": f"{T_g23}/{T_s23}",
|
||||||
|
"TShot1Percent": _pct(T_g1, T_s1),
|
||||||
|
"TShot2Percent": _pct(T_g2, T_s2),
|
||||||
|
"TShot3Percent": _pct(T_g3, T_s3),
|
||||||
|
"TShot23Percent": _pct(T_g23, T_s23),
|
||||||
|
"TAssist": T_assist,
|
||||||
|
"TBlocks": T_block,
|
||||||
|
"TDefRebound": T_dreb,
|
||||||
|
"TOffRebound": T_oreb,
|
||||||
|
"TRebound": T_reb,
|
||||||
|
"TSteal": T_steal,
|
||||||
|
"TTurnover": T_turn,
|
||||||
|
"TFoul": T_foul,
|
||||||
|
"TPlayedTime": format_time(T_sec),
|
||||||
|
"TGameCount": T_gms,
|
||||||
|
"TStartCount": T_starts,
|
||||||
|
|
||||||
|
# карьера — средние (из последнего Avg)
|
||||||
|
"Career_AvgPoints": career_avg.get("points") or "0.0",
|
||||||
|
"Career_AvgAssist": career_avg.get("assist") or "0.0",
|
||||||
|
"Career_AvgBlocks": career_avg.get("blockShot") or "0.0",
|
||||||
|
"Career_AvgDefRebound": career_avg.get("defRebound") or "0.0",
|
||||||
|
"Career_AvgOffRebound": career_avg.get("offRebound") or "0.0",
|
||||||
|
"Career_AvgRebound": career_avg.get("rebound") or "0.0",
|
||||||
|
"Career_AvgSteal": career_avg.get("steal") or "0.0",
|
||||||
|
"Career_AvgTurnover": career_avg.get("turnover") or "0.0",
|
||||||
|
"Career_AvgFoul": career_avg.get("foul") or "0.0",
|
||||||
|
"Career_AvgOpponentFoul": career_avg.get("foulsOnPlayer") or "0.0",
|
||||||
|
"Career_AvgDunk": career_avg.get("dunk") or "0.0",
|
||||||
|
"Career_AvgPlayedTime": career_avg.get("playedTime") or "0:00",
|
||||||
|
"Career_Shot1Percent": career_avg.get("shot1Percent") or "0.0%",
|
||||||
|
"Career_Shot2Percent": career_avg.get("shot2Percent") or "0.0%",
|
||||||
|
"Career_Shot3Percent": career_avg.get("shot3Percent") or "0.0%",
|
||||||
|
"Career_Shot23Percent": career_avg.get("shot23Percent") or "0.0%",
|
||||||
|
|
||||||
|
# карьера — Totals (суммы из Sum + live)
|
||||||
|
"Career_TPoints": car_T_points,
|
||||||
|
"Career_TShots1": f"{car_T_g1}/{car_T_s1}",
|
||||||
|
"Career_TShots2": f"{car_T_g2}/{car_T_s2}",
|
||||||
|
"Career_TShots3": f"{car_T_g3}/{car_T_s3}",
|
||||||
|
"Career_TShots23": f"{car_T_g23}/{car_T_s23}",
|
||||||
|
"Career_TShot1Percent": _pct(car_T_g1, car_T_s1),
|
||||||
|
"Career_TShot2Percent": _pct(car_T_g2, car_T_s2),
|
||||||
|
"Career_TShot3Percent": _pct(car_T_g3, car_T_s3),
|
||||||
|
"Career_TShot23Percent": _pct(car_T_g23, car_T_s23),
|
||||||
|
"Career_TAssist": car_T_assist,
|
||||||
|
"Career_TBlocks": car_T_block,
|
||||||
|
"Career_TDefRebound": car_T_dreb,
|
||||||
|
"Career_TOffRebound": car_T_oreb,
|
||||||
|
"Career_TRebound": car_T_reb,
|
||||||
|
"Career_TSteal": car_T_steal,
|
||||||
|
"Career_TTurnover": car_T_turn,
|
||||||
|
"Career_TFoul": car_T_foul,
|
||||||
|
"Career_TPlayedTime": format_time(car_T_sec),
|
||||||
|
"Career_TGameCount": car_T_gms,
|
||||||
|
"Career_TStartCount": car_T_starts,
|
||||||
|
|
||||||
|
}
|
||||||
|
team_rows.append(row)
|
||||||
|
|
||||||
# добиваем до 12 строк, чтобы UI был ровный
|
# добиваем до 12 строк, чтобы UI был ровный
|
||||||
count_player = sum(1 for x in team_rows if x["startRole"] == "Player")
|
count_player = sum(1 for x in team_rows if x["startRole"] == "Player")
|
||||||
if count_player < 12 and team_rows:
|
if count_player < 12 and team_rows:
|
||||||
|
|||||||
Reference in New Issue
Block a user