тест 8
This commit is contained in:
135
app.py
135
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,20 +133,18 @@ 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()
|
||||
|
||||
@@ -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)
|
||||
@@ -549,14 +557,18 @@ def select_match(
|
||||
},
|
||||
)
|
||||
|
||||
return JSONResponse({
|
||||
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:
|
||||
@@ -635,7 +647,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",
|
||||
request=request,
|
||||
@@ -1404,39 +1415,11 @@ 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):
|
||||
@@ -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({
|
||||
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()]
|
||||
@@ -1700,20 +1697,24 @@ async def publish_vmix_command(payload: PublishVmixCommandPayload, request: Requ
|
||||
"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={}
|
||||
)
|
||||
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):
|
||||
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user