обновление для Кубка России
переделаны все парсеры на ссылки из базы
This commit is contained in:
196
app.py
196
app.py
@@ -33,6 +33,10 @@ 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 parsers.parser_teams import run_parser_teams
|
||||
from parsers.parser_sources import build_empty_photo_path, list_parser_sources, get_default_source_key, get_parser_source
|
||||
from services.project_settings_service import build_project_settings_context, save_project_settings
|
||||
from repositories.project_settings_repository import ensure_project_settings_tables
|
||||
|
||||
|
||||
from db import get_connection
|
||||
@@ -55,7 +59,7 @@ from repositories.match_session_repository import (
|
||||
from repositories.match_view_repository import get_match_lineups_grouped
|
||||
from vmix.vmix_service import build_vmix_project_bytes, build_vmix_filename
|
||||
from repositories.match_referee_repository import get_match_referees
|
||||
from repositories.match_repository import get_tour_schedule_by_match_id
|
||||
from repositories.match_repository import get_tour_schedule_by_match_id, ensure_match_source_key_column
|
||||
from repositories.standings_repository import get_standings_by_match_id
|
||||
from repositories.match_coach_repository import get_match_coaches_grouped
|
||||
from repositories.player_repository import (
|
||||
@@ -186,7 +190,9 @@ def start_scheduler():
|
||||
import threading
|
||||
|
||||
try:
|
||||
ensure_project_settings_tables()
|
||||
ensure_player_photo_enabled_column()
|
||||
ensure_match_source_key_column()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -700,10 +706,13 @@ def download_vmix_project(request: Request, session_token: str):
|
||||
operator_login = current_user.get("username") or None
|
||||
|
||||
try:
|
||||
source_key = session_row[21] if len(session_row) > 21 else None
|
||||
|
||||
vmix_bytes = build_vmix_project_bytes(
|
||||
session_token=session_token,
|
||||
match_id=session_row[1],
|
||||
operator_login=operator_login,
|
||||
source_key=source_key,
|
||||
)
|
||||
filename = build_vmix_filename(session_row)
|
||||
|
||||
@@ -852,16 +861,6 @@ def close_session(session_token: str):
|
||||
return RedirectResponse(url="/admin/matches", status_code=303)
|
||||
|
||||
|
||||
@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),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/admin/db/players", response_class=HTMLResponse)
|
||||
def admin_db_players(
|
||||
@@ -1459,6 +1458,12 @@ EMPTY_PLAYER = {
|
||||
}
|
||||
|
||||
|
||||
def build_empty_player(source_key=None):
|
||||
item = EMPTY_PLAYER.copy()
|
||||
item["photo"] = build_empty_photo_path(source_key) or EMPTY_PHOTO_PATH
|
||||
return item
|
||||
|
||||
|
||||
def get_roster_data(session_token: str, name: str, count_player: int):
|
||||
session_row = get_match_session_by_token(session_token)
|
||||
if not session_row:
|
||||
@@ -1469,14 +1474,21 @@ def get_roster_data(session_token: str, name: str, count_player: int):
|
||||
home_team_name = session_row[14].replace("«", "").replace("»", "")
|
||||
away_team_id = session_row[17]
|
||||
away_team_name = session_row[18].replace("«", "").replace("»", "")
|
||||
source_key = session_row[21] if len(session_row) > 21 else None
|
||||
# print(session_row)
|
||||
|
||||
data = build_lineup_json(
|
||||
match_id, home_team_id, away_team_id, name, home_team_name, away_team_name
|
||||
match_id,
|
||||
home_team_id,
|
||||
away_team_id,
|
||||
name,
|
||||
home_team_name,
|
||||
away_team_name,
|
||||
source_key=source_key,
|
||||
)
|
||||
|
||||
players = data.get("players", [])
|
||||
players.extend([EMPTY_PLAYER.copy() for _ in range(count_player - len(players))])
|
||||
players.extend([build_empty_player(source_key) for _ in range(count_player - len(players))])
|
||||
|
||||
data["players"] = players[:count_player]
|
||||
return data
|
||||
@@ -1687,7 +1699,13 @@ EMPTY_PLAYER_FORMATION = {
|
||||
}
|
||||
|
||||
|
||||
def build_vmix_formation_response(rows, team_id, team_name):
|
||||
def build_empty_player_formation(source_key=None):
|
||||
item = EMPTY_PLAYER_FORMATION.copy()
|
||||
item["photo"] = build_empty_photo_path(source_key) or EMPTY_PHOTO_PATH
|
||||
return item
|
||||
|
||||
|
||||
def build_vmix_formation_response(rows, team_id, team_name, source_key=None):
|
||||
result = []
|
||||
|
||||
for p in rows:
|
||||
@@ -1719,18 +1737,21 @@ def build_vmix_formation_response(rows, team_id, team_name):
|
||||
"first_name": p[4] if len(p) > 4 else "",
|
||||
"number": number,
|
||||
"position": position,
|
||||
"photo": p[5] if len(p) > 5 else "",
|
||||
"photo_enabled": p[6] if len(p) > 6 else False,
|
||||
},
|
||||
generated_photo=build_generated_player_photo_path(
|
||||
team_name=team_name,
|
||||
last_name=lastname,
|
||||
first_name=p[4] if len(p) > 4 else "",
|
||||
source_key=source_key,
|
||||
),
|
||||
fallback_team_id=team_id,
|
||||
source_key=source_key,
|
||||
),
|
||||
}
|
||||
)
|
||||
result.extend([EMPTY_PLAYER_FORMATION.copy() for _ in range(11 - len(result))])
|
||||
result.extend([build_empty_player_formation(source_key) for _ in range(11 - len(result))])
|
||||
|
||||
return result
|
||||
|
||||
@@ -1743,8 +1764,9 @@ def vmix_home_formations(session_token: str):
|
||||
|
||||
home_team_id = session_row[13]
|
||||
home_team = session_row[14].replace("«", "").replace("»", "")
|
||||
source_key = session_row[21] if len(session_row) > 21 else None
|
||||
rows = get_vmix_team_formations(session_row, home_team_id)
|
||||
return build_vmix_formation_response(rows, home_team_id, home_team)
|
||||
return build_vmix_formation_response(rows, home_team_id, home_team, source_key=source_key)
|
||||
|
||||
|
||||
@app.get("/vmix/session/{session_token}/away-formations")
|
||||
@@ -1755,8 +1777,9 @@ def vmix_away_formations(session_token: str):
|
||||
|
||||
away_team_id = session_row[17]
|
||||
away_team = session_row[18].replace("«", "").replace("»", "")
|
||||
source_key = session_row[21] if len(session_row) > 21 else None
|
||||
rows = get_vmix_team_formations(session_row, away_team_id)
|
||||
return build_vmix_formation_response(rows, away_team_id, away_team)
|
||||
return build_vmix_formation_response(rows, away_team_id, away_team, source_key=source_key)
|
||||
|
||||
|
||||
@app.get("/vmix/session/{session_token}/scoreboard")
|
||||
@@ -2263,17 +2286,49 @@ def render_admin_db_index(
|
||||
parser_success: bool | None = None,
|
||||
update_output: str | None = None,
|
||||
update_success: bool | None = None,
|
||||
settings_output: str | None = None,
|
||||
settings_success: bool | None = None,
|
||||
):
|
||||
try:
|
||||
parser_sources = list_parser_sources()
|
||||
default_parser_source_key = get_default_source_key()
|
||||
except Exception:
|
||||
parser_sources = [
|
||||
{
|
||||
"key": "SUPERLEAGUE",
|
||||
"title": "Суперлига 2026",
|
||||
"logo_base_path": r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Teams Logos",
|
||||
"photo_base_path": r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Photo",
|
||||
},
|
||||
{
|
||||
"key": "RUSSIAN_CUP",
|
||||
"title": "Кубок России 2026",
|
||||
"logo_base_path": r"D:\Графика\ФУТБОЛ\Кубок России 2026\Teams Logos",
|
||||
"photo_base_path": r"D:\Графика\ФУТБОЛ\Кубок России 2026\Photo",
|
||||
},
|
||||
]
|
||||
default_parser_source_key = "SUPERLEAGUE"
|
||||
|
||||
try:
|
||||
project_settings = build_project_settings_context()
|
||||
except Exception:
|
||||
project_settings = {"storage_title": "База данных", "groups": [], "editable_keys": []}
|
||||
|
||||
return templates.TemplateResponse(
|
||||
name="admin_db_index.html",
|
||||
request=request,
|
||||
context={
|
||||
"current_user": getattr(request.state, "current_user", None),
|
||||
"parser_sources": parser_sources,
|
||||
"default_parser_source_key": default_parser_source_key,
|
||||
"parser_output": parser_output,
|
||||
"parser_name": parser_name,
|
||||
"parser_success": parser_success,
|
||||
"update_output": update_output,
|
||||
"update_success": update_success,
|
||||
"settings_output": settings_output,
|
||||
"settings_success": settings_success,
|
||||
"project_settings": project_settings,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2284,45 +2339,78 @@ def admin_db_index(request: Request):
|
||||
|
||||
|
||||
@app.post("/admin/db/run-parser", response_class=HTMLResponse)
|
||||
def admin_db_run_parser(request: Request, parser_name: str = Form(...)):
|
||||
def admin_db_run_parser(
|
||||
request: Request,
|
||||
parser_name: str | None = Form(default=None),
|
||||
parser_names: List[str] = Form(default=[]),
|
||||
parser_source: str | None = Form(default=None),
|
||||
):
|
||||
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 = {
|
||||
"teams": ("Команды", run_parser_teams),
|
||||
"players": ("Игроки", run_parser_players),
|
||||
"schedule": ("Расписание", run_parser_schedule),
|
||||
"standings": ("Турнирка", run_parser_standings),
|
||||
}
|
||||
|
||||
parser_meta = parser_map.get(parser_name)
|
||||
if not parser_meta:
|
||||
selected_parser_names = list(parser_names or [])
|
||||
if not selected_parser_names and parser_name:
|
||||
selected_parser_names = [parser_name]
|
||||
|
||||
selected_parser_names = [name for name in selected_parser_names if name in parser_map]
|
||||
if not selected_parser_names:
|
||||
return render_admin_db_index(
|
||||
request,
|
||||
parser_output="Неизвестный парсер.",
|
||||
parser_name=parser_name,
|
||||
parser_output="Выберите хотя бы один тип данных для парсинга.",
|
||||
parser_name="Импорт данных",
|
||||
parser_success=False,
|
||||
)
|
||||
|
||||
title, parser_func = parser_meta
|
||||
try:
|
||||
source = get_parser_source(parser_source)
|
||||
except Exception as exc:
|
||||
return render_admin_db_index(
|
||||
request,
|
||||
parser_output=str(exc),
|
||||
parser_name="Импорт данных",
|
||||
parser_success=False,
|
||||
)
|
||||
|
||||
titles = [parser_map[name][0] for name in selected_parser_names]
|
||||
title = f"{source['title']}: {', '.join(titles)}"
|
||||
|
||||
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())
|
||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Источник: {source['title']}")
|
||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Запуск: {', '.join(titles)}")
|
||||
|
||||
for selected_name in selected_parser_names:
|
||||
item_title, parser_func = parser_map[selected_name]
|
||||
print()
|
||||
print(f"===== {item_title} =====")
|
||||
try:
|
||||
parser_func(source_key=source["key"])
|
||||
print(
|
||||
f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {item_title}: успешно"
|
||||
)
|
||||
except Exception:
|
||||
success = False
|
||||
print(
|
||||
f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {item_title}: ошибка"
|
||||
)
|
||||
print(traceback.format_exc())
|
||||
|
||||
if success:
|
||||
print()
|
||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Импорт завершён успешно")
|
||||
else:
|
||||
print()
|
||||
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Импорт завершён с ошибками")
|
||||
|
||||
output = buffer.getvalue().strip() or "Парсер не вернул сообщений."
|
||||
return render_admin_db_index(
|
||||
@@ -2334,6 +2422,40 @@ def admin_db_run_parser(request: Request, parser_name: str = Form(...)):
|
||||
|
||||
|
||||
|
||||
@app.post("/admin/db/project-settings", response_class=HTMLResponse)
|
||||
async def admin_db_save_project_settings(request: Request):
|
||||
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)
|
||||
|
||||
try:
|
||||
form = await request.form()
|
||||
form_values = {key: str(value) for key, value in form.items()}
|
||||
updated_keys = save_project_settings(form_values)
|
||||
|
||||
output = [
|
||||
"Настройки проекта сохранены в базе данных.",
|
||||
f"Сохранено полей: {len(updated_keys)}",
|
||||
]
|
||||
if updated_keys:
|
||||
output.append("Поля:")
|
||||
output.extend([f"- {key}" for key in updated_keys])
|
||||
output.append("")
|
||||
output.append(".env больше не используется для ссылок, сезонов, путей логотипов и фотографий.")
|
||||
|
||||
return render_admin_db_index(
|
||||
request,
|
||||
settings_output="\n".join(output),
|
||||
settings_success=True,
|
||||
)
|
||||
except Exception:
|
||||
return render_admin_db_index(
|
||||
request,
|
||||
settings_output="Ошибка при сохранении настроек проекта:\n" + traceback.format_exc(),
|
||||
settings_success=False,
|
||||
)
|
||||
|
||||
|
||||
@app.post("/admin/db/add-update", response_class=HTMLResponse)
|
||||
def admin_db_add_update(
|
||||
request: Request,
|
||||
|
||||
Reference in New Issue
Block a user