Compare commits

...

2 Commits

View File

@@ -1,4 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -29,6 +28,7 @@ import time
import json
import argparse
import logging
import pandas as pd
import logging.config
import threading
import concurrent.futures
@@ -41,6 +41,7 @@ from zoneinfo import ZoneInfo
from typing import Any, Dict, List
import tempfile
from pathlib import Path
from threading import Event, Lock
import requests
@@ -72,20 +73,20 @@ DEFAULT_LANG = "en"
# URL-шаблоны (замени на реальные)
HOST = "ref.russiabasket.org"
URL_SEASON = "https://{host}/api/abc/comps/seasons?Tag={league}&Lang={lang}" # вернёт JSON со списком сезонов
URL_SCHEDULE = (
"https://{host}/api/abc/comps/calendar?Tag={league}&Season={season}&Lang={lang}&MaxResultCount=1000" # расписание лиги (или команды)
)
URL_SCHEDULE = "https://{host}/api/abc/comps/calendar?Tag={league}&Season={season}&Lang={lang}&MaxResultCount=1000" # расписание лиги (или команды)
# Статус конкретной игры (используется для проверки "онлайн?" раз в минуту)
URL_GAME = "https://{host}/api/abc/games/game?Id={game_id}&lang={lang}"
# Быстрые запросы, когда матч онлайн (каждую секунду)
URL_BOX_SCORE = "https://{host}/api/abc/games/box-score?Id={game_id}&lang={lang}"
URL_PLAY_BY_PLAY = "https://{host}/api/abc/games/play-by-play?Id={game_id}&lang={lang}"
URL_LIVE_STATUS = "https://{host}/api/abc/games/live-status?Id={game_id}&lang={lang}"
URL_STANDINGS = "https://{host}/api/abc/comps/actual-standings?tag={league}&season={season}&lang={lang}"
# Интервалы опроса
STATUS_CHECK_INTERVAL_SEC = 60 # проверять "онлайн?" раз в минуту
ONLINE_FETCH_INTERVAL_SEC = 1 # когда матч онлайн, дергать три запроса каждую секунду
POLL_INTERVAL_OFFLINE_SEC = 300 # резервный интервал сна при ошибках/до старта
TIMEOUT_DATA_OFF = 600
TELEGRAM_BOT_TOKEN = "7639240596:AAH0YtdQoWZSC-_R_EW4wKAHHNLIA0F_ARY"
# TELEGRAM_CHAT_ID = 228977654
@@ -146,7 +147,9 @@ logger.handlers[2].formatter.use_emoji = True
# ==========================
def fetch_json(url: str, params: dict | None = None, session: requests.Session | None = None) -> dict:
def fetch_json(
url: str, params: dict | None = None, session: requests.Session | None = None
) -> dict:
"""
GET JSON с таймаутом и внятными ошибками.
Использует переданный session для keep-alive.
@@ -237,7 +240,9 @@ def parse_game_start_dt(item: dict) -> datetime:
except Exception as e:
raise RuntimeError(f"Ошибка парсинга localDate/localTime '{ld} {lt}': {e}")
raise RuntimeError("Не найдено ни одного подходящего поля времени (defaultZoneDateTime/scheduledTime/startTime/localDate+localTime).")
raise RuntimeError(
"Не найдено ни одного подходящего поля времени (defaultZoneDateTime/scheduledTime/startTime/localDate+localTime)."
)
def extract_game_status(data: dict) -> str:
@@ -254,21 +259,32 @@ def extract_game_status(data: dict) -> str:
# ---- ДОП. ЗАПРОСЫ ПРИ ОНЛАЙНЕ
# ==========================
def fetch_box_score(league: str, game_id: str, lang: str, session: requests.Session | None = None) -> dict:
def fetch_box_score(
league: str, game_id: str, lang: str, session: requests.Session | None = None
) -> dict:
url = URL_BOX_SCORE.format(host=HOST, league=league, game_id=game_id, lang=lang)
return fetch_json(url, session=session)
def fetch_play_by_play(league: str, game_id: str, lang: str, session: requests.Session | None = None) -> dict:
def fetch_play_by_play(
league: str, game_id: str, lang: str, session: requests.Session | None = None
) -> dict:
url = URL_PLAY_BY_PLAY.format(host=HOST, league=league, game_id=game_id, lang=lang)
return fetch_json(url, session=session)
def fetch_live_status(league: str, game_id: str, lang: str, session: requests.Session | None = None) -> dict:
def fetch_live_status(
league: str, game_id: str, lang: str, session: requests.Session | None = None
) -> dict:
url = URL_LIVE_STATUS.format(host=HOST, league=league, game_id=game_id, lang=lang)
return fetch_json(url, session=session)
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def _get(d: dict | None, *path, default=None):
"""Безопасно достаём вложенные ключи: _get(d, "result", "fullScore", default={})"""
cur = d or {}
@@ -278,6 +294,7 @@ def _get(d: dict | None, *path, default=None):
cur = cur[p]
return cur
def _dedup_plays(plays: List[dict]) -> List[dict]:
"""
Удаляем дубли по стабильному идентификатору события.
@@ -298,9 +315,16 @@ def _dedup_plays(plays: List[dict]) -> List[dict]:
seen.add(key)
out.append(ev)
# если есть поле sequence/time — отсортируем, чтобы обработчик получал стабильный порядок
out.sort(key=lambda e: (e.get("sequence") is None, e.get("sequence"), e.get("time") or e.get("clock")))
out.sort(
key=lambda e: (
e.get("sequence") is None,
e.get("sequence"),
e.get("time") or e.get("clock"),
)
)
return out
def merge_online_payloads(
game: dict,
box_score: dict | None,
@@ -312,41 +336,38 @@ def merge_online_payloads(
Ничего не знает о внутренней логике обработки — только нормализует.
"""
# исходные куски
plays_raw: List[dict] = _get(play_by_play, "result", default=[]) or []
score_by_periods = _get(box_score, "result", "scoreByPeriods", default=[]) or []
full_score = _get(box_score, "result", "fullScore", default={}) or {}
teams = _get(box_score, "result", "teams", default={}) or {} # если пригодится в обработчике
players = _get(box_score, "result", "players", default=[]) or []
# live
period = _get(live_status, "result", "period")
clock = _get(live_status, "result", "clock")
status = _get(live_status, "result", "status") # e.g., "inprogress", "ended", "scheduled"
# plays_raw: List[dict] = _get(play_by_play, "result", default=[]) or []
# score_by_periods = _get(box_score, "result", "scoreByPeriods", default=[]) or []
# full_score = _get(box_score, "result", "fullScore", default={}) or {}
# teams = _get(box_score, "result", "teams", default={}) or {} # если пригодится в обработчике
# players = _get(box_score, "result", "players", default=[]) or []
# box_score = _get(box_score, "result", "teams", default=[]) or []
# fullScore = _get(box_score, "result", "fullScore", default="") or ""
# # live
# live_status = _get(live_status, "result", "live_status")
# period = _get(live_status, "result", "period")
# clock = _get(live_status, "result", "clock")
# status = _get(live_status, "result", "status") # e.g., "inprogress", "ended", "scheduled"
# нормализация/дедуп
plays = _dedup_plays(plays_raw)
# plays = _dedup_plays(plays_raw)
game["result"]["plays"] = play_by_play.get("result", [])
game["result"]["scoreByPeriods"] = box_score["result"].get("scoreByPeriods", [])
game["result"]["fullScore"] = box_score["result"].get("fullScore", {})
game["result"]["live_status"] = live_status["result"]
merged: Dict[str, Any] = {
"meta": {
"generatedAt": _now_iso(),
"sourceHints": {
"boxScoreHas": list((_get(box_score, "result") or {}).keys()),
"pbpLen": len(plays),
"pbpLen": "",
},
},
"result": {
# то, что просил: три ключа (плюс ещё полезные поля)
"plays": plays,
"scoreByPeriods": score_by_periods,
"fullScore": full_score,
# добавим live — обработчику пригодится
"period": period,
"clock": clock,
"status": status,
# опционально: передадим команды/игроков, если есть в box score
"teams": teams,
"players": players,
},
"result": game,
}
return merged
@@ -381,6 +402,7 @@ def is_already_merged(obj: dict) -> bool:
and isinstance(r.get("scoreByPeriods", []), list)
)
def ensure_merged_payload(
game_or_merged: dict | None = None,
*,
@@ -423,20 +445,26 @@ def ensure_merged_payload(
},
"result": g, # положим сырой ответ целиком — чтобы файл гарантированно записался
}
raise ValueError("ensure_merged_payload: не передан ни уже-склеенный game, ни box/pbp/live.")
raise ValueError(
"ensure_merged_payload: не передан ни уже-склеенный game, ни box/pbp/live."
)
def atomic_write_json(path: str | Path, data: dict, ensure_dirs: bool = True) -> None:
path = Path(path)
if ensure_dirs:
path.parent.mkdir(parents=True, exist_ok=True)
# атомарная запись: пишем во временный файл и переименовываем
with tempfile.NamedTemporaryFile("w", delete=False, dir=str(path.parent), encoding="utf-8") as tmp:
with tempfile.NamedTemporaryFile(
"w", delete=False, dir=str(path.parent), encoding="utf-8"
) as tmp:
json.dump(data, tmp, ensure_ascii=False, indent=2)
tmp.flush()
os.fsync(tmp.fileno())
tmp_name = tmp.name
os.replace(tmp_name, path)
def format_time(seconds: float | int) -> str:
"""
Форматирует время в секундах в строку "M:SS".
@@ -456,7 +484,9 @@ def format_time(seconds: float | int) -> str:
return "0:00"
def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | None = None) -> None:
def Json_Team_Generation(
merged: dict, *, out_dir: str = "static", who: str | None = None
) -> None:
"""
Единая точка: принимает уже нормализованный merged, делает нужные вычисления (если надо)
и сохраняет в JSON.
@@ -503,11 +533,7 @@ def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | No
team = []
for item in starts:
player = {
"id": (
item["personId"]
if item["personId"]
else ""
),
"id": (item["personId"] if item["personId"] else ""),
"num": item["displayNumber"],
"startRole": item["startRole"],
"role": item["positionName"],
@@ -517,29 +543,21 @@ def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | No
for r in role_list
if r[0].lower() == item["positionName"].lower()
][0]
if any(
r[0].lower() == item["positionName"].lower()
for r in role_list
)
if any(r[0].lower() == item["positionName"].lower() for r in role_list)
else ""
),
"NameGFX": (
f"{item['firstName'].strip()} {item['lastName'].strip()}"
if item["firstName"] is not None
and item["lastName"] is not None
if item["firstName"] is not None and item["lastName"] is not None
else "Команда"
),
"captain": item["isCapitan"],
"age": item["age"] if item["age"] is not None else 0,
"height": f'{item["height"]} cm' if item["height"] else 0,
"weight": f'{item["weight"]} kg' if item["weight"] else 0,
"isStart": (
item["stats"]["isStart"] if item["stats"] else False
),
"isStart": (item["stats"]["isStart"] if item["stats"] else False),
"isOn": (
"🏀"
if item["stats"] and item["stats"]["isOnCourt"] is True
else ""
"🏀" if item["stats"] and item["stats"]["isOnCourt"] is True else ""
),
"flag": f"https://flagicons.lipis.dev/flags/4x3/{'ru' if item['countryId'] is None and item['countryName'] == 'Russia' else '' if item['countryId'] is None else item['countryId'].lower() if item['countryName'] is not None else ''}.svg",
"pts": item["stats"]["points"] if item["stats"] else 0,
@@ -599,23 +617,15 @@ def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | No
if item["stats"]
else 0
),
"time": (
format_time(item["stats"]["second"])
if item["stats"]
else "0:00"
),
"time": (format_time(item["stats"]["second"]) if item["stats"] else "0:00"),
"pts1q": 0,
"pts2q": 0,
"pts3q": 0,
"pts4q": 0,
"pts1h": 0,
"pts2h": 0,
"Name1GFX": (
item["firstName"].strip() if item["firstName"] else ""
),
"Name2GFX": (
item["lastName"].strip() if item["lastName"] else ""
),
"Name1GFX": (item["firstName"].strip() if item["firstName"] else ""),
"Name2GFX": (item["lastName"].strip() if item["lastName"] else ""),
"photoGFX": (
os.path.join(
"D:\\Photos",
@@ -629,9 +639,7 @@ def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | No
else ""
),
# "season": text,
"isOnCourt": (
item["stats"]["isOnCourt"] if item["stats"] else False
),
"isOnCourt": (item["stats"]["isOnCourt"] if item["stats"] else False),
# "AvgPoints": (
# row_player_season_avg["points"]
# if row_player_season_avg
@@ -1012,9 +1020,7 @@ def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | No
)
for key in team[0].keys()
}
for _ in range(
(4 if count_player <= 4 else 12) - count_player
)
for _ in range((4 if count_player <= 4 else 12) - count_player)
]
team.extend(empty_rows)
role_priority = {
@@ -1069,6 +1075,7 @@ def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | No
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]:
"""
Вычисляет количество оставшихся таймаутов для обеих команд
@@ -1124,6 +1131,7 @@ def time_outs_func(data_pbp: list[dict]) -> tuple[str, int, str, int]:
return t1_str, t1_left, t2_str, t2_left
def add_data_for_teams(new_data: list[dict]) -> tuple[float, list, float]:
"""
Возвращает усреднённые статистики команды:
@@ -1174,6 +1182,7 @@ def add_data_for_teams(new_data: list[dict]) -> tuple[float, list, float]:
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,
@@ -1243,6 +1252,7 @@ def add_new_team_stat(
return data
stat_name_list = [
("points", "Очки", "points"),
("pt-1", "Штрафные", "free throws"),
@@ -1305,22 +1315,14 @@ def Team_Both_Stat(merged: dict, *, out_dir: str = "static") -> None:
# time.sleep()
# Таймауты
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_2, points_2, avg_height_2 = add_data_for_teams(
team_2.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", []))
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_1 = add_new_team_stat(
@@ -1340,19 +1342,14 @@ def Team_Both_Stat(merged: dict, *, out_dir: str = "static") -> None:
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]
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]
int(total_2[key]) if isinstance(total_2[key], float) else total_2[key]
)
stat_rus, stat_eng = "", ""
for s in stat_name_list:
@@ -1376,9 +1373,7 @@ def Team_Both_Stat(merged: dict, *, out_dir: str = "static") -> None:
logger.debug("Успешно записаны данные в team_stats.json")
except Exception as e:
logger.error(
f"Ошибка при обработке командной статистики: {e}", exc_info=True
)
logger.error(f"Ошибка при обработке командной статистики: {e}", exc_info=True)
def Referee(merged: dict, *, out_dir: str = "static") -> None:
@@ -1411,9 +1406,7 @@ def Referee(merged: dict, *, out_dir: str = "static") -> None:
referees = []
for r in referees_raw:
flag_code = (
r.get("countryId", "").lower() if r.get("countryName") else ""
)
flag_code = r.get("countryId", "").lower() if r.get("countryName") else ""
referees.append(
{
"displayNumber": r.get("displayNumber", ""),
@@ -1454,9 +1447,7 @@ def Scores_Quarter(merged: dict, *, out_dir: str = "static") -> None:
score_by_quarter = [{"Q": q, "score1": "", "score2": ""} for q in quarters]
try:
# Сначала пробуем fullScore
full_score_str = (
merged.get("result", {}).get("game", {}).get("fullScore", "")
)
full_score_str = merged.get("result", {}).get("game", {}).get("fullScore", "")
if full_score_str:
full_score_list = full_score_str.split(",")
for i, score_str in enumerate(full_score_list[: len(score_by_quarter)]):
@@ -1484,6 +1475,104 @@ def Scores_Quarter(merged: dict, *, out_dir: str = "static") -> None:
logger.error(f"Ошибка в Scores_Quarter: {e}", exc_info=True)
def status_online_func(merged: dict, *, out_dir: str = "static") -> None:
"""
Получает онлайн-статус игры и возвращает данные + путь к PNG-фолам.
"""
try:
out_path = Path(out_dir) / "live_status.json"
if "live_status" in merged["result"]:
status_data = merged["result"]["live_status"]
atomic_write_json(out_path, status_data)
else:
logger.warning("Матч не ОНЛАЙН!!!!")
atomic_write_json(
out_path,
[
{
"foulsA": 0,
"foulsB": 0,
}
],
)
logging.info("Сохранил payload: {out_path}")
except Exception as e:
logger.error(f"Ошибка в status_online_func: {e}", exc_info=True)
return None
def Standing_func(league, season, lang, stop_event: threading.Event, out_dir: str = "static") -> None:
logger.info("START making json for standings")
while not stop_event.is_set():
try:
url = URL_STANDINGS.format(host=HOST, league=league, season=season, lang=lang)
data_standings = fetch_json(url)
if data_standings and "items" in data_standings and data_standings["items"]:
standings_temp = data_standings["items"]
for item in standings_temp:
if "standings" in item and item["standings"] != []:
standings_temp = item["standings"]
df = pd.json_normalize(standings_temp)
del df["scores"]
if not df["totalWin"].isna().all():
df["w_l"] = (
df["totalWin"].astype(str)
+ " / "
+ df["totalDefeat"].astype(str)
)
df["procent"] = df.apply(
lambda row: (
0
if row["w_l"] == "0 / 0"
or row["totalGames"] == 0
or pd.isna(row["totalWin"])
else round(
row["totalWin"] * 100 / row["totalGames"]
+ 0.000005
)
),
axis=1,
)
df["plus_minus"] = (
df["totalGoalPlus"] - df["totalGoalMinus"]
)
filepath = os.path.join(
out_dir,
f"standings_{league}_{item['comp']['name'].replace(' ', '_')}.json",
)
df.to_json(
filepath,
orient="records",
force_ascii=False,
indent=4,
)
logger.info("Standings data saved successfully.")
elif "playoffPairs" in item and item["playoffPairs"] != []:
standings_temp = item["playoffPairs"]
df = pd.json_normalize(standings_temp)
filepath = os.path.join(
out_dir,
f"standings_{league}_{item['comp']['name'].replace(' ', '_')}.json",
)
df.to_json(
filepath,
orient="records",
force_ascii=False,
indent=4,
)
logger.info("Standings data saved successfully.")
except Exception as e:
logger.warning(f"Ошибка в турнирном положении: {e}")
stop_event.wait(TIMEOUT_DATA_OFF)
# ==========================
# ---- ДОМЕННАЯ ЛОГИКА
# ==========================
@@ -1492,7 +1581,9 @@ def Scores_Quarter(merged: dict, *, out_dir: str = "static") -> None:
def validate_league_or_die(league: str) -> str:
league = (league or DEFAULT_LEAGUE).lower().strip()
if league not in ALLOWED_LEAGUES:
logger.warning(f"Неверный тег лиги: '{league}'. Допустимо: {sorted(ALLOWED_LEAGUES)}")
logger.warning(
f"Неверный тег лиги: '{league}'. Допустимо: {sorted(ALLOWED_LEAGUES)}"
)
sys.exit(2)
return league
@@ -1502,16 +1593,16 @@ def get_last_season_or_die(league: str, lang: str) -> str:
try:
data = fetch_json(url)
season = extract_last_season(data)
logging.info(
f"Последний сезон для {league}: {season}"
)
logging.info(f"Последний сезон для {league}: {season}")
return season
except Exception as e:
logger.warning(f"Не получилось получить последний сезон для {league}: {e}")
sys.exit(3)
def get_team_schedule_or_die(league: str, season: str, team: str, lang: str) -> list[dict]:
def get_team_schedule_or_die(
league: str, season: str, team: str, lang: str
) -> list[dict]:
url = URL_SCHEDULE.format(host=HOST, league=league, season=season, lang=lang)
try:
data = fetch_json(url)
@@ -1584,6 +1675,7 @@ class PostProcessor:
Team_Both_Stat(merged, out_dir="static")
Referee(merged, out_dir="static")
Scores_Quarter(merged, out_dir="static")
status_online_func(merged, out_dir="static")
except Exception as e:
logging.exception(f"Postproc failed: {e}")
@@ -1592,7 +1684,9 @@ class PostProcessor:
class OnlinePoller:
def __init__(self, league: str, game_id: str, lang: str, on_update: callable | None = None):
def __init__(
self, league: str, game_id: str, lang: str, on_update: callable | None = None
):
self.league = league
self.game_id = game_id
self.lang = lang
@@ -1605,19 +1699,24 @@ class OnlinePoller:
# 1) Постоянная сессия и пул соединений
self._session = requests.Session()
retry = Retry(
total=2, connect=2, read=2, backoff_factor=0.1,
total=2,
connect=2,
read=2,
backoff_factor=0.1,
status_forcelist=(502, 503, 504),
allowed_methods=frozenset(["GET"])
allowed_methods=frozenset(["GET"]),
)
adapter = HTTPAdapter(pool_connections=1, pool_maxsize=10, max_retries=retry)
self._session.mount("http://", adapter)
self._session.mount("https://", adapter)
self._session.headers.update({
self._session.headers.update(
{
"Connection": "keep-alive",
"Accept": "application/json, */*",
"Accept-Encoding": "gzip, deflate, br",
"User-Agent": "game-watcher/1.0"
})
"User-Agent": "game-watcher/1.0",
}
)
def stop(self):
if self._thread and self._thread.is_alive():
@@ -1641,9 +1740,27 @@ class OnlinePoller:
started = time.perf_counter()
try:
futures = [
pool.submit(fetch_box_score, self.league, self.game_id, self.lang, self._session),
pool.submit(fetch_play_by_play, self.league, self.game_id, self.lang, self._session),
pool.submit(fetch_live_status, self.league, self.game_id, self.lang, self._session),
pool.submit(
fetch_box_score,
self.league,
self.game_id,
self.lang,
self._session,
),
pool.submit(
fetch_play_by_play,
self.league,
self.game_id,
self.lang,
self._session,
),
pool.submit(
fetch_live_status,
self.league,
self.game_id,
self.lang,
self._session,
),
]
bs, pbp, ls = (f.result() for f in futures)
merged = ensure_merged_payload(
@@ -1691,7 +1808,10 @@ class OnlinePoller:
self._thread.start()
self._log.info(f"Онлайн-поллер для игры {self.game_id} запущен.")
def monitor_game_loop(league: str, game_id: str, lang:str, stop_event: threading.Event) -> None:
def monitor_game_loop(
league: str, game_id: str, lang: str, stop_event: threading.Event
) -> None:
logger.info(f"Старт мониторинга игры {game_id} ({league}).")
poller = OnlinePoller(league, game_id, lang)
was_online = False
@@ -1707,10 +1827,14 @@ def monitor_game_loop(league: str, game_id: str, lang:str, stop_event: threading
break
if is_online and not was_online:
logger.info(f"Матч {game_id} перешёл в онлайн.\nЗапускаем быстрый опрос (1 сек).")
logger.info(
f"Матч {game_id} перешёл в онлайн.\nЗапускаем быстрый опрос (1 сек)."
)
poller.start()
elif not is_online and was_online:
logger.info(f"Матч {game_id} вышел из онлайна (или ещё не стартовал).\nОстанавливаем быстрый опрос.")
logger.info(
f"Матч {game_id} вышел из онлайна (или ещё не стартовал).\nОстанавливаем быстрый опрос."
)
poller.stop()
was_online = is_online
@@ -1730,7 +1854,9 @@ def monitor_game_loop(league: str, game_id: str, lang:str, stop_event: threading
def next_midnight_local(now: datetime) -> datetime:
tomorrow = (now + timedelta(days=1)).date()
return datetime.combine(tomorrow, datetime.min.time(), tzinfo=APP_TZ) + timedelta(minutes=5)
return datetime.combine(tomorrow, datetime.min.time(), tzinfo=APP_TZ) + timedelta(
minutes=5
)
# return now + timedelta(seconds=30)
@@ -1834,9 +1960,7 @@ def main():
parser.add_argument(
"--team", type=str, required=True, help="код/тег команды (например, BOS)"
)
parser.add_argument(
"--lang", type=str, default="en", help="язык получения данных"
)
parser.add_argument("--lang", type=str, default="en", help="язык получения данных")
parser.add_argument(
"--log-level", type=str, default="INFO", help="DEBUG|INFO|WARNING|ERROR"
)
@@ -1844,7 +1968,9 @@ def main():
print(args)
# logger.info(f"Запуск программы пользователем: {MYHOST}")
logger.info(f"Запуск с параметрами:\nleague={args.league}\nteam={args.team}\nlang={args.lang}")
logger.info(
f"Запуск с параметрами:\nleague={args.league}\nteam={args.team}\nlang={args.lang}"
)
league = validate_league_or_die(args.league)
team = args.team.lower()
@@ -1852,6 +1978,10 @@ def main():
# 1) Узнать последний сезон
season = get_last_season_or_die(league, args.lang)
# 2) Получить расписание для команды
team_games = get_team_schedule_or_die(league, season, team, args.lang)
if not team_games:
@@ -1875,20 +2005,23 @@ def main():
if last_played:
game_id = last_played["game"]["id"]
try:
url = URL_GAME.format(host=HOST, league=league, game_id=game_id, lang=args.lang)
url = URL_GAME.format(
host=HOST, league=league, game_id=game_id, lang=args.lang
)
game_json = fetch_json(url)
merged = ensure_merged_payload(
game_json,
game_meta={
"id": game_json.get("result", {}).get("gameId"),
"league": args.league
}
"league": args.league,
},
)
Json_Team_Generation(merged, out_dir="static", who="team1")
Json_Team_Generation(merged, out_dir="static", who="team2")
Team_Both_Stat(merged, out_dir="static")
Referee(merged, out_dir="static")
Scores_Quarter(merged, out_dir="static")
status_online_func(merged, out_dir="static")
# print(merged)
logger.info(
f"Сегодня у {team} нет игры.\nПоследняя сыгранная: gameID={game_id}.\nМониторинг не запускаю."
@@ -1918,12 +2051,27 @@ def main():
)
rollover_thread.start()
# 1.1) турнирная таблица
threads = [
threading.Thread(
target=Standing_func,
args=(league, season, args.lang, stop_event),
name="standings",)]
for t in threads:
t.start()
logger.debug(f"Поток {t.name} запущен.")
# Держим главный поток живым
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
logger.info("Завершение по Ctrl+C…")
stop_event.set()
for t in threads:
t.join()
logger.debug(f"Поток {t.name} завершён.")
finally:
stop_event.set()
monitor_mgr.stop()