Files
WFL/services/vmix_json_service.py

753 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# services/vmix_json_service.py
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,
home_team_id=home_team_id,
away_team_id=away_team_id,
)
players = lineups.get(name, [])
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():
suffix.append("ВР")
if p.get("is_captain"):
suffix.append("К")
number = p.get("number", "")
lastname = p.get("last_name", "")
suffix_str = f'({", ".join(suffix)})' if suffix else ""
result.append(
{
"number": p.get("number", ""),
"number_lastname_amp_K": f"{number} {lastname} {suffix_str}".strip(),
"first_name": p.get("first_name", ""),
"last_name": p.get("last_name", ""),
"number_fullname": f"{number} {p.get('first_name', '')} {p.get('last_name', '')}".strip(),
"full_name": (
p.get("first_name", "") + " " + p.get("last_name", "")
).strip(),
"full_name_K": (
p.get("first_name", "") + " " + p.get("last_name", "")
).strip()
+ (f" {', '.join(suffix)}" if suffix else ""),
"pos": p.get("pos", ""),
"position": p.get("position", ""),
"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}
def get_vmix_match_info_by_token(session_token: str):
query = """
SELECT
ms.match_id,
m.match_date,
m.place,
m.stadium_id,
ht.id AS home_team_id,
ht.name AS home_team_name,
COALESCE(ht.full_name, ht.name) AS home_team_full_name,
COALESCE(ht.short_name_3, '') AS home_team_short_name,
at.id AS away_team_id,
at.name AS away_team_name,
COALESCE(at.full_name, at.name) AS away_team_full_name,
COALESCE(at.short_name_3, '') AS away_team_short_name,
COALESCE(s.stadium_gfx, s.name, m.place, '') AS stadium_name,
m.tour,
ht.logo_path AS home_logo,
REPLACE(at.logo_path, 'HOME', 'AWAY') AS away_logo,
ref1.referee_name AS referee1,
ref2.referee_name AS referee2,
ref3.referee_name AS referee3,
ref4.referee_name AS referee4,
CASE
WHEN ht.name ILIKE '%%динамо%%'
THEN REPLACE(REPLACE(ht.logo_path, 'HOME\\', ''), 'Динамо', 'Динамоелый')
WHEN ht.name ILIKE '%%зенит%%'
THEN REPLACE(REPLACE(ht.logo_path, 'HOME\\', ''), 'Зенит', 'Зенит_Белый')
ELSE REPLACE(ht.logo_path, 'HOME\\', '')
END AS home_logo1,
CASE
WHEN at.name ILIKE '%%динамо%%'
THEN REPLACE(REPLACE(at.logo_path, 'HOME\\', ''), 'Динамо', 'Динамоелый')
WHEN at.name ILIKE '%%зенит%%'
THEN REPLACE(REPLACE(at.logo_path, 'HOME\\', ''), 'Зенит', 'Зенит_Белый')
ELSE REPLACE(at.logo_path, 'HOME\\', '')
END AS away_logo1,
CASE
WHEN ht.name ILIKE '%%динамо%%'
THEN REPLACE(REPLACE(ht.logo_path, 'HOME\\', ''), 'Динамо', 'Динамо_Синий')
WHEN ht.name ILIKE '%%зенит%%'
THEN REPLACE(REPLACE(ht.logo_path, 'HOME\\', ''), 'Зенит', 'Зенит_Синий')
ELSE REPLACE(ht.logo_path, 'HOME\\', '')
END AS home_logo2,
CASE
WHEN at.name ILIKE '%%динамо%%'
THEN REPLACE(REPLACE(at.logo_path, 'HOME\\', ''), 'Динамо', 'Динамо_Синий')
WHEN at.name ILIKE '%%зенит%%'
THEN REPLACE(REPLACE(at.logo_path, 'HOME\\', ''), 'Зенит', 'Зенит_Синий')
ELSE REPLACE(at.logo_path, 'HOME\\', '')
END AS away_logo2,
ht.city AS home_city,
at.city AS away_city,
TRIM(COALESCE(c1.name, '') || ' ' || COALESCE(c1.lastname, '')) AS coach_name1,
c1.amplua AS coach_amplua1,
TRIM(COALESCE(c2.name, '') || ' ' || COALESCE(c2.lastname, '')) AS coach_name2,
c2.amplua AS coach_amplua2
FROM match_sessions ms
JOIN matches m ON m.id = ms.match_id
JOIN teams ht ON ht.id = m.home_team_id
JOIN teams at ON at.id = m.away_team_id
LEFT JOIN match_referees ref1 ON ref1.match_id = m.id AND ref1.role = 'Главный судья'
LEFT JOIN match_referees ref2 ON ref2.match_id = m.id AND ref2.role = 'Ассистент судьи №1'
LEFT JOIN match_referees ref3 ON ref3.match_id = m.id AND ref3.role = 'Ассистент судьи №2'
LEFT JOIN match_referees ref4 ON ref4.match_id = m.id AND ref4.role = 'Резервный судья'
LEFT JOIN match_coaches mc1 ON mc1.match_id = m.id AND mc1.side = 'home'
LEFT JOIN coaches c1 ON c1.id = mc1.coach_id
LEFT JOIN match_coaches mc2 ON mc2.match_id = m.id AND mc2.side = 'away'
LEFT JOIN coaches c2 ON c2.id = mc2.coach_id
LEFT JOIN stadiums s ON s.id = m.stadium_id
WHERE ms.session_token = %s
LIMIT 1;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (session_token,))
return cur.fetchone()
finally:
conn.close()
def get_vmix_standings(session_token: str):
query = """
SELECT
s.position,
t.full_name,
CASE
WHEN t.full_name ILIKE '%%динамо%%'
THEN REPLACE(REPLACE(t.logo_path, 'HOME\\', ''), 'Динамо', 'Динамоелый')
WHEN t.full_name ILIKE '%%зенит%%'
THEN REPLACE(REPLACE(t.logo_path, 'HOME\\', ''), 'Зенит', 'Зенит_Белый')
ELSE REPLACE(t.logo_path, 'HOME\\', '')
END AS logo,
s.played,
s.wins,
s.losses,
s.draws,
s.points_for || ' - ' || s.points_against AS score,
s.points,
s.team_id
FROM standings s
LEFT JOIN teams t ON s.team_id = t.id
ORDER BY s.position
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (session_token,))
return cur.fetchall()
finally:
conn.close()
def get_vmix_schedule(session_token: str):
query = """
SELECT
CASE
WHEN t1.full_name ILIKE '%%динамо%%'
THEN REPLACE(REPLACE(t1.logo_path, 'HOME\\', ''), 'Динамо', 'Динамоелый')
WHEN t1.full_name ILIKE '%%зенит%%'
THEN REPLACE(REPLACE(t1.logo_path, 'HOME\\', ''), 'Зенит', 'Зенит_Белый')
ELSE REPLACE(t1.logo_path, 'HOME\\', '')
END AS logo1,
CASE
WHEN t2.full_name ILIKE '%%динамо%%'
THEN REPLACE(REPLACE(t2.logo_path, 'HOME\\', ''), 'Динамо', 'Динамоелый')
WHEN t2.full_name ILIKE '%%зенит%%'
THEN REPLACE(REPLACE(t2.logo_path, 'HOME\\', ''), 'Зенит', 'Зенит_Белый')
ELSE REPLACE(t2.logo_path, 'HOME\\', '')
END AS logo2,
m.home_score,
m.away_score,
m.match_date,
m.status,
m.id,
m.channel
FROM matches m
LEFT JOIN teams t1 ON m.home_team_id = t1.id
LEFT JOIN teams t2 ON m.away_team_id = t2.id
WHERE m.tour = %s
ORDER BY m.match_date, m.id
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (session_token[10],))
return cur.fetchall()
finally:
conn.close()
def get_vmix_team_formations(session_token: str, team_id: int):
query = """
SELECT
COALESCE(NULLIF(p.last_name, ''), NULLIF(mf.player_name, ''), '') AS last_name,
mf.is_captain,
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
ORDER BY mf.id
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (session_token[1], team_id))
return cur.fetchall()
finally:
conn.close()
def get_vmix_scoreboard_info(session_token: str):
query = """
WITH yellow_reds AS (
SELECT
side,
player_id
FROM match_events_ui
WHERE match_id = %s
AND type = 'yellow'
AND player_id IS NOT NULL
GROUP BY side, player_id
HAVING COUNT(*) >= 2
),
direct_reds AS (
SELECT
side,
COUNT(*) AS cnt
FROM match_events_ui
WHERE match_id = %s
AND type = 'red'
GROUP BY side
),
two_yellow_reds AS (
SELECT
side,
COUNT(*) AS cnt
FROM yellow_reds
GROUP BY side
),
red_totals AS (
SELECT
s.side,
COALESCE(dr.cnt, 0) + COALESCE(tyr.cnt, 0) AS red_count
FROM (
SELECT 'home' AS side
UNION ALL
SELECT 'away' AS side
) s
LEFT JOIN direct_reds dr ON dr.side = s.side
LEFT JOIN two_yellow_reds tyr ON tyr.side = s.side
)
SELECT
-- голы
COALESCE(SUM(
CASE
WHEN m.side = 'home' AND m.type IN ('goal', 'penalty', 'own_goal')
THEN 1 ELSE 0
END
), 0) AS home_goals,
COALESCE(SUM(
CASE
WHEN m.side = 'away' AND m.type IN ('goal', 'penalty', 'own_goal')
THEN 1 ELSE 0
END
), 0) AS away_goals,
-- домашние карточки
CASE WHEN (SELECT red_count FROM red_totals WHERE side = 'home') >= 1 THEN '#FF0000' ELSE '#FF000000' END AS home_red_1,
CASE WHEN (SELECT red_count FROM red_totals WHERE side = 'home') >= 2 THEN '#FF0000' ELSE '#FF000000' END AS home_red_2,
CASE WHEN (SELECT red_count FROM red_totals WHERE side = 'home') >= 3 THEN '#FF0000' ELSE '#FF000000' END AS home_red_3,
CASE WHEN (SELECT red_count FROM red_totals WHERE side = 'home') >= 4 THEN '#FF0000' ELSE '#FF000000' END AS home_red_4,
-- гостевые карточки
CASE WHEN (SELECT red_count FROM red_totals WHERE side = 'away') >= 1 THEN '#FF0000' ELSE '#FF000000' END AS away_red_1,
CASE WHEN (SELECT red_count FROM red_totals WHERE side = 'away') >= 2 THEN '#FF0000' ELSE '#FF000000' END AS away_red_2,
CASE WHEN (SELECT red_count FROM red_totals WHERE side = 'away') >= 3 THEN '#FF0000' ELSE '#FF000000' END AS away_red_3,
CASE WHEN (SELECT red_count FROM red_totals WHERE side = 'away') >= 4 THEN '#FF0000' ELSE '#FF000000' END AS away_red_4
FROM match_events_ui m
WHERE m.match_id = %s;
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (session_token[1],session_token[1],session_token[1]))
return cur.fetchone()
finally:
conn.close()
def get_vmix_players_goal2(match_id: int):
"""запрос для получения игроков, забивших голы, с указанием минут и типа гола (основной, пенальти, автогол)
Иванов 23', 25' (дубли пишуться вместе)
"""
query = """
WITH goal_events AS (
SELECT
meu.side,
COALESCE(p.last_name, meu.player_name) AS last_name,
meu.seconds,
meu.id,
meu.type,
CASE
WHEN EXISTS (
SELECT 1
FROM match_events_ui m2
WHERE m2.match_id = meu.match_id
AND m2.type = 'period_start_2h'
AND m2.seconds <= meu.seconds
) THEN 2
WHEN EXISTS (
SELECT 1
FROM match_events_ui m2
WHERE m2.match_id = meu.match_id
AND m2.type = 'period_start_1h'
AND m2.seconds <= meu.seconds
) THEN 1
ELSE NULL
END AS period_no
FROM match_events_ui meu
LEFT JOIN players p ON p.id = meu.player_id
WHERE meu.match_id = %s
AND meu.type IN ('goal', 'penalty', 'own_goal')
),
grouped_players AS (
SELECT
side,
last_name,
MIN(seconds) AS first_goal_seconds,
STRING_AGG(
(
CASE
WHEN period_no = 1 AND seconds > 2700
THEN '45''' || '+' || ((seconds - 2700) / 60)::int::text
WHEN period_no = 2 AND seconds > 5400
THEN '90''' || '+' || ((seconds - 5400) / 60)::int::text
WHEN period_no = 2
THEN (46 + ((seconds - 2700) / 60)::int)::text || ''''
ELSE (1 + (seconds / 60)::int)::text || ''''
END
) ||
CASE
WHEN type = 'own_goal' THEN ' (АГ)'
WHEN type = 'penalty' THEN ' (П)'
ELSE ''
END,
', ' ORDER BY seconds, id
) AS goal_minutes
FROM goal_events
GROUP BY side, last_name
),
home_rows AS (
SELECT
ROW_NUMBER() OVER (ORDER BY first_goal_seconds, last_name) AS rn,
last_name || ' ' || goal_minutes AS player_name1
FROM grouped_players
WHERE side = 'home'
),
away_rows AS (
SELECT
ROW_NUMBER() OVER (ORDER BY first_goal_seconds, last_name) AS rn,
last_name || ' ' || goal_minutes AS player_name2
FROM grouped_players
WHERE side = 'away'
)
SELECT
COALESCE(h.player_name1, '') AS player_name1,
COALESCE(a.player_name2, '') AS player_name2
FROM home_rows h
FULL OUTER JOIN away_rows a ON a.rn = h.rn
ORDER BY COALESCE(h.rn, a.rn);
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (match_id,))
return cur.fetchall()
finally:
conn.close()
def get_vmix_players_goal(match_id: int):
"""запрос для получения игроков, забивших голы, с указанием минут и типа гола (основной, пенальти, автогол)
Каждый гол в отдельной строке, дубли по игроку не объединяются
"""
query = """
WITH goal_events AS (
SELECT
meu.side,
COALESCE(p.last_name, meu.player_name) AS last_name,
meu.seconds,
meu.id,
meu.type,
CASE
WHEN EXISTS (
SELECT 1
FROM match_events_ui m2
WHERE m2.match_id = meu.match_id
AND m2.type = 'period_start_2h'
AND m2.seconds <= meu.seconds
) THEN 2
WHEN EXISTS (
SELECT 1
FROM match_events_ui m2
WHERE m2.match_id = meu.match_id
AND m2.type = 'period_start_1h'
AND m2.seconds <= meu.seconds
) THEN 1
ELSE NULL
END AS period_no
FROM match_events_ui meu
LEFT JOIN players p ON p.id = meu.player_id
WHERE meu.match_id = %s
AND meu.type IN ('goal', 'penalty', 'own_goal')
),
goal_rows AS (
SELECT
side,
last_name,
seconds,
id,
last_name || ' ' ||
(
CASE
WHEN period_no = 1 AND seconds > 2700
THEN '45''' || '+' || ((seconds - 2700) / 60)::int::text
WHEN period_no = 2 AND seconds > 5400
THEN '90''' || '+' || ((seconds - 5400) / 60)::int::text
WHEN period_no = 2
THEN (46 + ((seconds - 2700) / 60)::int)::text || ''''
ELSE (1 + (seconds / 60)::int)::text || ''''
END
) ||
CASE
WHEN type = 'own_goal' THEN ' (АГ)'
WHEN type = 'penalty' THEN ' (П)'
ELSE ''
END AS player_goal
FROM goal_events
),
home_rows AS (
SELECT
ROW_NUMBER() OVER (ORDER BY seconds, id, last_name) AS rn,
player_goal AS player_name1
FROM goal_rows
WHERE side = 'home'
),
away_rows AS (
SELECT
ROW_NUMBER() OVER (ORDER BY seconds, id, last_name) AS rn,
player_goal AS player_name2
FROM goal_rows
WHERE side = 'away'
)
SELECT
COALESCE(h.player_name1, '') AS player_name1,
COALESCE(a.player_name2, '') AS player_name2
FROM home_rows h
FULL OUTER JOIN away_rows a ON a.rn = h.rn
ORDER BY COALESCE(h.rn, a.rn);
"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(query, (match_id,))
return cur.fetchall()
finally:
conn.close()