расписание онлайн и логотипы из excel
This commit is contained in:
319
get_data.py
319
get_data.py
@@ -82,12 +82,14 @@ LEAGUE = args.league
|
||||
TEAM = args.team
|
||||
LANG = args.lang
|
||||
HOST = os.getenv("API_BASE_URL")
|
||||
SYNO_PATH = f'{os.getenv("SYNO_PATH")}MATCH INFO.xlsx'
|
||||
SYNO_PATH_EXCEL = f'{os.getenv("SYNO_PATH_EXCEL")}MATCH INFO.xlsx'
|
||||
SYNO_URL = os.getenv("SYNO_URL")
|
||||
SYNO_USERNAME = os.getenv("SYNO_USERNAME")
|
||||
SYNO_PASSWORD = os.getenv("SYNO_PASSWORD")
|
||||
SYNO_PATH_VMIX = os.getenv("SYNO_PATH_VMIX")
|
||||
|
||||
CALENDAR = None
|
||||
|
||||
|
||||
STATUS = False
|
||||
GAME_ID = None
|
||||
@@ -169,7 +171,7 @@ def start_offline_threads(season, game_id):
|
||||
args=(
|
||||
"game",
|
||||
URLS["game"].format(host=HOST, game_id=game_id, lang=LANG),
|
||||
300, # опрашиваем раз в секунду/реже
|
||||
150, # опрашиваем раз в секунду/реже
|
||||
stop_event_offline,
|
||||
False,
|
||||
True,
|
||||
@@ -183,7 +185,7 @@ def start_offline_threads(season, game_id):
|
||||
URLS["pregame-full-stats"].format(
|
||||
host=HOST, league=LEAGUE, season=season, game_id=game_id, lang=LANG
|
||||
),
|
||||
600,
|
||||
300,
|
||||
stop_event_offline,
|
||||
False,
|
||||
True,
|
||||
@@ -197,7 +199,7 @@ def start_offline_threads(season, game_id):
|
||||
URLS["actual-standings"].format(
|
||||
host=HOST, league=LEAGUE, season=season, lang=LANG
|
||||
),
|
||||
600,
|
||||
300,
|
||||
stop_event_offline,
|
||||
False,
|
||||
True,
|
||||
@@ -232,7 +234,7 @@ def start_live_threads(season, game_id):
|
||||
URLS["pregame"].format(
|
||||
host=HOST, league=LEAGUE, season=season, game_id=game_id, lang=LANG
|
||||
),
|
||||
600,
|
||||
300,
|
||||
stop_event_live,
|
||||
True,
|
||||
),
|
||||
@@ -245,7 +247,7 @@ def start_live_threads(season, game_id):
|
||||
URLS["pregame-full-stats"].format(
|
||||
host=HOST, league=LEAGUE, season=season, game_id=game_id, lang=LANG
|
||||
),
|
||||
600,
|
||||
300,
|
||||
stop_event_live,
|
||||
True,
|
||||
),
|
||||
@@ -258,7 +260,7 @@ def start_live_threads(season, game_id):
|
||||
URLS["actual-standings"].format(
|
||||
host=HOST, league=LEAGUE, season=season, lang=LANG
|
||||
),
|
||||
600,
|
||||
300,
|
||||
stop_event_live,
|
||||
),
|
||||
daemon=True,
|
||||
@@ -268,7 +270,7 @@ def start_live_threads(season, game_id):
|
||||
args=(
|
||||
"game",
|
||||
URLS["game"].format(host=HOST, game_id=game_id, lang=LANG),
|
||||
300, # часто
|
||||
150, # часто
|
||||
stop_event_live,
|
||||
True,
|
||||
),
|
||||
@@ -432,13 +434,6 @@ def get_data_from_API(
|
||||
)
|
||||
return
|
||||
|
||||
# сколько уже заняло
|
||||
# elapsed = time.time() - start
|
||||
# сколько надо доспать, чтобы в сумме вышла нужная частота
|
||||
# to_sleep = sleep_time - elapsed
|
||||
# print(to_sleep)
|
||||
# if to_sleep > 0:
|
||||
# умное ожидание с быстрым выходом при live
|
||||
slept = 0
|
||||
while slept < sleep_time:
|
||||
if stop_event.is_set():
|
||||
@@ -995,7 +990,7 @@ def start_offline_prevgame(season, prev_game_id: str):
|
||||
args=(
|
||||
"game",
|
||||
URLS["game"].format(host=HOST, game_id=prev_game_id, lang=LANG),
|
||||
300, # редкий опрос
|
||||
150, # редкий опрос
|
||||
stop_event_offline,
|
||||
False, # stop_when_live
|
||||
False, # ✅ stop_after_success=False (держим тред)
|
||||
@@ -1191,9 +1186,67 @@ def start_prestart_watcher(game_dt: datetime | None):
|
||||
t.start()
|
||||
|
||||
|
||||
def get_excel():
|
||||
return nasio.load_formatted(
|
||||
user=SYNO_USERNAME,
|
||||
password=SYNO_PASSWORD,
|
||||
nas_ip=SYNO_URL,
|
||||
nas_port="443",
|
||||
path=SYNO_PATH_EXCEL,
|
||||
# sheet="TEAMS LEGEND",
|
||||
)
|
||||
|
||||
|
||||
def excel_worker():
|
||||
"""
|
||||
Раз в минуту читает ВСЕ вкладки Excel
|
||||
и сохраняет их в latest_data с префиксом excel_<sheet_name>.
|
||||
"""
|
||||
global latest_data
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
sheets = get_excel() # <- теперь это dict: {sheet_name: DataFrame}
|
||||
|
||||
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
||||
|
||||
if isinstance(sheets, dict):
|
||||
for sheet_name, df in sheets.items():
|
||||
|
||||
# пропускаем странные объекты
|
||||
if not hasattr(df, "fillna"):
|
||||
logger.warning(f"[excel] Лист '{sheet_name}' не DataFrame")
|
||||
continue
|
||||
|
||||
# ЧИСТИМ NaN и конвертируем
|
||||
df = df.fillna("")
|
||||
data_json = df.to_dict(orient="records")
|
||||
|
||||
# ключ в latest_data: excel_<имя_вкладки>
|
||||
key = f"excel_{sheet_name}".replace(" ", "_").replace("-", "_")
|
||||
|
||||
latest_data[key] = {
|
||||
"ts": ts,
|
||||
"data": data_json,
|
||||
}
|
||||
|
||||
logger.info("[excel] Все вкладки Excel обновлены")
|
||||
|
||||
else:
|
||||
logger.warning("[excel] get_excel() вернул не словарь")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[excel] ошибка при чтении Excel: {e}")
|
||||
|
||||
# пауза 60 сек
|
||||
for _ in range(60):
|
||||
if stop_event.is_set():
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
global STATUS, GAME_ID, SEASON, GAME_START_DT, GAME_TODAY, GAME_SOON
|
||||
global STATUS, GAME_ID, SEASON, GAME_START_DT, GAME_TODAY, GAME_SOON, CALENDAR
|
||||
|
||||
# 1. проверяем API: seasons
|
||||
try:
|
||||
@@ -1219,7 +1272,7 @@ async def lifespan(app: FastAPI):
|
||||
logger.error(f"не получилось проверить работу API. код ошибки: {ex}")
|
||||
# тут можно вообще не запускать сервер, но оставим как есть
|
||||
calendar = None
|
||||
|
||||
CALENDAR = calendar
|
||||
# 3. определяем игру
|
||||
game_id, game_dt, is_today, cal_status = (
|
||||
pick_game_for_team(calendar) if calendar else (None, None, False, None)
|
||||
@@ -1230,6 +1283,12 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
logger.info(f"Лига: {LEAGUE}\nСезон: {season}\nКоманда: {TEAM}\nGame ID: {game_id}")
|
||||
|
||||
thread_excel = threading.Thread(
|
||||
target=excel_worker,
|
||||
daemon=True,
|
||||
)
|
||||
thread_excel.start()
|
||||
|
||||
# 4. запускаем "длинные" потоки (они у тебя и так всегда)
|
||||
thread_result_consumer = threading.Thread(
|
||||
target=results_consumer,
|
||||
@@ -1288,6 +1347,7 @@ async def lifespan(app: FastAPI):
|
||||
stop_offline_threads()
|
||||
thread_result_consumer.join(timeout=1)
|
||||
thread_status_broadcaster.join(timeout=1)
|
||||
thread_excel.join(timeout=1)
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
@@ -2723,48 +2783,107 @@ async def live_status():
|
||||
)
|
||||
|
||||
|
||||
def get_excel_row_for_team(team_name: str) -> dict:
|
||||
"""
|
||||
Ищем строку из Excel по имени команды (колонка 'Team').
|
||||
Читаем из latest_data["excel"]["data"] (список dict'ов).
|
||||
Возвращаем dict по команде или {} если не нашли / нет Excel.
|
||||
"""
|
||||
excel_wrap = latest_data.get("excel_TEAMS_LEGEND")
|
||||
if not excel_wrap:
|
||||
return {}
|
||||
|
||||
rows = excel_wrap.get("data") or [] # это list[dict] после df.to_dict("records")
|
||||
team_norm = (team_name or "").strip().casefold()
|
||||
|
||||
for row in rows:
|
||||
name = str(row.get("Team", "")).strip().casefold()
|
||||
if name == team_norm:
|
||||
return row
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
@app.get("/info")
|
||||
async def info():
|
||||
data = latest_data["game"]["data"]["result"]
|
||||
team1_name = data["team1"]["name"]
|
||||
team2_name = data["team2"]["name"]
|
||||
team1_name_short = data["team1"]["abcName"]
|
||||
team2_name_short = data["team2"]["abcName"]
|
||||
team1_logo = data["team1"]["logo"]
|
||||
team2_logo = data["team2"]["logo"]
|
||||
arena = data["arena"]["name"]
|
||||
arena_short = data["arena"]["shortName"]
|
||||
region = data["region"]["name"]
|
||||
date_obj = datetime.strptime(data["game"]["localDate"], "%d.%m.%Y")
|
||||
league = data["league"]["abcName"]
|
||||
league_full = data["league"]["name"]
|
||||
season = f'{str(data["league"]["season"]-1)}/{str(data["league"]["season"])[2:]}'
|
||||
stadia = data["comp"]["name"]
|
||||
team1_name_short_api = data["team1"]["abcName"]
|
||||
team2_name_short_api = data["team2"]["abcName"]
|
||||
team1_logo_api = data["team1"]["logo"]
|
||||
team2_logo_api = data["team2"]["logo"]
|
||||
arena_api = data["arena"]["name"]
|
||||
arena_short_api = data["arena"]["shortName"]
|
||||
region_api = data["region"]["name"]
|
||||
date_obj_api = datetime.strptime(data["game"]["localDate"], "%d.%m.%Y")
|
||||
league_api = data["league"]["abcName"]
|
||||
league_full_api = data["league"]["name"]
|
||||
season_api = (
|
||||
f'{str(data["league"]["season"]-1)}/{str(data["league"]["season"])[2:]}'
|
||||
)
|
||||
stadia_api = data["comp"]["name"]
|
||||
row_team1 = get_excel_row_for_team(team1_name)
|
||||
row_team2 = get_excel_row_for_team(team2_name)
|
||||
|
||||
team1_logo_exl = row_team1.get("TeamLogo", "")
|
||||
team1_logo_left_exl = row_team1.get("TeamLogo(LEFT-LOOP)", "")
|
||||
team1_logo_right_exl = row_team1.get("TeamLogo(RIGHT-LOOP)", "")
|
||||
team1_tla_exl = row_team1.get("TeamTLA", "")
|
||||
team1_teamstat_exl = row_team1.get("TeamStats", "")
|
||||
team1_2line_exl = row_team1.get("TeamName2Lines", "")
|
||||
|
||||
team2_logo_exl = row_team2.get("TeamLogo", "")
|
||||
team2_logo_left_exl = row_team2.get("TeamLogo(LEFT-LOOP)", "")
|
||||
team2_logo_right_exl = row_team2.get("TeamLogo(RIGHT-LOOP)", "")
|
||||
team2_tla_exl = row_team2.get("TeamTLA", "")
|
||||
team2_teamstat_exl = row_team2.get("TeamStats", "")
|
||||
team2_2line_exl = row_team2.get("TeamName2Lines", "")
|
||||
|
||||
fon_exl = latest_data.get("excel_INFO")["data"][1]["SELECT TEAM1"]
|
||||
swape_exl = latest_data.get("excel_INFO")["data"][2]["SELECT TEAM1"]
|
||||
logo_exl = latest_data.get("excel_INFO")["data"][3]["SELECT TEAM1"]
|
||||
|
||||
try:
|
||||
full_format = date_obj.strftime("%A, %-d %B %Y")
|
||||
short_format = date_obj.strftime("%A, %-d %b")
|
||||
full_format = date_obj_api.strftime("%A, %-d %B %Y")
|
||||
short_format = date_obj_api.strftime("%A, %-d %b")
|
||||
except ValueError:
|
||||
full_format = date_obj.strftime("%A, %#d %B %Y")
|
||||
short_format = date_obj.strftime("%A, %#d %b")
|
||||
full_format = date_obj_api.strftime("%A, %#d %B %Y")
|
||||
short_format = date_obj_api.strftime("%A, %#d %b")
|
||||
|
||||
return maybe_clear_for_vmix(
|
||||
[
|
||||
{
|
||||
"team1": team1_name,
|
||||
"team2": team2_name,
|
||||
"team1_short": team1_name_short,
|
||||
"team2_short": team2_name_short,
|
||||
"logo1": team1_logo,
|
||||
"logo2": team2_logo,
|
||||
"arena": arena,
|
||||
"short_arena": arena_short,
|
||||
"region": region,
|
||||
"league": league,
|
||||
"league_full": league_full,
|
||||
"season": season,
|
||||
"stadia": stadia,
|
||||
"date1": str(full_format),
|
||||
"date2": str(short_format),
|
||||
"team1_short_api": team1_name_short_api,
|
||||
"team2_short_api": team2_name_short_api,
|
||||
"logo1_api": team1_logo_api,
|
||||
"logo2_api": team2_logo_api,
|
||||
"arena_api": arena_api,
|
||||
"short_arena_api": arena_short_api,
|
||||
"region_api": region_api,
|
||||
"league_api": league_api,
|
||||
"league_full_api": league_full_api,
|
||||
"season_api": season_api,
|
||||
"stadia_api": stadia_api,
|
||||
"date1_api": str(full_format),
|
||||
"date2_api": str(short_format),
|
||||
"team1_logo_exl": team1_logo_exl,
|
||||
"team1_logo_left_exl": team1_logo_left_exl,
|
||||
"team1_logo_right_exl": team1_logo_right_exl,
|
||||
"team1_tla_exl": team1_tla_exl,
|
||||
"team1_teamstat_exl": team1_teamstat_exl,
|
||||
"team1_2line_exl": team1_2line_exl,
|
||||
"team2_logo_exl": team2_logo_exl,
|
||||
"team2_logo_left_exl": team2_logo_left_exl,
|
||||
"team2_logo_right_exl": team2_logo_right_exl,
|
||||
"team2_tla_exl": team2_tla_exl,
|
||||
"team2_teamstat_exl": team2_teamstat_exl,
|
||||
"team2_2line_exl": team2_2line_exl,
|
||||
"fon_exl": fon_exl,
|
||||
"swape_exl": swape_exl,
|
||||
"logo_exl": logo_exl,
|
||||
}
|
||||
]
|
||||
)
|
||||
@@ -2938,7 +3057,7 @@ def change_vmix_datasource_urls(xml_data, new_base_url: str) -> bytes:
|
||||
url_tag = inst.find(".//state/xml/url")
|
||||
if url_tag is not None and url_tag.text:
|
||||
old_url = url_tag.text.strip()
|
||||
pattern = r"https?\:\/\/\w+\.\w+\.\w{2,}|https?\:\/\/\d{,3}\.\d{,3}\.\d{,3}\.\d{,3}:\d*"
|
||||
pattern = r"https?\:\/\/\w+\.\w+\.\w{2,}|https?\:\/\/\d{,3}\.\d{,3}\.\d{,3}\.\d{,3}\:\d*"
|
||||
new_url = re.sub(pattern, new_base_url, old_url, count=0, flags=0)
|
||||
url_tag.text = new_url
|
||||
|
||||
@@ -2975,6 +3094,110 @@ async def vmix_project():
|
||||
)
|
||||
|
||||
|
||||
@app.get("/quarter")
|
||||
async def select_quarter():
|
||||
return latest_data["excel_QUARTER"]
|
||||
|
||||
|
||||
def resolve_period(ls: dict, game: dict) -> str:
|
||||
try:
|
||||
period_num = int(ls.get("period", 0))
|
||||
except (TypeError, ValueError):
|
||||
return game.get("period", "")
|
||||
try:
|
||||
seconds_left = int(ls.get("second", 0))
|
||||
except (TypeError, ValueError):
|
||||
seconds_left = 0
|
||||
|
||||
if period_num <= 0:
|
||||
return game.get("period", "")
|
||||
|
||||
score_a = ls.get("scoreA", game.get("score1"))
|
||||
score_b = ls.get("scoreB", game.get("score2"))
|
||||
|
||||
if seconds_left == 0: # период закончился
|
||||
if period_num == 1:
|
||||
return "After 1q"
|
||||
if period_num == 2:
|
||||
return "HT"
|
||||
if period_num == 3:
|
||||
return "After 3q"
|
||||
if period_num == 4:
|
||||
return "After 4q" if score_a == score_b else ""
|
||||
# овертаймы
|
||||
return f"After OT{period_num - 4}".replace("1", "")
|
||||
else: # период в процессе
|
||||
if period_num <= 4:
|
||||
return f"Q{period_num}"
|
||||
return f"OT{period_num - 4}".replace("1", "")
|
||||
|
||||
|
||||
@app.get("/games_online")
|
||||
async def games_online():
|
||||
if not CALENDAR or "items" not in CALENDAR:
|
||||
raise HTTPException(status_code=503, detail="calendar data not ready")
|
||||
# today = datetime.now().date()
|
||||
today = (datetime.now() + timedelta(days=1)).date()
|
||||
todays_games = []
|
||||
final_states = {"result", "resultconfirmed", "finished"}
|
||||
for item in CALENDAR["items"]:
|
||||
game = item.get("game") or {}
|
||||
status_raw = str(game.get("gameStatus", "") or "").lower()
|
||||
need_refresh = status_raw not in final_states
|
||||
dt_str = game.get("defaultZoneDateTime") or ""
|
||||
try:
|
||||
game_dt = datetime.fromisoformat(dt_str).date()
|
||||
except ValueError:
|
||||
continue
|
||||
if game_dt == today:
|
||||
row_team1 = get_excel_row_for_team(item["team1"]["name"]) or {}
|
||||
row_team2 = get_excel_row_for_team(item["team2"]["name"]) or {}
|
||||
game["team1_xls"] = row_team1.get("TeamName2Lines", "")
|
||||
game["team1_logo_xls"] = row_team1.get("TeamLogo", "")
|
||||
game["team2_xls"] = row_team2.get("TeamName2Lines", "")
|
||||
game["team2_logo_xls"] = row_team2.get("TeamLogo", "")
|
||||
game_id = game.get("id")
|
||||
if game_id and need_refresh:
|
||||
try:
|
||||
resp = requests.get(
|
||||
URLS["live-status"].format(host=HOST, game_id=game_id),
|
||||
timeout=5,
|
||||
).json()
|
||||
ls = resp.get("result") or resp
|
||||
msg = str(ls.get("message") or "").lower()
|
||||
status = str(ls.get("status") or "").lower()
|
||||
if msg == "not found" or status == "404": pass
|
||||
elif ls.get("message") != "Not found" and str(ls.get("gameStatus")).lower() == "online":
|
||||
game["score1"] = ls.get("scoreA", game.get("score1", ""))
|
||||
game["score2"] = ls.get("scoreB", game.get("score2", ""))
|
||||
game["period"] = resolve_period(ls, game)
|
||||
game["gameStatus"] = ls.get(
|
||||
"gameStatus", game.get("gameStatus", "")
|
||||
)
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
scores = [game.get("score1"), game.get("score2")]
|
||||
todays_games.append(
|
||||
{
|
||||
"gameStatus": game["gameStatus"],
|
||||
"score1": game["score1"] if any((s or 0) > 0 for s in scores) else "",
|
||||
"score2": game["score2"] if any((s or 0) > 0 for s in scores) else "",
|
||||
"period": game["period"] if "period" in game else "",
|
||||
"defaultZoneTime": game["defaultZoneTime"],
|
||||
"team1": item["team1"]["name"],
|
||||
"team2": item["team2"]["name"],
|
||||
"team1_xls": game["team1_xls"],
|
||||
"team1_logo_xls": game["team1_logo_xls"],
|
||||
"team2_xls": game["team2_xls"],
|
||||
"team2_logo_xls": game["team2_logo_xls"],
|
||||
"mask1": "#FFFFFF00" if any((s or 0) > 0 for s in scores) else "#FFFFFF",
|
||||
"mask2": "#FFFFFF00" if (game["period"] if "period" in game else "") == "" else "#FFFFFF",
|
||||
"mask3": "#FFFFFF00" if (game["period"] if "period" in game else "") != "" else "#FFFFFF",
|
||||
}
|
||||
)
|
||||
return todays_games
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"get_data:app", host="0.0.0.0", port=8000, reload=True, log_level="debug"
|
||||
|
||||
Reference in New Issue
Block a user