Compare commits

...

2 Commits

View File

@@ -62,6 +62,7 @@ ALLOWED_LEAGUES = {
"whl", # Высшая лига. Женщины "whl", # Высшая лига. Женщины
"wcup", # Кубок России. Женщины "wcup", # Кубок России. Женщины
"dubl-b", # Дюбл до 19 лет "dubl-b", # Дюбл до 19 лет
# "pr-mezhreg-w13", # Межрегиональные соревнования до 14 лет
# добавляй свои… # добавляй свои…
} }
@@ -452,7 +453,7 @@ def format_time(seconds: float | int) -> str:
return "0:00" return "0:00"
def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | None = None): def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | None = None) -> None:
""" """
Единая точка: принимает уже нормализованный merged, делает нужные вычисления (если надо) Единая точка: принимает уже нормализованный merged, делает нужные вычисления (если надо)
и сохраняет в JSON. и сохраняет в JSON.
@@ -1028,14 +1029,417 @@ def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | No
x.get("startRole", 99), 99 x.get("startRole", 99), 99
), # 99 — по умолчанию ), # 99 — по умолчанию
) )
out_path = Path(out_dir) / f"{who}.json" out_path = Path(out_dir) / f"{who}.json"
atomic_write_json(out_path, sorted_team) atomic_write_json(out_path, sorted_team)
logging.getLogger("game_watcher").info("Сохранил payload: %s", out_path) logging.info("Сохранил payload: {out_path}")
top_sorted_team = sorted(
filter(lambda x: x["startRole"] in ["Player", ""], sorted_team),
key=lambda x: (
x["pts"],
x["dreb"] + x["oreb"],
x["ast"],
x["stl"],
x["blk"],
x["time"],
),
reverse=True,
)
for item in top_sorted_team:
item["pts"] = "" if item["num"] == "" else item["pts"]
item["foul"] = "" if item["num"] == "" else item["foul"]
out_path = Path(out_dir) / f"top{who.replace('t','T')}.json"
atomic_write_json(out_path, top_sorted_team)
logging.info("Сохранил payload: {out_path}")
started_team = sorted(
filter(
lambda x: x["startRole"] == "Player" and x["isOnCourt"] is True,
sorted_team,
),
key=lambda x: int(x["num"]),
reverse=False,
)
out_path = Path(out_dir) / f"started_{who}.json"
atomic_write_json(out_path, started_team)
logging.info("Сохранил payload: {out_path}")
def time_outs_func(data_pbp: list[dict]) -> tuple[str, int, str, int]:
"""
Вычисляет количество оставшихся таймаутов для обеих команд
и формирует строку состояния.
Args:
data_pbp: Список игровых событий (play-by-play).
Returns:
Кортеж: (строка команды 1, остаток, строка команды 2, остаток)
"""
timeout1 = []
timeout2 = []
for event in data_pbp:
if event.get("play") == 23:
if event.get("startNum") == 1:
timeout1.append(event)
elif event.get("startNum") == 2:
timeout2.append(event)
def timeout_status(timeout_list: list[dict], last_event: dict) -> tuple[str, int]:
period = last_event.get("period", 0)
sec = last_event.get("sec", 0)
if period < 3:
timeout_max = 2
count = sum(1 for t in timeout_list if t.get("period", 0) <= period)
quarter = "1st half"
elif period < 5:
count = sum(1 for t in timeout_list if 3 <= t.get("period", 0) <= period)
quarter = "2nd half"
if period == 4 and sec >= 4800 and count in (0, 1):
timeout_max = 2
else:
timeout_max = 3
else:
timeout_max = 1
count = sum(1 for t in timeout_list if t.get("period", 0) == period)
quarter = f"OverTime {period - 4}"
left = max(0, timeout_max - count)
word = "Time-outs" if left != 1 else "Time-out"
text = f"{left if left != 0 else 'No'} {word} left in {quarter}"
return text, left
if not data_pbp:
return "", 0, "", 0
last_event = data_pbp[-1]
t1_str, t1_left = timeout_status(timeout1, last_event)
t2_str, t2_left = timeout_status(timeout2, last_event)
return t1_str, t1_left, t2_str, t2_left
def add_data_for_teams(new_data: list[dict]) -> tuple[float, list, float]:
"""
Возвращает усреднённые статистики команды:
- средний возраст
- очки со старта и скамейки + их доли
- средний рост
Args:
new_data (list[dict]): Список игроков с полями "startRole", "stats", "age", "height"
Returns:
tuple: (avg_age: float, points: list, avg_height: float)
"""
players = [item for item in new_data if item.get("startRole") == "Player"]
points_start = 0
points_bench = 0
total_age = 0
total_height = 0
player_count = len(players)
for player in players:
stats = player.get("stats")
if stats:
is_start = stats.get("isStart")
# Очки
if is_start is True:
points_start += stats.get("points", 0)
elif is_start is False:
points_bench += stats.get("points", 0)
# Возраст и рост
total_age += player.get("age", 0) or 0
total_height += player.get("height", 0) or 0
total_points = points_start + points_bench
points_start_pro = (
f"{round(points_start * 100 / total_points)}%" if total_points else "0%"
)
points_bench_pro = (
f"{round(points_bench * 100 / total_points)}%" if total_points else "0%"
)
avg_age = round(total_age / player_count, 1) if player_count else 0
avg_height = round(total_height / player_count, 1) if player_count else 0
points = [points_start, points_start_pro, points_bench, points_bench_pro]
return avg_age, points, avg_height
def add_new_team_stat(
data: dict,
avg_age: float,
points: float,
avg_height: float,
timeout_str: str,
timeout_left: str,
) -> dict:
"""
Добавляет в словарь команды форматированную статистику.
Все значения приводятся к строкам.
Args:
data: Исходная статистика команды.
avg_age: Средний возраст команды (строка).
points: Кортеж из 4 строк: ptsStart, ptsStart_pro, ptsBench, ptsBench_pro.
avg_height: Средний рост (в см).
timeout_str: Строка отображения таймаутов.
timeout_left: Остаток таймаутов.
Returns:
Обновлённый словарь `data` с новыми ключами.
"""
def safe_int(v): # Локальная защита от ValueError/TypeError
try:
return int(v)
except (ValueError, TypeError):
return 0
def format_percent(goal, shot):
goal, shot = safe_int(goal), safe_int(shot)
return f"{round(goal * 100 / shot)}%" if shot else "0%"
goal1, shot1 = safe_int(data.get("goal1")), safe_int(data.get("shot1"))
goal2, shot2 = safe_int(data.get("goal2")), safe_int(data.get("shot2"))
goal3, shot3 = safe_int(data.get("goal3")), safe_int(data.get("shot3"))
def_reb = safe_int(data.get("defReb"))
off_reb = safe_int(data.get("offReb"))
data.update(
{
"pt-1": f"{goal1}/{shot1}",
"pt-2": f"{goal2}/{shot2}",
"pt-3": f"{goal3}/{shot3}",
"fg": f"{goal2 + goal3}/{shot2 + shot3}",
"pt-1_pro": format_percent(goal1, shot1),
"pt-2_pro": format_percent(goal2, shot2),
"pt-3_pro": format_percent(goal3, shot3),
"fg_pro": format_percent(goal2 + goal3, shot2 + shot3),
"Reb": str(def_reb + off_reb),
"avgAge": str(avg_age),
"ptsStart": str(points[0]),
"ptsStart_pro": str(points[1]),
"ptsBench": str(points[2]),
"ptsBench_pro": str(points[3]),
"avgHeight": f"{avg_height} cm",
"timeout_left": str(timeout_left),
"timeout_str": str(timeout_str),
}
)
# Приводим все значения к строкам, если нужно строго для сериализации
for k in data:
data[k] = str(data[k])
return data
stat_name_list = [
("points", "Очки", "points"),
("pt-1", "Штрафные", "free throws"),
("pt-1_pro", "штрафные, процент", "free throws pro"),
("pt-2", "2-очковые", "2-points"),
("pt-2_pro", "2-очковые, процент", "2-points pro"),
("pt-3", "3-очковые", "3-points"),
("pt-3_pro", "3-очковые, процент", "3-points pro"),
("fg", "очки с игры", "field goals"),
("fg_pro", "Очки с игры, процент", "field goals pro"),
("assist", "Передачи", "assists"),
("pass", "", ""),
("defReb", "подборы в защите", ""),
("offReb", "подборы в нападении", ""),
("Reb", "Подборы", "rebounds"),
("steal", "Перехваты", "steals"),
("block", "Блокшоты", "blocks"),
("blocked", "", ""),
("turnover", "Потери", "turnovers"),
("foul", "Фолы", "fouls"),
("foulsOn", "", ""),
("foulT", "", ""),
("foulD", "", ""),
("foulC", "", ""),
("foulB", "", ""),
("second", "секунды", "seconds"),
("dunk", "данки", "dunks"),
("fastBreak", "", "fast breaks"),
("plusMinus", "+/-", "+/-"),
("avgAge", "", "avg Age"),
("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"),
]
def Team_Both_Stat(merged: dict, *, out_dir: str = "static") -> None:
"""
Обновляет файл team_stats.json, содержащий сравнение двух команд.
Аргументы:
stop_event (threading.Event): Событие для остановки цикла.
"""
logger.info("START making json for team statistics")
try:
teams = merged["result"]["teams"]
plays = merged["result"].get("plays", [])
# Разделение команд
team_1 = next((t for t in teams if t["teamNumber"] == 1), None)
team_2 = next((t for t in teams if t["teamNumber"] == 2), None)
if not team_1 or not team_2:
logger.warning("Не найдены обе команды в данных")
# time.sleep()
# Таймауты
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", [])
)
if not team_1.get("total") or not team_2.get("total"):
logger.debug(
"Нет total у команд — пропускаю перезапись team_stats.json"
)
# Форматирование общей статистики (как и было)
total_1 = add_new_team_stat(
team_1["total"],
avg_age_1,
points_1,
avg_height_1,
timeout_str1,
timeout_left1,
)
total_2 = add_new_team_stat(
team_2["total"],
avg_age_2,
points_2,
avg_height_2,
timeout_str2,
timeout_left2,
)
# Финальный JSON
result_json = []
for key in total_1:
val1 = (
int(total_1[key])
if isinstance(total_1[key], float)
else total_1[key]
)
val2 = (
int(total_2[key])
if isinstance(total_2[key], float)
else total_2[key]
)
stat_rus, stat_eng = "", ""
for s in stat_name_list:
if s[0] == key:
stat_rus, stat_eng = s[1], s[2]
break
result_json.append(
{
"name": key,
"nameGFX_rus": stat_rus,
"nameGFX_eng": stat_eng,
"val1": val1,
"val2": val2,
}
)
out_path = Path(out_dir) / "team_stats.json"
atomic_write_json(out_path, result_json)
logging.info("Сохранил payload: {out_path}")
logger.debug("Успешно записаны данные в team_stats.json")
except Exception as e:
logger.error(
f"Ошибка при обработке командной статистики: {e}", exc_info=True
)
def Referee(merged: dict, *, out_dir: str = "static") -> None:
"""
Поток, создающий JSON-файл с информацией о судьях матча.
"""
logger.info("START making json for referee")
desired_order = [
"Crew chief",
"Referee 1",
"Referee 2",
"Commissioner",
"Ст.судья",
"Судья 1",
"Судья 2",
"Комиссар",
]
try:
# Найти судей (teamNumber == 0)
team_ref = next(
(t for t in merged["result"]["teams"] if t["teamNumber"] == 0), None
)
if not team_ref:
logger.warning("Не найдена судейская бригада в данных.")
referees_raw = team_ref.get("starts", [])
# print(referees_raw)
referees = []
for r in referees_raw:
flag_code = (
r.get("countryId", "").lower() if r.get("countryName") else ""
)
referees.append(
{
"displayNumber": r.get("displayNumber", ""),
"positionName": r.get("positionName", ""),
"lastNameGFX": f"{r.get('firstName', '')} {r.get('lastName', '')}".strip(),
"secondName": r.get("secondName", ""),
"birthday": r.get("birthday", ""),
"age": r.get("age", 0),
"flag": f"https://flagicons.lipis.dev/flags/4x3/{flag_code}.svg",
}
)
# Сортировка по позиции
referees = sorted(
referees,
key=lambda x: (
desired_order.index(x["positionName"])
if x["positionName"] in desired_order
else len(desired_order)
),
)
out_path = Path(out_dir) / "referee.json"
atomic_write_json(out_path, referees)
logging.info("Сохранил payload: {out_path}")
except Exception as e:
logger.error(f"Ошибка в Referee потоке: {e}", exc_info=True)
# ========================== # ==========================
# ---- ДОМЕННАЯ ЛОГИКА # ---- ДОМЕННАЯ ЛОГИКА
@@ -1134,6 +1538,8 @@ class PostProcessor:
try: try:
Json_Team_Generation(merged, out_dir="static", who="team1") Json_Team_Generation(merged, out_dir="static", who="team1")
Json_Team_Generation(merged, out_dir="static", who="team2") Json_Team_Generation(merged, out_dir="static", who="team2")
Team_Both_Stat(merged, out_dir="static")
Referee(merged, out_dir="static")
except Exception as e: except Exception as e:
logging.exception(f"Postproc failed: {e}") logging.exception(f"Postproc failed: {e}")
@@ -1303,7 +1709,6 @@ def daily_rollover_loop(
while not stop_event.is_set(): while not stop_event.is_set():
now = datetime.now(APP_TZ) now = datetime.now(APP_TZ)
wakeup_at = next_midnight_local(now) wakeup_at = next_midnight_local(now)
print(type(wakeup_at))
seconds = (wakeup_at - now).total_seconds() seconds = (wakeup_at - now).total_seconds()
logger.info( logger.info(
# f"Ежедневка: проснусь {datetime.fromisoformat(wakeup_at.isoformat())} (через {int(seconds)} сек)." # f"Ежедневка: проснусь {datetime.fromisoformat(wakeup_at.isoformat())} (через {int(seconds)} сек)."
@@ -1437,6 +1842,8 @@ def main():
) )
Json_Team_Generation(merged, out_dir="static", who="team1") Json_Team_Generation(merged, out_dir="static", who="team1")
Json_Team_Generation(merged, out_dir="static", who="team2") Json_Team_Generation(merged, out_dir="static", who="team2")
Team_Both_Stat(merged, out_dir="static")
Referee(merged, out_dir="static")
# print(merged) # print(merged)
logger.info( logger.info(
f"Сегодня у {team} нет игры.\nПоследняя сыгранная: gameID={game_id}.\nМониторинг не запускаю." f"Сегодня у {team} нет игры.\nПоследняя сыгранная: gameID={game_id}.\nМониторинг не запускаю."