diff --git a/get_data.py b/get_data.py index 48b7b50..99d905a 100644 --- a/get_data.py +++ b/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_. + """ + 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"