diff --git a/get_data.py b/get_data.py
index 313c306..cf3ae57 100644
--- a/get_data.py
+++ b/get_data.py
@@ -442,11 +442,13 @@ def get_data_from_API(
sleep_time: float,
stop_event: threading.Event,
stop_when_live=False,
- stop_after_success: bool = False, # 👈 новый флаг
+ stop_after_success: bool = False, # 👈 флаг "останавливаемся после ОК"
):
did_first_fetch = False
while not stop_event.is_set():
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
+
+ # останов при live + полная игра уже есть
if (
stop_when_live
and globals().get("STATUS") == "live"
@@ -457,6 +459,7 @@ def get_data_from_API(
)
break
+ # ---------- запрос к API ----------
try:
value = requests.get(url, timeout=5).json()
did_first_fetch = True # помечаем, что один заход сделали
@@ -475,7 +478,8 @@ def get_data_from_API(
logger.warning(f"[{current_time}] [{name}] Неизвестная ошибка: {ex}")
value = {"error": str(ex)}
- # Проверяем, нет ли явного статуса ошибки в JSON
+ # ---------- проверка статуса ответа ----------
+ # если API сам вернул status = error/fail/no-status
if isinstance(value, dict) and str(value.get("status", "")).lower() in (
"error",
"fail",
@@ -488,17 +492,50 @@ def get_data_from_API(
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
results_q.put({"source": name, "ts": ts, "data": value})
logger.debug(f"[{ts}] name: {name}, status: {value.get('status', 'no-status')}")
+
ok_status = not (
isinstance(value, dict)
- and str(value.get("status", "")).lower() in ("error", "fail", "no-status")
+ and (
+ str(value.get("status", "")).lower() in ("error", "fail", "no-status")
+ or "error" in value
+ )
)
+ # print(name, ok_status)
+ # ---------- быстрый retry при плохом ответе ----------
+ if not ok_status:
+ # короткая задержка, чтобы не ушатать API частыми запросами
+ quick_delay = min(2, sleep_time if sleep_time > 0 else 1)
+ logger.warning(
+ f"[{current_time}] [{name}] плохой ответ (status={value.get('status', 'no-status')}) → быстрый повтор через {quick_delay} сек."
+ )
+ slept_q = 0.0
+ while slept_q < quick_delay:
+ if stop_event.is_set():
+ break
+ if (
+ stop_when_live
+ and globals().get("STATUS") == "live"
+ and has_full_game_ready()
+ ):
+ logger.info(
+ f"[{name}] stopping during quick retry sleep because STATUS='live' and full game is ready"
+ )
+ return
+ time.sleep(0.5)
+ slept_q += 0.5
+
+ # сразу на новую попытку, без длинного sleep_time
+ continue
+
+ # ---------- успешный ответ ----------
if stop_after_success and ok_status:
logger.info(
f"[{name}] got successful response → stopping thread (stop_after_success)"
)
return
+ # ---------- обычный сон между успешными запросами ----------
slept = 0
while slept < sleep_time:
if stop_event.is_set():
@@ -515,7 +552,6 @@ def get_data_from_API(
time.sleep(1)
slept += 1
- # если запрос занял дольше — просто сразу следующую итерацию
# Получение результатов из всех запущенных потоков
@@ -1151,7 +1187,6 @@ def start_prestart_watcher(game_dt: datetime | None):
continue
except Exception:
continue
- print(g)
gdt = extract_game_datetime(g)
if not gdt:
try:
@@ -2623,13 +2658,13 @@ stat_name_list = [
("foulB", "", ""),
("second", "секунды", "seconds"),
("dunk", "данки", "dunks"),
- ("fastBreak", "", "fast breaks"),
+ ("fastBreak", "быстрые отрывы", "fast breaks"),
("plusMinus", "+/-", "+/-"),
("avgAge", "", "avg Age"),
- ("ptsBench", "", "Bench PTS"),
- ("ptsBench_pro", "", "Bench PTS, %"),
- ("ptsStart", "", "Start PTS"),
- ("ptsStart_pro", "", "Start PTS, %"),
+ ("ptsBench", "Скамейка, очки", "Bench PTS"),
+ ("ptsBench_pro", "Скамейка, %", "Bench PTS, %"),
+ ("ptsStart", "Стартовая пятерка, очки", "Start PTS"),
+ ("ptsStart_pro", "Стартовая пятерка, %", "Start PTS, %"),
("avgHeight", "", "avg height"),
("timeout_left", "", "timeout left"),
("timeout_str", "", "timeout str"),
@@ -3441,7 +3476,7 @@ def get_image(points, bib, count_point):
buf = BytesIO(icon_bytes)
icon = Image.open(buf).convert("RGBA")
- size = point_radius * 2
+ size = point_radius * 3
icon = icon.resize((size, size), Image.LANCZOS)
base_image.paste(icon, (px - point_radius, py - point_radius), icon)
@@ -3780,6 +3815,1087 @@ async def last_5_games():
return data
+@app.get("/commentary", response_class=HTMLResponse)
+async def commentary():
+ game_wrap = get_latest_game_safe("game")
+ if not game_wrap:
+ return HTMLResponse(
+ "
Данные матча ещё не готовы ",
+ status_code=503,
+ )
+
+ game_data = game_wrap["data"] if "data" in game_wrap else game_wrap
+ result = game_data.get("result", {}) or {}
+
+ game_info = result.get("game", {}) or {}
+ team1_info = result.get("team1", {}) or {}
+ team2_info = result.get("team2", {}) or {}
+
+ team1_name = team1_info.get("name", "Team 1")
+ team2_name = team2_info.get("name", "Team 2")
+ score_now = game_info.get("score", "")
+ full_score = game_info.get("fullScore", "")
+
+ # live-status
+ ls_wrap = latest_data.get("live-status", {})
+ ls_raw = ls_wrap.get("data", {}) if isinstance(ls_wrap, dict) else {}
+ ls_dict = ls_raw.get("result") or ls_raw if isinstance(ls_raw, dict) else {}
+ live_status = (
+ ls_dict.get("status")
+ or ls_dict.get("gameStatus")
+ or ls_dict.get("state")
+ or "—"
+ )
+
+ # счёт по четвертям
+ quarters = ["Q1", "Q2", "Q3", "Q4", "OT1", "OT2", "OT3", "OT4"]
+ score_rows = []
+ if isinstance(full_score, str) and full_score:
+ fs_list = [x.strip() for x in full_score.split(",") if x.strip()]
+ for i, q in enumerate(quarters[: len(fs_list)]):
+ parts = fs_list[i].split(":")
+ s1, s2 = (parts + ["", ""])[:2]
+ score_rows.append((q, s1, s2))
+
+ # как в /team1 и /team2
+ team1_players = await team("team1")
+ team2_players = await team("team2")
+
+ team1_json = json.dumps(team1_players, ensure_ascii=False)
+ team2_json = json.dumps(team2_players, ensure_ascii=False)
+
+ def render_players_table(players, title, team_key: str) -> str:
+ header = """
+
+
+ #
+ Игрок
+ PTS
+ REB
+ AST
+ STL
+ BLK
+ MIN
+
+
+
+
+
+
+
+
+
+ """
+ rows = []
+ for p in players:
+ # только игроки
+ if p.get("startRole") not in ("Player", ""):
+ continue
+
+ num = p.get("num", "")
+ pid = p.get("id", "")
+ name = p.get("NameGFX") or p.get("name", "")
+ pts = p.get("pts", "")
+ reb = p.get("reb", (p.get("dreb", 0) or 0) + (p.get("oreb", 0) or 0))
+ ast = p.get("ast", "")
+ stl = p.get("stl", "")
+ blk = p.get("blk", "")
+ time_played = p.get("time", "")
+
+ fg = p.get("fg", "")
+ pt2 = p.get("pt-2", "")
+ pt3 = p.get("pt-3", "")
+ pt1 = p.get("pt-1", "")
+ to = p.get("to", "")
+ foul = p.get("foul", "")
+ plus_minus = p.get("plusMinus", "")
+ kpi = p.get("kpi", "")
+
+ rows.append(f"""
+
+ {num}
+
+ {name}
+
+ {pts}
+ {reb}
+ {ast}
+ {stl}
+ {blk}
+ {time_played}
+
+
+
+
+
+
+
+
+
+ """)
+
+ return f"""
+ {title}
+ {header}
+ {''.join(rows)}
+
+ """
+
+ # (pbp можно добавить сюда, я его пока опустил, чтобы не раздувать код)
+ pbp_html = ""
+
+ game_time_str = GAME_START_DT.strftime("%d.%m.%Y %H:%M") if GAME_START_DT else "N/A"
+
+ html = f"""
+
+
+
+ Комментаторский дашборд
+
+
+
+
+ {team1_name} vs {team2_name}
+
+ Счёт: {score_now or "—"} • Статус: {live_status} • Начало: {game_time_str}
+
+
+ Счёт по четвертям
+
+ Период {team1_name} {team2_name}
+ {''.join(f"{{q}} {{s1}} {{s2}} ".format(q=q, s1=s1, s2=s2) for (q, s1, s2) in score_rows)}
+
+
+
+
+
+ Показать всю статистику игроков
+
+
+
+
+
+ {render_players_table(team1_players, team1_name, "team1")}
+
+
+ {render_players_table(team2_players, team2_name, "team2")}
+
+
+
+
+
Кликни по фамилии игрока, чтобы показать сезон/карьеру.
+
+
+ {pbp_html}
+
+
+
+
+ """
+
+ return HTMLResponse(content=html)
+
+
+@app.get("/dashboard", response_class=HTMLResponse)
+async def dashboard():
+ """
+ HTML-дашборд для комментаторов:
+ - слева/справа: данные по командам и игрокам
+ - по центру: сравнительная командная статистика
+ Берём данные только из latest_data (game + team_stats-подобные агрегаты).
+ """
+ game_wrap = get_latest_game_safe("game")
+ if not game_wrap:
+ return HTMLResponse(
+ "Данные матча ещё не готовы ",
+ status_code=503,
+ )
+
+ game_data = game_wrap["data"] if "data" in game_wrap else game_wrap
+ result = game_data.get("result", {}) or {}
+
+ game_info = result.get("game", {}) or {}
+ team1_info = result.get("team1", {}) or {}
+ team2_info = result.get("team2", {}) or {}
+
+ score_now = game_info.get("score", "")
+ full_score = game_info.get("fullScore", "")
+
+ team1_name = team1_info.get("name", "Team 1")
+ team2_name = team2_info.get("name", "Team 2")
+
+ # --- счёт по четвертям, как в game.fullScore ---
+ quarters_labels = ["Q1", "Q2", "Q3", "Q4", "OT1", "OT2", "OT3", "OT4"]
+ quarter_rows = []
+ if isinstance(full_score, str) and full_score:
+ parts = [p.strip() for p in full_score.split(",") if p.strip()]
+ for i, part in enumerate(parts):
+ if i >= len(quarters_labels):
+ break
+ sep = ":" if ":" in part else "-" # на всякий случай
+ left, right = (part.split(sep) + ["", ""])[:2]
+ quarter_rows.append((quarters_labels[i], left.strip(), right.strip()))
+
+ # --- агрегированная командная статистика (как /team_stats) ---
+ teams = result.get("teams") or []
+ plays = result.get("plays") or []
+
+ team_1 = next((t for t in teams if t.get("teamNumber") == 1), None)
+ team_2 = next((t for t in teams if t.get("teamNumber") == 2), None)
+ if not team_1 or not team_2:
+ return HTMLResponse(
+ "Нет данных по командам в latest_data['game'] ",
+ status_code=503,
+ )
+
+ timeout_str1, timeout_left1, timeout_str2, timeout_left2 = time_outs_func(plays)
+
+ avg_age_1, points_1, avg_height_1 = add_data_for_teams(team_1.get("starts", []))
+ avg_age_2, points_2, avg_height_2 = add_data_for_teams(team_2.get("starts", []))
+
+ total_1 = add_new_team_stat(
+ team_1.get("total", {}),
+ avg_age_1,
+ points_1,
+ avg_height_1,
+ timeout_str1,
+ timeout_left1,
+ )
+ total_2 = add_new_team_stat(
+ team_2.get("total", {}),
+ avg_age_2,
+ points_2,
+ avg_height_2,
+ timeout_str2,
+ timeout_left2,
+ )
+
+ # словарь "метрика -> русское имя" из stat_name_list
+ stat_labels = {name: rus for (name, rus, eng) in stat_name_list}
+
+ # порядок вывода метрик в центральном столбце (как на твоём дашборде)
+ center_stats_order = [
+ "pt-1", # штрафные
+ "pt-1_pro",
+ "pt-2",
+ "pt-2_pro",
+ "pt-3",
+ "pt-3_pro",
+ "fg",
+ "fg_pro",
+ "defReb",
+ "offReb",
+ "Reb",
+ "assist",
+ "steal",
+ "turnover",
+ "block",
+ "foul",
+ "dunk",
+ "fastBreak",
+ "ptsStart",
+ "ptsStart_pro",
+ "ptsBench",
+ "ptsBench_pro",
+ ]
+
+ center_rows_html = []
+ for key in center_stats_order:
+ v1 = total_1.get(key, "")
+ v2 = total_2.get(key, "")
+ if v1 == "" and v2 == "":
+ continue
+ label_rus = stat_labels.get(key, key)
+ center_rows_html.append(
+ f""
+ f"{v1} "
+ f"{label_rus} "
+ f"{v2} "
+ f" "
+ )
+ center_stats_html = "".join(center_rows_html)
+
+ # --- данные по игрокам для левой / правой таблиц ---
+ team1_players = await team("team1")
+ team2_players = await team("team2")
+
+ def pick_coach(players_list):
+ for p in players_list:
+ if p.get("startRole") == "Coach":
+ return p
+ return None
+
+ coach1 = pick_coach(team1_players)
+ coach2 = pick_coach(team2_players)
+ coach1_name = (coach1 or {}).get("NameGFX") or (coach1 or {}).get("name", "")
+ coach2_name = (coach2 or {}).get("NameGFX") or (coach2 or {}).get("name", "")
+
+ avg_height1 = total_1.get("avgHeight", "")
+ avg_age1 = total_1.get("avgAge", "")
+ avg_height2 = total_2.get("avgHeight", "")
+ avg_age2 = total_2.get("avgAge", "")
+
+ timeouts_left1 = total_1.get("timeout_left", "")
+ timeouts_left2 = total_2.get("timeout_left", "")
+
+ def render_players_column(players_list, team_key):
+ rows = []
+ for p in players_list:
+ if p.get("startRole") != "Player":
+ continue
+ num = p.get("num", "")
+ name = p.get("NameGFX") or p.get("name", "")
+ pts = p.get("pts", "")
+ fouls = p.get("foul", "")
+ is_on = bool(p.get("isOnCourt")) # <-- тут берём флаг
+ ball = "🏀" if is_on else ""
+
+ rows.append(
+ f"""
+
+ {num}
+
+ {ball}
+ {name}
+
+ {pts}
+ {fouls}
+
+ """
+ )
+ return "".join(rows)
+
+ team1_players_html = render_players_column(team1_players, "team1")
+ team2_players_html = render_players_column(team2_players, "team2")
+
+ html = f"""
+
+
+
+ Game Dashboard
+
+
+
+
+
+
+
+
+
+
HEAD COACH
+
{coach1_name}
+
+ TIME-OUTS LEFT: {timeouts_left1}
+ AVG. HEIGHT: {avg_height1}
+ AVG. AGE: {avg_age1}
+
+
+
+
+
+ #
+ PLAYER
+ PTS
+ FOULS
+
+
+
+ {team1_players_html}
+
+
+
+
+
+
+
+
+ {team1_name}
+ vs
+ {team2_name}
+
+
{score_now}
+
+
+ {team1_name} {team2_name}
+ {''.join(f"{q} {s1} {s2} " for (q, s1, s2) in quarter_rows)}
+
+
+
+
+
+ {center_stats_html}
+
+
+
+
+
+
+
+
HEAD COACH
+
{coach2_name}
+
+ TIME-OUTS LEFT: {timeouts_left2}
+ AVG. HEIGHT: {avg_height2}
+ AVG. AGE: {avg_age2}
+
+
+
+
+
+ #
+ PLAYER
+ PTS
+ FOULS
+
+
+
+ {team2_players_html}
+
+
+
+
+
+
+
+
+ """
+
+ return HTMLResponse(content=html)
+
+
if __name__ == "__main__":
uvicorn.run(
"get_data:app", host="0.0.0.0", port=8000, reload=True, log_level="debug"