добавленны создание онлайн/оффлайн фолов и поправил функцию на склеивание онлайн данных (нужно проверить)

This commit is contained in:
2025-10-24 17:35:12 +03:00
parent 893d18ee23
commit 629854c104

View File

@@ -1,4 +1,3 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
@@ -72,9 +71,7 @@ DEFAULT_LANG = "en"
# URL-шаблоны (замени на реальные) # URL-шаблоны (замени на реальные)
HOST = "ref.russiabasket.org" HOST = "ref.russiabasket.org"
URL_SEASON = "https://{host}/api/abc/comps/seasons?Tag={league}&Lang={lang}" # вернёт JSON со списком сезонов URL_SEASON = "https://{host}/api/abc/comps/seasons?Tag={league}&Lang={lang}" # вернёт JSON со списком сезонов
URL_SCHEDULE = ( URL_SCHEDULE = "https://{host}/api/abc/comps/calendar?Tag={league}&Season={season}&Lang={lang}&MaxResultCount=1000" # расписание лиги (или команды)
"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_GAME = "https://{host}/api/abc/games/game?Id={game_id}&lang={lang}"
# Быстрые запросы, когда матч онлайн (каждую секунду) # Быстрые запросы, когда матч онлайн (каждую секунду)
@@ -146,7 +143,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 с таймаутом и внятными ошибками. GET JSON с таймаутом и внятными ошибками.
Использует переданный session для keep-alive. Использует переданный session для keep-alive.
@@ -237,7 +236,9 @@ def parse_game_start_dt(item: dict) -> datetime:
except Exception as e: except Exception as e:
raise RuntimeError(f"Ошибка парсинга localDate/localTime '{ld} {lt}': {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: def extract_game_status(data: dict) -> str:
@@ -254,21 +255,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) url = URL_BOX_SCORE.format(host=HOST, league=league, game_id=game_id, lang=lang)
return fetch_json(url, session=session) 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) url = URL_PLAY_BY_PLAY.format(host=HOST, league=league, game_id=game_id, lang=lang)
return fetch_json(url, session=session) 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) url = URL_LIVE_STATUS.format(host=HOST, league=league, game_id=game_id, lang=lang)
return fetch_json(url, session=session) return fetch_json(url, session=session)
def _now_iso() -> str: def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def _get(d: dict | None, *path, default=None): def _get(d: dict | None, *path, default=None):
"""Безопасно достаём вложенные ключи: _get(d, "result", "fullScore", default={})""" """Безопасно достаём вложенные ключи: _get(d, "result", "fullScore", default={})"""
cur = d or {} cur = d or {}
@@ -278,6 +290,7 @@ def _get(d: dict | None, *path, default=None):
cur = cur[p] cur = cur[p]
return cur return cur
def _dedup_plays(plays: List[dict]) -> List[dict]: def _dedup_plays(plays: List[dict]) -> List[dict]:
""" """
Удаляем дубли по стабильному идентификатору события. Удаляем дубли по стабильному идентификатору события.
@@ -298,9 +311,16 @@ def _dedup_plays(plays: List[dict]) -> List[dict]:
seen.add(key) seen.add(key)
out.append(ev) out.append(ev)
# если есть поле sequence/time — отсортируем, чтобы обработчик получал стабильный порядок # если есть поле 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 return out
def merge_online_payloads( def merge_online_payloads(
game: dict, game: dict,
box_score: dict | None, box_score: dict | None,
@@ -312,41 +332,38 @@ def merge_online_payloads(
Ничего не знает о внутренней логике обработки — только нормализует. Ничего не знает о внутренней логике обработки — только нормализует.
""" """
# исходные куски # исходные куски
plays_raw: List[dict] = _get(play_by_play, "result", default=[]) or [] # plays_raw: List[dict] = _get(play_by_play, "result", default=[]) or []
score_by_periods = _get(box_score, "result", "scoreByPeriods", default=[]) or [] # score_by_periods = _get(box_score, "result", "scoreByPeriods", default=[]) or []
full_score = _get(box_score, "result", "fullScore", default={}) or {} # full_score = _get(box_score, "result", "fullScore", default={}) or {}
teams = _get(box_score, "result", "teams", default={}) or {} # если пригодится в обработчике # teams = _get(box_score, "result", "teams", default={}) or {} # если пригодится в обработчике
players = _get(box_score, "result", "players", default=[]) or [] # players = _get(box_score, "result", "players", default=[]) or []
# live
period = _get(live_status, "result", "period") # box_score = _get(box_score, "result", "teams", default=[]) or []
clock = _get(live_status, "result", "clock") # fullScore = _get(box_score, "result", "fullScore", default="") or ""
status = _get(live_status, "result", "status") # e.g., "inprogress", "ended", "scheduled"
# # 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] = { merged: Dict[str, Any] = {
"meta": { "meta": {
"generatedAt": _now_iso(), "generatedAt": _now_iso(),
"sourceHints": { "sourceHints": {
"boxScoreHas": list((_get(box_score, "result") or {}).keys()), "boxScoreHas": list((_get(box_score, "result") or {}).keys()),
"pbpLen": len(plays), "pbpLen": "",
}, },
}, },
"result": { "result": game,
# то, что просил: три ключа (плюс ещё полезные поля)
"plays": plays,
"scoreByPeriods": score_by_periods,
"fullScore": full_score,
# добавим live — обработчику пригодится
"period": period,
"clock": clock,
"status": status,
# опционально: передадим команды/игроков, если есть в box score
"teams": teams,
"players": players,
},
} }
return merged return merged
@@ -381,6 +398,7 @@ def is_already_merged(obj: dict) -> bool:
and isinstance(r.get("scoreByPeriods", []), list) and isinstance(r.get("scoreByPeriods", []), list)
) )
def ensure_merged_payload( def ensure_merged_payload(
game_or_merged: dict | None = None, game_or_merged: dict | None = None,
*, *,
@@ -423,20 +441,26 @@ def ensure_merged_payload(
}, },
"result": g, # положим сырой ответ целиком — чтобы файл гарантированно записался "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: def atomic_write_json(path: str | Path, data: dict, ensure_dirs: bool = True) -> None:
path = Path(path) path = Path(path)
if ensure_dirs: if ensure_dirs:
path.parent.mkdir(parents=True, exist_ok=True) 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) json.dump(data, tmp, ensure_ascii=False, indent=2)
tmp.flush() tmp.flush()
os.fsync(tmp.fileno()) os.fsync(tmp.fileno())
tmp_name = tmp.name tmp_name = tmp.name
os.replace(tmp_name, path) os.replace(tmp_name, path)
def format_time(seconds: float | int) -> str: def format_time(seconds: float | int) -> str:
""" """
Форматирует время в секундах в строку "M:SS". Форматирует время в секундах в строку "M:SS".
@@ -456,7 +480,9 @@ 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) -> None: def Json_Team_Generation(
merged: dict, *, out_dir: str = "static", who: str | None = None
) -> None:
""" """
Единая точка: принимает уже нормализованный merged, делает нужные вычисления (если надо) Единая точка: принимает уже нормализованный merged, делает нужные вычисления (если надо)
и сохраняет в JSON. и сохраняет в JSON.
@@ -503,11 +529,7 @@ def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | No
team = [] team = []
for item in starts: for item in starts:
player = { player = {
"id": ( "id": (item["personId"] if item["personId"] else ""),
item["personId"]
if item["personId"]
else ""
),
"num": item["displayNumber"], "num": item["displayNumber"],
"startRole": item["startRole"], "startRole": item["startRole"],
"role": item["positionName"], "role": item["positionName"],
@@ -517,29 +539,21 @@ def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | No
for r in role_list for r in role_list
if r[0].lower() == item["positionName"].lower() if r[0].lower() == item["positionName"].lower()
][0] ][0]
if any( if any(r[0].lower() == item["positionName"].lower() for r in role_list)
r[0].lower() == item["positionName"].lower()
for r in role_list
)
else "" else ""
), ),
"NameGFX": ( "NameGFX": (
f"{item['firstName'].strip()} {item['lastName'].strip()}" f"{item['firstName'].strip()} {item['lastName'].strip()}"
if item["firstName"] is not None if item["firstName"] is not None and item["lastName"] is not None
and item["lastName"] is not None
else "Команда" else "Команда"
), ),
"captain": item["isCapitan"], "captain": item["isCapitan"],
"age": item["age"] if item["age"] is not None else 0, "age": item["age"] if item["age"] is not None else 0,
"height": f'{item["height"]} cm' if item["height"] else 0, "height": f'{item["height"]} cm' if item["height"] else 0,
"weight": f'{item["weight"]} kg' if item["weight"] else 0, "weight": f'{item["weight"]} kg' if item["weight"] else 0,
"isStart": ( "isStart": (item["stats"]["isStart"] if item["stats"] else False),
item["stats"]["isStart"] if item["stats"] else False
),
"isOn": ( "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", "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, "pts": item["stats"]["points"] if item["stats"] else 0,
@@ -599,23 +613,15 @@ def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | No
if item["stats"] if item["stats"]
else 0 else 0
), ),
"time": ( "time": (format_time(item["stats"]["second"]) if item["stats"] else "0:00"),
format_time(item["stats"]["second"])
if item["stats"]
else "0:00"
),
"pts1q": 0, "pts1q": 0,
"pts2q": 0, "pts2q": 0,
"pts3q": 0, "pts3q": 0,
"pts4q": 0, "pts4q": 0,
"pts1h": 0, "pts1h": 0,
"pts2h": 0, "pts2h": 0,
"Name1GFX": ( "Name1GFX": (item["firstName"].strip() if item["firstName"] else ""),
item["firstName"].strip() if item["firstName"] else "" "Name2GFX": (item["lastName"].strip() if item["lastName"] else ""),
),
"Name2GFX": (
item["lastName"].strip() if item["lastName"] else ""
),
"photoGFX": ( "photoGFX": (
os.path.join( os.path.join(
"D:\\Photos", "D:\\Photos",
@@ -629,9 +635,7 @@ def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | No
else "" else ""
), ),
# "season": text, # "season": text,
"isOnCourt": ( "isOnCourt": (item["stats"]["isOnCourt"] if item["stats"] else False),
item["stats"]["isOnCourt"] if item["stats"] else False
),
# "AvgPoints": ( # "AvgPoints": (
# row_player_season_avg["points"] # row_player_season_avg["points"]
# if row_player_season_avg # if row_player_season_avg
@@ -1012,9 +1016,7 @@ def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | No
) )
for key in team[0].keys() for key in team[0].keys()
} }
for _ in range( for _ in range((4 if count_player <= 4 else 12) - count_player)
(4 if count_player <= 4 else 12) - count_player
)
] ]
team.extend(empty_rows) team.extend(empty_rows)
role_priority = { role_priority = {
@@ -1069,6 +1071,7 @@ def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | No
atomic_write_json(out_path, started_team) atomic_write_json(out_path, started_team)
logging.info("Сохранил payload: {out_path}") logging.info("Сохранил payload: {out_path}")
def time_outs_func(data_pbp: list[dict]) -> tuple[str, int, str, int]: def time_outs_func(data_pbp: list[dict]) -> tuple[str, int, str, int]:
""" """
Вычисляет количество оставшихся таймаутов для обеих команд Вычисляет количество оставшихся таймаутов для обеих команд
@@ -1124,6 +1127,7 @@ def time_outs_func(data_pbp: list[dict]) -> tuple[str, int, str, int]:
return t1_str, t1_left, t2_str, t2_left return t1_str, t1_left, t2_str, t2_left
def add_data_for_teams(new_data: list[dict]) -> tuple[float, list, float]: def add_data_for_teams(new_data: list[dict]) -> tuple[float, list, float]:
""" """
Возвращает усреднённые статистики команды: Возвращает усреднённые статистики команды:
@@ -1174,6 +1178,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] points = [points_start, points_start_pro, points_bench, points_bench_pro]
return avg_age, points, avg_height return avg_age, points, avg_height
def add_new_team_stat( def add_new_team_stat(
data: dict, data: dict,
avg_age: float, avg_age: float,
@@ -1243,6 +1248,7 @@ def add_new_team_stat(
return data return data
stat_name_list = [ stat_name_list = [
("points", "Очки", "points"), ("points", "Очки", "points"),
("pt-1", "Штрафные", "free throws"), ("pt-1", "Штрафные", "free throws"),
@@ -1305,22 +1311,14 @@ def Team_Both_Stat(merged: dict, *, out_dir: str = "static") -> None:
# time.sleep() # time.sleep()
# Таймауты # Таймауты
timeout_str1, timeout_left1, timeout_str2, timeout_left2 = time_outs_func( timeout_str1, timeout_left1, timeout_str2, timeout_left2 = time_outs_func(plays)
plays
)
# Возраст, очки, рост # Возраст, очки, рост
avg_age_1, points_1, avg_height_1 = add_data_for_teams( avg_age_1, points_1, avg_height_1 = add_data_for_teams(team_1.get("starts", []))
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( logger.debug("Нет total у команд — пропускаю перезапись team_stats.json")
"Нет total у команд — пропускаю перезапись team_stats.json"
)
# Форматирование общей статистики (как и было) # Форматирование общей статистики (как и было)
total_1 = add_new_team_stat( total_1 = add_new_team_stat(
@@ -1340,19 +1338,14 @@ def Team_Both_Stat(merged: dict, *, out_dir: str = "static") -> None:
timeout_left2, timeout_left2,
) )
# Финальный JSON # Финальный JSON
result_json = [] result_json = []
for key in total_1: for key in total_1:
val1 = ( val1 = (
int(total_1[key]) int(total_1[key]) if isinstance(total_1[key], float) else total_1[key]
if isinstance(total_1[key], float)
else total_1[key]
) )
val2 = ( val2 = (
int(total_2[key]) int(total_2[key]) if isinstance(total_2[key], float) else total_2[key]
if isinstance(total_2[key], float)
else total_2[key]
) )
stat_rus, stat_eng = "", "" stat_rus, stat_eng = "", ""
for s in stat_name_list: for s in stat_name_list:
@@ -1376,9 +1369,7 @@ def Team_Both_Stat(merged: dict, *, out_dir: str = "static") -> None:
logger.debug("Успешно записаны данные в team_stats.json") logger.debug("Успешно записаны данные в team_stats.json")
except Exception as e: except Exception as e:
logger.error( logger.error(f"Ошибка при обработке командной статистики: {e}", exc_info=True)
f"Ошибка при обработке командной статистики: {e}", exc_info=True
)
def Referee(merged: dict, *, out_dir: str = "static") -> None: def Referee(merged: dict, *, out_dir: str = "static") -> None:
@@ -1411,9 +1402,7 @@ def Referee(merged: dict, *, out_dir: str = "static") -> None:
referees = [] referees = []
for r in referees_raw: for r in referees_raw:
flag_code = ( flag_code = r.get("countryId", "").lower() if r.get("countryName") else ""
r.get("countryId", "").lower() if r.get("countryName") else ""
)
referees.append( referees.append(
{ {
"displayNumber": r.get("displayNumber", ""), "displayNumber": r.get("displayNumber", ""),
@@ -1454,9 +1443,7 @@ def Scores_Quarter(merged: dict, *, out_dir: str = "static") -> None:
score_by_quarter = [{"Q": q, "score1": "", "score2": ""} for q in quarters] score_by_quarter = [{"Q": q, "score1": "", "score2": ""} for q in quarters]
try: try:
# Сначала пробуем fullScore # Сначала пробуем fullScore
full_score_str = ( full_score_str = merged.get("result", {}).get("game", {}).get("fullScore", "")
merged.get("result", {}).get("game", {}).get("fullScore", "")
)
if full_score_str: if full_score_str:
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)]):
@@ -1484,6 +1471,34 @@ def Scores_Quarter(merged: dict, *, out_dir: str = "static") -> None:
logger.error(f"Ошибка в Scores_Quarter: {e}", exc_info=True) 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
# ========================== # ==========================
# ---- ДОМЕННАЯ ЛОГИКА # ---- ДОМЕННАЯ ЛОГИКА
# ========================== # ==========================
@@ -1492,7 +1507,9 @@ def Scores_Quarter(merged: dict, *, out_dir: str = "static") -> None:
def validate_league_or_die(league: str) -> str: def validate_league_or_die(league: str) -> str:
league = (league or DEFAULT_LEAGUE).lower().strip() league = (league or DEFAULT_LEAGUE).lower().strip()
if league not in ALLOWED_LEAGUES: if league not in ALLOWED_LEAGUES:
logger.warning(f"Неверный тег лиги: '{league}'. Допустимо: {sorted(ALLOWED_LEAGUES)}") logger.warning(
f"Неверный тег лиги: '{league}'. Допустимо: {sorted(ALLOWED_LEAGUES)}"
)
sys.exit(2) sys.exit(2)
return league return league
@@ -1502,16 +1519,16 @@ def get_last_season_or_die(league: str, lang: str) -> str:
try: try:
data = fetch_json(url) data = fetch_json(url)
season = extract_last_season(data) season = extract_last_season(data)
logging.info( logging.info(f"Последний сезон для {league}: {season}")
f"Последний сезон для {league}: {season}"
)
return season return season
except Exception as e: except Exception as e:
logger.warning(f"Не получилось получить последний сезон для {league}: {e}") logger.warning(f"Не получилось получить последний сезон для {league}: {e}")
sys.exit(3) 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) url = URL_SCHEDULE.format(host=HOST, league=league, season=season, lang=lang)
try: try:
data = fetch_json(url) data = fetch_json(url)
@@ -1584,6 +1601,7 @@ class PostProcessor:
Team_Both_Stat(merged, out_dir="static") Team_Both_Stat(merged, out_dir="static")
Referee(merged, out_dir="static") Referee(merged, out_dir="static")
Scores_Quarter(merged, out_dir="static") Scores_Quarter(merged, out_dir="static")
status_online_func(merged, out_dir="static")
except Exception as e: except Exception as e:
logging.exception(f"Postproc failed: {e}") logging.exception(f"Postproc failed: {e}")
@@ -1592,7 +1610,9 @@ class PostProcessor:
class OnlinePoller: 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.league = league
self.game_id = game_id self.game_id = game_id
self.lang = lang self.lang = lang
@@ -1605,19 +1625,24 @@ class OnlinePoller:
# 1) Постоянная сессия и пул соединений # 1) Постоянная сессия и пул соединений
self._session = requests.Session() self._session = requests.Session()
retry = Retry( 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), status_forcelist=(502, 503, 504),
allowed_methods=frozenset(["GET"]) allowed_methods=frozenset(["GET"]),
) )
adapter = HTTPAdapter(pool_connections=1, pool_maxsize=10, max_retries=retry) adapter = HTTPAdapter(pool_connections=1, pool_maxsize=10, max_retries=retry)
self._session.mount("http://", adapter) self._session.mount("http://", adapter)
self._session.mount("https://", adapter) self._session.mount("https://", adapter)
self._session.headers.update({ self._session.headers.update(
{
"Connection": "keep-alive", "Connection": "keep-alive",
"Accept": "application/json, */*", "Accept": "application/json, */*",
"Accept-Encoding": "gzip, deflate, br", "Accept-Encoding": "gzip, deflate, br",
"User-Agent": "game-watcher/1.0" "User-Agent": "game-watcher/1.0",
}) }
)
def stop(self): def stop(self):
if self._thread and self._thread.is_alive(): if self._thread and self._thread.is_alive():
@@ -1641,9 +1666,27 @@ class OnlinePoller:
started = time.perf_counter() started = time.perf_counter()
try: try:
futures = [ futures = [
pool.submit(fetch_box_score, self.league, self.game_id, self.lang, self._session), pool.submit(
pool.submit(fetch_play_by_play, self.league, self.game_id, self.lang, self._session), fetch_box_score,
pool.submit(fetch_live_status, self.league, self.game_id, self.lang, self._session), 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) bs, pbp, ls = (f.result() for f in futures)
merged = ensure_merged_payload( merged = ensure_merged_payload(
@@ -1691,7 +1734,10 @@ class OnlinePoller:
self._thread.start() self._thread.start()
self._log.info(f"Онлайн-поллер для игры {self.game_id} запущен.") 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}).") logger.info(f"Старт мониторинга игры {game_id} ({league}).")
poller = OnlinePoller(league, game_id, lang) poller = OnlinePoller(league, game_id, lang)
was_online = False was_online = False
@@ -1707,10 +1753,14 @@ def monitor_game_loop(league: str, game_id: str, lang:str, stop_event: threading
break break
if is_online and not was_online: if is_online and not was_online:
logger.info(f"Матч {game_id} перешёл в онлайн.\nЗапускаем быстрый опрос (1 сек).") logger.info(
f"Матч {game_id} перешёл в онлайн.\nЗапускаем быстрый опрос (1 сек)."
)
poller.start() poller.start()
elif not is_online and was_online: elif not is_online and was_online:
logger.info(f"Матч {game_id} вышел из онлайна (или ещё не стартовал).\nОстанавливаем быстрый опрос.") logger.info(
f"Матч {game_id} вышел из онлайна (или ещё не стартовал).\nОстанавливаем быстрый опрос."
)
poller.stop() poller.stop()
was_online = is_online was_online = is_online
@@ -1730,7 +1780,9 @@ def monitor_game_loop(league: str, game_id: str, lang:str, stop_event: threading
def next_midnight_local(now: datetime) -> datetime: def next_midnight_local(now: datetime) -> datetime:
tomorrow = (now + timedelta(days=1)).date() 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) # return now + timedelta(seconds=30)
@@ -1834,9 +1886,7 @@ def main():
parser.add_argument( parser.add_argument(
"--team", type=str, required=True, help="код/тег команды (например, BOS)" "--team", type=str, required=True, help="код/тег команды (например, BOS)"
) )
parser.add_argument( parser.add_argument("--lang", type=str, default="en", help="язык получения данных")
"--lang", type=str, default="en", help="язык получения данных"
)
parser.add_argument( parser.add_argument(
"--log-level", type=str, default="INFO", help="DEBUG|INFO|WARNING|ERROR" "--log-level", type=str, default="INFO", help="DEBUG|INFO|WARNING|ERROR"
) )
@@ -1844,7 +1894,9 @@ def main():
print(args) print(args)
# logger.info(f"Запуск программы пользователем: {MYHOST}") # 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) league = validate_league_or_die(args.league)
team = args.team.lower() team = args.team.lower()
@@ -1875,20 +1927,23 @@ def main():
if last_played: if last_played:
game_id = last_played["game"]["id"] game_id = last_played["game"]["id"]
try: 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) game_json = fetch_json(url)
merged = ensure_merged_payload( merged = ensure_merged_payload(
game_json, game_json,
game_meta={ game_meta={
"id": game_json.get("result", {}).get("gameId"), "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="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") Team_Both_Stat(merged, out_dir="static")
Referee(merged, out_dir="static") Referee(merged, out_dir="static")
Scores_Quarter(merged, out_dir="static") Scores_Quarter(merged, out_dir="static")
status_online_func(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Мониторинг не запускаю."