diff --git a/.gitignore b/.gitignore index e69de29..2eea525 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/__pycache__/get_data.cpython-312.pyc b/__pycache__/get_data.cpython-312.pyc new file mode 100644 index 0000000..3cab7a8 Binary files /dev/null and b/__pycache__/get_data.cpython-312.pyc differ diff --git a/get_data.py b/get_data.py index e69de29..fb373b2 100644 --- a/get_data.py +++ b/get_data.py @@ -0,0 +1,330 @@ +from fastapi import FastAPI +from fastapi.responses import Response, JSONResponse, HTMLResponse +import pandas as pd +import requests, io, os +from requests.auth import HTTPBasicAuth +from dotenv import load_dotenv +from datetime import datetime +import uvicorn +from threading import Thread, Event, Lock +import time +from contextlib import asynccontextmanager + + +# --- Глобальные переменные --- +selected_game_id: int | None = None +current_tournament_id: int | None = None # будем обновлять при загрузке расписания +latest_game_data: dict | None = None # сюда кладём последние данные по матчу +latest_game_error: str | None = None +_latest_lock = Lock() +_stop_event = Event() +_worker_thread: Thread | None = None + +# Загружаем переменные из .env +load_dotenv() +api_user = os.getenv("API_USER") +api_pass = os.getenv("API_PASS") +league = os.getenv("LEAGUE") +POLL_SEC = int(os.getenv("GAME_POLL_SECONDS")) + + + +def load_today_schedule(): + """Возвращает DataFrame матчей на сегодня с нужными колонками (или пустой DF).""" + url_tournaments = "http://stat2tv.khl.ru/tournaments.xml" + r = requests.get( + url_tournaments, auth=HTTPBasicAuth(api_user, api_pass), verify=False + ) + df = pd.read_xml(io.StringIO(r.text)) + + df["startDate"] = pd.to_datetime(df["startDate"], errors="coerce") + df["endDate"] = pd.to_datetime(df["endDate"], errors="coerce") + now = datetime.now() + + filtered = df[ + (df["level"] == league) + & (df["startDate"] <= now) + & (df["endDate"] >= now) + & (df["seasonPart"] == "regular") + ] + if filtered.empty: + return pd.DataFrame() + + tournament_id = int(filtered.iloc[0]["id"]) + global current_tournament_id + current_tournament_id = tournament_id + url_schedule = f"http://stat2tv.khl.ru/{tournament_id}/schedule-{tournament_id}.xml" + r = requests.get(url_schedule, auth=HTTPBasicAuth(api_user, api_pass), verify=False) + schedule_df = pd.read_xml(io.StringIO(r.text)) + + # Нужные колонки (скорректируй под реальные имена из XML) + needed_columns = ["id", "date", "time", "homeName_en", "visitorName_en", "arena"] + exist = [c for c in needed_columns if c in schedule_df.columns] + schedule_df = schedule_df[exist].copy() + + # Преобразуем дату и время + schedule_df["date"] = pd.to_datetime(schedule_df["date"], errors="coerce") + today = now.date() + schedule_today = schedule_df[schedule_df["date"].dt.date == today].copy() + + # --- Нормализуем время и строим единый datetime для сортировки --- + # 1) берём из time только HH:MM (на случай '19:30', '19:30:00', '19:30 MSK', и т.п.) + time_clean = ( + schedule_today.get("time") + .astype(str) + .str.extract(r"(?P\d{1,2}:\d{2})", expand=True)["hhmm"] + ) + + # 2) собираем строку "YYYY-MM-DD HH:MM" и парсим в Timestamp + date_str = schedule_today["date"].dt.strftime("%Y-%m-%d") + kickoff_str = (date_str + " " + time_clean.fillna("")).str.strip() + schedule_today["kickoff_dt"] = pd.to_datetime(kickoff_str, errors="coerce") + + # 3) сортировка: сначала по времени (NaT в конец), потом по id + schedule_today = schedule_today.sort_values( + by=["kickoff_dt", "id"], ascending=[True, True], na_position="last" + ) + + # 4) человекочитаемая строка даты/времени для таблицы + schedule_today["datetime_str"] = schedule_today["kickoff_dt"].dt.strftime( + "%d.%m.%Y %H:%M" + ) + # если time отсутствует и kickoff_dt = NaT — показываем просто дату + mask_nat = schedule_today["kickoff_dt"].isna() + schedule_today.loc[mask_nat, "datetime_str"] = schedule_today.loc[ + mask_nat, "date" + ].dt.strftime("%d.%m.%Y") + + return schedule_today + + +def _build_game_url(tournament_id: int, game_id: int) -> str: + # URL по аналогии с расписанием: .../{tournament_id}/json_en/{game_id}.json + # Если у тебя другой шаблон — просто поменяй строку ниже. + return f"http://stat2tv.khl.ru/{tournament_id}/json_en/{game_id}.json" + +def _fetch_game_once(tournament_id: int, game_id: int) -> dict: + """Один запрос к API матча -> чистый JSON из API.""" + url = _build_game_url(tournament_id, game_id) + r = requests.get( + url, + auth=HTTPBasicAuth(api_user, api_pass), + verify=False, + timeout=10 + ) + r.raise_for_status() + + # Пробуем распарсить JSON + try: + data = r.json() + except ValueError: + # если это не JSON — вернём текст + data = {"raw": r.text} + + return {"url": url, "json": data} + + +def _game_poll_worker(): + """Фоновый цикл: опрашивает API для выбранного game_id.""" + global latest_game_data, latest_game_error + while not _stop_event.is_set(): + gid = selected_game_id + tid = current_tournament_id + if gid and tid: + try: + data = _fetch_game_once(tid, gid) + with _latest_lock: + latest_game_data = { + "tournament_id": tid, + "game_id": gid, + "fetched_at": datetime.now().isoformat(), + "data": data, + } + latest_game_error = None + except Exception as e: + with _latest_lock: + latest_game_error = f"{type(e).__name__}: {e}" + # Ждём интервал (с возможностью ранней остановки) + _stop_event.wait(POLL_SEC) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Запускаем и останавливаем поток чтения данных при старте/остановке приложения.""" + global _worker_thread + _stop_event.clear() + _worker_thread = Thread(target=_game_poll_worker, daemon=True) + _worker_thread.start() + print("✅ Background thread started") + + # Отдаём управление FastAPI + yield + + # Останавливаем при завершении + _stop_event.set() + if _worker_thread.is_alive(): + _worker_thread.join(timeout=2) + print("🛑 Background thread stopped") + +app = FastAPI(lifespan=lifespan) + + +@app.get("/games") +async def games(): + df = load_today_schedule() + if df.empty: + return JSONResponse({"message": "Сегодня матчей нет"}) + + json_schedule = df.to_json(orient="records", force_ascii=False, date_format="iso") + return Response(content=json_schedule, media_type="application/json") + + +@app.get("/games/html") +async def games_html(): + df = load_today_schedule() + if df.empty: + return HTMLResponse( + "

Сегодня матчей нет

" + ) + + # Строим строки таблицы + rows_html = [] + for _, row in df.iterrows(): + gid = int(row["id"]) + home = row.get("homeName_en", "") + away = row.get("visitorName_en", "") + when = row.get("datetime_str", "") + arena = row.get("arena", "") + rows_html.append( + f""" + + {gid} + {when} + {home} + {away} + {arena} + + + """ + ) + + # ✅ Весь HTML, включая JS, внутри тройных кавычек + html = f""" + + + +Выбор матча + +

Матчи сегодня

+ + + + + + + + {''.join(rows_html)} + +
IDДата/времяХозяеваГостиАрена
+
Ничего не выбрано
+ + +""" + return HTMLResponse(html) + + +@app.post("/select-game/{game_id}") +async def select_game(game_id: int): + global selected_game_id, latest_game_data, latest_game_error + selected_game_id = game_id + + # моментально подтянуть первое состояние, если известен турнир + if current_tournament_id: + try: + data = _fetch_game_once(current_tournament_id, selected_game_id) + with _latest_lock: + latest_game_data = { + "tournament_id": current_tournament_id, + "game_id": selected_game_id, + "fetched_at": datetime.now().isoformat(), + "data": data, + } + latest_game_error = None + except Exception as e: + with _latest_lock: + latest_game_error = f"{type(e).__name__}: {e}" + + return JSONResponse({"selected_id": selected_game_id}) + + +@app.get("/selected-game") +async def get_selected_game(): + return JSONResponse({"selected_id": selected_game_id}) + + +@app.get("/game/url") +async def game_url(): + if not (selected_game_id and current_tournament_id): + return JSONResponse({"message": "game_id или tournament_id не задан"}) + return JSONResponse({ + "url": _build_game_url(current_tournament_id, selected_game_id), + "game_id": selected_game_id, + "tournament_id": current_tournament_id + }) + +@app.get("/game/data") +async def game_data(): + with _latest_lock: + if latest_game_data: + return JSONResponse(latest_game_data) + if latest_game_error: + return JSONResponse({"error": latest_game_error}, status_code=502) + return JSONResponse({"message": "Ещё нет данных. Выберите матч и подождите первое обновление."}) + +if __name__ == "__main__": + uvicorn.run( + "get_data:app", host="0.0.0.0", port=8000, reload=True, log_level="debug" + ) diff --git a/requirements.txt b/requirements.txt index 85d6550..775676b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ numpy>=1.24.0 fastapi>=0.115.0 uvicorn>=0.30.0 requests>=2.31.0 -python-telegram-handler \ No newline at end of file +python-telegram-handler +python-dotenv>=1.1.0 \ No newline at end of file