добавил новый endpoint /play-by-play

This commit is contained in:
2025-11-13 14:42:24 +03:00
parent 4ff05fa50c
commit ced3220d62

View File

@@ -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 <b>[{MYHOST.upper()}] [{user_name}]</b>\n%(message)s",
"format": f"%(levelname)s <b>[{MYHOST.upper()}]</b>\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__":