1. добавил в админку сортировку игроков и Фото (если галочка стоит, то путь к фото генерируется автоматически)
2. фото обновляются прям в json
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user