тест 8

This commit is contained in:
2026-05-15 14:28:14 +03:00
parent a8947556a5
commit 4e6806c553

179
app.py
View File

@@ -1,4 +1,12 @@
from fastapi import FastAPI, Form, Request, Query, HTTPException, WebSocket, WebSocketDisconnect from fastapi import (
FastAPI,
Form,
Request,
Query,
HTTPException,
WebSocket,
WebSocketDisconnect,
)
from datetime import datetime from datetime import datetime
from fastapi.responses import ( from fastapi.responses import (
HTMLResponse, HTMLResponse,
@@ -125,24 +133,22 @@ app = FastAPI(
docs_url=None, # ❌ отключает /docs docs_url=None, # ❌ отключает /docs
redoc_url=None, # ❌ отключает /redoc redoc_url=None, # ❌ отключает /redoc
openapi_url=None, # ❌ отключает /openapi.json openapi_url=None, # ❌ отключает /openapi.json
) )
templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static") app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
@app.on_event("startup") @app.on_event("startup")
def start_scheduler(): def start_scheduler():
import threading import threading
thread = threading.Thread( thread = threading.Thread(
target=run_scheduler, target=run_scheduler, kwargs={"with_signal_handlers": False}, daemon=True
kwargs={"with_signal_handlers": False},
daemon=True
) )
thread.start() thread.start()
class PublishVmixCommandPayload(BaseModel): class PublishVmixCommandPayload(BaseModel):
match_id: int match_id: int
session_token: str | None = None session_token: str | None = None
@@ -257,7 +263,7 @@ class VmixConnectionManager:
except Exception: except Exception:
await self.disconnect(client_id) await self.disconnect(client_id)
return False return False
async def send_to_match( async def send_to_match(
self, self,
match_id: int, match_id: int,
@@ -288,6 +294,7 @@ class VmixConnectionManager:
vmix_ws_manager = VmixConnectionManager() vmix_ws_manager = VmixConnectionManager()
def get_current_user(request: Request): def get_current_user(request: Request):
return getattr(request.state, "current_user", None) return getattr(request.state, "current_user", None)
@@ -304,6 +311,7 @@ def require_role(request: Request, allowed_roles: set[str]):
return RedirectResponse("/admin/matches", status_code=303) return RedirectResponse("/admin/matches", status_code=303)
return None return None
@app.get("/") @app.get("/")
def root(request: Request): def root(request: Request):
user = get_current_user_from_request(request) user = get_current_user_from_request(request)
@@ -318,8 +326,8 @@ def static_version(filename: str) -> str:
return str(int(os.path.getmtime(path))) return str(int(os.path.getmtime(path)))
except OSError: except OSError:
return "1" return "1"
def log_action( def log_action(
request: Request, request: Request,
action: str, action: str,
@@ -519,7 +527,7 @@ def admin_matches(
"current_user": getattr(request.state, "current_user", None), "current_user": getattr(request.state, "current_user", None),
}, },
) )
@app.get("/admin/matches/{match_id}/select") @app.get("/admin/matches/{match_id}/select")
def select_match( def select_match(
@@ -549,14 +557,18 @@ def select_match(
}, },
) )
return JSONResponse({ return JSONResponse(
"success": True, {
"session_token": session_token, "success": True,
"session_url": f"/admin/session/{session_token}", "session_token": session_token,
}) "session_url": f"/admin/session/{session_token}",
}
)
@app.get("/admin/session/{session_token}/download-vmix-page", response_class=HTMLResponse) @app.get(
"/admin/session/{session_token}/download-vmix-page", response_class=HTMLResponse
)
def download_vmix_project_page(request: Request, session_token: str): def download_vmix_project_page(request: Request, session_token: str):
session_row = get_match_session_by_token(session_token) session_row = get_match_session_by_token(session_token)
if not session_row: if not session_row:
@@ -601,8 +613,8 @@ def download_vmix_project(request: Request, session_token: str):
}, },
) )
except Exception as e: except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500) return JSONResponse({"error": str(e)}, status_code=500)
@app.get("/admin/session/{session_token}", response_class=HTMLResponse) @app.get("/admin/session/{session_token}", response_class=HTMLResponse)
def session_workspace( def session_workspace(
@@ -634,7 +646,6 @@ def session_workspace(
home_formations = get_match_formations(match_id, home_team_id) home_formations = get_match_formations(match_id, home_team_id)
away_formations = get_match_formations(match_id, away_team_id) away_formations = get_match_formations(match_id, away_team_id)
return templates.TemplateResponse( return templates.TemplateResponse(
name="match_workspace.html", name="match_workspace.html",
@@ -728,9 +739,9 @@ def load_match_data(request: Request, session_token: str):
return RedirectResponse( return RedirectResponse(
url=f"/admin/session/{session_token}?tab=game", url=f"/admin/session/{session_token}?tab=game",
status_code=303, status_code=303,
) )
@app.post("/admin/session/{session_token}/close") @app.post("/admin/session/{session_token}/close")
def close_session(session_token: str): def close_session(session_token: str):
deactivate_match_session(session_token) deactivate_match_session(session_token)
@@ -1404,40 +1415,12 @@ def vmix_standings(session_token: str):
for row in rows for row in rows
] ]
class MatchChannelPayload(BaseModel): class MatchChannelPayload(BaseModel):
match_external_id: str match_external_id: str
channel: str = "" channel: str = ""
@app.post("/admin/session/{session_token}/schedule/channel")
def update_schedule_channel(session_token: str, payload: MatchChannelPayload):
session_row = get_match_session_by_token(session_token)
if not session_row:
return JSONResponse({"error": "session_not_found"}, status_code=404)
allowed = {"", "match", "match_premier", "match_and_premier"}
if payload.channel not in allowed:
return JSONResponse({"error": "invalid_channel"}, status_code=400)
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
UPDATE matches
SET channel = %s
WHERE external_id = %s
""",
(payload.channel, payload.match_external_id),
)
conn.commit()
return {"success": True}
except Exception as e:
conn.rollback()
return JSONResponse({"error": str(e)}, status_code=500)
finally:
conn.close()
@app.get("/vmix/session/{session_token}/schedule") @app.get("/vmix/session/{session_token}/schedule")
def vmix_schedule(session_token: str): def vmix_schedule(session_token: str):
session_row = get_match_session_by_token(session_token) session_row = get_match_session_by_token(session_token)
@@ -1462,9 +1445,17 @@ def vmix_schedule(session_token: str):
"color": ( "color": (
"#37F193" if row[5] not in ["scheduled", "finished"] else "#FFFFFF00" "#37F193" if row[5] not in ["scheduled", "finished"] else "#FFFFFF00"
), ),
"mask_time": "#FFFFFF00" if row[2] is not None and row[3] is not None else "#FFFFFF", "mask_time": (
"mask_score": "#FFFFFF00" if row[2] is None and row[3] is None else "#FFFFFF", "#FFFFFF00" if row[2] is not None and row[3] is not None else "#FFFFFF"
"channel": row[7] or "", ),
"mask_score": (
"#FFFFFF00" if row[2] is None and row[3] is None else "#FFFFFF"
),
"channel": (
rf"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Лого каналов\{row[7]}.png"
if row[7]
else r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Лого каналов\EMPTY.png"
),
} }
for row in rows for row in rows
] ]
@@ -1590,6 +1581,7 @@ def vmix_match_events(session_token: str):
return new_events return new_events
@app.websocket("/ws/vmix-client") @app.websocket("/ws/vmix-client")
async def ws_vmix_client( async def ws_vmix_client(
websocket: WebSocket, websocket: WebSocket,
@@ -1609,13 +1601,15 @@ async def ws_vmix_client(
) )
try: try:
await websocket.send_json({ await websocket.send_json(
"type": "connected", {
"client_id": client_id, "type": "connected",
"match_id": match_id, "client_id": client_id,
"session_token": session_token, "match_id": match_id,
"operator_name": operator_name, "session_token": session_token,
}) "operator_name": operator_name,
}
)
while True: while True:
msg = await websocket.receive_json() msg = await websocket.receive_json()
@@ -1635,7 +1629,9 @@ async def ws_vmix_client(
operator_name=msg.get("operator_name"), operator_name=msg.get("operator_name"),
group_name=msg.get("group_name"), group_name=msg.get("group_name"),
) )
await websocket.send_json({"type": "registered", "client_id": client_id}) await websocket.send_json(
{"type": "registered", "client_id": client_id}
)
continue continue
if msg_type == "vmix_result": if msg_type == "vmix_result":
@@ -1652,6 +1648,7 @@ async def ws_vmix_client(
except Exception: except Exception:
pass pass
@app.post("/api/vmix/publish-command") @app.post("/api/vmix/publish-command")
async def publish_vmix_command(payload: PublishVmixCommandPayload, request: Request): async def publish_vmix_command(payload: PublishVmixCommandPayload, request: Request):
commands = [str(x).strip() for x in (payload.commands or []) if str(x).strip()] commands = [str(x).strip() for x in (payload.commands or []) if str(x).strip()]
@@ -1699,21 +1696,25 @@ async def publish_vmix_command(payload: PublishVmixCommandPayload, request: Requ
"delivered_to": results, "delivered_to": results,
"commands_count": len(commands), "commands_count": len(commands),
} }
@app.get("/api/vmix/clients") @app.get("/api/vmix/clients")
async def list_vmix_clients(): async def list_vmix_clients():
clients = await vmix_ws_manager.list_clients() clients = await vmix_ws_manager.list_clients()
return {"items": clients} return {"items": clients}
@app.get("/info", response_class=HTMLResponse) @app.get("/info", response_class=HTMLResponse)
def info_page(request: Request): def info_page(request: Request):
return templates.TemplateResponse( return templates.TemplateResponse(name="info.html", request=request, context={})
name="info.html",
request=request,
context={} def render_admin_db_index(
) request: Request,
parser_output: str | None = None,
def render_admin_db_index(request: Request, parser_output: str | None = None, parser_name: str | None = None, parser_success: bool | None = None): parser_name: str | None = None,
parser_success: bool | None = None,
):
return templates.TemplateResponse( return templates.TemplateResponse(
name="admin_db_index.html", name="admin_db_index.html",
request=request, request=request,
@@ -1757,13 +1758,19 @@ def admin_db_run_parser(request: Request, parser_name: str = Form(...)):
success = True success = True
with contextlib.redirect_stdout(buffer), contextlib.redirect_stderr(buffer): with contextlib.redirect_stdout(buffer), contextlib.redirect_stderr(buffer):
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Запуск парсера: {title}") print(
f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Запуск парсера: {title}"
)
try: try:
parser_func() parser_func()
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Парсер завершён успешно") print(
f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Парсер завершён успешно"
)
except Exception: except Exception:
success = False success = False
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Ошибка при выполнении парсера") print(
f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Ошибка при выполнении парсера"
)
print(traceback.format_exc()) print(traceback.format_exc())
output = buffer.getvalue().strip() or "Парсер не вернул сообщений." output = buffer.getvalue().strip() or "Парсер не вернул сообщений."
@@ -1775,8 +1782,6 @@ def admin_db_run_parser(request: Request, parser_name: str = Form(...)):
) )
@app.post("/admin/db/create-account", response_class=HTMLResponse) @app.post("/admin/db/create-account", response_class=HTMLResponse)
def admin_db_create_account( def admin_db_create_account(
request: Request, request: Request,
@@ -1792,16 +1797,28 @@ def admin_db_create_account(
role = (role or "operator").strip().lower() role = (role or "operator").strip().lower()
if not username: if not username:
return render_admin_db_index(request, parser_output="Логин обязателен.", parser_success=False) return render_admin_db_index(
request, parser_output="Логин обязателен.", parser_success=False
)
if not password: if not password:
return render_admin_db_index(request, parser_output="Пароль обязателен.", parser_success=False) return render_admin_db_index(
request, parser_output="Пароль обязателен.", parser_success=False
)
if len(password) < 6: if len(password) < 6:
return render_admin_db_index(request, parser_output="Пароль должен быть не короче 6 символов.", parser_success=False) return render_admin_db_index(
request,
parser_output="Пароль должен быть не короче 6 символов.",
parser_success=False,
)
if role not in {"admin", "operator"}: if role not in {"admin", "operator"}:
return render_admin_db_index(request, parser_output="Роль должна быть admin или operator.", parser_success=False) return render_admin_db_index(
request,
parser_output="Роль должна быть admin или operator.",
parser_success=False,
)
password_hash = hash_password(password) password_hash = hash_password(password)
conn = get_connection() conn = get_connection()
@@ -1841,4 +1858,4 @@ def admin_db_create_account(
parser_success=False, parser_success=False,
) )
finally: finally:
conn.close() conn.close()