обновление для Кубка России
переделаны все парсеры на ссылки из базы
This commit is contained in:
@@ -5,6 +5,24 @@ from repositories.team_repository import get_team_id_by_external_id
|
||||
from repositories.stadium_repository import get_or_create_stadium
|
||||
|
||||
|
||||
def ensure_match_source_key_column() -> None:
|
||||
"""Добавляет источник турнира для матчей, чтобы vMix выбирал правильную папку логотипов."""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
ALTER TABLE matches
|
||||
ADD COLUMN IF NOT EXISTS source_key VARCHAR(50);
|
||||
"""
|
||||
)
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def upsert_match(
|
||||
external_id: str,
|
||||
@@ -20,6 +38,7 @@ def upsert_match(
|
||||
stadium_id: int | None = None,
|
||||
date_raw: str | None = None,
|
||||
score_add: str | None = None,
|
||||
source_key: str | None = None,
|
||||
) -> None:
|
||||
query = """
|
||||
INSERT INTO matches (
|
||||
@@ -36,10 +55,11 @@ INSERT INTO matches (
|
||||
stadium_id,
|
||||
date_raw,
|
||||
score_add,
|
||||
source_key,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
|
||||
ON CONFLICT (external_id)
|
||||
DO UPDATE SET
|
||||
home_team_id = EXCLUDED.home_team_id,
|
||||
@@ -54,6 +74,7 @@ DO UPDATE SET
|
||||
stadium_id = EXCLUDED.stadium_id,
|
||||
date_raw = EXCLUDED.date_raw,
|
||||
score_add = EXCLUDED.score_add,
|
||||
source_key = COALESCE(EXCLUDED.source_key, matches.source_key),
|
||||
updated_at = NOW();
|
||||
"""
|
||||
|
||||
@@ -76,6 +97,7 @@ DO UPDATE SET
|
||||
stadium_id,
|
||||
date_raw,
|
||||
score_add,
|
||||
source_key,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
@@ -100,6 +122,7 @@ def upsert_match_by_team_external_ids(
|
||||
stadium_id: int | None = None,
|
||||
date_raw: str | None = None,
|
||||
score_add: str | None = None,
|
||||
source_key: str | None = None,
|
||||
) -> None:
|
||||
home_team_id = get_team_id_by_external_id(home_team_external_id)
|
||||
away_team_id = get_team_id_by_external_id(away_team_external_id)
|
||||
@@ -125,6 +148,7 @@ def upsert_match_by_team_external_ids(
|
||||
stadium_id=stadium_id,
|
||||
date_raw=date_raw,
|
||||
score_add=score_add,
|
||||
source_key=source_key,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import secrets
|
||||
|
||||
from db import get_connection
|
||||
from parsers.parser_sources import build_logo_path
|
||||
|
||||
|
||||
def create_match_session(
|
||||
@@ -66,7 +67,8 @@ def get_match_session_by_token(session_token: str):
|
||||
at.id AS away_team_id,
|
||||
at.name AS away_team_name,
|
||||
at.logo_url AS away_team_logo,
|
||||
at.logo_path AS away_team_logo_path
|
||||
at.logo_path AS away_team_logo_path,
|
||||
m.source_key AS source_key
|
||||
|
||||
FROM match_sessions ms
|
||||
JOIN matches m ON m.id = ms.match_id
|
||||
@@ -80,7 +82,16 @@ def get_match_session_by_token(session_token: str):
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(query, (session_token,))
|
||||
return cur.fetchone()
|
||||
row = cur.fetchone()
|
||||
|
||||
if not row:
|
||||
return None
|
||||
|
||||
row = list(row)
|
||||
source_key = row[21] if len(row) > 21 else None
|
||||
row[16] = build_logo_path(source_key, row[16])
|
||||
row[20] = build_logo_path(source_key, row[20])
|
||||
return tuple(row)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
361
repositories/project_settings_repository.py
Normal file
361
repositories/project_settings_repository.py
Normal file
@@ -0,0 +1,361 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from db import get_connection
|
||||
|
||||
|
||||
DEFAULT_APP_SETTINGS = {
|
||||
"default_parser_source_key": "SUPERLEAGUE",
|
||||
"rfs_base_url": "https://wfl.rfs.ru",
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_PARSER_SOURCES = [
|
||||
{
|
||||
"key": "SUPERLEAGUE",
|
||||
"title": "Суперлига 2026",
|
||||
"tournament_id": "1061879",
|
||||
"round_id": "1117550",
|
||||
"season": "2025/2026",
|
||||
"calendar_type": "tours",
|
||||
"logo_base_path": r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Teams Logos",
|
||||
"photo_base_path": r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Photo",
|
||||
"teams_url": "https://wfl.rfs.ru/tournament/1061879/teams",
|
||||
"schedule_url": "https://wfl.rfs.ru/tournament/1061879/calendar?round_id=1117550&type=tours",
|
||||
"standings_url": "https://wfl.rfs.ru/tournament/1061879/tables",
|
||||
"match_base_url": "https://wfl.rfs.ru/match/",
|
||||
"base_url": "https://wfl.rfs.ru",
|
||||
"sort_order": 10,
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"key": "RUSSIAN_CUP",
|
||||
"title": "Кубок России 2026",
|
||||
"tournament_id": "1064908",
|
||||
"round_id": "1125159",
|
||||
"season": "2026",
|
||||
"calendar_type": "stages",
|
||||
"logo_base_path": r"D:\Графика\ФУТБОЛ\Кубок России 2026\Teams Logos",
|
||||
"photo_base_path": r"D:\Графика\ФУТБОЛ\Кубок России 2026\Photo",
|
||||
"teams_url": "https://wfl.rfs.ru/tournament/1064908/teams",
|
||||
"schedule_url": "https://wfl.rfs.ru/tournament/1064908/calendar?round_id=1125159&type=stages",
|
||||
"standings_url": "https://wfl.rfs.ru/tournament/1064908/tables",
|
||||
"match_base_url": "https://wfl.rfs.ru/match/",
|
||||
"base_url": "https://wfl.rfs.ru",
|
||||
"sort_order": 20,
|
||||
"is_active": True,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _row_to_source(row: tuple) -> dict[str, Any]:
|
||||
return {
|
||||
"key": row[0] or "",
|
||||
"title": row[1] or "",
|
||||
"tournament_id": row[2] or "",
|
||||
"round_id": row[3] or "",
|
||||
"season": row[4] or "",
|
||||
"calendar_type": row[5] or "tours",
|
||||
"logo_base_path": row[6] or "",
|
||||
"photo_base_path": row[7] or "",
|
||||
"teams_url": row[8] or "",
|
||||
"schedule_url": row[9] or "",
|
||||
"standings_url": row[10] or "",
|
||||
"match_base_url": row[11] or "",
|
||||
"base_url": row[12] or "https://wfl.rfs.ru",
|
||||
"sort_order": row[13] or 0,
|
||||
"is_active": bool(row[14]),
|
||||
}
|
||||
|
||||
|
||||
def ensure_project_settings_tables() -> None:
|
||||
"""Создаёт таблицы настроек проекта и добавляет стандартные источники, если их ещё нет."""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS app_settings (
|
||||
key VARCHAR(100) PRIMARY KEY,
|
||||
value TEXT NOT NULL DEFAULT '',
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
"""
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS parser_sources (
|
||||
key VARCHAR(50) PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
tournament_id VARCHAR(100),
|
||||
round_id VARCHAR(100),
|
||||
season VARCHAR(50),
|
||||
calendar_type VARCHAR(50) NOT NULL DEFAULT 'tours',
|
||||
logo_base_path TEXT,
|
||||
photo_base_path TEXT,
|
||||
teams_url TEXT,
|
||||
schedule_url TEXT,
|
||||
standings_url TEXT,
|
||||
match_base_url TEXT,
|
||||
base_url TEXT NOT NULL DEFAULT 'https://wfl.rfs.ru',
|
||||
sort_order INTEGER NOT NULL DEFAULT 100,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
"""
|
||||
)
|
||||
cur.execute("ALTER TABLE parser_sources ADD COLUMN IF NOT EXISTS photo_base_path TEXT;")
|
||||
|
||||
for key, value in DEFAULT_APP_SETTINGS.items():
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO app_settings (key, value, updated_at)
|
||||
VALUES (%s, %s, NOW())
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
""",
|
||||
(key, value),
|
||||
)
|
||||
|
||||
for source in DEFAULT_PARSER_SOURCES:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO parser_sources (
|
||||
key,
|
||||
title,
|
||||
tournament_id,
|
||||
round_id,
|
||||
season,
|
||||
calendar_type,
|
||||
logo_base_path,
|
||||
photo_base_path,
|
||||
teams_url,
|
||||
schedule_url,
|
||||
standings_url,
|
||||
match_base_url,
|
||||
base_url,
|
||||
sort_order,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
""",
|
||||
(
|
||||
source["key"],
|
||||
source["title"],
|
||||
source["tournament_id"],
|
||||
source["round_id"],
|
||||
source["season"],
|
||||
source["calendar_type"],
|
||||
source["logo_base_path"],
|
||||
source["photo_base_path"],
|
||||
source["teams_url"],
|
||||
source["schedule_url"],
|
||||
source["standings_url"],
|
||||
source["match_base_url"],
|
||||
source["base_url"],
|
||||
source["sort_order"],
|
||||
source["is_active"],
|
||||
),
|
||||
)
|
||||
|
||||
for source in DEFAULT_PARSER_SOURCES:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE parser_sources
|
||||
SET photo_base_path = %s, updated_at = NOW()
|
||||
WHERE key = %s
|
||||
AND (photo_base_path IS NULL OR TRIM(photo_base_path) = '');
|
||||
""",
|
||||
(source.get("photo_base_path") or "", source["key"]),
|
||||
)
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_app_settings() -> dict[str, str]:
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT key, value FROM app_settings;")
|
||||
rows = cur.fetchall()
|
||||
settings = dict(DEFAULT_APP_SETTINGS)
|
||||
settings.update({row[0]: row[1] or "" for row in rows})
|
||||
return settings
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_app_setting(key: str, default: str = "") -> str:
|
||||
return get_app_settings().get(key, default)
|
||||
|
||||
|
||||
def update_app_setting(key: str, value: str) -> None:
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO app_settings (key, value, updated_at)
|
||||
VALUES (%s, %s, NOW())
|
||||
ON CONFLICT (key)
|
||||
DO UPDATE SET value = EXCLUDED.value, updated_at = NOW();
|
||||
""",
|
||||
(key, value or ""),
|
||||
)
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def list_parser_sources_from_db(active_only: bool = True) -> list[dict[str, Any]]:
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
if active_only:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT key, title, tournament_id, round_id, season, calendar_type,
|
||||
logo_base_path, photo_base_path, teams_url, schedule_url, standings_url,
|
||||
match_base_url, base_url, sort_order, is_active
|
||||
FROM parser_sources
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY sort_order ASC, title ASC;
|
||||
"""
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT key, title, tournament_id, round_id, season, calendar_type,
|
||||
logo_base_path, photo_base_path, teams_url, schedule_url, standings_url,
|
||||
match_base_url, base_url, sort_order, is_active
|
||||
FROM parser_sources
|
||||
ORDER BY sort_order ASC, title ASC;
|
||||
"""
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
return [_row_to_source(row) for row in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_parser_source_from_db(source_key: str) -> dict[str, Any] | None:
|
||||
source_key = (source_key or "").upper().strip()
|
||||
if not source_key:
|
||||
return None
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT key, title, tournament_id, round_id, season, calendar_type,
|
||||
logo_base_path, photo_base_path, teams_url, schedule_url, standings_url,
|
||||
match_base_url, base_url, sort_order, is_active
|
||||
FROM parser_sources
|
||||
WHERE key = %s
|
||||
LIMIT 1;
|
||||
""",
|
||||
(source_key,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return _row_to_source(row) if row else None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def update_parser_source(source_key: str, values: dict[str, str]) -> None:
|
||||
source_key = (source_key or "").upper().strip()
|
||||
if not source_key:
|
||||
raise ValueError("Не указан ключ источника")
|
||||
|
||||
current = get_parser_source_from_db(source_key)
|
||||
if not current:
|
||||
current = next((item for item in DEFAULT_PARSER_SOURCES if item["key"] == source_key), None)
|
||||
if not current:
|
||||
current = {
|
||||
"key": source_key,
|
||||
"title": source_key,
|
||||
"tournament_id": "",
|
||||
"round_id": "",
|
||||
"season": "",
|
||||
"calendar_type": "tours",
|
||||
"logo_base_path": "",
|
||||
"photo_base_path": "",
|
||||
"teams_url": "",
|
||||
"schedule_url": "",
|
||||
"standings_url": "",
|
||||
"match_base_url": "",
|
||||
"base_url": DEFAULT_APP_SETTINGS["rfs_base_url"],
|
||||
"sort_order": 100,
|
||||
"is_active": True,
|
||||
}
|
||||
|
||||
merged = {**current, **{key: value for key, value in values.items() if value is not None}}
|
||||
merged["key"] = source_key
|
||||
merged["calendar_type"] = merged.get("calendar_type") or "tours"
|
||||
merged["base_url"] = merged.get("base_url") or DEFAULT_APP_SETTINGS["rfs_base_url"]
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO parser_sources (
|
||||
key, title, tournament_id, round_id, season, calendar_type,
|
||||
logo_base_path, photo_base_path, teams_url, schedule_url, standings_url,
|
||||
match_base_url, base_url, sort_order, is_active, created_at, updated_at
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
|
||||
ON CONFLICT (key)
|
||||
DO UPDATE SET
|
||||
title = EXCLUDED.title,
|
||||
tournament_id = EXCLUDED.tournament_id,
|
||||
round_id = EXCLUDED.round_id,
|
||||
season = EXCLUDED.season,
|
||||
calendar_type = EXCLUDED.calendar_type,
|
||||
logo_base_path = EXCLUDED.logo_base_path,
|
||||
photo_base_path = EXCLUDED.photo_base_path,
|
||||
teams_url = EXCLUDED.teams_url,
|
||||
schedule_url = EXCLUDED.schedule_url,
|
||||
standings_url = EXCLUDED.standings_url,
|
||||
match_base_url = EXCLUDED.match_base_url,
|
||||
base_url = EXCLUDED.base_url,
|
||||
sort_order = EXCLUDED.sort_order,
|
||||
is_active = EXCLUDED.is_active,
|
||||
updated_at = NOW();
|
||||
""",
|
||||
(
|
||||
merged.get("key", source_key),
|
||||
merged.get("title") or source_key,
|
||||
merged.get("tournament_id") or "",
|
||||
merged.get("round_id") or "",
|
||||
merged.get("season") or "",
|
||||
merged.get("calendar_type") or "tours",
|
||||
merged.get("logo_base_path") or "",
|
||||
merged.get("photo_base_path") or "",
|
||||
merged.get("teams_url") or "",
|
||||
merged.get("schedule_url") or "",
|
||||
merged.get("standings_url") or "",
|
||||
merged.get("match_base_url") or "",
|
||||
merged.get("base_url") or DEFAULT_APP_SETTINGS["rfs_base_url"],
|
||||
int(merged.get("sort_order") or 100),
|
||||
bool(merged.get("is_active", True)),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -1,4 +1,5 @@
|
||||
from db import get_connection
|
||||
from parsers.parser_sources import extract_logo_filename
|
||||
|
||||
|
||||
def upsert_team(
|
||||
@@ -153,7 +154,7 @@ def search_teams_for_admin(q: str = "") -> list[dict]:
|
||||
"full_name": row[2] or "",
|
||||
"short_name_3": row[3] or "",
|
||||
"city": row[4] or "",
|
||||
"logo_path": row[5] or "",
|
||||
"logo_path": extract_logo_filename(row[5]),
|
||||
"external_id": row[6] or "",
|
||||
}
|
||||
for row in rows
|
||||
@@ -192,7 +193,7 @@ def get_team_by_id(team_id: int) -> dict | None:
|
||||
"full_name": row[2] or "",
|
||||
"short_name_3": row[3] or "",
|
||||
"city": row[4] or "",
|
||||
"logo_path": row[5] or "",
|
||||
"logo_path": extract_logo_filename(row[5]),
|
||||
"external_id": row[6] or "",
|
||||
}
|
||||
finally:
|
||||
@@ -227,7 +228,7 @@ def update_team_admin(
|
||||
full_name.strip(),
|
||||
short_name_3.strip().upper(),
|
||||
city.strip(),
|
||||
logo_path.strip(),
|
||||
extract_logo_filename(logo_path),
|
||||
external_id.strip(),
|
||||
team_id,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user