тест 8
This commit is contained in:
179
app.py
179
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 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()
|
||||||
|
|||||||
Reference in New Issue
Block a user