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

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
# -*- coding: utf-8 -*-
@@ -72,9 +71,7 @@ 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}"
# Быстрые запросы, когда матч онлайн (каждую секунду)
@@ -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 с таймаутом и внятными ошибками.
Использует переданный session для keep-alive.
@@ -237,7 +236,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 +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)
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 +290,7 @@ def _get(d: dict | None, *path, default=None):
cur = cur[p]
return cur
def _dedup_plays(plays: List[dict]) -> List[dict]:
"""
Удаляем дубли по стабильному идентификатору события.
@@ -298,9 +311,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 +332,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 +398,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 +441,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 +480,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.
@@ -498,16 +524,12 @@ def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | No
("Shooting Guard", "SG"),
("Point Guard", "PG"),
("Forward-Center", "FC"),
]
]
starts = payload["starts"]
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 +539,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 +613,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 +635,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 +1016,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 +1071,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 +1127,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 +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]
return avg_age, points, avg_height
def add_new_team_stat(
data: dict,
avg_age: float,
@@ -1243,6 +1248,7 @@ def add_new_team_stat(
return data
stat_name_list = [
("points", "Очки", "points"),
("pt-1", "Штрафные", "free throws"),
@@ -1305,22 +1311,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 +1338,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 +1369,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 +1402,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 +1443,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 +1471,34 @@ 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
# ==========================
# ---- ДОМЕННАЯ ЛОГИКА
# ==========================
@@ -1492,7 +1507,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 +1519,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)
@@ -1545,7 +1562,7 @@ def pick_today_or_last_played(
return today_game, last_played
def is_game_online(league: str, game_id: str, lang:str) -> str:
def is_game_online(league: str, game_id: str, lang: str) -> str:
"""
Возвращает статус: inprogress|scheduled|finished (или то, что твой API даёт).
"""
@@ -1584,6 +1601,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 +1610,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 +1625,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 +1666,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 +1734,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 +1753,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 +1780,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 +1886,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 +1894,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()
@@ -1875,20 +1927,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Мониторинг не запускаю."