1. добавил в админку сортировку игроков и Фото (если галочка стоит, то путь к фото генерируется автоматически)

2. фото обновляются прям в json
This commit is contained in:
2026-06-22 15:31:07 +03:00
parent cd370cf04f
commit f63061a06d
10 changed files with 620 additions and 98 deletions

View File

@@ -3,6 +3,232 @@ from db import get_connection
from repositories.match_lineup_repository import get_match_lineup_for_vmix
PHOTO_BASE_PATH = r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Photo"
EMPTY_PHOTO_PATH = PHOTO_BASE_PATH + r"\EMPTY.png"
def build_generated_player_photo_path(team_name: str, last_name: str, first_name: str) -> str:
return (
PHOTO_BASE_PATH
+ "\\"
+ str(team_name or "")
+ "\\"
+ (str(last_name or "") + " " + str(first_name or "")).strip()
+ ".png"
)
def resolve_player_photo(photo_enabled: bool, generated_photo: str) -> str:
if photo_enabled:
return generated_photo
return EMPTY_PHOTO_PATH
def _normalize_text(value) -> str:
return " ".join(str(value or "").strip().lower().split())
def _player_name_variants(player: dict) -> set[str]:
first_name = _normalize_text(player.get("first_name"))
last_name = _normalize_text(player.get("last_name"))
player_name = _normalize_text(player.get("player_name"))
full_name = _normalize_text(player.get("full_name"))
variants = {player_name, full_name}
if first_name or last_name:
variants.add(f"{last_name} {first_name}".strip())
variants.add(f"{first_name} {last_name}".strip())
return {v for v in variants if v}
def _candidate_name_variants(row: tuple) -> set[str]:
_, full_name, last_name, first_name, *_ = row
full_name = _normalize_text(full_name)
last_name = _normalize_text(last_name)
first_name = _normalize_text(first_name)
variants = {full_name}
if first_name or last_name:
variants.add(f"{last_name} {first_name}".strip())
variants.add(f"{first_name} {last_name}".strip())
return {v for v in variants if v}
def _find_player_photo_state(
*,
player_id=None,
team_id=None,
player_name: str = "",
first_name: str = "",
last_name: str = "",
full_name: str = "",
number: str = "",
) -> tuple[str | None, bool] | None:
"""
Берёт актуальное состояние галочки photo_enabled прямо из players.
Если в составе нет корректного player_id, пробует найти игрока по команде, номеру и имени.
Это важно для старых/загруженных с сайта составов, где player_id мог быть пустым.
Значение p.photo здесь не используется для JSON-пути: путь генерируется по старой схеме.
"""
try:
player_id_int = int(player_id) if str(player_id or "").strip().isdigit() else None
except Exception:
player_id_int = None
try:
team_id_int = int(team_id) if str(team_id or "").strip().isdigit() else None
except Exception:
team_id_int = None
target_number = str(number or "").strip()
target_player = {
"player_name": player_name,
"first_name": first_name,
"last_name": last_name,
"full_name": full_name,
}
target_names = _player_name_variants(target_player)
where_parts = []
params = []
if player_id_int is not None:
where_parts.append("p.id = %s")
params.append(player_id_int)
if team_id_int is not None:
team_conditions = []
team_params = []
if target_number:
team_conditions.append("COALESCE(p.number::text, '') = %s")
team_params.append(target_number)
for name in sorted(target_names):
like = f"%{name}%"
team_conditions.append(
"""
(
LOWER(COALESCE(p.full_name, '')) = %s
OR LOWER(TRIM(COALESCE(p.last_name, '') || ' ' || COALESCE(p.first_name, ''))) = %s
OR LOWER(TRIM(COALESCE(p.first_name, '') || ' ' || COALESCE(p.last_name, ''))) = %s
OR LOWER(COALESCE(p.full_name, '')) LIKE %s
)
"""
)
team_params.extend([name, name, name, like])
if team_conditions:
where_parts.append(f"(p.team_id = %s AND ({' OR '.join(team_conditions)}))")
params.append(team_id_int)
params.extend(team_params)
if not where_parts:
return None
query = f"""
SELECT
p.id,
COALESCE(p.full_name, ''),
COALESCE(p.last_name, ''),
COALESCE(p.first_name, ''),
COALESCE(p.number::text, ''),
p.team_id,
p.photo,
COALESCE(p.photo_enabled, FALSE) AS photo_enabled
FROM players p
WHERE {' OR '.join(where_parts)}
LIMIT 50
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, tuple(params))
rows = cur.fetchall()
finally:
conn.close()
best_row = None
best_score = -1
for row in rows:
row_id, _, _, _, row_number, row_team_id, *_ = row
row_names = _candidate_name_variants(row)
id_match = player_id_int is not None and int(row_id) == player_id_int
team_match = team_id_int is not None and int(row_team_id) == team_id_int
number_match = bool(target_number) and str(row_number or "").strip() == target_number
name_match = bool(target_names.intersection(row_names))
partial_name_match = False
if target_names and row_names:
partial_name_match = any(
target in candidate or candidate in target
for target in target_names
for candidate in row_names
if len(target) >= 4 and len(candidate) >= 4
)
# Если ID выглядит как номер игрока и случайно совпал с чужим players.id,
# не считаем его надёжным без совпадения команды/имени/номера.
trusted_id_match = id_match and (
(team_id_int is None or team_match)
and (not target_number and not target_names or number_match or name_match or partial_name_match)
)
score = 0
if trusted_id_match:
score += 100
if team_match:
score += 40
if number_match:
score += 30
if name_match:
score += 35
elif partial_name_match:
score += 15
# Для поиска без надёжного ID нужно хотя бы совпадение команды и номера/имени.
if not trusted_id_match and not (team_match and (number_match or name_match or partial_name_match)):
continue
if score > best_score:
best_score = score
best_row = row
if not best_row:
return None
return best_row[6], bool(best_row[7])
def resolve_player_photo_for_json(
player: dict,
generated_photo: str,
fallback_team_id=None,
) -> str:
state = _find_player_photo_state(
player_id=player.get("player_id") or player.get("id"),
team_id=player.get("team_id") or fallback_team_id,
player_name=player.get("player_name") or "",
first_name=player.get("first_name") or "",
last_name=player.get("last_name") or "",
full_name=player.get("full_name") or "",
number=player.get("number") or player.get("player_number") or "",
)
if state is not None:
_photo, photo_enabled = state
return resolve_player_photo(photo_enabled=photo_enabled, generated_photo=generated_photo)
return resolve_player_photo(
photo_enabled=player.get("photo_enabled"),
generated_photo=generated_photo,
)
def build_lineup_json(match_id, home_team_id, away_team_id, name, team_a_name, team_b_name):
lineups = get_match_lineup_for_vmix(
match_id=match_id,
@@ -14,6 +240,8 @@ def build_lineup_json(match_id, home_team_id, away_team_id, name, team_a_name, t
result = []
fallback_team_id = home_team_id if str(name).startswith("home") else away_team_id
for p in players:
suffix = []
if "вратарь" in (p.get("pos") or "").lower():
@@ -40,17 +268,16 @@ def build_lineup_json(match_id, home_team_id, away_team_id, name, team_a_name, t
+ (f" {', '.join(suffix)}" if suffix else ""),
"pos": p.get("pos", ""),
"position": p.get("position", ""),
"photo": (
r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Photo"
+ "\\"
+ (team_a_name if "home" in name else team_b_name)
+ "\\"
+ (p.get("last_name", "")
+ " "
+ p.get("first_name", "")).strip()
+ ".png"
),
}
"photo": resolve_player_photo_for_json(
p,
generated_photo=build_generated_player_photo_path(
team_name=team_a_name if "home" in name else team_b_name,
last_name=p.get("last_name", ""),
first_name=p.get("first_name", ""),
),
fallback_team_id=fallback_team_id,
),
}
)
return {"players": result}
@@ -231,11 +458,16 @@ def get_vmix_schedule(session_token: str):
def get_vmix_team_formations(session_token: str, team_id: int):
query = """
SELECT
p.last_name,
COALESCE(NULLIF(p.last_name, ''), NULLIF(mf.player_name, ''), '') AS last_name,
mf.is_captain,
p.number,
p.position,
p.first_name
COALESCE(NULLIF(mf.number, ''), p.number::text, '') AS number,
COALESCE(NULLIF(mf.position, ''), p.position, '') AS position,
COALESCE(p.first_name, '') AS first_name,
p.photo,
COALESCE(p.photo_enabled, FALSE) AS photo_enabled,
mf.player_id,
mf.team_id,
COALESCE(mf.player_name, '') AS player_name
FROM match_formations mf
LEFT JOIN players p ON p.id = mf.player_id
WHERE mf.match_id = %s and mf.team_id = %s