diff --git a/app.py b/app.py index b388481..6a7d117 100644 --- a/app.py +++ b/app.py @@ -59,7 +59,7 @@ from repositories.match_session_repository import ( from repositories.match_view_repository import get_match_lineups_grouped from vmix.vmix_service import build_vmix_project_bytes, build_vmix_filename from repositories.match_referee_repository import get_match_referees -from repositories.match_repository import get_tour_schedule_by_match_id, ensure_match_source_key_column +from repositories.match_repository import get_tour_schedule_by_match_id, ensure_match_source_key_column, resolve_match_source_key from repositories.standings_repository import get_standings_by_match_id from repositories.match_coach_repository import get_match_coaches_grouped from repositories.player_repository import ( @@ -706,7 +706,7 @@ def download_vmix_project(request: Request, session_token: str): operator_login = current_user.get("username") or None try: - source_key = session_row[21] if len(session_row) > 21 else None + source_key = resolve_match_source_key(session_row[1], session_row[21] if len(session_row) > 21 else None) vmix_bytes = build_vmix_project_bytes( session_token=session_token, @@ -1474,7 +1474,7 @@ def get_roster_data(session_token: str, name: str, count_player: int): home_team_name = session_row[14].replace("«", "").replace("»", "") away_team_id = session_row[17] away_team_name = session_row[18].replace("«", "").replace("»", "") - source_key = session_row[21] if len(session_row) > 21 else None + source_key = resolve_match_source_key(match_id, session_row[21] if len(session_row) > 21 else None) # print(session_row) data = build_lineup_json( @@ -1764,7 +1764,7 @@ def vmix_home_formations(session_token: str): home_team_id = session_row[13] home_team = session_row[14].replace("«", "").replace("»", "") - source_key = session_row[21] if len(session_row) > 21 else None + source_key = resolve_match_source_key(session_row[1], session_row[21] if len(session_row) > 21 else None) rows = get_vmix_team_formations(session_row, home_team_id) return build_vmix_formation_response(rows, home_team_id, home_team, source_key=source_key) @@ -1777,7 +1777,7 @@ def vmix_away_formations(session_token: str): away_team_id = session_row[17] away_team = session_row[18].replace("«", "").replace("»", "") - source_key = session_row[21] if len(session_row) > 21 else None + source_key = resolve_match_source_key(session_row[1], session_row[21] if len(session_row) > 21 else None) rows = get_vmix_team_formations(session_row, away_team_id) return build_vmix_formation_response(rows, away_team_id, away_team, source_key=source_key) diff --git a/repositories/match_repository.py b/repositories/match_repository.py index c6aca84..b68bf5b 100644 --- a/repositories/match_repository.py +++ b/repositories/match_repository.py @@ -3,6 +3,7 @@ from numpy import place from db import get_connection from repositories.team_repository import get_team_id_by_external_id from repositories.stadium_repository import get_or_create_stadium +from parsers.parser_sources import get_default_source_key, list_parser_sources def ensure_match_source_key_column() -> None: @@ -24,6 +25,121 @@ def ensure_match_source_key_column() -> None: conn.close() + + +def _normalize_source_key(value: str | None) -> str: + return str(value or "").upper().strip() + + +def infer_match_source_key( + explicit_source_key: str | None = None, + season: str | None = None, + tour: str | None = None, +) -> str: + """Определяет источник матча, даже если старые строки matches.source_key ещё пустые. + + Приоритет: + 1. matches.source_key; + 2. точное совпадение season с parser_sources.season; + 3. кубковые слова в tour; + 4. обычный дефолт из настроек проекта. + """ + explicit = _normalize_source_key(explicit_source_key) + if explicit: + return explicit + + season_value = str(season or "").strip() + tour_value = str(tour or "").lower().strip() + + try: + sources = list_parser_sources() + except Exception: + sources = [] + + if season_value and sources: + matches = [ + _normalize_source_key(source.get("key")) + for source in sources + if str(source.get("season") or "").strip() == season_value + ] + matches = [key for key in matches if key] + if len(matches) == 1: + return matches[0] + + cup_markers = ( + "кубок", + "финал", + "полуфинал", + "четвертьфинал", + "1/2", + "1/4", + "1/8", + "1/16", + "групп", + "этап", + "стад", + ) + if any(marker in tour_value for marker in cup_markers): + known_keys = {_normalize_source_key(source.get("key")) for source in sources} + if "RUSSIAN_CUP" in known_keys: + return "RUSSIAN_CUP" + + try: + return _normalize_source_key(get_default_source_key()) or "SUPERLEAGUE" + except Exception: + return "SUPERLEAGUE" + + +def resolve_match_source_key(match_id: int | str | None, fallback: str | None = None) -> str: + """Возвращает source_key для матча и, если возможно, записывает его в matches. + + Это нужно для старых матчей Кубка России, которые могли быть загружены до появления + колонки source_key. Без этого vMix скачивает пресет и собирает пути как для Суперлиги. + """ + fallback_key = _normalize_source_key(fallback) + if not match_id: + return fallback_key or infer_match_source_key(fallback_key) + + conn = get_connection() + try: + with conn.cursor() as cur: + cur.execute( + """ + SELECT source_key, season, tour + FROM matches + WHERE id = %s + LIMIT 1; + """, + (match_id,), + ) + row = cur.fetchone() + + if not row: + return fallback_key or infer_match_source_key(fallback_key) + + explicit_key, season, tour = row + source_key = infer_match_source_key(explicit_key or fallback_key, season, tour) + + if source_key and not _normalize_source_key(explicit_key): + cur.execute( + """ + UPDATE matches + SET source_key = %s, updated_at = NOW() + WHERE id = %s + AND (source_key IS NULL OR TRIM(source_key) = ''); + """, + (source_key, match_id), + ) + conn.commit() + + return source_key + except Exception: + conn.rollback() + return fallback_key or infer_match_source_key(fallback_key) + finally: + conn.close() + + def upsert_match( external_id: str, home_team_id: int, diff --git a/repositories/match_session_repository.py b/repositories/match_session_repository.py index 4889613..f12c6ae 100644 --- a/repositories/match_session_repository.py +++ b/repositories/match_session_repository.py @@ -2,6 +2,7 @@ import secrets from db import get_connection from parsers.parser_sources import build_logo_path +from repositories.match_repository import resolve_match_source_key def create_match_session( @@ -88,7 +89,9 @@ def get_match_session_by_token(session_token: str): return None row = list(row) - source_key = row[21] if len(row) > 21 else None + source_key = resolve_match_source_key(row[1], row[21] if len(row) > 21 else None) + if len(row) > 21: + row[21] = source_key row[16] = build_logo_path(source_key, row[16]) row[20] = build_logo_path(source_key, row[20]) return tuple(row) diff --git a/services/vmix_json_service.py b/services/vmix_json_service.py index 94c7504..9d363b9 100644 --- a/services/vmix_json_service.py +++ b/services/vmix_json_service.py @@ -1,6 +1,7 @@ # services/vmix_json_service.py from db import get_connection from repositories.match_lineup_repository import get_match_lineup_for_vmix +from repositories.match_repository import resolve_match_source_key from parsers.parser_sources import build_empty_photo_path, build_logo_path, build_logo_variant_path, build_photo_path @@ -9,11 +10,17 @@ EMPTY_PHOTO_PATH = DEFAULT_PHOTO_BASE_PATH + r"\EMPTY.png" def _session_source_key(session_row) -> str | None: + try: + match_id = session_row[1] + except Exception: + match_id = None + try: value = session_row[21] except Exception: value = None - return str(value).strip() if value else None + + return resolve_match_source_key(match_id, value) def build_generated_player_photo_path( @@ -386,7 +393,9 @@ def get_vmix_match_info_by_token(session_token: str): return None row = list(row) - source_key = row[30] if len(row) > 30 else None + source_key = resolve_match_source_key(row[0], row[30] if len(row) > 30 else None) + if len(row) > 30: + row[30] = source_key row[14] = build_logo_path(source_key, row[14]) row[15] = build_logo_path(source_key, row[15]) row[20] = build_logo_variant_path(source_key, row[20], "white") diff --git a/vmix/vmix_service.py b/vmix/vmix_service.py index bdf13d7..65b09c4 100644 --- a/vmix/vmix_service.py +++ b/vmix/vmix_service.py @@ -7,13 +7,23 @@ import os from urllib.parse import urlparse from dotenv import load_dotenv -load_dotenv() +load_dotenv(override=True) + + +def _refresh_env() -> None: + # Читаем .env заново перед скачиванием, чтобы изменение SYNO_PATH_VMIX_2 + # применялось без пересборки кода. + load_dotenv(override=True) + + +def get_syno_settings() -> tuple[str | None, str | None, str | None]: + _refresh_env() + return ( + os.getenv("SYNO_URL"), + os.getenv("SYNO_USERNAME"), + os.getenv("SYNO_PASSWORD"), + ) -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") -SYNO_PATH_VMIX_2 = os.getenv("SYNO_PATH_VMIX_2") def normalize_source_key(source_key: str | None) -> str: @@ -26,14 +36,17 @@ def get_vmix_preset_path(source_key: str | None = None) -> str | None: SUPERLEAGUE -> SYNO_PATH_VMIX RUSSIAN_CUP -> SYNO_PATH_VMIX_2, если он заполнен - Если второй путь не задан, используем основной, чтобы скачивание не ломалось. + Значения читаются из .env прямо перед скачиванием. """ + _refresh_env() key = normalize_source_key(source_key) + primary_path = os.getenv("SYNO_PATH_VMIX") + cup_path = os.getenv("SYNO_PATH_VMIX_2") - if key == "RUSSIAN_CUP" and SYNO_PATH_VMIX_2: - return SYNO_PATH_VMIX_2 + if key == "RUSSIAN_CUP" and cup_path: + return cup_path - return SYNO_PATH_VMIX + return primary_path def get_fqdn(): @@ -63,6 +76,15 @@ def rebuild_vmix_url(old_url: str, new_base_url: str, session_token: str) -> str endpoint = match.group(1) return f"{new_base_url}/vmix/session/{session_token}/{endpoint}" + # Старые пресеты могли хранить ссылки без session-токена: + # /vmix/home-lineup, /vmix/info.json и т.п. + old_style_match = re.match(r"^/vmix/(?!session/)(.+)$", path) + if old_style_match: + endpoint = old_style_match.group(1).strip("/") + if endpoint.endswith(".json"): + endpoint = endpoint[:-5] + return f"{new_base_url}/vmix/session/{session_token}/{endpoint}" + # fallback: если структура другая, просто меняем host return re.sub(r"https?://[^/]+", new_base_url, old_url) @@ -73,6 +95,7 @@ def change_vmix_datasource_urls( session_token: str, match_id: str | int | None = None, operator_login: str | None = None, + source_key: str | None = None, ) -> bytes: if isinstance(xml_data, dict): candidate = None @@ -155,6 +178,9 @@ def change_vmix_datasource_urls( # value2 = match_id get_or_create_dynamic_value(1).text = "" if match_id is None else str(match_id) + # value3 = source_key (SUPERLEAGUE / RUSSIAN_CUP) + get_or_create_dynamic_value(2).text = "" if not source_key else normalize_source_key(source_key) + # value4 = operator_login get_or_create_dynamic_value(3).text = "" if not operator_login else str(operator_login) @@ -168,14 +194,18 @@ def build_vmix_project_bytes( operator_login: str | None = None, source_key: str | None = None, ) -> bytes: + source_key = normalize_source_key(source_key) vmix_preset_path = get_vmix_preset_path(source_key) if not vmix_preset_path: raise RuntimeError("Не задан путь к vMix-пресету: SYNO_PATH_VMIX или SYNO_PATH_VMIX_2") + syno_url, syno_username, syno_password = get_syno_settings() + print(f"[vmix] source_key={source_key or '-'} preset_path={vmix_preset_path}") + vmix_bio = nasio.load_bio( - user=SYNO_USERNAME, - password=SYNO_PASSWORD, - nas_ip=SYNO_URL, + user=syno_username, + password=syno_password, + nas_ip=syno_url, nas_port="443", path=vmix_preset_path, ) @@ -186,6 +216,7 @@ def build_vmix_project_bytes( session_token, match_id, operator_login, + source_key, ) if isinstance(edited_vmix, str):