обновил еще капитонов
This commit is contained in:
24
app.py
24
app.py
@@ -836,26 +836,10 @@ def load_match_data(request: Request, session_token: str):
|
|||||||
match_external_id = session_row[8]
|
match_external_id = session_row[8]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from db import get_connection
|
# Очистка и замена матчевых данных выполняется внутри sync_match_page().
|
||||||
|
# Важно не удалять ручной состав здесь заранее: сервис сначала должен
|
||||||
# Сначала очищаем ручные данные матча,
|
# увидеть ранее выбранного капитана и, если сайт забыл его указать,
|
||||||
# чтобы парсер потом записал свежие составы и тренеров
|
# сохранить этот выбор при повторной загрузке.
|
||||||
conn = get_connection()
|
|
||||||
try:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
"DELETE FROM match_lineup_players WHERE match_id = %s",
|
|
||||||
(match_id,),
|
|
||||||
)
|
|
||||||
cur.execute(
|
|
||||||
"DELETE FROM match_coaches WHERE match_id = %s",
|
|
||||||
(match_id,),
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
# Потом загружаем свежие данные с сайта
|
|
||||||
run_parser_game(str(match_external_id))
|
run_parser_game(str(match_external_id))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -21,12 +21,37 @@ def extract_player_id_from_href(href: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def detect_captain(item) -> bool:
|
def detect_captain(item) -> bool:
|
||||||
|
"""Определяет капитана по текстовой метке или отдельному HTML-маркеру сайта.
|
||||||
|
|
||||||
|
На странице протокола капитан может быть обозначен как ``(К)`` / ``(C)``,
|
||||||
|
а в некоторых версиях вёрстки — отдельным элементом/иконкой с captain в
|
||||||
|
class, title, aria-label или data-атрибуте. Если сайт вообще не указал
|
||||||
|
капитана, функция корректно возвращает False.
|
||||||
|
"""
|
||||||
if not item:
|
if not item:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
text = item.get_text(" ", strip=True).lower()
|
text = item.get_text(" ", strip=True).lower().replace("ё", "е")
|
||||||
|
if any(marker in text for marker in ("(к)", "(c)", "капитан", "captain")):
|
||||||
|
return True
|
||||||
|
|
||||||
return any(x in text for x in ["(к)", "(c)"])
|
# Поддержка отдельной иконки/элемента капитана, если буква не входит
|
||||||
|
# в видимый текст строки игрока. Не привязываемся к одной версии вёрстки.
|
||||||
|
for node in [item, *item.find_all(True)]:
|
||||||
|
classes = " ".join(node.get("class", [])).lower()
|
||||||
|
attrs_text = " ".join(
|
||||||
|
str(node.get(attr) or "")
|
||||||
|
for attr in ("title", "aria-label", "data-title", "data-role", "data-captain")
|
||||||
|
).lower().replace("ё", "е")
|
||||||
|
|
||||||
|
if "captain" in classes or "капитан" in classes:
|
||||||
|
return True
|
||||||
|
if "captain" in attrs_text or "капитан" in attrs_text:
|
||||||
|
return True
|
||||||
|
if str(node.get("data-captain") or "").strip().lower() in {"1", "true", "yes"}:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def parse_starting_teams(soup: BeautifulSoup) -> tuple[list[dict], list[dict]]:
|
def parse_starting_teams(soup: BeautifulSoup) -> tuple[list[dict], list[dict]]:
|
||||||
home_starting = []
|
home_starting = []
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ from repositories.coach_repository import (
|
|||||||
create_coach_from_lineup,
|
create_coach_from_lineup,
|
||||||
)
|
)
|
||||||
from repositories.referee_repository import get_referee_id_by_name, upsert_referee
|
from repositories.referee_repository import get_referee_id_by_name, upsert_referee
|
||||||
from repositories.match_lineup_repository import replace_match_lineups, save_match_lineup_for_editor
|
from repositories.match_lineup_repository import (
|
||||||
|
replace_match_lineups,
|
||||||
|
get_match_lineup_for_editor,
|
||||||
|
)
|
||||||
from repositories.match_coach_repository import replace_match_coaches
|
from repositories.match_coach_repository import replace_match_coaches
|
||||||
from repositories.match_referee_repository import replace_match_referees
|
from repositories.match_referee_repository import replace_match_referees
|
||||||
|
|
||||||
@@ -35,6 +38,31 @@ def sync_match_page(
|
|||||||
raise ValueError(f"Match not found by external_id: {match_external_id}")
|
raise ValueError(f"Match not found by external_id: {match_external_id}")
|
||||||
|
|
||||||
match_id, _, home_team_id, away_team_id = match_row
|
match_id, _, home_team_id, away_team_id = match_row
|
||||||
|
|
||||||
|
# Сохраняем текущий выбор капитана ДО очистки данных матча. Это важно
|
||||||
|
# при повторной загрузке с сайта: на сайте капитана иногда не указывают,
|
||||||
|
# и в таком случае нельзя молча терять уже сделанный оператором выбор.
|
||||||
|
previous_lineup = get_match_lineup_for_editor(
|
||||||
|
match_id=match_id,
|
||||||
|
home_team_id=home_team_id,
|
||||||
|
away_team_id=away_team_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def previous_captain(side: str) -> dict | None:
|
||||||
|
rows = previous_lineup.get(f"{side}_starting", []) or []
|
||||||
|
for player in rows:
|
||||||
|
if bool(player.get("is_captain")):
|
||||||
|
return {
|
||||||
|
"player_id": player.get("player_id"),
|
||||||
|
"number": str(player.get("number") or "").strip(),
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
previous_captains = {
|
||||||
|
"home": previous_captain("home"),
|
||||||
|
"away": previous_captain("away"),
|
||||||
|
}
|
||||||
|
|
||||||
clear_match_squad_data(match_id)
|
clear_match_squad_data(match_id)
|
||||||
|
|
||||||
created_players_count = 0
|
created_players_count = 0
|
||||||
@@ -117,7 +145,10 @@ def sync_match_page(
|
|||||||
"number": player.get("number"),
|
"number": player.get("number"),
|
||||||
"position": player.get("position"),
|
"position": player.get("position"),
|
||||||
"position_full": player_position,
|
"position_full": player_position,
|
||||||
"is_captain": bool(player.get("is_captain")),
|
# Капитаном может быть только игрок основного состава.
|
||||||
|
# Если сайт по ошибке пометил игрока запаса, не переносим
|
||||||
|
# такой флаг в матчевые данные.
|
||||||
|
"is_captain": bool(player.get("is_captain")) and lineup_type == "starting",
|
||||||
"lineup_type": lineup_type,
|
"lineup_type": lineup_type,
|
||||||
"source": "parser",
|
"source": "parser",
|
||||||
}
|
}
|
||||||
@@ -128,6 +159,72 @@ def sync_match_page(
|
|||||||
append_players(home_bench, home_team_id, "bench")
|
append_players(home_bench, home_team_id, "bench")
|
||||||
append_players(away_bench, away_team_id, "bench")
|
append_players(away_bench, away_team_id, "bench")
|
||||||
|
|
||||||
|
def restore_previous_captain_if_missing(side: str, team_id: int) -> bool:
|
||||||
|
"""Возвращает старого капитана только когда сайт не указал нового.
|
||||||
|
|
||||||
|
Восстановление разрешено исключительно для игрока, который всё ещё
|
||||||
|
присутствует в основном составе. Если прежний капитан отсутствует или
|
||||||
|
ушёл в запас, капитан остаётся не выбран — веб-интерфейс покажет
|
||||||
|
обязательное предупреждение оператору.
|
||||||
|
"""
|
||||||
|
starting_rows = [
|
||||||
|
row for row in lineup_rows
|
||||||
|
if row.get("team_id") == team_id and row.get("lineup_type") == "starting"
|
||||||
|
]
|
||||||
|
|
||||||
|
imported_captains = [row for row in starting_rows if bool(row.get("is_captain"))]
|
||||||
|
|
||||||
|
# Ровно один капитан с сайта — корректные данные, они имеют приоритет.
|
||||||
|
if len(imported_captains) == 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Если сайт по ошибке передал больше одного капитана, не выбираем
|
||||||
|
# случайного игрока. Сбрасываем конфликт и используем тот же безопасный
|
||||||
|
# fallback, что и при полностью отсутствующем капитане.
|
||||||
|
if len(imported_captains) > 1:
|
||||||
|
for row in imported_captains:
|
||||||
|
row["is_captain"] = False
|
||||||
|
|
||||||
|
old_captain = previous_captains.get(side)
|
||||||
|
if not old_captain:
|
||||||
|
return False
|
||||||
|
|
||||||
|
old_player_id = old_captain.get("player_id")
|
||||||
|
old_number = str(old_captain.get("number") or "").strip()
|
||||||
|
|
||||||
|
# Сначала точное совпадение по player_id.
|
||||||
|
if old_player_id is not None:
|
||||||
|
for row in starting_rows:
|
||||||
|
if row.get("player_id") is not None and str(row.get("player_id")) == str(old_player_id):
|
||||||
|
row["is_captain"] = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Fallback для старых/неполных данных — по игровому номеру.
|
||||||
|
if old_number:
|
||||||
|
matches = [
|
||||||
|
row for row in starting_rows
|
||||||
|
if str(row.get("number") or "").strip() == old_number
|
||||||
|
]
|
||||||
|
if len(matches) == 1:
|
||||||
|
matches[0]["is_captain"] = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
restored_home_captain = restore_previous_captain_if_missing("home", home_team_id)
|
||||||
|
restored_away_captain = restore_previous_captain_if_missing("away", away_team_id)
|
||||||
|
|
||||||
|
if restored_home_captain or restored_away_captain:
|
||||||
|
restored = []
|
||||||
|
if restored_home_captain:
|
||||||
|
restored.append("home")
|
||||||
|
if restored_away_captain:
|
||||||
|
restored.append("away")
|
||||||
|
print(
|
||||||
|
f"[parser_game] website captain missing for match={match_external_id}; "
|
||||||
|
f"preserved previous captain for: {', '.join(restored)}"
|
||||||
|
)
|
||||||
|
|
||||||
coach_rows = []
|
coach_rows = []
|
||||||
|
|
||||||
def append_coaches(coaches: list[dict], team_id: int, side: str) -> None:
|
def append_coaches(coaches: list[dict], team_id: int, side: str) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user