import nasio import re import xml.etree.ElementTree as ET import io import platform import os from urllib.parse import urlparse from dotenv import load_dotenv load_dotenv() 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: return str(source_key or "").upper().strip() def get_vmix_preset_path(source_key: str | None = None) -> str | None: """Выбирает NAS-путь к vMix-пресету по источнику матча. SUPERLEAGUE -> SYNO_PATH_VMIX RUSSIAN_CUP -> SYNO_PATH_VMIX_2, если он заполнен Если второй путь не задан, используем основной, чтобы скачивание не ломалось. """ key = normalize_source_key(source_key) if key == "RUSSIAN_CUP" and SYNO_PATH_VMIX_2: return SYNO_PATH_VMIX_2 return SYNO_PATH_VMIX def get_fqdn(): system_name = platform.system() if system_name == "Linux": hostname = platform.node().lower() return f"https://{hostname}.tvstart.ru" return "http://127.0.0.1:8000" FQDN = get_fqdn() def rebuild_vmix_url(old_url: str, new_base_url: str, session_token: str) -> str: """ Меняет URL вида: http://127.0.0.1:8000/vmix/session/OLDTOKEN/home-lineup на: https://hostname.tvstart.ru/vmix/session/NEWTOKEN/home-lineup """ old_url = (old_url or "").strip() parsed = urlparse(old_url) path = parsed.path or "" match = re.match(r"^/vmix/session/[^/]+/(.+)$", path) if match: endpoint = match.group(1) return f"{new_base_url}/vmix/session/{session_token}/{endpoint}" # fallback: если структура другая, просто меняем host return re.sub(r"https?://[^/]+", new_base_url, old_url) def change_vmix_datasource_urls( xml_data, new_base_url: str, session_token: str, match_id: str | int | None = None, operator_login: str | None = None, ) -> bytes: if isinstance(xml_data, dict): candidate = None for key in ("content", "data", "body", "text", "file", "bio"): if key in xml_data and xml_data[key] is not None: candidate = xml_data[key] break if candidate is None: raise TypeError( f"Unsupported xml_data dict structure. Keys: {list(xml_data.keys())}" ) if isinstance(candidate, (bytes, bytearray)): raw_bytes = bytes(candidate) elif isinstance(candidate, str): raw_bytes = candidate.encode("utf-8") elif isinstance(candidate, io.IOBase) or hasattr(candidate, "read"): raw_bytes = candidate.read() try: candidate.seek(0) except Exception: pass else: raise TypeError( f"Unsupported xml_data payload inside dict: {type(candidate)}; " f"available keys: {list(xml_data.keys())}" ) elif isinstance(xml_data, (bytes, bytearray)): raw_bytes = bytes(xml_data) elif isinstance(xml_data, str): raw_bytes = xml_data.encode("utf-8") elif isinstance(xml_data, io.IOBase) or hasattr(xml_data, "read"): raw_bytes = xml_data.read() try: xml_data.seek(0) except Exception: pass else: raise TypeError(f"Unsupported xml_data type: {type(xml_data)}") text = raw_bytes.decode("utf-8", errors="replace") root = ET.fromstring(text) for url_tag in root.findall( ".//datasource[@friendlyName='JSON']//instance//state/xml/url" ): old_url = (url_tag.text or "").strip() url_tag.text = rebuild_vmix_url(old_url, new_base_url, session_token) dynamic_settings = root.find(".//DynamicSettings") if dynamic_settings is None: dynamic_settings = ET.SubElement(root, "DynamicSettings") dynamic = dynamic_settings.find("Dynamic") if dynamic is None: dynamic = ET.SubElement(dynamic_settings, "Dynamic") def get_or_create_dynamic_value(index: int): values = dynamic.findall("DynamicValue") # гарантируем нужное количество while len(values) <= index: ET.SubElement(dynamic, "DynamicInput") ET.SubElement(dynamic, "DynamicValue") values = dynamic.findall("DynamicValue") return values[index] # value1 = session get_or_create_dynamic_value(0).text = str(session_token) # value2 = match_id get_or_create_dynamic_value(1).text = "" if match_id is None else str(match_id) # value4 = operator_login get_or_create_dynamic_value(3).text = "" if not operator_login else str(operator_login) return ET.tostring(root, encoding="utf-8", method="xml") def build_vmix_project_bytes( session_token: str, match_id: str | int | None = None, operator_login: str | None = None, source_key: str | None = None, ) -> bytes: vmix_preset_path = get_vmix_preset_path(source_key) if not vmix_preset_path: raise RuntimeError("Не задан путь к vMix-пресету: SYNO_PATH_VMIX или SYNO_PATH_VMIX_2") vmix_bio = nasio.load_bio( user=SYNO_USERNAME, password=SYNO_PASSWORD, nas_ip=SYNO_URL, nas_port="443", path=vmix_preset_path, ) edited_vmix = change_vmix_datasource_urls( vmix_bio, FQDN, session_token, match_id, operator_login, ) if isinstance(edited_vmix, str): edited_vmix = edited_vmix.encode("utf-8") return edited_vmix def build_vmix_filename(session_row) -> str: home = session_row[14].replace("«", "").replace("»", "") away = session_row[18].replace("«", "").replace("»", "") tour = session_row[10] date_str = session_row[9].strftime("%d%m%Y") filename = f"ЖФЛ_{home} VS {away}_{tour}_{date_str}.vmix" return filename