в процессе логирования
This commit is contained in:
452
get_data.py
452
get_data.py
@@ -9,13 +9,14 @@ import time
|
|||||||
import queue
|
import queue
|
||||||
import argparse
|
import argparse
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from pprint import pprint
|
|
||||||
import os
|
import os
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, time as dtime, timedelta
|
from datetime import datetime, time as dtime, timedelta
|
||||||
from fastapi.responses import Response
|
from fastapi.responses import Response
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
import platform
|
||||||
|
|
||||||
# передадим параметры через аргументы или глобальные переменные
|
# передадим параметры через аргументы или глобальные переменные
|
||||||
|
|
||||||
@@ -26,16 +27,72 @@ parser.add_argument("--team", required=True)
|
|||||||
parser.add_argument("--lang", default="en")
|
parser.add_argument("--lang", default="en")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
MYHOST = platform.node()
|
||||||
|
|
||||||
|
if not os.path.exists("logs"):
|
||||||
|
os.makedirs("logs")
|
||||||
|
|
||||||
|
telegram_bot_token = "7639240596:AAH0YtdQoWZSC-_R_EW4wKAHHNLIA0F_ARY"
|
||||||
|
# TELEGRAM_CHAT_ID = 228977654
|
||||||
|
telegram_chat_id = -4803699526
|
||||||
|
log_config = {
|
||||||
|
"version": 1,
|
||||||
|
"handlers": {
|
||||||
|
"telegram": {
|
||||||
|
"class": "telegram_handler.TelegramHandler",
|
||||||
|
"level": "INFO",
|
||||||
|
"token": telegram_bot_token,
|
||||||
|
"chat_id": telegram_chat_id,
|
||||||
|
"formatter": "telegram",
|
||||||
|
},
|
||||||
|
"console": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"level": "INFO",
|
||||||
|
"formatter": "simple",
|
||||||
|
"stream": "ext://sys.stdout",
|
||||||
|
},
|
||||||
|
"file": {
|
||||||
|
"class": "logging.FileHandler",
|
||||||
|
"level": "DEBUG",
|
||||||
|
"formatter": "simple",
|
||||||
|
"filename": f"logs/GFX_{MYHOST}.log",
|
||||||
|
"encoding": "utf-8",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
__name__: {"handlers": ["console", "file", "telegram"], "level": "DEBUG"},
|
||||||
|
},
|
||||||
|
"formatters": {
|
||||||
|
"telegram": {
|
||||||
|
"class": "telegram_handler.HtmlFormatter",
|
||||||
|
"format": f"%(levelname)s [{MYHOST.upper()}] %(message)s",
|
||||||
|
"use_emoji": "True",
|
||||||
|
},
|
||||||
|
"simple": {
|
||||||
|
"class": "logging.Formatter",
|
||||||
|
"format": "%(asctime)s %(levelname)-8s %(funcName)s() - %(message)s",
|
||||||
|
"datefmt": "%d.%m.%Y %H:%M:%S",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.config.dictConfig(log_config)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.handlers[2].formatter.use_emoji = True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
LEAGUE = args.league
|
LEAGUE = args.league
|
||||||
TEAM = args.team
|
TEAM = args.team
|
||||||
LANG = args.lang
|
LANG = args.lang
|
||||||
HOST = "https://pro.russiabasket.org"
|
HOST = "https://deti.russiabasket.org"
|
||||||
STATUS = False
|
STATUS = False
|
||||||
GAME_ID = None
|
GAME_ID = None
|
||||||
SEASON = None
|
SEASON = None
|
||||||
GAME_START_DT = None # datetime начала матча (локальная из календаря)
|
GAME_START_DT = None # datetime начала матча (локальная из календаря)
|
||||||
GAME_TODAY = False # флаг: игра сегодня
|
GAME_TODAY = False # флаг: игра сегодня
|
||||||
GAME_SOON = False # флаг: игра сегодня и скоро (<1 часа)
|
GAME_SOON = False # флаг: игра сегодня и скоро (<1 часа)
|
||||||
|
|
||||||
URLS = {
|
URLS = {
|
||||||
"seasons": "{host}/api/abc/comps/seasons?Tag={league}",
|
"seasons": "{host}/api/abc/comps/seasons?Tag={league}",
|
||||||
@@ -50,6 +107,158 @@ URLS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def start_offline_threads(season, game_id):
|
||||||
|
"""Запускаем редкие запросы, когда матча нет или он уже сыгран."""
|
||||||
|
global threads_offline, CURRENT_THREADS_MODE, stop_event_offline
|
||||||
|
|
||||||
|
# если уже работаем в офлайне — не дублируем
|
||||||
|
if CURRENT_THREADS_MODE == "offline":
|
||||||
|
return
|
||||||
|
|
||||||
|
# на всякий случай гасим лайв
|
||||||
|
stop_live_threads()
|
||||||
|
|
||||||
|
stop_event_offline.clear()
|
||||||
|
threads_offline = [
|
||||||
|
threading.Thread(
|
||||||
|
target=get_data_from_API,
|
||||||
|
args=(
|
||||||
|
"game",
|
||||||
|
URLS["game"].format(host=HOST, game_id=game_id, lang=LANG),
|
||||||
|
1, # раз в секунду/реже
|
||||||
|
stop_event_offline,
|
||||||
|
),
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
for t in threads_offline:
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
CURRENT_THREADS_MODE = "offline"
|
||||||
|
print("[threads] OFFLINE threads started")
|
||||||
|
|
||||||
|
|
||||||
|
def start_live_threads(season, game_id):
|
||||||
|
"""Запускаем частые онлайн-запросы, когда матч идёт/вот-вот."""
|
||||||
|
global threads_live, CURRENT_THREADS_MODE, stop_event_live
|
||||||
|
|
||||||
|
# если уже в лайве — не дублируем
|
||||||
|
if CURRENT_THREADS_MODE == "live":
|
||||||
|
return
|
||||||
|
|
||||||
|
# на всякий случай гасим офлайн
|
||||||
|
stop_offline_threads()
|
||||||
|
|
||||||
|
stop_event_live.clear()
|
||||||
|
threads_live = [
|
||||||
|
threading.Thread(
|
||||||
|
target=get_data_from_API,
|
||||||
|
args=(
|
||||||
|
"pregame",
|
||||||
|
URLS["pregame"].format(
|
||||||
|
host=HOST, league=LEAGUE, season=season, game_id=game_id, lang=LANG
|
||||||
|
),
|
||||||
|
0.0016667,
|
||||||
|
stop_event_live,
|
||||||
|
),
|
||||||
|
daemon=True,
|
||||||
|
),
|
||||||
|
threading.Thread(
|
||||||
|
target=get_data_from_API,
|
||||||
|
args=(
|
||||||
|
"pregame-full-stats",
|
||||||
|
URLS["pregame-full-stats"].format(
|
||||||
|
host=HOST, league=LEAGUE, season=season, game_id=game_id, lang=LANG
|
||||||
|
),
|
||||||
|
0.0016667,
|
||||||
|
stop_event_live,
|
||||||
|
),
|
||||||
|
daemon=True,
|
||||||
|
),
|
||||||
|
threading.Thread(
|
||||||
|
target=get_data_from_API,
|
||||||
|
args=(
|
||||||
|
"actual-standings",
|
||||||
|
URLS["actual-standings"].format(
|
||||||
|
host=HOST, league=LEAGUE, season=season, lang=LANG
|
||||||
|
),
|
||||||
|
0.0016667,
|
||||||
|
stop_event_live,
|
||||||
|
),
|
||||||
|
daemon=True,
|
||||||
|
),
|
||||||
|
threading.Thread(
|
||||||
|
target=get_data_from_API,
|
||||||
|
args=(
|
||||||
|
"game",
|
||||||
|
URLS["game"].format(host=HOST, game_id=game_id, lang=LANG),
|
||||||
|
0.00016, # часто
|
||||||
|
stop_event_live,
|
||||||
|
),
|
||||||
|
daemon=True,
|
||||||
|
),
|
||||||
|
threading.Thread(
|
||||||
|
target=get_data_from_API,
|
||||||
|
args=(
|
||||||
|
"live-status",
|
||||||
|
URLS["live-status"].format(host=HOST, game_id=game_id),
|
||||||
|
1,
|
||||||
|
stop_event_live,
|
||||||
|
),
|
||||||
|
daemon=True,
|
||||||
|
),
|
||||||
|
threading.Thread(
|
||||||
|
target=get_data_from_API,
|
||||||
|
args=(
|
||||||
|
"box-score",
|
||||||
|
URLS["box-score"].format(host=HOST, game_id=game_id),
|
||||||
|
1,
|
||||||
|
stop_event_live,
|
||||||
|
),
|
||||||
|
daemon=True,
|
||||||
|
),
|
||||||
|
threading.Thread(
|
||||||
|
target=get_data_from_API,
|
||||||
|
args=(
|
||||||
|
"play-by-play",
|
||||||
|
URLS["play-by-play"].format(host=HOST, game_id=game_id),
|
||||||
|
1,
|
||||||
|
stop_event_live,
|
||||||
|
),
|
||||||
|
daemon=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
for t in threads_live:
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
CURRENT_THREADS_MODE = "live"
|
||||||
|
print("[threads] LIVE threads started")
|
||||||
|
|
||||||
|
|
||||||
|
def stop_live_threads():
|
||||||
|
"""Гасим только live-треды."""
|
||||||
|
global threads_live
|
||||||
|
if not threads_live:
|
||||||
|
return
|
||||||
|
stop_event_live.set()
|
||||||
|
for t in threads_live:
|
||||||
|
t.join(timeout=1)
|
||||||
|
threads_live = []
|
||||||
|
print("[threads] LIVE threads stopped")
|
||||||
|
|
||||||
|
|
||||||
|
def stop_offline_threads():
|
||||||
|
"""Гасим только offline-треды."""
|
||||||
|
global threads_offline
|
||||||
|
if not threads_offline:
|
||||||
|
return
|
||||||
|
stop_event_offline.set()
|
||||||
|
for t in threads_offline:
|
||||||
|
t.join(timeout=1)
|
||||||
|
threads_offline = []
|
||||||
|
print("[threads] OFFLINE threads stopped")
|
||||||
|
|
||||||
|
|
||||||
# общая очередь
|
# общая очередь
|
||||||
results_q = queue.Queue()
|
results_q = queue.Queue()
|
||||||
# тут будем хранить последние данные
|
# тут будем хранить последние данные
|
||||||
@@ -57,6 +266,17 @@ latest_data = {}
|
|||||||
# событие для остановки потоков
|
# событие для остановки потоков
|
||||||
stop_event = threading.Event()
|
stop_event = threading.Event()
|
||||||
|
|
||||||
|
# отдельные события для разных наборов потоков
|
||||||
|
stop_event_live = threading.Event()
|
||||||
|
stop_event_offline = threading.Event()
|
||||||
|
|
||||||
|
# чтобы из consumer можно было их гасить
|
||||||
|
threads_live = []
|
||||||
|
threads_offline = []
|
||||||
|
|
||||||
|
# какой режим сейчас запущен: "live" / "offline" / None
|
||||||
|
CURRENT_THREADS_MODE = None
|
||||||
|
|
||||||
|
|
||||||
# Функция запускаемая в потоках
|
# Функция запускаемая в потоках
|
||||||
def get_data_from_API(
|
def get_data_from_API(
|
||||||
@@ -133,7 +353,9 @@ def results_consumer():
|
|||||||
and "teams" in payload["result"]
|
and "teams" in payload["result"]
|
||||||
):
|
):
|
||||||
# обновляем команды
|
# обновляем команды
|
||||||
game["data"]["result"]["game"]["fullScore"] = payload["result"]["fullScore"]
|
game["data"]["result"]["game"]["fullScore"] = payload["result"][
|
||||||
|
"fullScore"
|
||||||
|
]
|
||||||
for team in game["data"]["result"]["teams"]:
|
for team in game["data"]["result"]["teams"]:
|
||||||
if team["teamNumber"] != 0:
|
if team["teamNumber"] != 0:
|
||||||
box_team = [
|
box_team = [
|
||||||
@@ -169,17 +391,13 @@ def results_consumer():
|
|||||||
}
|
}
|
||||||
|
|
||||||
elif "live-status" in source:
|
elif "live-status" in source:
|
||||||
# просто сохраним, как и остальные
|
|
||||||
latest_data[source] = {
|
latest_data[source] = {
|
||||||
"ts": msg["ts"],
|
"ts": msg["ts"],
|
||||||
"data": payload,
|
"data": payload,
|
||||||
}
|
}
|
||||||
|
|
||||||
# попытка ДОПОЛНИТЕЛЬНО обновить глобальный STATUS по live-status
|
|
||||||
try:
|
try:
|
||||||
ls_data = payload.get("result") or payload # иногда сразу result
|
ls_data = payload.get("result") or payload
|
||||||
# тут нужно посмотреть, какое именно поле у тебя в live-status
|
|
||||||
# допустим, там есть что-то вроде "status" или "gameStatus"
|
|
||||||
raw_ls_status = (
|
raw_ls_status = (
|
||||||
ls_data.get("status")
|
ls_data.get("status")
|
||||||
or ls_data.get("gameStatus")
|
or ls_data.get("gameStatus")
|
||||||
@@ -187,27 +405,42 @@ def results_consumer():
|
|||||||
)
|
)
|
||||||
|
|
||||||
if raw_ls_status:
|
if raw_ls_status:
|
||||||
raw_ls_status = str(raw_ls_status).lower()
|
raw_ls_status_low = str(raw_ls_status).lower()
|
||||||
|
|
||||||
# варианты, которые считаем "матч окончен"
|
|
||||||
finished_markers = [
|
finished_markers = [
|
||||||
"finished",
|
"finished",
|
||||||
"result",
|
"result",
|
||||||
"resultconfirmed",
|
"resultconfirmed",
|
||||||
"ended",
|
"ended",
|
||||||
"game over",
|
|
||||||
"final",
|
"final",
|
||||||
|
"game over",
|
||||||
]
|
]
|
||||||
|
|
||||||
if any(m in raw_ls_status for m in finished_markers):
|
# матч ЗАКОНЧЕН → гасим live и включаем offline
|
||||||
# перезатираем глобальный статус — он более актуальный
|
if any(m in raw_ls_status_low for m in finished_markers):
|
||||||
# если матч сегодня — делаем finished_today, иначе просто finished
|
print("[status] match finished → switch to OFFLINE")
|
||||||
from datetime import datetime
|
if (
|
||||||
|
GAME_START_DT
|
||||||
if GAME_START_DT and GAME_START_DT.date() == datetime.now().date():
|
and GAME_START_DT.date() == datetime.now().date()
|
||||||
|
):
|
||||||
globals()["STATUS"] = "finished_today"
|
globals()["STATUS"] = "finished_today"
|
||||||
else:
|
else:
|
||||||
globals()["STATUS"] = "finished"
|
globals()["STATUS"] = "finished"
|
||||||
|
|
||||||
|
stop_live_threads()
|
||||||
|
start_offline_threads(SEASON, GAME_ID)
|
||||||
|
|
||||||
|
# матч СТАЛ онлайном (напр., из Scheduled → Online)
|
||||||
|
elif (
|
||||||
|
"online" in raw_ls_status_low or "live" in raw_ls_status_low
|
||||||
|
):
|
||||||
|
if globals().get("STATUS") not in ["live", "live_soon"]:
|
||||||
|
print(
|
||||||
|
"[status] match became LIVE → switch to LIVE threads"
|
||||||
|
)
|
||||||
|
globals()["STATUS"] = "live"
|
||||||
|
start_live_threads(SEASON, GAME_ID)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("results_consumer: live-status postprocess error:", e)
|
print("results_consumer: live-status postprocess error:", e)
|
||||||
|
|
||||||
@@ -238,7 +471,9 @@ def results_consumer():
|
|||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
# 👉 уже есть какой-то game — неполным НЕ затираем
|
# 👉 уже есть какой-то game — неполным НЕ затираем
|
||||||
print("results_consumer: got partial game, keeping previous one")
|
print(
|
||||||
|
"results_consumer: got partial game, keeping previous one"
|
||||||
|
)
|
||||||
|
|
||||||
# и обязательно continue/return из этого elif/if
|
# и обязательно continue/return из этого elif/if
|
||||||
else:
|
else:
|
||||||
@@ -253,6 +488,7 @@ def results_consumer():
|
|||||||
print("results_consumer error:", repr(e))
|
print("results_consumer error:", repr(e))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
def get_items(data: dict) -> list:
|
def get_items(data: dict) -> list:
|
||||||
"""
|
"""
|
||||||
Мелкий хелпер: берём первый список в ответе API.
|
Мелкий хелпер: берём первый список в ответе API.
|
||||||
@@ -268,6 +504,7 @@ def get_items(data: dict) -> list:
|
|||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
def pick_game_for_team(calendar_json):
|
def pick_game_for_team(calendar_json):
|
||||||
"""
|
"""
|
||||||
Возвращает:
|
Возвращает:
|
||||||
@@ -292,7 +529,11 @@ def pick_game_for_team(calendar_json):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
gdt = extract_game_datetime(game)
|
gdt = extract_game_datetime(game)
|
||||||
gdate = gdt.date() if gdt else datetime.strptime(game["game"]["localDate"], "%d.%m.%Y").date()
|
gdate = (
|
||||||
|
gdt.date()
|
||||||
|
if gdt
|
||||||
|
else datetime.strptime(game["game"]["localDate"], "%d.%m.%Y").date()
|
||||||
|
)
|
||||||
|
|
||||||
if gdate == today:
|
if gdate == today:
|
||||||
cal_status = game["game"].get("gameStatus")
|
cal_status = game["game"].get("gameStatus")
|
||||||
@@ -308,7 +549,11 @@ def pick_game_for_team(calendar_json):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
gdt = extract_game_datetime(game)
|
gdt = extract_game_datetime(game)
|
||||||
gdate = gdt.date() if gdt else datetime.strptime(game["game"]["localDate"], "%d.%m.%Y").date()
|
gdate = (
|
||||||
|
gdt.date()
|
||||||
|
if gdt
|
||||||
|
else datetime.strptime(game["game"]["localDate"], "%d.%m.%Y").date()
|
||||||
|
)
|
||||||
|
|
||||||
if gdate <= today:
|
if gdate <= today:
|
||||||
last_id = game["game"]["id"]
|
last_id = game["game"]["id"]
|
||||||
@@ -319,7 +564,6 @@ def pick_game_for_team(calendar_json):
|
|||||||
return last_id, last_dt, False, last_status
|
return last_id, last_dt, False, last_status
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def extract_game_datetime(game_item: dict) -> datetime | None:
|
def extract_game_datetime(game_item: dict) -> datetime | None:
|
||||||
"""
|
"""
|
||||||
Из элемента календаря достаём datetime матча.
|
Из элемента календаря достаём datetime матча.
|
||||||
@@ -336,7 +580,7 @@ def extract_game_datetime(game_item: dict) -> datetime | None:
|
|||||||
dt_time = dtime(hour=0, minute=0)
|
dt_time = dtime(hour=0, minute=0)
|
||||||
return datetime.combine(dt_date, dt_time)
|
return datetime.combine(dt_date, dt_time)
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -345,7 +589,9 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
# 1. проверяем API: seasons
|
# 1. проверяем API: seasons
|
||||||
try:
|
try:
|
||||||
seasons_resp = requests.get(URLS["seasons"].format(host=HOST, league=LEAGUE)).json()
|
seasons_resp = requests.get(
|
||||||
|
URLS["seasons"].format(host=HOST, league=LEAGUE)
|
||||||
|
).json()
|
||||||
season = seasons_resp["items"][0]["season"]
|
season = seasons_resp["items"][0]["season"]
|
||||||
except Exception:
|
except Exception:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
@@ -367,7 +613,9 @@ async def lifespan(app: FastAPI):
|
|||||||
calendar = None
|
calendar = None
|
||||||
|
|
||||||
# 3. определяем игру
|
# 3. определяем игру
|
||||||
game_id, game_dt, is_today, cal_status = pick_game_for_team(calendar) if calendar else (None, None, False, None)
|
game_id, game_dt, is_today, cal_status = (
|
||||||
|
pick_game_for_team(calendar) if calendar else (None, None, False, None)
|
||||||
|
)
|
||||||
GAME_ID = game_id
|
GAME_ID = game_id
|
||||||
GAME_START_DT = game_dt
|
GAME_START_DT = game_dt
|
||||||
GAME_TODAY = is_today
|
GAME_TODAY = is_today
|
||||||
@@ -381,7 +629,7 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
# 5. Подготовим онлайн и офлайн наборы (как у тебя)
|
# 5. Подготовим онлайн и офлайн наборы (как у тебя)
|
||||||
threads_live = [
|
threads_live = [
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=get_data_from_API,
|
target=get_data_from_API,
|
||||||
args=(
|
args=(
|
||||||
"pregame",
|
"pregame",
|
||||||
@@ -417,7 +665,6 @@ async def lifespan(app: FastAPI):
|
|||||||
),
|
),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
),
|
),
|
||||||
|
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=get_data_from_API,
|
target=get_data_from_API,
|
||||||
args=(
|
args=(
|
||||||
@@ -472,66 +719,43 @@ async def lifespan(app: FastAPI):
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
# 6. Решение: сегодня / не сегодня
|
# 5. решаем, что запускать
|
||||||
if not is_today:
|
if not is_today:
|
||||||
# ИГРЫ СЕГОДНЯ НЕТ → крутим только офлайн
|
|
||||||
STATUS = "no_game_today"
|
STATUS = "no_game_today"
|
||||||
for t in threads_offline:
|
start_offline_threads(SEASON, GAME_ID)
|
||||||
t.start()
|
|
||||||
else:
|
else:
|
||||||
# игра сегодня
|
# игра сегодня
|
||||||
if cal_status is None:
|
if cal_status is None:
|
||||||
# нет статуса в календаре — считаем, что ещё не началась
|
|
||||||
STATUS = "today_not_started"
|
STATUS = "today_not_started"
|
||||||
for t in threads_offline:
|
start_offline_threads(SEASON, GAME_ID)
|
||||||
t.start()
|
|
||||||
elif cal_status == "Scheduled":
|
elif cal_status == "Scheduled":
|
||||||
# ещё не началась
|
|
||||||
# проверим, не меньше ли часа до начала
|
|
||||||
if game_dt:
|
if game_dt:
|
||||||
delta = game_dt - datetime.now()
|
delta = game_dt - datetime.now()
|
||||||
if delta <= timedelta(hours=1):
|
if delta <= timedelta(hours=1):
|
||||||
# уже скоро → можно запускать онлайн
|
|
||||||
STATUS = "live_soon"
|
STATUS = "live_soon"
|
||||||
GAME_SOON = True
|
start_live_threads(SEASON, GAME_ID)
|
||||||
for t in threads_live:
|
|
||||||
t.start()
|
|
||||||
else:
|
else:
|
||||||
# ещё далеко → офлайн, но говорим что сегодня
|
|
||||||
STATUS = "today_not_started"
|
STATUS = "today_not_started"
|
||||||
for t in threads_offline:
|
start_offline_threads(SEASON, GAME_ID)
|
||||||
t.start()
|
|
||||||
# и можно повесить будильник, как раньше
|
|
||||||
else:
|
else:
|
||||||
# нет времени — просто офлайн, но сегодня
|
|
||||||
STATUS = "today_not_started"
|
STATUS = "today_not_started"
|
||||||
for t in threads_offline:
|
start_offline_threads(SEASON, GAME_ID)
|
||||||
t.start()
|
|
||||||
elif cal_status == "Online":
|
elif cal_status == "Online":
|
||||||
# матч идёт → сразу онлайн
|
|
||||||
STATUS = "live"
|
STATUS = "live"
|
||||||
GAME_SOON = False
|
start_live_threads(SEASON, GAME_ID)
|
||||||
for t in threads_live:
|
|
||||||
t.start()
|
|
||||||
elif cal_status in ["Result", "ResultConfirmed"]:
|
elif cal_status in ["Result", "ResultConfirmed"]:
|
||||||
# матч уже сыгран, но дата всё ещё сегодня
|
|
||||||
STATUS = "finished_today"
|
STATUS = "finished_today"
|
||||||
for t in threads_offline:
|
start_offline_threads(SEASON, GAME_ID)
|
||||||
t.start()
|
|
||||||
else:
|
else:
|
||||||
# неизвестный статус — безопасный вариант
|
|
||||||
STATUS = "today_not_started"
|
STATUS = "today_not_started"
|
||||||
for t in threads_offline:
|
start_offline_threads(SEASON, GAME_ID)
|
||||||
t.start()
|
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
# -------- shutdown --------
|
# -------- shutdown --------
|
||||||
stop_event.set()
|
stop_event.set() # завершить consumer
|
||||||
# офлайн/онлайн ты можешь не делить тут, но оставлю
|
stop_live_threads()
|
||||||
stop_event.set()
|
stop_offline_threads()
|
||||||
for t in threads_live + threads_offline:
|
|
||||||
t.join(timeout=1)
|
|
||||||
thread_result_consumer.join(timeout=1)
|
thread_result_consumer.join(timeout=1)
|
||||||
|
|
||||||
|
|
||||||
@@ -554,7 +778,7 @@ def format_time(seconds: float | int) -> str:
|
|||||||
return "0:00"
|
return "0:00"
|
||||||
|
|
||||||
|
|
||||||
@app.get("/team1.json")
|
@app.get("/team1")
|
||||||
async def team1():
|
async def team1():
|
||||||
game = get_latest_game_safe()
|
game = get_latest_game_safe()
|
||||||
if not game:
|
if not game:
|
||||||
@@ -562,7 +786,7 @@ async def team1():
|
|||||||
return await team("team1")
|
return await team("team1")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/team2.json")
|
@app.get("/team2")
|
||||||
async def team2():
|
async def team2():
|
||||||
game = get_latest_game_safe()
|
game = get_latest_game_safe()
|
||||||
if not game:
|
if not game:
|
||||||
@@ -570,36 +794,36 @@ async def team2():
|
|||||||
return await team("team2")
|
return await team("team2")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/top_team1.json")
|
@app.get("/top_team1")
|
||||||
async def top_team1():
|
async def top_team1():
|
||||||
data = await team("team1")
|
data = await team("team1")
|
||||||
return await top_sorted_team(data)
|
return await top_sorted_team(data)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/top_team2.json")
|
@app.get("/top_team2")
|
||||||
async def top_team2():
|
async def top_team2():
|
||||||
data = await team("team2")
|
data = await team("team2")
|
||||||
return await top_sorted_team(data)
|
return await top_sorted_team(data)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/started_team1.json")
|
@app.get("/started_team1")
|
||||||
async def started_team1():
|
async def started_team1():
|
||||||
data = await team("team1")
|
data = await team("team1")
|
||||||
return await started_team(data)
|
return await started_team(data)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/started_team2.json")
|
@app.get("/started_team2")
|
||||||
async def started_team2():
|
async def started_team2():
|
||||||
data = await team("team2")
|
data = await team("team2")
|
||||||
return await started_team(data)
|
return await started_team(data)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/game.json")
|
@app.get("/game")
|
||||||
async def game():
|
async def game():
|
||||||
return latest_data["game"]
|
return latest_data["game"]
|
||||||
|
|
||||||
|
|
||||||
@app.get("/status.json")
|
@app.get("/status")
|
||||||
async def status(request: Request):
|
async def status(request: Request):
|
||||||
def color_for_status(status_value: str) -> str:
|
def color_for_status(status_value: str) -> str:
|
||||||
"""Подбор цвета статуса в HEX"""
|
"""Подбор цвета статуса в HEX"""
|
||||||
@@ -608,13 +832,25 @@ async def status(request: Request):
|
|||||||
return "#00FF00" # зелёный
|
return "#00FF00" # зелёный
|
||||||
elif status_value in ["scheduled", "today_not_started", "upcoming"]:
|
elif status_value in ["scheduled", "today_not_started", "upcoming"]:
|
||||||
return "#FFFF00" # жёлтый
|
return "#FFFF00" # жёлтый
|
||||||
elif status_value in ["result", "resultconfirmed", "finished", "finished_today"]:
|
elif status_value in [
|
||||||
|
"result",
|
||||||
|
"resultconfirmed",
|
||||||
|
"finished",
|
||||||
|
"finished_today",
|
||||||
|
]:
|
||||||
return "#FF0000" # красный
|
return "#FF0000" # красный
|
||||||
elif status_value in ["no_game_today", "unknown", "none"]:
|
elif status_value in ["no_game_today", "unknown", "none"]:
|
||||||
return "#FFFFFF" # белый
|
return "#FFFFFF" # белый
|
||||||
else:
|
else:
|
||||||
return "#808080" # серый (неизвестный статус)
|
return "#808080" # серый (неизвестный статус)
|
||||||
|
|
||||||
|
# ✳️ сортируем latest_data в нужном порядке
|
||||||
|
sort_order = ["game", "live-status", "box-score", "play-by-play"]
|
||||||
|
sorted_keys = (
|
||||||
|
[k for k in sort_order if k in latest_data] +
|
||||||
|
sorted([k for k in latest_data if k not in sort_order])
|
||||||
|
)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"league": LEAGUE,
|
"league": LEAGUE,
|
||||||
"team": TEAM,
|
"team": TEAM,
|
||||||
@@ -624,16 +860,20 @@ async def status(request: Request):
|
|||||||
{
|
{
|
||||||
"name": TEAM,
|
"name": TEAM,
|
||||||
"status": STATUS,
|
"status": STATUS,
|
||||||
"ts": GAME_START_DT.strftime("%Y-%m-%d %H:%M") if GAME_START_DT else "N/A",
|
"ts": (
|
||||||
|
GAME_START_DT.strftime("%Y-%m-%d %H:%M") if GAME_START_DT else "N/A"
|
||||||
|
),
|
||||||
"link": LEAGUE,
|
"link": LEAGUE,
|
||||||
"color": color_for_status(STATUS) # ← добавлено
|
"color": color_for_status(STATUS),
|
||||||
}
|
}
|
||||||
] + [
|
]
|
||||||
|
+ [
|
||||||
{
|
{
|
||||||
"name": item,
|
"name": item,
|
||||||
"status": (
|
"status": (
|
||||||
latest_data[item]["data"]["status"]
|
latest_data[item]["data"]["status"]
|
||||||
if isinstance(latest_data[item]["data"], dict) and "status" in latest_data[item]["data"]
|
if isinstance(latest_data[item]["data"], dict)
|
||||||
|
and "status" in latest_data[item]["data"]
|
||||||
else latest_data[item]["data"]
|
else latest_data[item]["data"]
|
||||||
),
|
),
|
||||||
"ts": latest_data[item]["ts"],
|
"ts": latest_data[item]["ts"],
|
||||||
@@ -646,14 +886,15 @@ async def status(request: Request):
|
|||||||
),
|
),
|
||||||
"color": color_for_status(
|
"color": color_for_status(
|
||||||
latest_data[item]["data"]["status"]
|
latest_data[item]["data"]["status"]
|
||||||
if isinstance(latest_data[item]["data"], dict) and "status" in latest_data[item]["data"]
|
if isinstance(latest_data[item]["data"], dict)
|
||||||
|
and "status" in latest_data[item]["data"]
|
||||||
else latest_data[item]["data"]
|
else latest_data[item]["data"]
|
||||||
) # ← добавлено
|
),
|
||||||
}
|
}
|
||||||
for item in latest_data
|
for item in sorted_keys # ← используем отсортированный порядок
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
accept = request.headers.get("accept", "")
|
accept = request.headers.get("accept", "")
|
||||||
if "text/html" in accept:
|
if "text/html" in accept:
|
||||||
status_raw = str(STATUS).lower()
|
status_raw = str(STATUS).lower()
|
||||||
@@ -675,7 +916,7 @@ async def status(request: Request):
|
|||||||
else:
|
else:
|
||||||
gs_class = "unknown"
|
gs_class = "unknown"
|
||||||
gs_text = "⚪ UNKNOWN"
|
gs_text = "⚪ UNKNOWN"
|
||||||
|
|
||||||
html = f"""
|
html = f"""
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -732,11 +973,19 @@ async def status(request: Request):
|
|||||||
for s in data["statuses"]:
|
for s in data["statuses"]:
|
||||||
status_text = str(s["status"]).strip().lower()
|
status_text = str(s["status"]).strip().lower()
|
||||||
|
|
||||||
if any(x in status_text for x in ["ok", "success", "live", "live_soon", "online"]):
|
if any(
|
||||||
|
x in status_text
|
||||||
|
for x in ["ok", "success", "live", "live_soon", "online"]
|
||||||
|
):
|
||||||
color_class = "ok"
|
color_class = "ok"
|
||||||
elif any(x in status_text for x in ["scheduled", "today_not_started", "upcoming"]):
|
elif any(
|
||||||
|
x in status_text for x in ["scheduled", "today_not_started", "upcoming"]
|
||||||
|
):
|
||||||
color_class = "warn"
|
color_class = "warn"
|
||||||
elif any(x in status_text for x in ["result", "resultconfirmed", "finished", "finished_today"]):
|
elif any(
|
||||||
|
x in status_text
|
||||||
|
for x in ["result", "resultconfirmed", "finished", "finished_today"]
|
||||||
|
):
|
||||||
color_class = "fail"
|
color_class = "fail"
|
||||||
else:
|
else:
|
||||||
color_class = "unknown"
|
color_class = "unknown"
|
||||||
@@ -762,7 +1011,8 @@ async def status(request: Request):
|
|||||||
response.headers["Refresh"] = "1"
|
response.headers["Refresh"] = "1"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@app.get("/scores.json")
|
|
||||||
|
@app.get("/scores")
|
||||||
async def scores():
|
async def scores():
|
||||||
game = get_latest_game_safe()
|
game = get_latest_game_safe()
|
||||||
if not game:
|
if not game:
|
||||||
@@ -803,7 +1053,6 @@ async def scores():
|
|||||||
return score_by_quarter
|
return score_by_quarter
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def top_sorted_team(data):
|
async def top_sorted_team(data):
|
||||||
top_sorted_team = sorted(
|
top_sorted_team = sorted(
|
||||||
(p for p in data if p.get("startRole") in ["Player", ""]),
|
(p for p in data if p.get("startRole") in ["Player", ""]),
|
||||||
@@ -869,7 +1118,9 @@ async def team(who: str):
|
|||||||
|
|
||||||
# нормализуем доступ к данным
|
# нормализуем доступ к данным
|
||||||
game_data = game["data"] if "data" in game else game
|
game_data = game["data"] if "data" in game else game
|
||||||
result = game_data["result"] # здесь уже безопасно, мы проверили в get_latest_game_safe
|
result = game_data[
|
||||||
|
"result"
|
||||||
|
] # здесь уже безопасно, мы проверили в get_latest_game_safe
|
||||||
|
|
||||||
# в result ожидаем "teams"
|
# в result ожидаем "teams"
|
||||||
teams = result.get("teams")
|
teams = result.get("teams")
|
||||||
@@ -1069,6 +1320,7 @@ async def team(who: str):
|
|||||||
|
|
||||||
return sorted_team
|
return sorted_team
|
||||||
|
|
||||||
|
|
||||||
async def started_team(data):
|
async def started_team(data):
|
||||||
started_team = sorted(
|
started_team = sorted(
|
||||||
(
|
(
|
||||||
@@ -1285,7 +1537,7 @@ stat_name_list = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@app.get("/team_stats.json")
|
@app.get("/team_stats")
|
||||||
async def team_stats():
|
async def team_stats():
|
||||||
teams = latest_data["game"]["data"]["result"]["teams"]
|
teams = latest_data["game"]["data"]["result"]["teams"]
|
||||||
plays = latest_data["game"]["data"]["result"]["plays"]
|
plays = latest_data["game"]["data"]["result"]["plays"]
|
||||||
@@ -1339,7 +1591,7 @@ async def team_stats():
|
|||||||
return result_json
|
return result_json
|
||||||
|
|
||||||
|
|
||||||
@app.get("/referee.json")
|
@app.get("/referee")
|
||||||
async def referee():
|
async def referee():
|
||||||
desired_order = [
|
desired_order = [
|
||||||
"Crew chief",
|
"Crew chief",
|
||||||
@@ -1391,7 +1643,7 @@ async def referee():
|
|||||||
return referees
|
return referees
|
||||||
|
|
||||||
|
|
||||||
@app.get("/team_comparison.json")
|
@app.get("/team_comparison")
|
||||||
async def team_comparison():
|
async def team_comparison():
|
||||||
if STATUS not in ["no_game_today", "finished_today"]:
|
if STATUS not in ["no_game_today", "finished_today"]:
|
||||||
data = latest_data["pregame"]["data"]["result"]
|
data = latest_data["pregame"]["data"]["result"]
|
||||||
@@ -1467,9 +1719,7 @@ async def team_comparison():
|
|||||||
return [{"Данных о сравнении команд нет!"}]
|
return [{"Данных о сравнении команд нет!"}]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/standings")
|
||||||
|
|
||||||
@app.get("/standings.json")
|
|
||||||
async def regular_standings():
|
async def regular_standings():
|
||||||
data = latest_data["actual-standings"]["data"]["items"]
|
data = latest_data["actual-standings"]["data"]["items"]
|
||||||
for item in data:
|
for item in data:
|
||||||
@@ -1540,7 +1790,7 @@ async def regular_standings():
|
|||||||
return standings_payload
|
return standings_payload
|
||||||
|
|
||||||
|
|
||||||
@app.get("/live_status.json")
|
@app.get("/live_status")
|
||||||
async def live_status():
|
async def live_status():
|
||||||
# если матч реально идёт/вот-вот — пытаемся отдать то, что есть
|
# если матч реально идёт/вот-вот — пытаемся отдать то, что есть
|
||||||
if STATUS in ["live", "live_soon"]:
|
if STATUS in ["live", "live_soon"]:
|
||||||
@@ -1563,9 +1813,7 @@ async def live_status():
|
|||||||
|
|
||||||
# 2) если это просто строка статуса ("ok" / "no-status" / "error")
|
# 2) если это просто строка статуса ("ok" / "no-status" / "error")
|
||||||
if isinstance(raw, str):
|
if isinstance(raw, str):
|
||||||
return [{
|
return [{"status": raw}]
|
||||||
"status": raw
|
|
||||||
}]
|
|
||||||
|
|
||||||
# fallback
|
# fallback
|
||||||
return [{"foulsA": 0, "foulsB": 0}]
|
return [{"foulsA": 0, "foulsB": 0}]
|
||||||
@@ -1575,4 +1823,4 @@ async def live_status():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run("get_data:app", host="0.0.0.0", port=8000, reload=True)
|
uvicorn.run("get_data:app", host="0.0.0.0", port=8000, reload=True, log_level="critical")
|
||||||
|
|||||||
Reference in New Issue
Block a user