diff --git a/app.py b/app.py index a95afd1..a091190 100644 --- a/app.py +++ b/app.py @@ -34,7 +34,7 @@ from parsers.parser_players import run_parser_players from parsers.parser_schedule import run_parser_schedule from parsers.parser_standings import run_parser_standings from parsers.parser_teams import run_parser_teams -from parsers.parser_sources import build_empty_photo_path, list_parser_sources, get_default_source_key, get_parser_source +from parsers.parser_sources import build_empty_photo_path, build_channel_logo_path, build_empty_channel_logo_path, list_parser_sources, get_default_source_key, get_parser_source from services.project_settings_service import build_project_settings_context, save_project_settings from repositories.project_settings_repository import ensure_project_settings_tables @@ -1681,11 +1681,12 @@ def vmix_schedule(session_token: str): "#FFFFFF00" if row[2] is None and row[3] is None else "#FFFFFF" ), "channel": ( - (rf"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Лого каналов\{row[7]}.png" if row[-1] != "RUSSIAN_CUP" else rf"D:\Графика\ФУТБОЛ\ЖФЛ Кубок России 2026\Лого каналов\{row[7]}.png") - # if row[7] and row[5] == "scheduled" - if row[2] is None and row[3] is None - else (r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Лого каналов\EMPTY.png" if row[-1] != "RUSSIAN_CUP" else r"D:\Графика\ФУТБОЛ\ЖФЛ Кубок России 2026\Лого каналов\EMPTY.png") + build_channel_logo_path(row[8] if len(row) > 8 else None, row[7]) + if str(row[7] or "").strip() + else build_empty_channel_logo_path(row[8] if len(row) > 8 else None) ), + "channel_value": row[7] or "", + "source_key": row[8] if len(row) > 8 else "", } for row in rows ] @@ -2299,12 +2300,14 @@ def render_admin_db_index( "title": "Суперлига 2026", "logo_base_path": r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Teams Logos", "photo_base_path": r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Photo", + "channel_logo_base_path": r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Лого каналов", }, { "key": "RUSSIAN_CUP", "title": "Кубок России 2026", "logo_base_path": r"D:\Графика\ФУТБОЛ\Кубок России 2026\Teams Logos", "photo_base_path": r"D:\Графика\ФУТБОЛ\Кубок России 2026\Photo", + "channel_logo_base_path": r"D:\Графика\ФУТБОЛ\ЖФЛ Кубок России 2026\Лого каналов", }, ] default_parser_source_key = "SUPERLEAGUE" diff --git a/parsers/parser_sources.py b/parsers/parser_sources.py index d7f598c..ec03c59 100644 --- a/parsers/parser_sources.py +++ b/parsers/parser_sources.py @@ -228,3 +228,45 @@ def build_photo_path(source_key: str | None, filename: str | None) -> str: def build_empty_photo_path(source_key: str | None = None) -> str: """Путь к пустой картинке фото для выбранного источника.""" return build_photo_path(source_key, "EMPTY.png") + + + +def extract_channel_logo_filename(value: str | None) -> str: + """Возвращает имя файла логотипа телеканала из старого полного пути или нового значения.""" + value = str(value or "").strip().strip('"').strip("'") + if not value: + return "" + normalized = value.replace("/", "\\") + filename = normalized.split("\\")[-1].strip() + return filename + + +def ensure_channel_logo_extension(filename: str) -> str: + filename = extract_channel_logo_filename(filename) + if not filename: + return "" + if "." not in filename.rsplit("\\", 1)[-1]: + return f"{filename}.png" + return filename + + +def get_channel_logo_base_path(source_key: str | None = None) -> str: + source = get_parser_source(source_key) + return str(source.get("channel_logo_base_path") or "").rstrip("\\/") + + +def build_channel_logo_path(source_key: str | None, filename: str | None) -> str: + """Собирает полный путь к логотипу телеканала по источнику турнира.""" + logo_file = ensure_channel_logo_extension(filename) + if not logo_file: + return "" + + base_path = get_channel_logo_base_path(source_key) + if not base_path: + return logo_file + + return f"{base_path}\\{logo_file}" + + +def build_empty_channel_logo_path(source_key: str | None = None) -> str: + return build_channel_logo_path(source_key, "EMPTY.png") diff --git a/repositories/match_repository.py b/repositories/match_repository.py index b68bf5b..08dcefa 100644 --- a/repositories/match_repository.py +++ b/repositories/match_repository.py @@ -355,7 +355,7 @@ def get_tour_schedule_by_match_id(match_id: int) -> list[dict]: with conn.cursor() as cur: cur.execute( """ - SELECT m.tour + SELECT m.tour, m.season, m.source_key FROM matches m WHERE m.id = %s """, @@ -366,6 +366,8 @@ def get_tour_schedule_by_match_id(match_id: int) -> list[dict]: return [] tour = row[0] + season = row[1] + source_key = resolve_match_source_key(match_id, row[2] if len(row) > 2 else None) cur.execute( """ @@ -384,11 +386,13 @@ def get_tour_schedule_by_match_id(match_id: int) -> list[dict]: LEFT JOIN teams ht ON ht.id = m.home_team_id LEFT JOIN teams at ON at.id = m.away_team_id WHERE m.tour = %s + AND (%s IS NULL OR m.season = %s) + AND COALESCE(NULLIF(m.source_key, ''), %s) = %s ORDER BY m.match_date NULLS LAST, m.id """, - (tour,), + (tour, season, season, source_key, source_key), ) rows = cur.fetchall() diff --git a/repositories/project_settings_repository.py b/repositories/project_settings_repository.py index 997f629..0afd791 100644 --- a/repositories/project_settings_repository.py +++ b/repositories/project_settings_repository.py @@ -21,6 +21,7 @@ DEFAULT_PARSER_SOURCES = [ "calendar_type": "tours", "logo_base_path": r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Teams Logos", "photo_base_path": r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Photo", + "channel_logo_base_path": r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Лого каналов", "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", @@ -38,6 +39,7 @@ DEFAULT_PARSER_SOURCES = [ "calendar_type": "stages", "logo_base_path": r"D:\Графика\ФУТБОЛ\Кубок России 2026\Teams Logos", "photo_base_path": r"D:\Графика\ФУТБОЛ\Кубок России 2026\Photo", + "channel_logo_base_path": r"D:\Графика\ФУТБОЛ\ЖФЛ Кубок России 2026\Лого каналов", "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", @@ -49,6 +51,15 @@ DEFAULT_PARSER_SOURCES = [ ] +SOURCE_SELECT_SQL = """ + SELECT key, title, tournament_id, round_id, season, calendar_type, + logo_base_path, photo_base_path, channel_logo_base_path, + teams_url, schedule_url, standings_url, + match_base_url, base_url, sort_order, is_active + FROM parser_sources +""" + + def _row_to_source(row: tuple) -> dict[str, Any]: return { "key": row[0] or "", @@ -59,13 +70,14 @@ def _row_to_source(row: tuple) -> dict[str, Any]: "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]), + "channel_logo_base_path": row[8] or "", + "teams_url": row[9] or "", + "schedule_url": row[10] or "", + "standings_url": row[11] or "", + "match_base_url": row[12] or "", + "base_url": row[13] or "https://wfl.rfs.ru", + "sort_order": row[14] or 0, + "is_active": bool(row[15]), } @@ -94,6 +106,7 @@ def ensure_project_settings_tables() -> None: calendar_type VARCHAR(50) NOT NULL DEFAULT 'tours', logo_base_path TEXT, photo_base_path TEXT, + channel_logo_base_path TEXT, teams_url TEXT, schedule_url TEXT, standings_url TEXT, @@ -107,6 +120,7 @@ def ensure_project_settings_tables() -> None: """ ) cur.execute("ALTER TABLE parser_sources ADD COLUMN IF NOT EXISTS photo_base_path TEXT;") + cur.execute("ALTER TABLE parser_sources ADD COLUMN IF NOT EXISTS channel_logo_base_path TEXT;") for key, value in DEFAULT_APP_SETTINGS.items(): cur.execute( @@ -122,25 +136,12 @@ def ensure_project_settings_tables() -> None: 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 + key, title, tournament_id, round_id, season, calendar_type, + logo_base_path, photo_base_path, channel_logo_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()) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) ON CONFLICT (key) DO NOTHING; """, ( @@ -152,6 +153,7 @@ def ensure_project_settings_tables() -> None: source["calendar_type"], source["logo_base_path"], source["photo_base_path"], + source["channel_logo_base_path"], source["teams_url"], source["schedule_url"], source["standings_url"], @@ -172,6 +174,15 @@ def ensure_project_settings_tables() -> None: """, (source.get("photo_base_path") or "", source["key"]), ) + cur.execute( + """ + UPDATE parser_sources + SET channel_logo_base_path = %s, updated_at = NOW() + WHERE key = %s + AND (channel_logo_base_path IS NULL OR TRIM(channel_logo_base_path) = ''); + """, + (source.get("channel_logo_base_path") or "", source["key"]), + ) conn.commit() except Exception: conn.rollback() @@ -223,26 +234,9 @@ def list_parser_sources_from_db(active_only: bool = True) -> list[dict[str, Any] 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; - """ - ) + cur.execute(SOURCE_SELECT_SQL + " 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; - """ - ) + cur.execute(SOURCE_SELECT_SQL + " ORDER BY sort_order ASC, title ASC;") rows = cur.fetchall() return [_row_to_source(row) for row in rows] finally: @@ -257,17 +251,7 @@ def get_parser_source_from_db(source_key: str) -> dict[str, Any] | 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,), - ) + cur.execute(SOURCE_SELECT_SQL + " WHERE key = %s LIMIT 1;", (source_key,)) row = cur.fetchone() return _row_to_source(row) if row else None finally: @@ -292,6 +276,7 @@ def update_parser_source(source_key: str, values: dict[str, str]) -> None: "calendar_type": "tours", "logo_base_path": "", "photo_base_path": "", + "channel_logo_base_path": "", "teams_url": "", "schedule_url": "", "standings_url": "", @@ -313,10 +298,11 @@ def update_parser_source(source_key: str, values: dict[str, str]) -> None: """ 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, + logo_base_path, photo_base_path, channel_logo_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()) + VALUES (%s, %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, @@ -326,6 +312,7 @@ def update_parser_source(source_key: str, values: dict[str, str]) -> None: calendar_type = EXCLUDED.calendar_type, logo_base_path = EXCLUDED.logo_base_path, photo_base_path = EXCLUDED.photo_base_path, + channel_logo_base_path = EXCLUDED.channel_logo_base_path, teams_url = EXCLUDED.teams_url, schedule_url = EXCLUDED.schedule_url, standings_url = EXCLUDED.standings_url, @@ -344,6 +331,7 @@ def update_parser_source(source_key: str, values: dict[str, str]) -> None: merged.get("calendar_type") or "tours", merged.get("logo_base_path") or "", merged.get("photo_base_path") or "", + merged.get("channel_logo_base_path") or "", merged.get("teams_url") or "", merged.get("schedule_url") or "", merged.get("standings_url") or "", diff --git a/services/project_settings_service.py b/services/project_settings_service.py index e298c4b..41c4c39 100644 --- a/services/project_settings_service.py +++ b/services/project_settings_service.py @@ -87,6 +87,7 @@ def build_project_settings_context() -> dict[str, Any]: ), _field(_source_field_key(source_key, "LOGO_BASE_PATH"), "Папка логотипов", source.get("logo_base_path", ""), wide=True), _field(_source_field_key(source_key, "PHOTO_BASE_PATH"), "Папка фотографий", source.get("photo_base_path", ""), wide=True), + _field(_source_field_key(source_key, "CHANNEL_LOGO_BASE_PATH"), "Папка логотипов каналов", source.get("channel_logo_base_path", ""), wide=True), _field(_source_field_key(source_key, "TEAMS_URL"), "Ссылка на команды", source.get("teams_url", ""), wide=True), _field(_source_field_key(source_key, "SCHEDULE_URL"), "Ссылка на расписание", source.get("schedule_url", ""), wide=True), _field(_source_field_key(source_key, "STANDINGS_URL"), "Ссылка на турнирку", source.get("standings_url", ""), wide=True), @@ -123,6 +124,7 @@ def save_project_settings(form_values: dict[str, str]) -> list[str]: "CALENDAR_TYPE": "calendar_type", "LOGO_BASE_PATH": "logo_base_path", "PHOTO_BASE_PATH": "photo_base_path", + "CHANNEL_LOGO_BASE_PATH": "channel_logo_base_path", "TEAMS_URL": "teams_url", "SCHEDULE_URL": "schedule_url", "STANDINGS_URL": "standings_url", diff --git a/services/vmix_json_service.py b/services/vmix_json_service.py index 9d363b9..a640e53 100644 --- a/services/vmix_json_service.py +++ b/services/vmix_json_service.py @@ -460,27 +460,29 @@ def get_vmix_schedule(session_row): m.match_date, m.status, m.id, - m.channel + COALESCE(m.channel, '') AS channel, + COALESCE(NULLIF(m.source_key, ''), %s) AS source_key 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 AND (%s IS NULL OR m.season = %s) - AND COALESCE(m.source_key, %s) = %s + AND COALESCE(NULLIF(m.source_key, ''), %s) = %s ORDER BY m.match_date, m.id """ conn = get_connection() try: with conn.cursor() as cur: - cur.execute(query, (tour, season, season, source_filter, source_filter)) + cur.execute(query, (source_filter, tour, season, season, source_filter, source_filter)) rows = cur.fetchall() result = [] for row in rows: row = list(row) - row[0] = build_logo_variant_path(source_key, row[0], "white") - row[1] = build_logo_variant_path(source_key, row[1], "white") + row_source_key = row[8] if len(row) > 8 else source_key + row[0] = build_logo_variant_path(row_source_key, row[0], "white") + row[1] = build_logo_variant_path(row_source_key, row[1], "white") result.append(tuple(row)) return result finally: diff --git a/sql/006_parser_sources_channel_logo_base_path.sql b/sql/006_parser_sources_channel_logo_base_path.sql new file mode 100644 index 0000000..8578745 --- /dev/null +++ b/sql/006_parser_sources_channel_logo_base_path.sql @@ -0,0 +1,12 @@ +ALTER TABLE parser_sources +ADD COLUMN IF NOT EXISTS channel_logo_base_path TEXT; + +UPDATE parser_sources +SET channel_logo_base_path = 'D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Лого каналов' +WHERE key = 'SUPERLEAGUE' + AND (channel_logo_base_path IS NULL OR TRIM(channel_logo_base_path) = ''); + +UPDATE parser_sources +SET channel_logo_base_path = 'D:\Графика\ФУТБОЛ\ЖФЛ Кубок России 2026\Лого каналов' +WHERE key = 'RUSSIAN_CUP' + AND (channel_logo_base_path IS NULL OR TRIM(channel_logo_base_path) = ''); diff --git a/templates/admin_db_index.html b/templates/admin_db_index.html index b354f9a..b7cab79 100644 --- a/templates/admin_db_index.html +++ b/templates/admin_db_index.html @@ -542,18 +542,19 @@
Список источников хранится в базе данных и редактируется в настройках проекта.
Путь к логотипам:
Путь к фотографиям:
+
Путь к логотипам каналов:
@@ -851,6 +852,7 @@ const parserSourceSelect = document.getElementById("parserSource"); const selectedLogoBasePath = document.getElementById("selectedLogoBasePath"); const selectedPhotoBasePath = document.getElementById("selectedPhotoBasePath"); + const selectedChannelLogoBasePath = document.getElementById("selectedChannelLogoBasePath"); const openCreateAccountBtn = document.getElementById("openCreateAccountBtn"); const closeCreateAccountBtn = document.getElementById("closeCreateAccountBtn"); const accountModal = document.getElementById("accountModal"); @@ -1000,6 +1002,9 @@ if (selectedPhotoBasePath) { selectedPhotoBasePath.textContent = option?.dataset?.photoBasePath || "—"; } + if (selectedChannelLogoBasePath) { + selectedChannelLogoBasePath.textContent = option?.dataset?.channelLogoBasePath || "—"; + } } document.addEventListener("click", (event) => { diff --git a/vMix.zip b/vMix.zip index d8a0fbb..de9c0d2 100644 Binary files a/vMix.zip and b/vMix.zip differ