расписание онлайн и логотипы из excel

This commit is contained in:
2025-11-14 17:08:07 +03:00
parent c9d02b5991
commit afa184f8e7

View File

@@ -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"