добавил новый endpoint /play-by-play
This commit is contained in:
246
get_data.py
246
get_data.py
@@ -17,9 +17,6 @@ from fastapi.responses import Response
|
|||||||
import logging
|
import logging
|
||||||
import logging.config
|
import logging.config
|
||||||
import platform
|
import platform
|
||||||
import socket
|
|
||||||
|
|
||||||
# передадим параметры через аргументы или глобальные переменные
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
@@ -29,7 +26,6 @@ parser.add_argument("--lang", default="en")
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
MYHOST = platform.node()
|
MYHOST = platform.node()
|
||||||
user_name = socket.gethostname()
|
|
||||||
|
|
||||||
if not os.path.exists("logs"):
|
if not os.path.exists("logs"):
|
||||||
os.makedirs("logs")
|
os.makedirs("logs")
|
||||||
@@ -68,7 +64,7 @@ log_config = {
|
|||||||
"formatters": {
|
"formatters": {
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"class": "telegram_handler.HtmlFormatter",
|
"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",
|
"use_emoji": "True",
|
||||||
},
|
},
|
||||||
"simple": {
|
"simple": {
|
||||||
@@ -136,6 +132,7 @@ URLS = {
|
|||||||
"play-by-play": "{host}/api/abc/games/play-by-play?id={game_id}",
|
"play-by-play": "{host}/api/abc/games/play-by-play?id={game_id}",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def maybe_clear_for_vmix(payload):
|
def maybe_clear_for_vmix(payload):
|
||||||
"""
|
"""
|
||||||
Если включён режим очистки — возвращаем payload,
|
Если включён режим очистки — возвращаем payload,
|
||||||
@@ -158,20 +155,6 @@ def start_offline_threads(season, game_id):
|
|||||||
stop_live_threads()
|
stop_live_threads()
|
||||||
stop_offline_threads()
|
stop_offline_threads()
|
||||||
logger.info("[threads] switching to OFFLINE mode ...")
|
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()
|
stop_event_offline.clear()
|
||||||
|
|
||||||
@@ -619,10 +602,14 @@ def results_consumer():
|
|||||||
and GAME_START_DT.date() == datetime.now().date()
|
and GAME_START_DT.date() == datetime.now().date()
|
||||||
):
|
):
|
||||||
globals()["STATUS"] = "finished_wait"
|
globals()["STATUS"] = "finished_wait"
|
||||||
globals()["CLEAR_OUTPUT_FOR_VMIX"] = True # 👈 включаем режим "пустых" данных
|
globals()[
|
||||||
|
"CLEAR_OUTPUT_FOR_VMIX"
|
||||||
|
] = True # 👈 включаем режим "пустых" данных
|
||||||
else:
|
else:
|
||||||
globals()["STATUS"] = "finished_wait"
|
globals()["STATUS"] = "finished_wait"
|
||||||
globals()["CLEAR_OUTPUT_FOR_VMIX"] = True # 👈 включаем режим "пустых" данных
|
globals()[
|
||||||
|
"CLEAR_OUTPUT_FOR_VMIX"
|
||||||
|
] = True # 👈 включаем режим "пустых" данных
|
||||||
|
|
||||||
human_time = datetime.fromtimestamp(switch_at).strftime(
|
human_time = datetime.fromtimestamp(switch_at).strftime(
|
||||||
"%H:%M:%S"
|
"%H:%M:%S"
|
||||||
@@ -642,7 +629,9 @@ def results_consumer():
|
|||||||
"online" in raw_ls_status_low or "live" in raw_ls_status_low
|
"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:
|
if globals().get("OFFLINE_SWITCH_AT") is not None:
|
||||||
logger.info(
|
logger.info(
|
||||||
"[status] match back to LIVE → cancel scheduled OFFLINE"
|
"[status] match back to LIVE → cancel scheduled OFFLINE"
|
||||||
@@ -699,25 +688,7 @@ def results_consumer():
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
"results_consumer: LIVE & partial game → keep previous one"
|
"results_consumer: LIVE & partial game → keep previous one"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2) Когда матч УЖЕ online (STATUS == 'live'):
|
|
||||||
# - поток 'game' в live-режиме погаснет сам (stop_when_live=True),
|
|
||||||
# но если вдруг что-то долетит, кладём только полный JSON.
|
|
||||||
continue
|
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:
|
else:
|
||||||
latest_data[source] = {
|
latest_data[source] = {
|
||||||
"ts": msg["ts"],
|
"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) ...")
|
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()
|
stop_event_offline.clear()
|
||||||
threads_offline = [
|
threads_offline = [
|
||||||
threading.Thread(
|
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"
|
f"[prestart] {now:%H:%M:%S}, игра в {game_dt:%H:%M}, включаем LIVE threads по правилу T-1:10"
|
||||||
)
|
)
|
||||||
STATUS = "live_soon"
|
STATUS = "live_soon"
|
||||||
globals()["CLEAR_OUTPUT_FOR_VMIX"] = False # можно оставить пустоту до первых живых данных
|
globals()[
|
||||||
|
"CLEAR_OUTPUT_FOR_VMIX"
|
||||||
|
] = False # можно оставить пустоту до первых живых данных
|
||||||
stop_offline_threads() # на всякий случай
|
stop_offline_threads() # на всякий случай
|
||||||
start_live_threads(SEASON, GAME_ID)
|
start_live_threads(SEASON, GAME_ID)
|
||||||
did_live = True
|
did_live = True
|
||||||
@@ -1431,6 +1390,7 @@ def wipe_json_values(obj):
|
|||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@app.get("/started_team1")
|
@app.get("/started_team1")
|
||||||
async def started_team1(sort_by: str = None):
|
async def started_team1(sort_by: str = None):
|
||||||
data = await team("team1")
|
data = await team("team1")
|
||||||
@@ -1769,25 +1729,6 @@ def get_latest_game_safe(name: str):
|
|||||||
return game
|
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]:
|
def _pick_last_avg_and_sum(stats_list: list) -> tuple[dict, dict]:
|
||||||
"""Возвращает (season_sum, season_avg) из seasonStats. Безопасно при пустых данных."""
|
"""Возвращает (season_sum, season_avg) из seasonStats. Безопасно при пустых данных."""
|
||||||
if not isinstance(stats_list, list) or len(stats_list) == 0:
|
if not isinstance(stats_list, list) or len(stats_list) == 0:
|
||||||
@@ -2747,7 +2688,9 @@ async def live_status():
|
|||||||
|
|
||||||
if not ls:
|
if not ls:
|
||||||
# live-status ещё не прилетел
|
# 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")
|
raw = ls.get("data")
|
||||||
|
|
||||||
@@ -2765,10 +2708,14 @@ async def live_status():
|
|||||||
return maybe_clear_for_vmix([{"status": raw}])
|
return maybe_clear_for_vmix([{"status": raw}])
|
||||||
|
|
||||||
# fallback
|
# 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:
|
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")
|
@app.get("/info")
|
||||||
@@ -2795,7 +2742,8 @@ async def info():
|
|||||||
full_format = date_obj.strftime("%A, %#d %B %Y")
|
full_format = date_obj.strftime("%A, %#d %B %Y")
|
||||||
short_format = date_obj.strftime("%A, %#d %b")
|
short_format = date_obj.strftime("%A, %#d %b")
|
||||||
|
|
||||||
return maybe_clear_for_vmix([
|
return maybe_clear_for_vmix(
|
||||||
|
[
|
||||||
{
|
{
|
||||||
"team1": team1_name,
|
"team1": team1_name,
|
||||||
"team2": team2_name,
|
"team2": team2_name,
|
||||||
@@ -2813,7 +2761,147 @@ async def info():
|
|||||||
"date1": str(full_format),
|
"date1": str(full_format),
|
||||||
"date2": str(short_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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user