diff --git a/agent.py b/agent.py index 6707002..95cfe4f 100644 --- a/agent.py +++ b/agent.py @@ -36,12 +36,14 @@ def read_vmix_dynamic_values(): session_token = get_text("./dynamic/value1") match_id = get_text("./dynamic/value2") group_name = get_text("./dynamic/value3") + operator_login = get_text("./dynamic/value4") return { "ok": True, "session_token": session_token, "match_id": int(match_id) if match_id and match_id.isdigit() else None, "group_name": group_name, + "operator_login": operator_login, } except Exception as e: return { @@ -50,6 +52,7 @@ def read_vmix_dynamic_values(): "session_token": None, "match_id": None, "group_name": None, + "operator_login": None, } def execute_vmix_command(path: str): @@ -82,7 +85,7 @@ async def wait_for_session(): print("[agent] waiting for vMix session...", data) await asyncio.sleep(POLL_INTERVAL) -def build_ws_url(session_token: str, match_id: int | None, group_name: str | None): +def build_ws_url(session_token: str, match_id: int | None, group_name: str | None, operator_login: str | None): params = { "client_id": CLIENT_ID, "session_token": session_token, @@ -91,6 +94,8 @@ def build_ws_url(session_token: str, match_id: int | None, group_name: str | Non params["match_id"] = match_id if group_name: params["group_name"] = group_name + if operator_login: + params["operator_name"] = operator_login return f"{WS_BASE}?{urlencode(params)}" async def ping_loop(ws): @@ -100,6 +105,9 @@ async def ping_loop(ws): async def run_agent(): current_session = None + current_match_id = None + current_group_name = None + current_operator_login = None while True: vmix_data = await wait_for_session() @@ -107,12 +115,13 @@ async def run_agent(): session_token = vmix_data["session_token"] match_id = vmix_data["match_id"] group_name = vmix_data["group_name"] + operator_login = vmix_data["operator_login"] if current_session != session_token: print(f"[agent] found session: {session_token}") current_session = session_token - ws_url = build_ws_url(session_token, match_id, group_name) + ws_url = build_ws_url(session_token, match_id, group_name, operator_login) try: async with websockets.connect(ws_url, ping_interval=None, max_size=2**20) as ws: @@ -124,6 +133,7 @@ async def run_agent(): "session_token": session_token, "match_id": match_id, "group_name": group_name, + "operator_name": operator_login, })) pinger = asyncio.create_task(ping_loop(ws)) @@ -132,9 +142,17 @@ async def run_agent(): while True: # следим, не сменился ли проект/session в vMix latest = read_vmix_dynamic_values() - if latest["ok"] and latest["session_token"] and latest["session_token"] != current_session: - print("[agent] session changed in vMix, reconnecting") - break + if latest["ok"]: + latest_match_id = latest.get("match_id") + latest_group_name = latest.get("group_name") + latest_operator_login = latest.get("operator_login") + if ( + latest_match_id != match_id + or latest_group_name != group_name + or latest_operator_login != operator_login + ): + print("[agent] routing meta changed in vMix, reconnecting") + break try: raw = await asyncio.wait_for(ws.recv(), timeout=3) @@ -150,6 +168,8 @@ async def run_agent(): "type": "vmix_result", "client_id": CLIENT_ID, "session_token": session_token, + "match_id": match_id, + "operator_name": operator_login, "results": results, "sent_at": time.time(), })) diff --git a/app.py b/app.py index c917311..51039f1 100644 --- a/app.py +++ b/app.py @@ -16,11 +16,8 @@ import json from pathlib import Path import io import asyncio -import contextlib -import traceback import time from urllib.parse import quote -from db import get_connection from repositories.match_formation_repository import ( get_match_formations, @@ -29,9 +26,6 @@ from repositories.match_formation_repository import ( ) from parsers.parser_game import run_parser_game -from parsers.parser_players import run_parser_players -from parsers.parser_schedule import run_parser_schedule -from parsers.parser_standings import run_parser_standings from repositories.match_session_repository import ( create_match_session, get_match_session_by_token, @@ -91,7 +85,6 @@ from repositories.match_event_repository import ( from repositories.auth_repository import get_user_by_username from services.auth_service import ( create_auth_session, - hash_password, verify_password, get_current_user_from_request, build_not_authenticated_response, @@ -128,6 +121,7 @@ class PublishVmixCommandPayload(BaseModel): commands: List[str] = Field(default_factory=list) target_client_id: str | None = None target_group: str | None = None + target_login: str | None = None meta: dict | None = None @@ -242,6 +236,7 @@ class VmixConnectionManager: message: dict, target_group: str | None = None, session_token: str | None = None, + target_login: str | None = None, ): async with self.lock: items = list(self.connections.values()) @@ -252,6 +247,8 @@ class VmixConnectionManager: continue if target_group and conn.get("group_name") != target_group: continue + if target_login and conn.get("operator_name") != target_login: + continue if session_token and conn.get("session_token") != session_token: continue matched.append(conn["client_id"]) @@ -578,13 +575,20 @@ def download_vmix_project_page(request: Request, session_token: str): @app.get("/admin/session/{session_token}/download-vmix") -def download_vmix_project(session_token: str): +def download_vmix_project(request: Request, session_token: str): session_row = get_match_session_by_token(session_token) if not session_row: return JSONResponse({"error": "session_not_found"}, status_code=404) + current_user = getattr(request.state, "current_user", None) or {} + operator_login = current_user.get("username") or None + try: - vmix_bytes = build_vmix_project_bytes(session_token=session_token, match_id=session_row[1]) + vmix_bytes = build_vmix_project_bytes( + session_token=session_token, + match_id=session_row[1], + operator_login=operator_login, + ) filename = build_vmix_filename(session_row) return StreamingResponse( @@ -732,143 +736,17 @@ def close_session(session_token: str): return RedirectResponse(url="/admin/matches", status_code=303) -def render_admin_db_index( - request: Request, - parser_output: str | None = None, - parser_name: str | None = None, - parser_success: bool | None = None, - account_output: str | None = None, - account_success: bool | None = None, -): +@app.get("/admin/db", response_class=HTMLResponse) +def admin_db_index(request: Request): return templates.TemplateResponse( name="admin_db_index.html", request=request, context={ "current_user": getattr(request.state, "current_user", None), - "parser_output": parser_output, - "parser_name": parser_name, - "parser_success": parser_success, - "account_output": account_output, - "account_success": account_success, }, ) -@app.get("/admin/db", response_class=HTMLResponse) -def admin_db_index(request: Request): - return render_admin_db_index(request) - - -@app.post("/admin/db/run-parser", response_class=HTMLResponse) -def admin_db_run_parser(request: Request, parser_name: str = Form(...)): - current_user = getattr(request.state, "current_user", None) - if not current_user or current_user.get("role") != "admin": - return RedirectResponse(url="/login", status_code=303) - - parser_map = { - "players": ("Игроки", run_parser_players), - "schedule": ("Расписание", run_parser_schedule), - "standings": ("Турнирка", run_parser_standings), - } - - parser_meta = parser_map.get(parser_name) - if not parser_meta: - return render_admin_db_index( - request, - parser_output="Неизвестный парсер.", - parser_name=parser_name, - parser_success=False, - ) - - title, parser_func = parser_meta - buffer = io.StringIO() - success = True - - with contextlib.redirect_stdout(buffer), contextlib.redirect_stderr(buffer): - print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Запуск парсера: {title}") - try: - parser_func() - print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Парсер завершён успешно") - except Exception: - success = False - print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Ошибка при выполнении парсера") - print(traceback.format_exc()) - - output = buffer.getvalue().strip() or "Парсер не вернул сообщений." - return render_admin_db_index( - request, - parser_output=output, - parser_name=title, - parser_success=success, - ) - - -@app.post("/admin/db/create-account", response_class=HTMLResponse) -def admin_db_create_account( - request: Request, - username: str = Form(...), - password: str = Form(...), - role: str = Form(default="operator"), -): - current_user = getattr(request.state, "current_user", None) - if not current_user or current_user.get("role") != "admin": - return RedirectResponse(url="/login", status_code=303) - - username = username.strip() - role = (role or "operator").strip().lower() - - if not username: - return render_admin_db_index(request, account_output="Логин обязателен.", account_success=False) - - if not password: - return render_admin_db_index(request, account_output="Пароль обязателен.", account_success=False) - - if len(password) < 6: - return render_admin_db_index(request, account_output="Пароль должен быть не короче 6 символов.", account_success=False) - - if role not in {"admin", "operator"}: - return render_admin_db_index(request, account_output="Роль должна быть admin или operator.", account_success=False) - - password_hash = hash_password(password) - conn = get_connection() - - try: - with conn.cursor() as cur: - cur.execute( - """ - INSERT INTO admin_users (username, password_hash, role, is_active) - VALUES (%s, %s, %s, TRUE) - ON CONFLICT (username) DO NOTHING - RETURNING id; - """, - (username, password_hash, role), - ) - row = cur.fetchone() - conn.commit() - - if row: - return render_admin_db_index( - request, - account_output=f"Пользователь создан: {username} (id={row[0]})\nРоль: {role}", - account_success=True, - ) - - return render_admin_db_index( - request, - account_output=f"Пользователь '{username}' уже существует.", - account_success=False, - ) - - except Exception: - conn.rollback() - return render_admin_db_index( - request, - account_output="Ошибка при создании аккаунта:\n" + traceback.format_exc(), - account_success=False, - ) - finally: - conn.close() - @app.get("/admin/db/players", response_class=HTMLResponse) def admin_db_players(request: Request, q: str = Query(default="")): # denied = require_role(request, {"admin"}) @@ -1700,6 +1578,7 @@ async def ws_vmix_client( "client_id": client_id, "match_id": match_id, "session_token": session_token, + "operator_name": operator_name, }) while True: @@ -1748,6 +1627,7 @@ async def publish_vmix_command(payload: PublishVmixCommandPayload, request: Requ "match_id": payload.match_id, "session_token": payload.session_token, "commands": commands, + "target_login": payload.target_login, "meta": payload.meta or {}, "sent_at": time.time(), } @@ -1761,6 +1641,7 @@ async def publish_vmix_command(payload: PublishVmixCommandPayload, request: Requ message=message, target_group=payload.target_group, session_token=payload.session_token, + target_login=payload.target_login, ) log_action( @@ -1773,6 +1654,7 @@ async def publish_vmix_command(payload: PublishVmixCommandPayload, request: Requ details={ "target_client_id": payload.target_client_id, "target_group": payload.target_group, + "target_login": payload.target_login, "commands_count": len(commands), "results": results, }, diff --git a/static/script.js b/static/script.js index 6fa86ee..b895fa5 100644 --- a/static/script.js +++ b/static/script.js @@ -39,6 +39,7 @@ const REFEREE_ROLE_OPTIONS = [ const VMIX_TARGET_CLIENT_ID = MATCH_DATA.vmixTargetClientId || null; const VMIX_TARGET_GROUP = MATCH_DATA.vmixTargetGroup || null; +const CURRENT_USER_LOGIN = MATCH_DATA.currentUser?.username || null; function vmixCmd(path) { const value = String(path || "").trim(); @@ -1467,9 +1468,9 @@ async function sendDirectVmixCommands(commands) { }, body: JSON.stringify({ match_id: MATCH_ID, - session_token: SESSION_TOKEN, target_client_id: VMIX_TARGET_CLIENT_ID, target_group: VMIX_TARGET_GROUP, + target_login: CURRENT_USER_LOGIN, commands: items }) }); diff --git a/templates/match_workspace.html b/templates/match_workspace.html index c7f65ea..c69e397 100644 --- a/templates/match_workspace.html +++ b/templates/match_workspace.html @@ -1072,6 +1072,8 @@ data-role="{{ p.position or '' }}" referees: {{ referees | tojson }}, refereePool: {{ referee_pool | tojson }}, + currentUser: {{ current_user | tojson }}, + homeSquadPool: [], awaySquadPool: [], homeCoachPool: [], diff --git a/vmix/vmix_service.py b/vmix/vmix_service.py index bfdc862..a23dd6e 100644 --- a/vmix/vmix_service.py +++ b/vmix/vmix_service.py @@ -133,12 +133,18 @@ def change_vmix_datasource_urls( # 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) -> bytes: +def build_vmix_project_bytes( + session_token: str, + match_id: str | int | None = None, + operator_login: str | None = None, +) -> bytes: vmix_bio = nasio.load_bio( user=SYNO_USERNAME, password=SYNO_PASSWORD, @@ -152,6 +158,7 @@ def build_vmix_project_bytes(session_token: str, match_id: str | int | None = No FQDN, session_token, match_id, + operator_login, ) if isinstance(edited_vmix, str):