переделал систему подключения агента к веб-сокету
This commit is contained in:
30
agent.py
30
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(),
|
||||
}))
|
||||
|
||||
156
app.py
156
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,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1072,6 +1072,8 @@ data-role="{{ p.position or '' }}"
|
||||
referees: {{ referees | tojson }},
|
||||
refereePool: {{ referee_pool | tojson }},
|
||||
|
||||
currentUser: {{ current_user | tojson }},
|
||||
|
||||
homeSquadPool: [],
|
||||
awaySquadPool: [],
|
||||
homeCoachPool: [],
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user