подгрузка данных для кубка россии. тест 2
This commit is contained in:
10
app.py
10
app.py
@@ -59,7 +59,7 @@ from repositories.match_session_repository import (
|
|||||||
from repositories.match_view_repository import get_match_lineups_grouped
|
from repositories.match_view_repository import get_match_lineups_grouped
|
||||||
from vmix.vmix_service import build_vmix_project_bytes, build_vmix_filename
|
from vmix.vmix_service import build_vmix_project_bytes, build_vmix_filename
|
||||||
from repositories.match_referee_repository import get_match_referees
|
from repositories.match_referee_repository import get_match_referees
|
||||||
from repositories.match_repository import get_tour_schedule_by_match_id, ensure_match_source_key_column
|
from repositories.match_repository import get_tour_schedule_by_match_id, ensure_match_source_key_column, resolve_match_source_key
|
||||||
from repositories.standings_repository import get_standings_by_match_id
|
from repositories.standings_repository import get_standings_by_match_id
|
||||||
from repositories.match_coach_repository import get_match_coaches_grouped
|
from repositories.match_coach_repository import get_match_coaches_grouped
|
||||||
from repositories.player_repository import (
|
from repositories.player_repository import (
|
||||||
@@ -706,7 +706,7 @@ def download_vmix_project(request: Request, session_token: str):
|
|||||||
operator_login = current_user.get("username") or None
|
operator_login = current_user.get("username") or None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
source_key = session_row[21] if len(session_row) > 21 else None
|
source_key = resolve_match_source_key(session_row[1], session_row[21] if len(session_row) > 21 else None)
|
||||||
|
|
||||||
vmix_bytes = build_vmix_project_bytes(
|
vmix_bytes = build_vmix_project_bytes(
|
||||||
session_token=session_token,
|
session_token=session_token,
|
||||||
@@ -1474,7 +1474,7 @@ def get_roster_data(session_token: str, name: str, count_player: int):
|
|||||||
home_team_name = session_row[14].replace("«", "").replace("»", "")
|
home_team_name = session_row[14].replace("«", "").replace("»", "")
|
||||||
away_team_id = session_row[17]
|
away_team_id = session_row[17]
|
||||||
away_team_name = session_row[18].replace("«", "").replace("»", "")
|
away_team_name = session_row[18].replace("«", "").replace("»", "")
|
||||||
source_key = session_row[21] if len(session_row) > 21 else None
|
source_key = resolve_match_source_key(match_id, session_row[21] if len(session_row) > 21 else None)
|
||||||
# print(session_row)
|
# print(session_row)
|
||||||
|
|
||||||
data = build_lineup_json(
|
data = build_lineup_json(
|
||||||
@@ -1764,7 +1764,7 @@ def vmix_home_formations(session_token: str):
|
|||||||
|
|
||||||
home_team_id = session_row[13]
|
home_team_id = session_row[13]
|
||||||
home_team = session_row[14].replace("«", "").replace("»", "")
|
home_team = session_row[14].replace("«", "").replace("»", "")
|
||||||
source_key = session_row[21] if len(session_row) > 21 else None
|
source_key = resolve_match_source_key(session_row[1], session_row[21] if len(session_row) > 21 else None)
|
||||||
rows = get_vmix_team_formations(session_row, home_team_id)
|
rows = get_vmix_team_formations(session_row, home_team_id)
|
||||||
return build_vmix_formation_response(rows, home_team_id, home_team, source_key=source_key)
|
return build_vmix_formation_response(rows, home_team_id, home_team, source_key=source_key)
|
||||||
|
|
||||||
@@ -1777,7 +1777,7 @@ def vmix_away_formations(session_token: str):
|
|||||||
|
|
||||||
away_team_id = session_row[17]
|
away_team_id = session_row[17]
|
||||||
away_team = session_row[18].replace("«", "").replace("»", "")
|
away_team = session_row[18].replace("«", "").replace("»", "")
|
||||||
source_key = session_row[21] if len(session_row) > 21 else None
|
source_key = resolve_match_source_key(session_row[1], session_row[21] if len(session_row) > 21 else None)
|
||||||
rows = get_vmix_team_formations(session_row, away_team_id)
|
rows = get_vmix_team_formations(session_row, away_team_id)
|
||||||
return build_vmix_formation_response(rows, away_team_id, away_team, source_key=source_key)
|
return build_vmix_formation_response(rows, away_team_id, away_team, source_key=source_key)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from numpy import place
|
|||||||
from db import get_connection
|
from db import get_connection
|
||||||
from repositories.team_repository import get_team_id_by_external_id
|
from repositories.team_repository import get_team_id_by_external_id
|
||||||
from repositories.stadium_repository import get_or_create_stadium
|
from repositories.stadium_repository import get_or_create_stadium
|
||||||
|
from parsers.parser_sources import get_default_source_key, list_parser_sources
|
||||||
|
|
||||||
|
|
||||||
def ensure_match_source_key_column() -> None:
|
def ensure_match_source_key_column() -> None:
|
||||||
@@ -24,6 +25,121 @@ def ensure_match_source_key_column() -> None:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_source_key(value: str | None) -> str:
|
||||||
|
return str(value or "").upper().strip()
|
||||||
|
|
||||||
|
|
||||||
|
def infer_match_source_key(
|
||||||
|
explicit_source_key: str | None = None,
|
||||||
|
season: str | None = None,
|
||||||
|
tour: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Определяет источник матча, даже если старые строки matches.source_key ещё пустые.
|
||||||
|
|
||||||
|
Приоритет:
|
||||||
|
1. matches.source_key;
|
||||||
|
2. точное совпадение season с parser_sources.season;
|
||||||
|
3. кубковые слова в tour;
|
||||||
|
4. обычный дефолт из настроек проекта.
|
||||||
|
"""
|
||||||
|
explicit = _normalize_source_key(explicit_source_key)
|
||||||
|
if explicit:
|
||||||
|
return explicit
|
||||||
|
|
||||||
|
season_value = str(season or "").strip()
|
||||||
|
tour_value = str(tour or "").lower().strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
sources = list_parser_sources()
|
||||||
|
except Exception:
|
||||||
|
sources = []
|
||||||
|
|
||||||
|
if season_value and sources:
|
||||||
|
matches = [
|
||||||
|
_normalize_source_key(source.get("key"))
|
||||||
|
for source in sources
|
||||||
|
if str(source.get("season") or "").strip() == season_value
|
||||||
|
]
|
||||||
|
matches = [key for key in matches if key]
|
||||||
|
if len(matches) == 1:
|
||||||
|
return matches[0]
|
||||||
|
|
||||||
|
cup_markers = (
|
||||||
|
"кубок",
|
||||||
|
"финал",
|
||||||
|
"полуфинал",
|
||||||
|
"четвертьфинал",
|
||||||
|
"1/2",
|
||||||
|
"1/4",
|
||||||
|
"1/8",
|
||||||
|
"1/16",
|
||||||
|
"групп",
|
||||||
|
"этап",
|
||||||
|
"стад",
|
||||||
|
)
|
||||||
|
if any(marker in tour_value for marker in cup_markers):
|
||||||
|
known_keys = {_normalize_source_key(source.get("key")) for source in sources}
|
||||||
|
if "RUSSIAN_CUP" in known_keys:
|
||||||
|
return "RUSSIAN_CUP"
|
||||||
|
|
||||||
|
try:
|
||||||
|
return _normalize_source_key(get_default_source_key()) or "SUPERLEAGUE"
|
||||||
|
except Exception:
|
||||||
|
return "SUPERLEAGUE"
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_match_source_key(match_id: int | str | None, fallback: str | None = None) -> str:
|
||||||
|
"""Возвращает source_key для матча и, если возможно, записывает его в matches.
|
||||||
|
|
||||||
|
Это нужно для старых матчей Кубка России, которые могли быть загружены до появления
|
||||||
|
колонки source_key. Без этого vMix скачивает пресет и собирает пути как для Суперлиги.
|
||||||
|
"""
|
||||||
|
fallback_key = _normalize_source_key(fallback)
|
||||||
|
if not match_id:
|
||||||
|
return fallback_key or infer_match_source_key(fallback_key)
|
||||||
|
|
||||||
|
conn = get_connection()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT source_key, season, tour
|
||||||
|
FROM matches
|
||||||
|
WHERE id = %s
|
||||||
|
LIMIT 1;
|
||||||
|
""",
|
||||||
|
(match_id,),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return fallback_key or infer_match_source_key(fallback_key)
|
||||||
|
|
||||||
|
explicit_key, season, tour = row
|
||||||
|
source_key = infer_match_source_key(explicit_key or fallback_key, season, tour)
|
||||||
|
|
||||||
|
if source_key and not _normalize_source_key(explicit_key):
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
UPDATE matches
|
||||||
|
SET source_key = %s, updated_at = NOW()
|
||||||
|
WHERE id = %s
|
||||||
|
AND (source_key IS NULL OR TRIM(source_key) = '');
|
||||||
|
""",
|
||||||
|
(source_key, match_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return source_key
|
||||||
|
except Exception:
|
||||||
|
conn.rollback()
|
||||||
|
return fallback_key or infer_match_source_key(fallback_key)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def upsert_match(
|
def upsert_match(
|
||||||
external_id: str,
|
external_id: str,
|
||||||
home_team_id: int,
|
home_team_id: int,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import secrets
|
|||||||
|
|
||||||
from db import get_connection
|
from db import get_connection
|
||||||
from parsers.parser_sources import build_logo_path
|
from parsers.parser_sources import build_logo_path
|
||||||
|
from repositories.match_repository import resolve_match_source_key
|
||||||
|
|
||||||
|
|
||||||
def create_match_session(
|
def create_match_session(
|
||||||
@@ -88,7 +89,9 @@ def get_match_session_by_token(session_token: str):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
row = list(row)
|
row = list(row)
|
||||||
source_key = row[21] if len(row) > 21 else None
|
source_key = resolve_match_source_key(row[1], row[21] if len(row) > 21 else None)
|
||||||
|
if len(row) > 21:
|
||||||
|
row[21] = source_key
|
||||||
row[16] = build_logo_path(source_key, row[16])
|
row[16] = build_logo_path(source_key, row[16])
|
||||||
row[20] = build_logo_path(source_key, row[20])
|
row[20] = build_logo_path(source_key, row[20])
|
||||||
return tuple(row)
|
return tuple(row)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# services/vmix_json_service.py
|
# services/vmix_json_service.py
|
||||||
from db import get_connection
|
from db import get_connection
|
||||||
from repositories.match_lineup_repository import get_match_lineup_for_vmix
|
from repositories.match_lineup_repository import get_match_lineup_for_vmix
|
||||||
|
from repositories.match_repository import resolve_match_source_key
|
||||||
from parsers.parser_sources import build_empty_photo_path, build_logo_path, build_logo_variant_path, build_photo_path
|
from parsers.parser_sources import build_empty_photo_path, build_logo_path, build_logo_variant_path, build_photo_path
|
||||||
|
|
||||||
|
|
||||||
@@ -9,11 +10,17 @@ EMPTY_PHOTO_PATH = DEFAULT_PHOTO_BASE_PATH + r"\EMPTY.png"
|
|||||||
|
|
||||||
|
|
||||||
def _session_source_key(session_row) -> str | None:
|
def _session_source_key(session_row) -> str | None:
|
||||||
|
try:
|
||||||
|
match_id = session_row[1]
|
||||||
|
except Exception:
|
||||||
|
match_id = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
value = session_row[21]
|
value = session_row[21]
|
||||||
except Exception:
|
except Exception:
|
||||||
value = None
|
value = None
|
||||||
return str(value).strip() if value else None
|
|
||||||
|
return resolve_match_source_key(match_id, value)
|
||||||
|
|
||||||
|
|
||||||
def build_generated_player_photo_path(
|
def build_generated_player_photo_path(
|
||||||
@@ -386,7 +393,9 @@ def get_vmix_match_info_by_token(session_token: str):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
row = list(row)
|
row = list(row)
|
||||||
source_key = row[30] if len(row) > 30 else None
|
source_key = resolve_match_source_key(row[0], row[30] if len(row) > 30 else None)
|
||||||
|
if len(row) > 30:
|
||||||
|
row[30] = source_key
|
||||||
row[14] = build_logo_path(source_key, row[14])
|
row[14] = build_logo_path(source_key, row[14])
|
||||||
row[15] = build_logo_path(source_key, row[15])
|
row[15] = build_logo_path(source_key, row[15])
|
||||||
row[20] = build_logo_variant_path(source_key, row[20], "white")
|
row[20] = build_logo_variant_path(source_key, row[20], "white")
|
||||||
|
|||||||
@@ -7,13 +7,23 @@ import os
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _refresh_env() -> None:
|
||||||
|
# Читаем .env заново перед скачиванием, чтобы изменение SYNO_PATH_VMIX_2
|
||||||
|
# применялось без пересборки кода.
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_syno_settings() -> tuple[str | None, str | None, str | None]:
|
||||||
|
_refresh_env()
|
||||||
|
return (
|
||||||
|
os.getenv("SYNO_URL"),
|
||||||
|
os.getenv("SYNO_USERNAME"),
|
||||||
|
os.getenv("SYNO_PASSWORD"),
|
||||||
|
)
|
||||||
|
|
||||||
SYNO_URL = os.getenv("SYNO_URL")
|
|
||||||
SYNO_USERNAME = os.getenv("SYNO_USERNAME")
|
|
||||||
SYNO_PASSWORD = os.getenv("SYNO_PASSWORD")
|
|
||||||
SYNO_PATH_VMIX = os.getenv("SYNO_PATH_VMIX")
|
|
||||||
SYNO_PATH_VMIX_2 = os.getenv("SYNO_PATH_VMIX_2")
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_source_key(source_key: str | None) -> str:
|
def normalize_source_key(source_key: str | None) -> str:
|
||||||
@@ -26,14 +36,17 @@ def get_vmix_preset_path(source_key: str | None = None) -> str | None:
|
|||||||
SUPERLEAGUE -> SYNO_PATH_VMIX
|
SUPERLEAGUE -> SYNO_PATH_VMIX
|
||||||
RUSSIAN_CUP -> SYNO_PATH_VMIX_2, если он заполнен
|
RUSSIAN_CUP -> SYNO_PATH_VMIX_2, если он заполнен
|
||||||
|
|
||||||
Если второй путь не задан, используем основной, чтобы скачивание не ломалось.
|
Значения читаются из .env прямо перед скачиванием.
|
||||||
"""
|
"""
|
||||||
|
_refresh_env()
|
||||||
key = normalize_source_key(source_key)
|
key = normalize_source_key(source_key)
|
||||||
|
primary_path = os.getenv("SYNO_PATH_VMIX")
|
||||||
|
cup_path = os.getenv("SYNO_PATH_VMIX_2")
|
||||||
|
|
||||||
if key == "RUSSIAN_CUP" and SYNO_PATH_VMIX_2:
|
if key == "RUSSIAN_CUP" and cup_path:
|
||||||
return SYNO_PATH_VMIX_2
|
return cup_path
|
||||||
|
|
||||||
return SYNO_PATH_VMIX
|
return primary_path
|
||||||
|
|
||||||
|
|
||||||
def get_fqdn():
|
def get_fqdn():
|
||||||
@@ -63,6 +76,15 @@ def rebuild_vmix_url(old_url: str, new_base_url: str, session_token: str) -> str
|
|||||||
endpoint = match.group(1)
|
endpoint = match.group(1)
|
||||||
return f"{new_base_url}/vmix/session/{session_token}/{endpoint}"
|
return f"{new_base_url}/vmix/session/{session_token}/{endpoint}"
|
||||||
|
|
||||||
|
# Старые пресеты могли хранить ссылки без session-токена:
|
||||||
|
# /vmix/home-lineup, /vmix/info.json и т.п.
|
||||||
|
old_style_match = re.match(r"^/vmix/(?!session/)(.+)$", path)
|
||||||
|
if old_style_match:
|
||||||
|
endpoint = old_style_match.group(1).strip("/")
|
||||||
|
if endpoint.endswith(".json"):
|
||||||
|
endpoint = endpoint[:-5]
|
||||||
|
return f"{new_base_url}/vmix/session/{session_token}/{endpoint}"
|
||||||
|
|
||||||
# fallback: если структура другая, просто меняем host
|
# fallback: если структура другая, просто меняем host
|
||||||
return re.sub(r"https?://[^/]+", new_base_url, old_url)
|
return re.sub(r"https?://[^/]+", new_base_url, old_url)
|
||||||
|
|
||||||
@@ -73,6 +95,7 @@ def change_vmix_datasource_urls(
|
|||||||
session_token: str,
|
session_token: str,
|
||||||
match_id: str | int | None = None,
|
match_id: str | int | None = None,
|
||||||
operator_login: str | None = None,
|
operator_login: str | None = None,
|
||||||
|
source_key: str | None = None,
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
if isinstance(xml_data, dict):
|
if isinstance(xml_data, dict):
|
||||||
candidate = None
|
candidate = None
|
||||||
@@ -155,6 +178,9 @@ def change_vmix_datasource_urls(
|
|||||||
# value2 = match_id
|
# value2 = match_id
|
||||||
get_or_create_dynamic_value(1).text = "" if match_id is None else str(match_id)
|
get_or_create_dynamic_value(1).text = "" if match_id is None else str(match_id)
|
||||||
|
|
||||||
|
# value3 = source_key (SUPERLEAGUE / RUSSIAN_CUP)
|
||||||
|
get_or_create_dynamic_value(2).text = "" if not source_key else normalize_source_key(source_key)
|
||||||
|
|
||||||
# value4 = operator_login
|
# value4 = operator_login
|
||||||
get_or_create_dynamic_value(3).text = "" if not operator_login else str(operator_login)
|
get_or_create_dynamic_value(3).text = "" if not operator_login else str(operator_login)
|
||||||
|
|
||||||
@@ -168,14 +194,18 @@ def build_vmix_project_bytes(
|
|||||||
operator_login: str | None = None,
|
operator_login: str | None = None,
|
||||||
source_key: str | None = None,
|
source_key: str | None = None,
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
|
source_key = normalize_source_key(source_key)
|
||||||
vmix_preset_path = get_vmix_preset_path(source_key)
|
vmix_preset_path = get_vmix_preset_path(source_key)
|
||||||
if not vmix_preset_path:
|
if not vmix_preset_path:
|
||||||
raise RuntimeError("Не задан путь к vMix-пресету: SYNO_PATH_VMIX или SYNO_PATH_VMIX_2")
|
raise RuntimeError("Не задан путь к vMix-пресету: SYNO_PATH_VMIX или SYNO_PATH_VMIX_2")
|
||||||
|
|
||||||
|
syno_url, syno_username, syno_password = get_syno_settings()
|
||||||
|
print(f"[vmix] source_key={source_key or '-'} preset_path={vmix_preset_path}")
|
||||||
|
|
||||||
vmix_bio = nasio.load_bio(
|
vmix_bio = nasio.load_bio(
|
||||||
user=SYNO_USERNAME,
|
user=syno_username,
|
||||||
password=SYNO_PASSWORD,
|
password=syno_password,
|
||||||
nas_ip=SYNO_URL,
|
nas_ip=syno_url,
|
||||||
nas_port="443",
|
nas_port="443",
|
||||||
path=vmix_preset_path,
|
path=vmix_preset_path,
|
||||||
)
|
)
|
||||||
@@ -186,6 +216,7 @@ def build_vmix_project_bytes(
|
|||||||
session_token,
|
session_token,
|
||||||
match_id,
|
match_id,
|
||||||
operator_login,
|
operator_login,
|
||||||
|
source_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(edited_vmix, str):
|
if isinstance(edited_vmix, str):
|
||||||
|
|||||||
Reference in New Issue
Block a user