first commit

This commit is contained in:
2026-04-20 11:56:11 +03:00
commit 8d80fadb56
103 changed files with 19756 additions and 0 deletions

0
services/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

155
services/auth_service.py Normal file
View File

@@ -0,0 +1,155 @@
from __future__ import annotations
import base64
import hashlib
import hmac
import secrets
from datetime import datetime, timedelta, timezone
from fastapi import Request
from fastapi.responses import JSONResponse, RedirectResponse
from repositories.auth_repository import (
create_auth_session_record,
get_auth_session_by_token,
revoke_auth_session,
)
IDLE_TIMEOUT_SECONDS = 2 * 60 * 60
SESSION_TOUCH_THROTTLE_SECONDS = 60
PBKDF2_ITERATIONS = 260_000
def hash_password(password: str) -> str:
salt = secrets.token_bytes(16)
derived = hashlib.pbkdf2_hmac(
"sha256",
password.encode("utf-8"),
salt,
PBKDF2_ITERATIONS,
)
return (
f"pbkdf2_sha256${PBKDF2_ITERATIONS}$"
f"{base64.b64encode(salt).decode()}$"
f"{base64.b64encode(derived).decode()}"
)
def verify_password(password: str, stored_hash: str) -> bool:
try:
algorithm, iterations_raw, salt_b64, hash_b64 = stored_hash.split("$", 3)
if algorithm != "pbkdf2_sha256":
return False
iterations = int(iterations_raw)
salt = base64.b64decode(salt_b64)
expected = base64.b64decode(hash_b64)
except Exception:
return False
actual = hashlib.pbkdf2_hmac(
"sha256",
password.encode("utf-8"),
salt,
iterations,
)
return hmac.compare_digest(actual, expected)
def create_auth_session(
user_id: int,
ip_address: str | None = None,
user_agent: str | None = None,
) -> str:
token = secrets.token_urlsafe(48)
expires_at = datetime.now(timezone.utc) + timedelta(seconds=IDLE_TIMEOUT_SECONDS)
create_auth_session_record(
user_id=user_id,
session_token=token,
expires_at=expires_at,
ip_address=ip_address,
user_agent=user_agent,
)
return token
def _is_api_request(request: Request) -> bool:
path = request.url.path
if request.method in {"POST", "PUT", "PATCH", "DELETE"}:
return True
if path.endswith("/events") or "/event/" in path or path.endswith("/event"):
return True
accept = request.headers.get("accept", "")
requested_with = request.headers.get("x-requested-with", "")
return "application/json" in accept or requested_with.lower() == "xmlhttprequest"
def build_not_authenticated_response(request: Request):
if _is_api_request(request):
return JSONResponse({"error": "auth_expired"}, status_code=401)
return RedirectResponse(url="/login?reason=idle", status_code=303)
def revoke_auth_session_by_request(request: Request):
token = request.cookies.get("auth_token")
if token:
revoke_auth_session(token)
def get_current_user_from_request(request: Request):
token = request.cookies.get("auth_token")
if not token:
return None
row = get_auth_session_by_token(token)
if not row:
return None
(
session_id,
user_id,
session_token,
created_at,
last_activity_at,
expires_at,
revoked_at,
ip_address,
user_agent,
username,
is_active,
role,
) = row
if revoked_at is not None or not is_active:
return None
now = datetime.now(timezone.utc)
if last_activity_at.tzinfo is None:
last_activity_at = last_activity_at.replace(tzinfo=timezone.utc)
if expires_at.tzinfo is None:
expires_at = expires_at.replace(tzinfo=timezone.utc)
idle_seconds = (now - last_activity_at).total_seconds()
if idle_seconds > IDLE_TIMEOUT_SECONDS or expires_at < now:
revoke_auth_session(token)
return None
new_expires_at = now + timedelta(seconds=IDLE_TIMEOUT_SECONDS)
from repositories.auth_repository import touch_auth_session_if_needed
touch_auth_session_if_needed(
session_token=token,
expires_at=new_expires_at,
throttle_seconds=SESSION_TOUCH_THROTTLE_SECONDS,
)
return {
"session_id": session_id,
"user_id": user_id,
"username": username,
"created_at": created_at,
"last_activity_at": last_activity_at,
"expires_at": expires_at,
"ip_address": ip_address,
"user_agent": user_agent,
"role": role,
}

211
services/game_service.py Normal file
View File

@@ -0,0 +1,211 @@
from repositories.match_repository import (
get_match_by_external_id,
mark_match_parsed,
mark_match_parse_error,
clear_match_squad_data,
)
from repositories.player_repository import (
get_player_id_by_name_and_team,
get_player_id_by_external_id,
)
from repositories.coach_repository import (
get_coach_id_by_name_and_team,
get_coach_id_by_external_id,
)
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_coach_repository import replace_match_coaches
from repositories.match_referee_repository import replace_match_referees
def sync_match_page(
match_external_id: str,
home_starting: list[dict],
away_starting: list[dict],
home_bench: list[dict],
away_bench: list[dict],
home_coaches: list[dict],
away_coaches: list[dict],
referees: list[dict],
) -> None:
match_row = get_match_by_external_id(match_external_id)
if not match_row:
raise ValueError(f"Match not found by external_id: {match_external_id}")
match_id, _, home_team_id, away_team_id = match_row
clear_match_squad_data(match_id)
lineup_rows = []
for player in home_starting:
player_id = None
if player.get("player_external_id"):
player_id = get_player_id_by_external_id(player["player_external_id"])
if player_id is None:
player_id = get_player_id_by_name_and_team(
player["player_name"], home_team_id
)
lineup_rows.append(
{
"match_id": match_id,
"team_id": home_team_id,
"player_id": player_id,
"player_name": player["player_name"],
"number": player.get("number"),
"position": player.get("position"),
"is_captain": bool(player.get("is_captain")),
"lineup_type": "starting",
"source": "parser",
}
)
for player in away_starting:
player_id = None
if player.get("player_external_id"):
player_id = get_player_id_by_external_id(player["player_external_id"])
if player_id is None:
player_id = get_player_id_by_name_and_team(
player["player_name"], away_team_id
)
lineup_rows.append(
{
"match_id": match_id,
"team_id": away_team_id,
"player_id": player_id,
"player_name": player["player_name"],
"number": player.get("number"),
"position": player.get("position"),
"is_captain": bool(player.get("is_captain")),
"lineup_type": "starting",
"source": "parser",
}
)
for player in home_bench:
player_id = None
if player.get("player_external_id"):
player_id = get_player_id_by_external_id(player["player_external_id"])
if player_id is None:
player_id = get_player_id_by_name_and_team(
player["player_name"], home_team_id
)
lineup_rows.append(
{
"match_id": match_id,
"team_id": home_team_id,
"player_id": player_id,
"player_name": player["player_name"],
"number": player.get("number"),
"position": player.get("position"),
"is_captain": bool(player.get("is_captain")),
"lineup_type": "bench",
"source": "parser",
}
)
for player in away_bench:
player_id = None
if player.get("player_external_id"):
player_id = get_player_id_by_external_id(player["player_external_id"])
if player_id is None:
player_id = get_player_id_by_name_and_team(
player["player_name"], away_team_id
)
lineup_rows.append(
{
"match_id": match_id,
"team_id": away_team_id,
"player_id": player_id,
"player_name": player["player_name"],
"number": player.get("number"),
"position": player.get("position"),
"is_captain": bool(player.get("is_captain")),
"lineup_type": "bench",
"source": "parser",
}
)
coach_rows = []
for coach in home_coaches:
coach_id = None
if coach.get("coach_external_id"):
coach_id = get_coach_id_by_external_id(coach["coach_external_id"])
if coach_id is None:
coach_id = get_coach_id_by_name_and_team(coach["coach_name"], home_team_id)
coach_rows.append(
{
"match_id": match_id,
"team_id": home_team_id,
"coach_id": coach_id,
"coach_name": coach["coach_name"],
"role": coach.get("role"),
"source": "parser",
}
)
for coach in away_coaches:
coach_id = None
if coach.get("coach_external_id"):
coach_id = get_coach_id_by_external_id(coach["coach_external_id"])
if coach_id is None:
coach_id = get_coach_id_by_name_and_team(coach["coach_name"], away_team_id)
coach_rows.append(
{
"match_id": match_id,
"team_id": away_team_id,
"coach_id": coach_id,
"coach_name": coach["coach_name"],
"role": coach.get("role"),
"source": "parser",
}
)
referee_rows = []
for referee in referees:
referee_name = referee["referee_name"].strip()
referee_id = get_referee_id_by_name(referee_name)
if referee_id is None:
referee_id = upsert_referee(full_name=referee_name)
referee_rows.append(
{
"match_id": match_id,
"referee_id": referee_id,
"referee_name": referee_name,
"role": referee.get("role"),
"source": "parser",
}
)
try:
replace_match_lineups(match_id, lineup_rows)
for row in coach_rows:
row["side"] = "home" if row["team_id"] == home_team_id else "away"
replace_match_coaches(match_id, coach_rows)
replace_match_referees(match_id, referee_rows)
mark_match_parsed(match_external_id)
except Exception as e:
mark_match_parse_error(match_external_id, str(e))
raise

View File

@@ -0,0 +1,46 @@
from repositories.player_repository import upsert_player
from repositories.coach_repository import upsert_coach
def to_int(value, default=0) -> int:
if value is None:
return default
value = str(value).strip()
return int(value) if value.isdigit() else default
def sync_team_roster(team_external_id: str, team_data: dict) -> None:
players = team_data.get("players") or []
coaches = team_data.get("coaches") or []
for player in players:
upsert_player(
external_id=player.get("player_id", ""),
team_external_id=team_external_id,
player=player.get("player", ""),
lastname=player.get("lastname", ""),
name=player.get("name", ""),
number=player.get("number", ""),
pos=player.get("pos", ""),
amplua=player.get("amplua", ""),
born=player.get("born", ""),
games=to_int(player.get("games")),
goals=to_int(player.get("goals")),
penaltys=to_int(player.get("penaltys")),
assists=to_int(player.get("assists")),
yellows=to_int(player.get("yellows")),
reds=to_int(player.get("reds")),
is_active=True,
)
for coach in coaches:
upsert_coach(
external_id=coach.get("coach_id", ""),
team_external_id=team_external_id,
player=coach.get("player", ""),
lastname=coach.get("lastname", ""),
name=coach.get("name", ""),
born=coach.get("born", ""),
amplua=coach.get("amplua", ""),
is_active=True,
)

View File

@@ -0,0 +1,19 @@
from repositories.match_repository import upsert_match_by_team_external_ids
def sync_matches(matches_data: list[dict]) -> None:
for match in matches_data:
upsert_match_by_team_external_ids(
external_id=match["external_id"],
home_team_external_id=match["home_team_external_id"],
away_team_external_id=match["away_team_external_id"],
match_date=match.get("match_date"),
status=match.get("status", "scheduled"),
home_score=match.get("home_score"),
away_score=match.get("away_score"),
tour=match.get("tour"),
season=match.get("season"),
place=match.get("place"),
date_raw=match.get("date_raw"),
score_add=match.get("score_add"),
)

View File

@@ -0,0 +1,8 @@
from repositories.standings_repository import replace_standings_for_season
def sync_standings(season: str, standings_rows: list[dict]) -> None:
replace_standings_for_season(
season=season,
standings_rows=standings_rows,
)

14
services/teams_service.py Normal file
View File

@@ -0,0 +1,14 @@
from repositories.team_repository import upsert_team
def sync_teams(teams_data: list[dict]) -> None:
for team in teams_data:
upsert_team(
external_id=team["external_id"],
name=team["name"],
logo_url=team["logo_url"],
games=int(team.get("games", 0) or 0),
wins=int(team.get("wins", 0) or 0),
goals=int(team.get("goals", 0) or 0),
tournaments=int(team.get("tournaments", 0) or 0),
)

View File

@@ -0,0 +1,424 @@
# services/vmix_json_service.py
from db import get_connection
from repositories.match_lineup_repository import get_match_lineup_for_vmix
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 = []
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": (
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"
),
}
)
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
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
p.last_name,
mf.is_captain,
p.number,
p.position,
p.first_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_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')
),
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()