обновил статусы матчей, чтобы правильнее ожидалось, когда матч сегодня
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@
|
|||||||
/static/*
|
/static/*
|
||||||
get_data_new copy 2.py
|
get_data_new copy 2.py
|
||||||
get_data_new copy.py
|
get_data_new copy.py
|
||||||
temp.json
|
temp.json
|
||||||
|
get_data_new copy 3.py
|
||||||
282
get_data_new.py
282
get_data_new.py
@@ -450,11 +450,29 @@ def is_game_live(game_obj: dict) -> bool:
|
|||||||
|
|
||||||
if status in ("resultconfirmed", "finished", "result"):
|
if status in ("resultconfirmed", "finished", "result"):
|
||||||
return False
|
return False
|
||||||
if status in ("scheduled", "notstarted", "draft"):
|
if status in ("notstarted", "draft"):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def classify_game_state_from_status(status_raw: str) -> str:
|
||||||
|
"""
|
||||||
|
Делит статус игры на три фазы:
|
||||||
|
- "finished" -> матч точно завершён
|
||||||
|
- "upcoming" -> матч ещё не начался, но он сегодня
|
||||||
|
- "live" -> матч идёт
|
||||||
|
|
||||||
|
Используется в get_data_API(), чтобы решить, что делать дальше.
|
||||||
|
"""
|
||||||
|
status = (status_raw or "").lower()
|
||||||
|
if status in ("resultconfirmed", "finished", "result"):
|
||||||
|
return "finished"
|
||||||
|
if status in ("scheduled", "notstarted", "draft"):
|
||||||
|
return "upcoming"
|
||||||
|
# всё остальное считаем лайвом
|
||||||
|
return "live"
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# 6. Лайв-петля: опрос API и поток рендера
|
# 6. Лайв-петля: опрос API и поток рендера
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -738,7 +756,6 @@ def Referee(merged: dict, *, out_dir: str = "static") -> None:
|
|||||||
logger.warning("Не найдена судейская бригада в данных.")
|
logger.warning("Не найдена судейская бригада в данных.")
|
||||||
|
|
||||||
referees_raw = team_ref.get("starts", [])
|
referees_raw = team_ref.get("starts", [])
|
||||||
# print(referees_raw)
|
|
||||||
referees = []
|
referees = []
|
||||||
|
|
||||||
for r in referees_raw:
|
for r in referees_raw:
|
||||||
@@ -777,7 +794,7 @@ def Play_By_Play(data: dict) -> None:
|
|||||||
Поток, обновляющий JSON-файл с последовательностью бросков в матче.
|
Поток, обновляющий JSON-файл с последовательностью бросков в матче.
|
||||||
"""
|
"""
|
||||||
logger.info("START making json for play-by-play")
|
logger.info("START making json for play-by-play")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
game_data = data["result"] if "result" in data else data
|
game_data = data["result"] if "result" in data else data
|
||||||
|
|
||||||
@@ -811,10 +828,13 @@ def Play_By_Play(data: dict) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Получение текущего времени игры
|
# Получение текущего времени игры
|
||||||
json_live_status = data["result"]["live_status"] if "result" in data and "live_status" in data["result"] else None
|
json_live_status = (
|
||||||
|
data["result"]["live_status"]
|
||||||
|
if "result" in data and "live_status" in data["result"]
|
||||||
|
else None
|
||||||
|
)
|
||||||
last_event = plays[-1]
|
last_event = plays[-1]
|
||||||
|
|
||||||
# if not json_live_status or json_live_status.get("message") == "Not Found":
|
|
||||||
if json_live_status is None:
|
if json_live_status is None:
|
||||||
period = last_event.get("period", 1)
|
period = last_event.get("period", 1)
|
||||||
second = 0
|
second = 0
|
||||||
@@ -825,10 +845,6 @@ def Play_By_Play(data: dict) -> None:
|
|||||||
# Создание DataFrame из событий
|
# Создание DataFrame из событий
|
||||||
df = pd.DataFrame(plays[::-1])
|
df = pd.DataFrame(plays[::-1])
|
||||||
|
|
||||||
# Преобразование для лиги 3x3
|
|
||||||
# if "3x3" in LEAGUE:
|
|
||||||
# df["play"].replace({2: 1, 3: 2}, inplace=True)
|
|
||||||
|
|
||||||
df_goals = df[df["play"].isin([1, 2, 3])].copy()
|
df_goals = df[df["play"].isin([1, 2, 3])].copy()
|
||||||
if df_goals.empty:
|
if df_goals.empty:
|
||||||
logger.debug("нет данных о голах в play-by-play")
|
logger.debug("нет данных о голах в play-by-play")
|
||||||
@@ -846,8 +862,7 @@ def Play_By_Play(data: dict) -> None:
|
|||||||
df_goals["score_sum2"] = df_goals["score2"].fillna(0).cumsum()
|
df_goals["score_sum2"] = df_goals["score2"].fillna(0).cumsum()
|
||||||
|
|
||||||
df_goals["new_sec"] = (
|
df_goals["new_sec"] = (
|
||||||
pd.to_numeric(df_goals["sec"], errors="coerce").fillna(0).astype(int)
|
pd.to_numeric(df_goals["sec"], errors="coerce").fillna(0).astype(int) // 10
|
||||||
// 10
|
|
||||||
)
|
)
|
||||||
df_goals["time_now"] = (600 if period < 5 else 300) - second
|
df_goals["time_now"] = (600 if period < 5 else 300) - second
|
||||||
df_goals["quar"] = period - df_goals["period"]
|
df_goals["quar"] = period - df_goals["period"]
|
||||||
@@ -859,9 +874,7 @@ def Play_By_Play(data: dict) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
df_goals["diff_time_str"] = df_goals["diff_time"].apply(
|
df_goals["diff_time_str"] = df_goals["diff_time"].apply(
|
||||||
lambda x: (
|
lambda x: (f"{x // 60}:{str(x % 60).zfill(2)}" if isinstance(x, int) else x)
|
||||||
f"{x // 60}:{str(x % 60).zfill(2)}" if isinstance(x, int) else x
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Текстовые поля
|
# Текстовые поля
|
||||||
@@ -873,7 +886,7 @@ def Play_By_Play(data: dict) -> None:
|
|||||||
else team2_name
|
else team2_name
|
||||||
)
|
)
|
||||||
|
|
||||||
# ✅ Правильный порядок счёта в зависимости от команды
|
# правильный порядок счёта в зависимости от команды
|
||||||
if team == team1_name:
|
if team == team1_name:
|
||||||
score = f"{s1}-{s2}"
|
score = f"{s1}-{s2}"
|
||||||
else:
|
else:
|
||||||
@@ -921,9 +934,7 @@ def Play_By_Play(data: dict) -> None:
|
|||||||
|
|
||||||
# Порядок колонок
|
# Порядок колонок
|
||||||
main_cols = ["text", "text_time"]
|
main_cols = ["text", "text_time"]
|
||||||
all_cols = main_cols + [
|
all_cols = main_cols + [col for col in df_goals.columns if col not in main_cols]
|
||||||
col for col in df_goals.columns if col not in main_cols
|
|
||||||
]
|
|
||||||
df_goals = df_goals[all_cols]
|
df_goals = df_goals[all_cols]
|
||||||
|
|
||||||
# Сохранение JSON
|
# Сохранение JSON
|
||||||
@@ -937,7 +948,6 @@ def Play_By_Play(data: dict) -> None:
|
|||||||
logger.error(f"Ошибка в Play_By_Play: {e}", exc_info=True)
|
logger.error(f"Ошибка в Play_By_Play: {e}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def render_once_after_game(
|
def render_once_after_game(
|
||||||
session: requests.Session,
|
session: requests.Session,
|
||||||
league: str,
|
league: str,
|
||||||
@@ -962,7 +972,7 @@ def render_once_after_game(
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.info(f"[RENDER_ONCE] Fetching final game snapshot for game_id={game_id}")
|
logger.info(f"[RENDER_ONCE] Fetching final game snapshot for game_id={game_id}")
|
||||||
# === 1. один запрос к API (ручка "game") ===
|
# один запрос к API (ручка "game")
|
||||||
state = fetch_api_data(
|
state = fetch_api_data(
|
||||||
session,
|
session,
|
||||||
"game",
|
"game",
|
||||||
@@ -971,7 +981,6 @@ def render_once_after_game(
|
|||||||
lang=lang,
|
lang=lang,
|
||||||
)
|
)
|
||||||
|
|
||||||
# === 3. прогнать вычисления как в render_loop ===
|
|
||||||
Team_Both_Stat(state)
|
Team_Both_Stat(state)
|
||||||
Json_Team_Generation(state, who="team1")
|
Json_Team_Generation(state, who="team1")
|
||||||
Json_Team_Generation(state, who="team2")
|
Json_Team_Generation(state, who="team2")
|
||||||
@@ -979,7 +988,6 @@ def render_once_after_game(
|
|||||||
Referee(state)
|
Referee(state)
|
||||||
Play_By_Play(state)
|
Play_By_Play(state)
|
||||||
|
|
||||||
# === 4. live_status и общий state ===
|
|
||||||
atomic_write_json(state["result"], out_name)
|
atomic_write_json(state["result"], out_name)
|
||||||
|
|
||||||
logger.info("[RENDER_ONCE] финальные json сохранены успешно")
|
logger.info("[RENDER_ONCE] финальные json сохранены успешно")
|
||||||
@@ -1054,7 +1062,6 @@ def Json_Team_Generation(
|
|||||||
|
|
||||||
for item in starts:
|
for item in starts:
|
||||||
stats = item.get("stats") or {}
|
stats = item.get("stats") or {}
|
||||||
# маппинг одной строки игрока
|
|
||||||
row = {
|
row = {
|
||||||
"id": item.get("personId") or "",
|
"id": item.get("personId") or "",
|
||||||
"num": item.get("displayNumber"),
|
"num": item.get("displayNumber"),
|
||||||
@@ -1290,7 +1297,6 @@ def time_outs_func(data_pbp: List[dict]) -> Tuple[str, int, str, int]:
|
|||||||
elif period < 5:
|
elif period < 5:
|
||||||
count = sum(1 for t in timeout_list if 3 <= t.get("period", 0) <= period)
|
count = sum(1 for t in timeout_list if 3 <= t.get("period", 0) <= period)
|
||||||
quarter = "2nd half"
|
quarter = "2nd half"
|
||||||
# в концовке 4-й четверти лимит может ужиматься
|
|
||||||
if period == 4 and sec >= 4800 and count in (0, 1):
|
if period == 4 and sec >= 4800 and count in (0, 1):
|
||||||
timeout_max = 2
|
timeout_max = 2
|
||||||
else:
|
else:
|
||||||
@@ -1418,14 +1424,12 @@ def add_new_team_stat(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# всё -> строки (UI не должен думать о типах)
|
|
||||||
for k in data:
|
for k in data:
|
||||||
data[k] = str(data[k])
|
data[k] = str(data[k])
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
# Статическая таблица "как назвать метрику"
|
|
||||||
stat_name_list = [
|
stat_name_list = [
|
||||||
("points", "Очки", "points"),
|
("points", "Очки", "points"),
|
||||||
("pt-1", "Штрафные", "free throws"),
|
("pt-1", "Штрафные", "free throws"),
|
||||||
@@ -1469,13 +1473,6 @@ stat_name_list = [
|
|||||||
def Team_Both_Stat(merged: dict) -> None:
|
def Team_Both_Stat(merged: dict) -> None:
|
||||||
"""
|
"""
|
||||||
Формирует сводку по двум командам и пишет её в static/team_stats.json.
|
Формирует сводку по двум командам и пишет её в static/team_stats.json.
|
||||||
|
|
||||||
Делает:
|
|
||||||
- считает таймауты для обеих команд,
|
|
||||||
- считает средний возраст / рост,
|
|
||||||
- считает очки старт / скамейка,
|
|
||||||
- добавляет проценты попаданий, подборы и т.д.,
|
|
||||||
- мапит имена метрик на удобные подписи.
|
|
||||||
"""
|
"""
|
||||||
logger.info("START making json for team statistics")
|
logger.info("START making json for team statistics")
|
||||||
|
|
||||||
@@ -1483,7 +1480,6 @@ def Team_Both_Stat(merged: dict) -> None:
|
|||||||
teams = merged["result"]["teams"]
|
teams = merged["result"]["teams"]
|
||||||
plays = merged["result"].get("plays", [])
|
plays = merged["result"].get("plays", [])
|
||||||
|
|
||||||
# Разделяем команды по teamNumber
|
|
||||||
team_1 = next((t for t in teams if t["teamNumber"] == 1), None)
|
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)
|
team_2 = next((t for t in teams if t["teamNumber"] == 2), None)
|
||||||
|
|
||||||
@@ -1491,17 +1487,14 @@ def Team_Both_Stat(merged: dict) -> None:
|
|||||||
logger.warning("Не найдены обе команды в данных")
|
logger.warning("Не найдены обе команды в данных")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Таймауты
|
|
||||||
timeout_str1, timeout_left1, timeout_str2, timeout_left2 = time_outs_func(plays)
|
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_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", []))
|
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"):
|
if not team_1.get("total") or not team_2.get("total"):
|
||||||
logger.debug("Нет total у команд — пропускаю перезапись team_stats.json")
|
logger.debug("Нет total у команд — пропускаю перезапись team_stats.json")
|
||||||
|
|
||||||
# Добавляем в total агрегаты
|
|
||||||
total_1 = add_new_team_stat(
|
total_1 = add_new_team_stat(
|
||||||
team_1["total"],
|
team_1["total"],
|
||||||
avg_age_1,
|
avg_age_1,
|
||||||
@@ -1519,7 +1512,6 @@ def Team_Both_Stat(merged: dict) -> None:
|
|||||||
timeout_left2,
|
timeout_left2,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Готовим список пар "метрика -> команда1 vs команда2"
|
|
||||||
result_json = []
|
result_json = []
|
||||||
for key in total_1:
|
for key in total_1:
|
||||||
val1 = total_1[key]
|
val1 = total_1[key]
|
||||||
@@ -1552,10 +1544,6 @@ def Team_Both_Stat(merged: dict) -> None:
|
|||||||
def Scores_Quarter(merged: dict) -> None:
|
def Scores_Quarter(merged: dict) -> None:
|
||||||
"""
|
"""
|
||||||
Пишет счёт по четвертям и овертаймам в static/scores.json.
|
Пишет счёт по четвертям и овертаймам в static/scores.json.
|
||||||
|
|
||||||
Логика:
|
|
||||||
- если есть game.result.game.fullScore -> парсим "XX:YY,AA:BB,..."
|
|
||||||
- иначе используем scoreByPeriods из box-score
|
|
||||||
"""
|
"""
|
||||||
logger.info("START making json for scores quarter")
|
logger.info("START making json for scores quarter")
|
||||||
|
|
||||||
@@ -1566,7 +1554,6 @@ def Scores_Quarter(merged: dict) -> None:
|
|||||||
full_score_str = merged.get("result", {}).get("game", {}).get("fullScore", "")
|
full_score_str = merged.get("result", {}).get("game", {}).get("fullScore", "")
|
||||||
|
|
||||||
if full_score_str:
|
if full_score_str:
|
||||||
# пример: "19:15,20:22,18:18,25:10"
|
|
||||||
full_score_list = full_score_str.split(",")
|
full_score_list = full_score_str.split(",")
|
||||||
for i, score_str in enumerate(full_score_list[: len(score_by_quarter)]):
|
for i, score_str in enumerate(full_score_list[: len(score_by_quarter)]):
|
||||||
parts = score_str.split(":")
|
parts = score_str.split(":")
|
||||||
@@ -1600,41 +1587,25 @@ def Standing_func(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Фоновый поток с турнирной таблицей (standings).
|
Фоновый поток с турнирной таблицей (standings).
|
||||||
|
|
||||||
Что делает:
|
|
||||||
- Периодически (не чаще, чем interval для "standings" в URLS) тянет /standings
|
|
||||||
для лиги+сезона.
|
|
||||||
- Для каждой подтаблицы (regular season, playoffs и т.д.) нормализует данные,
|
|
||||||
досчитывает полезные колонки (W/L, %) и сохраняет в
|
|
||||||
static/standings_<league>_<compName>.json
|
|
||||||
- Останавливается, когда поднят stop_event.
|
|
||||||
|
|
||||||
Почему отдельный поток?
|
|
||||||
- standings нам нужна даже во время лайва игры, но не каждую секунду.
|
|
||||||
- Она не должна блокировать рендер и не должна блокировать poll_game_live.
|
|
||||||
"""
|
"""
|
||||||
logger.info("[STANDINGS_THREAD] start standings loop")
|
logger.info("[STANDINGS_THREAD] start standings loop")
|
||||||
|
|
||||||
# когда мы последний раз успешно обновили standings
|
|
||||||
last_call_ts = 0
|
last_call_ts = 0
|
||||||
json_seasons = fetch_api_data(
|
json_seasons = fetch_api_data(
|
||||||
session, "seasons", host=HOST, league=league, lang=lang
|
session, "seasons", host=HOST, league=league, lang=lang
|
||||||
)
|
)
|
||||||
season = json_seasons[0]["season"]
|
season = json_seasons[0]["season"]
|
||||||
|
|
||||||
# как часто вообще можно дёргать standings
|
|
||||||
interval = get_interval_by_name("standings")
|
interval = get_interval_by_name("standings")
|
||||||
|
|
||||||
while not stop_event.is_set():
|
while not stop_event.is_set():
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
# достаточно рано? если нет — просто подожди немного
|
|
||||||
if now - last_call_ts < interval:
|
if now - last_call_ts < interval:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# тянем свежие данные standings тем же способом, что в get_data_API
|
|
||||||
data_standings = fetch_api_data(
|
data_standings = fetch_api_data(
|
||||||
session,
|
session,
|
||||||
"standings",
|
"standings",
|
||||||
@@ -1644,17 +1615,11 @@ def Standing_func(
|
|||||||
lang=lang,
|
lang=lang,
|
||||||
)
|
)
|
||||||
|
|
||||||
# fetch_api_data для standings вернёт либо:
|
|
||||||
# - dict с "items": [...], либо
|
|
||||||
# - сам массив items (если get_items нашёл список)
|
|
||||||
# Мы хотим привести к единому формату, как было в твоём коде.
|
|
||||||
if not data_standings:
|
if not data_standings:
|
||||||
logger.debug("[STANDINGS_THREAD] standings empty")
|
logger.debug("[STANDINGS_THREAD] standings empty")
|
||||||
# не обновляем last_call_ts, чтобы через секунду попытаться снова
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Если data_standings оказался списком, приведём к виду {"items": [...]}:
|
|
||||||
if isinstance(data_standings, list):
|
if isinstance(data_standings, list):
|
||||||
items = data_standings
|
items = data_standings
|
||||||
else:
|
else:
|
||||||
@@ -1662,26 +1627,21 @@ def Standing_func(
|
|||||||
|
|
||||||
if not items:
|
if not items:
|
||||||
logger.debug("[STANDINGS_THREAD] no items in standings")
|
logger.debug("[STANDINGS_THREAD] no items in standings")
|
||||||
last_call_ts = now # запрос был успешным, но пустым
|
last_call_ts = now
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Обрабатываем каждый "item" внутри standings:
|
|
||||||
for item in items:
|
for item in items:
|
||||||
comp = item.get("comp", {})
|
comp = item.get("comp", {})
|
||||||
comp_name = (comp.get("name") or "unknown_comp").replace(" ", "_")
|
comp_name = (comp.get("name") or "unknown_comp").replace(" ", "_").replace("|", "")
|
||||||
|
|
||||||
# 1) обычная таблица регулярки
|
|
||||||
if item.get("standings"):
|
if item.get("standings"):
|
||||||
standings_rows = item["standings"]
|
standings_rows = item["standings"]
|
||||||
|
|
||||||
# pandas нормализация
|
|
||||||
df = pd.json_normalize(standings_rows)
|
df = pd.json_normalize(standings_rows)
|
||||||
|
|
||||||
# убираем поле 'scores', если есть
|
|
||||||
if "scores" in df.columns:
|
if "scores" in df.columns:
|
||||||
df = df.drop(columns=["scores"])
|
df = df.drop(columns=["scores"])
|
||||||
|
|
||||||
# добавляем w_l, procent, plus_minus если есть нужные столбцы
|
|
||||||
if (
|
if (
|
||||||
"totalWin" in df.columns
|
"totalWin" in df.columns
|
||||||
and "totalDefeat" in df.columns
|
and "totalDefeat" in df.columns
|
||||||
@@ -1689,43 +1649,43 @@ def Standing_func(
|
|||||||
and "totalGoalPlus" in df.columns
|
and "totalGoalPlus" in df.columns
|
||||||
and "totalGoalMinus" in df.columns
|
and "totalGoalMinus" in df.columns
|
||||||
):
|
):
|
||||||
# W / L
|
tw = pd.to_numeric(df["totalWin"], errors="coerce").fillna(0).astype(int)
|
||||||
df["w_l"] = (
|
td = pd.to_numeric(df["totalDefeat"], errors="coerce").fillna(0).astype(int)
|
||||||
df["totalWin"].fillna(0).astype(int).astype(str)
|
|
||||||
+ " / "
|
df["w_l"] = tw.astype(str) + " / " + td.astype(str)
|
||||||
+ df["totalDefeat"].fillna(0).astype(int).astype(str)
|
|
||||||
)
|
|
||||||
|
|
||||||
# % побед
|
|
||||||
def calc_percent(row):
|
def calc_percent(row):
|
||||||
win = row.get("totalWin", 0)
|
win = row.get("totalWin", 0)
|
||||||
games = row.get("totalGames", 0)
|
games = row.get("totalGames", 0)
|
||||||
if (
|
|
||||||
pd.isna(win)
|
# гарантируем числа
|
||||||
or pd.isna(games)
|
try:
|
||||||
or games == 0
|
win = int(win)
|
||||||
or (row["w_l"] == "0 / 0")
|
except (TypeError, ValueError):
|
||||||
):
|
win = 0
|
||||||
|
try:
|
||||||
|
games = int(games)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
games = 0
|
||||||
|
|
||||||
|
if games == 0 or row["w_l"] == "0 / 0":
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
return round(win * 100 / games + 0.000005)
|
return round(win * 100 / games + 0.000005)
|
||||||
|
|
||||||
df["procent"] = df.apply(calc_percent, axis=1)
|
df["procent"] = df.apply(calc_percent, axis=1)
|
||||||
|
|
||||||
# +/- по очкам
|
tg_plus = pd.to_numeric(df["totalGoalPlus"], errors="coerce").fillna(0).astype(int)
|
||||||
df["plus_minus"] = df["totalGoalPlus"].fillna(0).astype(
|
tg_minus = pd.to_numeric(df["totalGoalMinus"], errors="coerce").fillna(0).astype(int)
|
||||||
int
|
|
||||||
) - df["totalGoalMinus"].fillna(0).astype(int)
|
df["plus_minus"] = tg_plus - tg_minus
|
||||||
|
|
||||||
# готовим питоновский список словарей для атомарной записи
|
|
||||||
standings_payload = df.to_dict(orient="records")
|
standings_payload = df.to_dict(orient="records")
|
||||||
|
|
||||||
filename = f"standings_{league}_{comp_name}"
|
filename = f"standings_{league}_{comp_name}"
|
||||||
atomic_write_json(standings_payload, filename, out_dir)
|
atomic_write_json(standings_payload, filename, out_dir)
|
||||||
logger.info(
|
logger.info(f"[STANDINGS_THREAD] сохранил {filename}.json")
|
||||||
f"[STANDINGS_THREAD] сохранил {filename}.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2) плейофф-пары (playoffPairs)
|
|
||||||
elif item.get("playoffPairs"):
|
elif item.get("playoffPairs"):
|
||||||
playoff_rows = item["playoffPairs"]
|
playoff_rows = item["playoffPairs"]
|
||||||
df = pd.json_normalize(playoff_rows)
|
df = pd.json_normalize(playoff_rows)
|
||||||
@@ -1738,17 +1698,14 @@ def Standing_func(
|
|||||||
f"[STANDINGS_THREAD] saved {filename}.json (playoffPairs, {len(standings_payload)} rows)"
|
f"[STANDINGS_THREAD] saved {filename}.json (playoffPairs, {len(standings_payload)} rows)"
|
||||||
)
|
)
|
||||||
|
|
||||||
# если ни standings ни playoffPairs — просто пропускаем этот блок
|
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# если всё прошло без исключения — фиксируем время удачного апдейта
|
|
||||||
last_call_ts = now
|
last_call_ts = now
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[STANDINGS_THREAD] ошибка в турнирном положении: {e}")
|
logger.warning(f"[STANDINGS_THREAD] ошибка в турнирном положении: {e}")
|
||||||
|
|
||||||
# не жрём CPU впустую
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
logger.info("[STANDINGS_THREAD] stop standings loop")
|
logger.info("[STANDINGS_THREAD] stop standings loop")
|
||||||
@@ -1765,22 +1722,15 @@ def get_data_API(
|
|||||||
team: str,
|
team: str,
|
||||||
lang: str,
|
lang: str,
|
||||||
stop_event: threading.Event,
|
stop_event: threading.Event,
|
||||||
) -> None:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Один "дневной прогон" логики:
|
Один "дневной прогон" логики.
|
||||||
1. Узнать текущий сезон
|
Возвращает day_state:
|
||||||
2. Обновить standings и calendar
|
- "upcoming" -> матч сегодня (домашний), но ещё не начался
|
||||||
3. Найти игру для нашей команды сегодня (today_game) или последнюю законченную (last_played)
|
- "live_done" -> был live_loop и завершился
|
||||||
4. Если есть last_played и нет игры сегодня:
|
- "finished_now" -> матч уже завершён, отрендерили финал
|
||||||
- забрать /game по last_played
|
- "no_game" -> сегодня игры нет вообще
|
||||||
- один раз сгенерировать финальные JSON (render_once_after_game)
|
- "error" -> что-то упало по дороге
|
||||||
5. Если есть игра сегодня:
|
|
||||||
- забрать /game по today_game
|
|
||||||
- если статус игры live:
|
|
||||||
· запустить live-петлю (run_live_loop) с рендером в реальном времени
|
|
||||||
иначе (игра уже финальная, не live):
|
|
||||||
· один раз сгенерировать финальные JSON (render_once_after_game)
|
|
||||||
6. Если нет ничего -> просто логируем и выходим
|
|
||||||
"""
|
"""
|
||||||
# 1. сезоны
|
# 1. сезоны
|
||||||
json_seasons = fetch_api_data(
|
json_seasons = fetch_api_data(
|
||||||
@@ -1788,7 +1738,7 @@ def get_data_API(
|
|||||||
)
|
)
|
||||||
if not json_seasons:
|
if not json_seasons:
|
||||||
logger.error("Не удалось получить список сезонов")
|
logger.error("Не удалось получить список сезонов")
|
||||||
return
|
return "error"
|
||||||
|
|
||||||
season = json_seasons[0]["season"]
|
season = json_seasons[0]["season"]
|
||||||
|
|
||||||
@@ -1811,59 +1761,90 @@ def get_data_API(
|
|||||||
)
|
)
|
||||||
if not json_calendar:
|
if not json_calendar:
|
||||||
logger.error("Не удалось получить список матчей")
|
logger.error("Не удалось получить список матчей")
|
||||||
return
|
return "error"
|
||||||
|
|
||||||
# 3. какая игра нас интересует?
|
# 3. определяем игру
|
||||||
today_game, last_played = get_game_id(json_calendar, team)
|
today_game, last_played = get_game_id(json_calendar, team)
|
||||||
|
print(today_game, last_played)
|
||||||
|
|
||||||
# 4. уже сыграли матч, а сегодня не играем
|
# Ветка А: есть завершённая игра, но сегодня нет матча
|
||||||
if last_played and not today_game:
|
if last_played and not today_game:
|
||||||
game_id = last_played["game"]["id"]
|
game_id = last_played["game"]["id"]
|
||||||
logger.info(f"Последний завершённый матч id={game_id}")
|
logger.info(f"Последний завершённый матч id={game_id}")
|
||||||
render_once_after_game(session, league, season, game_id, lang)
|
render_once_after_game(session, league, season, game_id, lang)
|
||||||
return
|
return "finished_now"
|
||||||
|
|
||||||
# 5. матч сегодня есть
|
# Ветка Б: матч сегодня есть (для домашней команды)
|
||||||
if today_game:
|
if today_game:
|
||||||
game_id = today_game["game"]["id"]
|
game_id = today_game["game"]["id"]
|
||||||
logger.info(f"Онлайн матч id={game_id}")
|
logger.info(f"Онлайн матч id={game_id}")
|
||||||
|
|
||||||
# Всегда обновляем /game прямо сейчас
|
# Обновляем api_* файлы сразу
|
||||||
fetch_api_data(session, "game", host=HOST, game_id=game_id, lang=lang)
|
fetch_api_data(session, "game", host=HOST, game_id=game_id, lang=lang)
|
||||||
|
|
||||||
# и эти ручки тоже сразу дергаем, чтобы у нас были свежие api_*.json
|
|
||||||
fetch_api_data(session, "box-score", host=HOST, game_id=game_id)
|
fetch_api_data(session, "box-score", host=HOST, game_id=game_id)
|
||||||
fetch_api_data(session, "play-by-play", host=HOST, game_id=game_id)
|
fetch_api_data(session, "play-by-play", host=HOST, game_id=game_id)
|
||||||
fetch_api_data(session, "live-status", host=HOST, game_id=game_id)
|
live_status_raw = fetch_api_data(
|
||||||
|
session, "live-status", host=HOST, game_id=game_id
|
||||||
|
)
|
||||||
|
|
||||||
# если матч идёт — запускаем live поток, он сам будет рендерить в цикле
|
# Определяем состояние матча
|
||||||
if is_game_live(today_game["game"]):
|
status_calendar = today_game["game"].get("gameStatus", "")
|
||||||
|
status_live = ""
|
||||||
|
# print(live_status_raw)
|
||||||
|
if isinstance(live_status_raw, dict):
|
||||||
|
# бывают ответы вида {"status":"404","message":"Not found","result":None}
|
||||||
|
ls_result = live_status_raw.get("result")
|
||||||
|
if isinstance(ls_result, dict):
|
||||||
|
status_live = ls_result.get("gameStatus", "")
|
||||||
|
# если result == None -> просто считаем, что live-статуса нет (ещё не начался)
|
||||||
|
|
||||||
|
effective_status = status_live or status_calendar
|
||||||
|
phase = classify_game_state_from_status(effective_status)
|
||||||
|
|
||||||
|
if phase == "live":
|
||||||
|
# матч идёт → запускаем live_loop блокирующе
|
||||||
t = threading.Thread(
|
t = threading.Thread(
|
||||||
target=run_live_loop,
|
target=run_live_loop,
|
||||||
args=(league, season, game_id, lang, today_game["game"], stop_event),
|
args=(league, season, game_id, lang, today_game["game"], stop_event),
|
||||||
daemon=False,
|
daemon=False,
|
||||||
)
|
)
|
||||||
t.start()
|
t.start()
|
||||||
logger.info("live thread spawned, waiting for it to finish...")
|
logger.info("[get_data_API] live thread spawned, waiting for it to finish")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
t.join()
|
t.join()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info("KeyboardInterrupt while waiting live thread -> stop_event")
|
logger.info(
|
||||||
|
"[get_data_API] KeyboardInterrupt while waiting live thread -> stop_event"
|
||||||
|
)
|
||||||
stop_event.set()
|
stop_event.set()
|
||||||
t.join()
|
t.join()
|
||||||
|
|
||||||
logger.info("live thread finished")
|
logger.info("[get_data_API] live thread finished")
|
||||||
|
return "live_done"
|
||||||
|
|
||||||
else:
|
if phase == "upcoming":
|
||||||
# матч сегодня, но он уже финальный (resultconfirmed / finished)
|
# матч сегодня, но ещё не начался (Scheduled / NotStarted / Draft)
|
||||||
# значит просто один раз считаем все json-ы
|
# не генерим финал, не спим до завтра
|
||||||
|
logger.info(
|
||||||
|
f"Матч {game_id} сегодня, но ещё не начался (status={effective_status}). Ждём старт."
|
||||||
|
)
|
||||||
|
return "upcoming"
|
||||||
|
|
||||||
|
if phase == "finished":
|
||||||
|
# матч уже закончен → однократно пререндерили всё и можем спать
|
||||||
render_once_after_game(session, league, season, game_id, lang)
|
render_once_after_game(session, league, season, game_id, lang)
|
||||||
|
return "finished_now"
|
||||||
|
|
||||||
return
|
# на всякий случай (если API дал что-то новое)
|
||||||
|
logger.info(
|
||||||
|
f"[get_data_API] Неожиданная фаза '{phase}', status={effective_status}. Считаем как 'upcoming'."
|
||||||
|
)
|
||||||
|
return "upcoming"
|
||||||
|
|
||||||
# 6. ничего подходящего вообще
|
# Ветка В: нет матча сегодня, нет последнего завершённого
|
||||||
logger.info("Для этой команды игр сегодня нет и нет завершённой последней игры.")
|
logger.info("Для этой команды игр сегодня нет и нет завершённой последней игры.")
|
||||||
|
return "no_game"
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -1873,14 +1854,9 @@ def main():
|
|||||||
parser.add_argument("--lang", default="en")
|
parser.add_argument("--lang", default="en")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Один общий stop_event на всё приложение.
|
|
||||||
stop_event = threading.Event()
|
stop_event = threading.Event()
|
||||||
|
|
||||||
# Одна сессия для standings-потока.
|
|
||||||
# Её достаточно, потому что standings не требует суперчастого обновления.
|
|
||||||
standings_session = create_session()
|
standings_session = create_session()
|
||||||
|
|
||||||
# Запускаем standings-поток навсегда (пока процесс жив).
|
|
||||||
standings_thread = threading.Thread(
|
standings_thread = threading.Thread(
|
||||||
target=Standing_func,
|
target=Standing_func,
|
||||||
args=(standings_session, args.league, None, args.lang, stop_event),
|
args=(standings_session, args.league, None, args.lang, stop_event),
|
||||||
@@ -1889,20 +1865,37 @@ def main():
|
|||||||
standings_thread.start()
|
standings_thread.start()
|
||||||
logger.info("[MAIN] standings thread started (global)")
|
logger.info("[MAIN] standings thread started (global)")
|
||||||
|
|
||||||
# Основной дневной цикл.
|
|
||||||
while True:
|
while True:
|
||||||
session = create_session()
|
session = create_session()
|
||||||
try:
|
try:
|
||||||
get_data_API(session, args.league, args.team, args.lang, stop_event)
|
day_state = get_data_API(
|
||||||
|
session, args.league, args.team, args.lang, stop_event
|
||||||
|
)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info("KeyboardInterrupt -> останавливаем всё")
|
logger.info("KeyboardInterrupt -> останавливаем всё")
|
||||||
stop_event.set()
|
stop_event.set()
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"main loop crash: {e}")
|
logger.exception(f"main loop crash: {e}")
|
||||||
|
day_state = "error"
|
||||||
|
|
||||||
# спим до завтра 00:05
|
|
||||||
now = datetime.now(APP_TZ)
|
now = datetime.now(APP_TZ)
|
||||||
|
|
||||||
|
if day_state == "upcoming":
|
||||||
|
# матч сегодня, но ещё не начался → НЕ спим до завтра.
|
||||||
|
time.sleep(120) # 2 минуты опроса статуса до старта
|
||||||
|
continue
|
||||||
|
|
||||||
|
if day_state == "error":
|
||||||
|
# что-то пошло не так → подожди минуту и попробуем ещё раз
|
||||||
|
time.sleep(60)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# сюда мы попадаем если:
|
||||||
|
# - live_done (лайв отработался до конца)
|
||||||
|
# - finished_now (матч уже был, всё посчитали)
|
||||||
|
# - no_game (сегодня матчей вообще нет)
|
||||||
|
# -> можно лечь до завтра 00:05
|
||||||
tomorrow = (now + timedelta(days=1)).replace(
|
tomorrow = (now + timedelta(days=1)).replace(
|
||||||
hour=0, minute=5, second=0, microsecond=0
|
hour=0, minute=5, second=0, microsecond=0
|
||||||
)
|
)
|
||||||
@@ -1913,9 +1906,12 @@ def main():
|
|||||||
)
|
)
|
||||||
sleep_seconds = (tomorrow - now).total_seconds()
|
sleep_seconds = (tomorrow - now).total_seconds()
|
||||||
|
|
||||||
|
hours_left = int(sleep_seconds // 3600)
|
||||||
|
mins_left = int((sleep_seconds % 3600) // 60)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Работа за день завершена. Засыпаем до {tomorrow.strftime('%d.%m %H:%M')} "
|
f"Работа за день завершена. Засыпаем до {tomorrow.strftime('%d.%m %H:%M')} "
|
||||||
f"(~{round(sleep_seconds/3600, 2)} ч)."
|
f"(~{hours_left}.{mins_left} ч)."
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -1925,14 +1921,10 @@ def main():
|
|||||||
stop_event.set()
|
stop_event.set()
|
||||||
break
|
break
|
||||||
|
|
||||||
# Выход из while True → стопаем standings-поток
|
|
||||||
stop_event.set()
|
stop_event.set()
|
||||||
standings_thread.join()
|
standings_thread.join()
|
||||||
logger.info("[MAIN] standings thread stopped, shutdown complete")
|
logger.info("[MAIN] standings thread stopped, shutdown complete")
|
||||||
|
|
||||||
# идём на новую итерацию while True
|
|
||||||
# (новая сессия / новый stop_event создаются в начале цикла)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# 9. Точка входа
|
# 9. Точка входа
|
||||||
|
|||||||
10
visual.py
10
visual.py
@@ -439,7 +439,7 @@ cached_referee = st.session_state.get("referee")
|
|||||||
league_tag = None
|
league_tag = None
|
||||||
if isinstance(cached_game_online, dict):
|
if isinstance(cached_game_online, dict):
|
||||||
league_tag = (cached_game_online.get("league") or {}).get("tag")
|
league_tag = (cached_game_online.get("league") or {}).get("tag")
|
||||||
comp_name = (cached_game_online.get("comp") or {}).get("name").replace(" ", "_")
|
comp_name = (cached_game_online.get("comp") or {}).get("name").replace(" ", "_").replace("|", "")
|
||||||
if league_tag:
|
if league_tag:
|
||||||
load_data_from_json(f"standings_{league_tag}_{comp_name}")
|
load_data_from_json(f"standings_{league_tag}_{comp_name}")
|
||||||
cached_standings = (
|
cached_standings = (
|
||||||
@@ -1866,7 +1866,7 @@ try:
|
|||||||
except (FileNotFoundError, json.JSONDecodeError):
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
play_type_id = []
|
play_type_id = []
|
||||||
|
|
||||||
teams_section = cached_game_online.get("teams") or {}
|
teams_section = (cached_game_online or {}).get("teams") or {}
|
||||||
# Если teams_section — список (например, [{"starts": [...]}, {...}])
|
# Если teams_section — список (например, [{"starts": [...]}, {...}])
|
||||||
if isinstance(teams_section, list):
|
if isinstance(teams_section, list):
|
||||||
if len(teams_section) >= 2:
|
if len(teams_section) >= 2:
|
||||||
@@ -1925,7 +1925,7 @@ def get_event_time(row):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
teams_section = cached_game_online.get("teams") or {}
|
teams_section = (cached_game_online or {}).get("teams") or {}
|
||||||
|
|
||||||
# Если teams_section — список (например, [{"starts": [...]}, {...}])
|
# Если teams_section — список (например, [{"starts": [...]}, {...}])
|
||||||
if isinstance(teams_section, list):
|
if isinstance(teams_section, list):
|
||||||
@@ -1959,9 +1959,9 @@ list_fullname = [None] + [
|
|||||||
if x.get("startRole") == "Player"
|
if x.get("startRole") == "Player"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
plays = plays if 'plays' in locals() else []
|
||||||
with tab_pbp:
|
with tab_pbp:
|
||||||
# plays = ((cached_game_online or {}).get("result") or {}).get("plays") or []
|
plays = ((cached_game_online or {}).get("result") or {}).get("plays") or []
|
||||||
if plays:
|
if plays:
|
||||||
temp_data_pbp = pd.DataFrame(plays)
|
temp_data_pbp = pd.DataFrame(plays)
|
||||||
col1_pbp, col2_pbp = tab_pbp.columns((3, 4))
|
col1_pbp, col2_pbp = tab_pbp.columns((3, 4))
|
||||||
|
|||||||
Reference in New Issue
Block a user