From ced3220d62c9a9d2d9243b72f0e62d117f0ab43e 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: Thu, 13 Nov 2025 14:42:24 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20endpoint=20/play-by-play?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- get_data.py | 280 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 184 insertions(+), 96 deletions(-) diff --git a/get_data.py b/get_data.py index c5d9245..5ebfa32 100644 --- a/get_data.py +++ b/get_data.py @@ -17,9 +17,6 @@ from fastapi.responses import Response import logging import logging.config import platform -import socket - -# передадим параметры через аргументы или глобальные переменные parser = argparse.ArgumentParser() parser = argparse.ArgumentParser() @@ -29,7 +26,6 @@ parser.add_argument("--lang", default="en") args = parser.parse_args() MYHOST = platform.node() -user_name = socket.gethostname() if not os.path.exists("logs"): os.makedirs("logs") @@ -68,7 +64,7 @@ log_config = { "formatters": { "telegram": { "class": "telegram_handler.HtmlFormatter", - "format": f"%(levelname)s [{MYHOST.upper()}] [{user_name}]\n%(message)s", + "format": f"%(levelname)s [{MYHOST.upper()}]\n%(message)s", "use_emoji": "True", }, "simple": { @@ -136,6 +132,7 @@ URLS = { "play-by-play": "{host}/api/abc/games/play-by-play?id={game_id}", } + def maybe_clear_for_vmix(payload): """ Если включён режим очистки — возвращаем payload, @@ -158,20 +155,6 @@ def start_offline_threads(season, game_id): stop_live_threads() stop_offline_threads() logger.info("[threads] switching to OFFLINE mode ...") - # for key in latest_data: - # latest_data[key] = wipe_json_values(latest_data[key]) - # 🔹 очищаем latest_data безопасно, чтобы не ломать структуру - # keep_keys = { - # "game", - # "pregame", - # "pregame-full-stats", - # "actual-standings", - # "calendar", - # } - # for key in list(latest_data.keys()): - # if key not in keep_keys: - # del latest_data[key] - stop_event_offline.clear() @@ -619,10 +602,14 @@ def results_consumer(): and GAME_START_DT.date() == datetime.now().date() ): globals()["STATUS"] = "finished_wait" - globals()["CLEAR_OUTPUT_FOR_VMIX"] = True # 👈 включаем режим "пустых" данных + globals()[ + "CLEAR_OUTPUT_FOR_VMIX" + ] = True # 👈 включаем режим "пустых" данных else: globals()["STATUS"] = "finished_wait" - globals()["CLEAR_OUTPUT_FOR_VMIX"] = True # 👈 включаем режим "пустых" данных + globals()[ + "CLEAR_OUTPUT_FOR_VMIX" + ] = True # 👈 включаем режим "пустых" данных human_time = datetime.fromtimestamp(switch_at).strftime( "%H:%M:%S" @@ -642,7 +629,9 @@ def results_consumer(): "online" in raw_ls_status_low or "live" in raw_ls_status_low ): # если до этого стояла отложка — уберём - globals()["CLEAR_OUTPUT_FOR_VMIX"] = False # 👈 выключаем очистку + globals()[ + "CLEAR_OUTPUT_FOR_VMIX" + ] = False # 👈 выключаем очистку if globals().get("OFFLINE_SWITCH_AT") is not None: logger.info( "[status] match back to LIVE → cancel scheduled OFFLINE" @@ -699,25 +688,7 @@ def results_consumer(): logger.debug( "results_consumer: LIVE & partial game → keep previous one" ) - - # 2) Когда матч УЖЕ online (STATUS == 'live'): - # - поток 'game' в live-режиме погаснет сам (stop_when_live=True), - # но если вдруг что-то долетит, кладём только полный JSON. continue - # # game неполный - # if not has_game_already: - # # 👉 раньше game вообще не было — лучше положить хоть что-то - # latest_data["game"] = { - # "ts": msg["ts"], - # "data": payload, - # } - # else: - # # 👉 уже есть какой-то game — неполным НЕ затираем - # logger.debug( - # "results_consumer: got partial game, keeping previous one" - # ) - - # и обязательно continue/return из этого elif/if else: latest_data[source] = { "ts": msg["ts"], @@ -1012,20 +983,6 @@ def start_offline_prevgame(season, prev_game_id: str): logger.info("[threads] switching to OFFLINE mode (previous game) ...") - # оставим только полезные ключи - keep_keys = { - "game", - "pregame", - "pregame-full-stats", - "actual-standings", - "calendar", - } - # for key in list(latest_data.keys()): - # if key not in keep_keys: - # del latest_data[key] - # for key in latest_data: - # latest_data[key] = wipe_json_values(latest_data[key]) - stop_event_offline.clear() threads_offline = [ threading.Thread( @@ -1215,7 +1172,9 @@ def start_prestart_watcher(game_dt: datetime | None): f"[prestart] {now:%H:%M:%S}, игра в {game_dt:%H:%M}, включаем LIVE threads по правилу T-1:10" ) STATUS = "live_soon" - globals()["CLEAR_OUTPUT_FOR_VMIX"] = False # можно оставить пустоту до первых живых данных + globals()[ + "CLEAR_OUTPUT_FOR_VMIX" + ] = False # можно оставить пустоту до первых живых данных stop_offline_threads() # на всякий случай start_live_threads(SEASON, GAME_ID) did_live = True @@ -1431,6 +1390,7 @@ def wipe_json_values(obj): else: return "" + @app.get("/started_team1") async def started_team1(sort_by: str = None): data = await team("team1") @@ -1769,25 +1729,6 @@ def get_latest_game_safe(name: str): 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: @@ -2747,7 +2688,9 @@ async def live_status(): if not ls: # live-status ещё не прилетел - return maybe_clear_for_vmix([{"foulsA": 0, "foulsB": 0, "scoreA": 0, "scoreB": 0}]) + return maybe_clear_for_vmix( + [{"foulsA": 0, "foulsB": 0, "scoreA": 0, "scoreB": 0}] + ) raw = ls.get("data") @@ -2765,10 +2708,14 @@ async def live_status(): return maybe_clear_for_vmix([{"status": raw}]) # fallback - return maybe_clear_for_vmix([{"foulsA": 0, "foulsB": 0, "scoreA": 0, "scoreB": 0}]) + return maybe_clear_for_vmix( + [{"foulsA": 0, "foulsB": 0, "scoreA": 0, "scoreB": 0}] + ) else: # матч не идёт — как у тебя было - return maybe_clear_for_vmix([{"foulsA": 0, "foulsB": 0, "scoreA": 0, "scoreB": 0}]) + return maybe_clear_for_vmix( + [{"foulsA": 0, "foulsB": 0, "scoreA": 0, "scoreB": 0}] + ) @app.get("/info") @@ -2795,25 +2742,166 @@ async def info(): full_format = date_obj.strftime("%A, %#d %B %Y") short_format = date_obj.strftime("%A, %#d %b") - return maybe_clear_for_vmix([ - { - "team1": team1_name, - "team2": team2_name, - "team1_short": team1_name_short, - "team2_short": team2_name_short, - "logo1": team1_logo, - "logo2": team2_logo, - "arena": arena, - "short_arena": arena_short, - "region": region, - "league": league, - "league_full": league_full, - "season": season, - "stadia": stadia, - "date1": str(full_format), - "date2": str(short_format), - } - ]) + return maybe_clear_for_vmix( + [ + { + "team1": team1_name, + "team2": team2_name, + "team1_short": team1_name_short, + "team2_short": team2_name_short, + "logo1": team1_logo, + "logo2": team2_logo, + "arena": arena, + "short_arena": arena_short, + "region": region, + "league": league, + "league_full": league_full, + "season": season, + "stadia": stadia, + "date1": str(full_format), + "date2": str(short_format), + } + ] + ) + + +@app.get("/play_by_play") +async def play_by_play(): + data = latest_data["game"]["data"]["result"] + data_pbp = data["plays"] + + team1_name = data["team1"]["name"] + team2_name = data["team2"]["name"] + + json_live_status = latest_data["live-status"]["data"] + + team1_startnum = [ + i["startNum"] + for i in next( + (t for t in data["teams"] if t["teamNumber"] == 1), + None, + )["starts"] + if i["startRole"] == "Player" + ] + team2_startnum = [ + i["startNum"] + for i in next( + (t for t in data["teams"] if t["teamNumber"] == 2), + None, + )["starts"] + if i["startRole"] == "Player" + ] + + # если вообще нет плей-бай-плея — просто отдаём пустой список + if not data_pbp: + return maybe_clear_for_vmix([]) + + df_data_pbp = pd.DataFrame(data_pbp[::-1]) + last_event = data_pbp[-1] + + if "play" not in df_data_pbp: + return maybe_clear_for_vmix([]) + + if json_live_status["status"] != "Not Found": + json_quarter = json_live_status["result"]["period"] + json_second = json_live_status["result"]["second"] + else: + json_quarter = last_event["period"] + json_second = 0 + + if "3x3" in LEAGUE: + df_data_pbp["play"].replace({2: 1, 3: 2}, inplace=True) + + df_goals = df_data_pbp.loc[df_data_pbp["play"].isin([1, 2, 3])].copy() + if df_goals.empty: + return maybe_clear_for_vmix([]) + + df_goals.loc[df_goals["startNum"].isin(team1_startnum), "score1"] = df_goals["play"] + df_goals.loc[df_goals["startNum"].isin(team2_startnum), "score2"] = df_goals["play"] + + df_goals["score_sum1"] = df_goals["score1"].fillna(0).cumsum() + df_goals["score_sum2"] = df_goals["score2"].fillna(0).cumsum() + df_goals["new_sec"] = df_goals["sec"].astype(str).str.slice(0, -1).astype(int) + df_goals["time_now"] = (600 if json_quarter < 5 else 300) - json_second + df_goals["quar"] = json_quarter - df_goals["period"] + + # без numpy: diff_time через маски pandas + same_quarter = df_goals["quar"] == 0 + other_quarter = ~same_quarter + + df_goals.loc[same_quarter, "diff_time"] = ( + df_goals.loc[same_quarter, "time_now"] - df_goals.loc[same_quarter, "new_sec"] + ) + + df_goals.loc[other_quarter, "diff_time"] = ( + 600 * df_goals.loc[other_quarter, "quar"] + - df_goals.loc[other_quarter, "new_sec"] + + df_goals.loc[other_quarter, "time_now"] + ) + + df_goals["diff_time"] = df_goals["diff_time"].astype(int) + + df_goals["diff_time_str"] = df_goals["diff_time"].apply( + lambda x: f"{x // 60}:{str(x % 60).zfill(2)}" + ) + df_goals["team"] = df_goals.apply( + lambda row: team1_name if not pd.isna(row["score1"]) else team2_name, + axis=1, + ) + df_goals["text_rus"] = df_goals.apply( + lambda row: ( + f"рывок {int(row['score_sum1'])}-{int(row['score_sum2'])}" + if not pd.isna(row["score1"]) + else f"рывок {int(row['score_sum2'])}-{int(row['score_sum1'])}" + ), + axis=1, + ) + df_goals["text_time_rus"] = df_goals.apply( + lambda row: ( + f"рывок {int(row['score_sum1'])}-{int(row['score_sum2'])} за {row['diff_time_str']}" + if not pd.isna(row["score1"]) + else f"рывок {int(row['score_sum2'])}-{int(row['score_sum1'])} за {row['diff_time_str']}" + ), + axis=1, + ) + df_goals["text"] = df_goals.apply( + lambda row: ( + f"{team1_name} {int(row['score_sum1'])}-{int(row['score_sum2'])} run" + if not pd.isna(row["score1"]) + else f"{team2_name} {int(row['score_sum2'])}-{int(row['score_sum1'])} run" + ), + axis=1, + ) + df_goals["text_time"] = df_goals.apply( + lambda row: ( + f"{team1_name} {int(row['score_sum1'])}-{int(row['score_sum2'])} run in last {row['diff_time_str']}" + if not pd.isna(row["score1"]) + else f"{team2_name} {int(row['score_sum2'])}-{int(row['score_sum1'])} run in last {row['diff_time_str']}" + ), + axis=1, + ) + + new_order = ["text", "text_time"] + [ + col for col in df_goals.columns if col not in ["text", "text_time"] + ] + df_goals = df_goals[new_order] + + for _ in ["children", "start", "stop", "hl", "sort", "startNum", "zone", "x", "y"]: + if _ in df_goals.columns: + del df_goals[_] + + # 👉 здесь избавляемся от NaN: только для score1/score2 + df_goals["score1"] = df_goals["score1"].fillna("") + df_goals["score2"] = df_goals["score2"].fillna("") + + # если хочешь вообще никаких NaN во всём JSON — можно так: + # df_goals = df_goals.fillna("") + + payload = df_goals.to_dict(orient="records") + return maybe_clear_for_vmix(payload) + + + if __name__ == "__main__":