добавил новый endpoint /play-by-play
This commit is contained in:
280
get_data.py
280
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 <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__":
|
||||
|
||||
Reference in New Issue
Block a user