добавленны создание онлайн/оффлайн фолов и поправил функцию на склеивание онлайн данных (нужно проверить)
This commit is contained in:
303
get_data.py
303
get_data.py
@@ -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Мониторинг не запускаю."
|
||||||
|
|||||||
Reference in New Issue
Block a user