From 4e6806c5538f693e41dcea2653d3bf08d2fccc62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AE=D1=80=D0=B8=D0=B9=20=D0=A7=D0=B5=D1=80=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE?= Date: Fri, 15 May 2026 14:28:14 +0300 Subject: [PATCH] =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=208?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 179 +++++++++++++++++++++++++++++++-------------------------- 1 file changed, 98 insertions(+), 81 deletions(-) diff --git a/app.py b/app.py index e55cf02..927b669 100644 --- a/app.py +++ b/app.py @@ -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 fastapi.responses import ( HTMLResponse, @@ -125,24 +133,22 @@ app = FastAPI( docs_url=None, # ❌ отключает /docs redoc_url=None, # ❌ отключает /redoc openapi_url=None, # ❌ отключает /openapi.json - ) templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static") + @app.on_event("startup") def start_scheduler(): import threading thread = threading.Thread( - target=run_scheduler, - kwargs={"with_signal_handlers": False}, - daemon=True + target=run_scheduler, kwargs={"with_signal_handlers": False}, daemon=True ) thread.start() - - + + class PublishVmixCommandPayload(BaseModel): match_id: int session_token: str | None = None @@ -257,7 +263,7 @@ class VmixConnectionManager: except Exception: await self.disconnect(client_id) return False - + async def send_to_match( self, match_id: int, @@ -288,6 +294,7 @@ class VmixConnectionManager: vmix_ws_manager = VmixConnectionManager() + def get_current_user(request: Request): 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 None + @app.get("/") def root(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))) except OSError: return "1" - - + + def log_action( request: Request, action: str, @@ -519,7 +527,7 @@ def admin_matches( "current_user": getattr(request.state, "current_user", None), }, ) - + @app.get("/admin/matches/{match_id}/select") def select_match( @@ -549,14 +557,18 @@ def select_match( }, ) - return JSONResponse({ - "success": True, - "session_token": session_token, - "session_url": f"/admin/session/{session_token}", - }) + return JSONResponse( + { + "success": True, + "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): session_row = get_match_session_by_token(session_token) if not session_row: @@ -601,8 +613,8 @@ def download_vmix_project(request: Request, session_token: str): }, ) 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) def session_workspace( @@ -634,7 +646,6 @@ def session_workspace( home_formations = get_match_formations(match_id, home_team_id) away_formations = get_match_formations(match_id, away_team_id) - return templates.TemplateResponse( name="match_workspace.html", @@ -728,9 +739,9 @@ def load_match_data(request: Request, session_token: str): return RedirectResponse( url=f"/admin/session/{session_token}?tab=game", status_code=303, - ) - - + ) + + @app.post("/admin/session/{session_token}/close") def close_session(session_token: str): deactivate_match_session(session_token) @@ -1404,40 +1415,12 @@ def vmix_standings(session_token: str): for row in rows ] + class MatchChannelPayload(BaseModel): match_external_id: 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") def vmix_schedule(session_token: str): session_row = get_match_session_by_token(session_token) @@ -1462,9 +1445,17 @@ def vmix_schedule(session_token: str): "color": ( "#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_score": "#FFFFFF00" if row[2] is None and row[3] is None else "#FFFFFF", - "channel": row[7] or "", + "mask_time": ( + "#FFFFFF00" if row[2] is not None and row[3] is not None else "#FFFFFF" + ), + "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 ] @@ -1590,6 +1581,7 @@ def vmix_match_events(session_token: str): return new_events + @app.websocket("/ws/vmix-client") async def ws_vmix_client( websocket: WebSocket, @@ -1609,13 +1601,15 @@ async def ws_vmix_client( ) try: - await websocket.send_json({ - "type": "connected", - "client_id": client_id, - "match_id": match_id, - "session_token": session_token, - "operator_name": operator_name, - }) + await websocket.send_json( + { + "type": "connected", + "client_id": client_id, + "match_id": match_id, + "session_token": session_token, + "operator_name": operator_name, + } + ) while True: msg = await websocket.receive_json() @@ -1635,7 +1629,9 @@ async def ws_vmix_client( operator_name=msg.get("operator_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 if msg_type == "vmix_result": @@ -1652,6 +1648,7 @@ async def ws_vmix_client( except Exception: pass + @app.post("/api/vmix/publish-command") async def publish_vmix_command(payload: PublishVmixCommandPayload, request: Request): 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, "commands_count": len(commands), } - + + @app.get("/api/vmix/clients") async def list_vmix_clients(): clients = await vmix_ws_manager.list_clients() return {"items": clients} + @app.get("/info", response_class=HTMLResponse) def info_page(request: Request): - return templates.TemplateResponse( - name="info.html", - request=request, - context={} - ) - -def render_admin_db_index(request: Request, parser_output: str | None = None, parser_name: str | None = None, parser_success: bool | None = None): + return templates.TemplateResponse(name="info.html", request=request, context={}) + + +def render_admin_db_index( + request: Request, + parser_output: str | None = None, + parser_name: str | None = None, + parser_success: bool | None = None, +): return templates.TemplateResponse( name="admin_db_index.html", request=request, @@ -1757,13 +1758,19 @@ def admin_db_run_parser(request: Request, parser_name: str = Form(...)): success = True 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: 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: 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()) 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) def admin_db_create_account( request: Request, @@ -1792,16 +1797,28 @@ def admin_db_create_account( role = (role or "operator").strip().lower() 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: - 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: - 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"}: - 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) conn = get_connection() @@ -1841,4 +1858,4 @@ def admin_db_create_account( parser_success=False, ) finally: - conn.close() \ No newline at end of file + conn.close()