From edf044b0abf97fbfa95664eb06c654dec744d7f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AE=D1=80=D0=B8=D0=B9=20=D0=A7=D0=B5=D1=80=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE?= Date: Wed, 26 Nov 2025 11:33:28 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=202=20dashboarda?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- get_data.py | 1138 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1127 insertions(+), 11 deletions(-) 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 = """ + + + + + + + + + + + + + + + + + + + + """ + 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""" + + + + + + + + + + + + + + + + + + + """) + + return f""" +

{title}

+ {header} + {''.join(rows)} +
#ИгрокPTSREBASTSTLBLKMINFG2PT3PTFTTOFoul+/-KPI
{num} + {name} + {pts}{reb}{ast}{stl}{blk}{time_played}{fg}{pt2}{pt3}{pt1}{to}{foul}{plus_minus}{kpi}
+ """ + + # (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} +
+ +

Счёт по четвертям

+ + + {''.join(f"".format(q=q, s1=s1, s2=s2) for (q, s1, s2) in score_rows)} +
Период{team1_name}{team2_name}
{{q}}{{s1}}{{s2}}
+ +
+ +
+ +
+
+ {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} +
+
+ + + + + + + + + + + {team1_players_html} + +
#PLAYERPTSFOULS
+
+ + +
+
+
+ {team1_name} + vs + {team2_name} +
+
{score_now}
+
+ + + {''.join(f"" for (q, s1, s2) in quarter_rows)} +
{team1_name}{team2_name}
{q}{s1}{s2}
+
+
+ + + {center_stats_html} + +
+
+ + +
+
+
HEAD COACH
+
{coach2_name}
+
+ TIME-OUTS LEFT: {timeouts_left2}
+ AVG. HEIGHT: {avg_height2}
+ AVG. AGE: {avg_age2} +
+
+ + + + + + + + + + + {team2_players_html} + +
#PLAYERPTSFOULS
+
+
+ + + + + """ + + return HTMLResponse(content=html) + + if __name__ == "__main__": uvicorn.run( "get_data:app", host="0.0.0.0", port=8000, reload=True, log_level="debug"