diff --git a/repositories/match_coach_repository.py b/repositories/match_coach_repository.py index 5d4d04c..bda6c86 100644 --- a/repositories/match_coach_repository.py +++ b/repositories/match_coach_repository.py @@ -15,7 +15,7 @@ def get_match_coaches_grouped( mc.side, mc.coach_id, COALESCE(c.player, c.name, '') AS coach_name, - COALESCE(mc.role, c.amplua, '') AS role + COALESCE(NULLIF(c.amplua, ''), mc.role, '') AS role FROM match_coaches mc JOIN coaches c ON c.id = mc.coach_id diff --git a/repositories/match_lineup_repository.py b/repositories/match_lineup_repository.py index 5ca029f..cedaee9 100644 --- a/repositories/match_lineup_repository.py +++ b/repositories/match_lineup_repository.py @@ -108,7 +108,7 @@ def get_match_lineup_for_editor( mc.sort_order, c.id AS coach_id, COALESCE(c.player, '') AS coach_name, - COALESCE(mc.role, c.amplua, '') AS role + COALESCE(NULLIF(c.amplua, ''), mc.role, '') AS role FROM match_coaches mc JOIN coaches c ON c.id = mc.coach_id @@ -259,7 +259,7 @@ def get_match_lineup_for_vmix( mc.sort_order, c.id AS coach_id, COALESCE(c.player, '') AS coach_name, - COALESCE(mc.role, c.amplua, '') AS role + COALESCE(NULLIF(c.amplua, ''), mc.role, '') AS role FROM match_coaches mc JOIN coaches c ON c.id = mc.coach_id diff --git a/scheduler.py b/scheduler.py index c52aa0f..4b9ab90 100644 --- a/scheduler.py +++ b/scheduler.py @@ -19,13 +19,13 @@ TZ = ZoneInfo("Europe/Moscow") MATCH_START_LEAD_MINUTES = 1 # Как часто обновлять live-матч -LIVE_MATCH_POLL_SECONDS = 60 +LIVE_MATCH_POLL_SECONDS = 30 # Как часто проверять матчи дня MATCHES_LOOP_SECONDS = 60 # Как часто обновлять турнирную таблицу -STANDINGS_LOOP_SECONDS = 60 +STANDINGS_LOOP_SECONDS = 30 # Через сколько секунд между попытками после ошибки в worker WORKER_ERROR_RETRY_SECONDS = 30 @@ -202,47 +202,25 @@ def parse_score(value: str) -> int | None: def fetch_match_live_data(match_id: int) -> dict: - """ - Получение live-данных матча с сайта. - - На вход получаешь ID матча, например: - { - "id": 123, - "external_id": "1069999", - "match_date": datetime(...), - "status": "scheduled", - "home_score": None, - "away_score": None, - "tour": "1 тур", - "season": "2025/2026", - "place": "...", - "date_raw": "...", - "score_add": None, - "home_team_id": 10, - "away_team_id": 11, - } - - Должна вернуть dict минимум такого вида: - { - "status": "scheduled" | "live" | "finished", - "home_score": int | None, - "away_score": int | None, - } - - Можно вернуть и больше полей, но scheduler использует только эти. - """ - html = fetch_html(f"https://wfl.rfs.ru/match/{match_id}") soup = BeautifulSoup(html, "html.parser") - live = soup.find("section", class_="game game--future game--live js-game-live-label game--shadow game--progress") - if live: - scores = soup.find("div", class_="score__container").find_all("div", class_="score__item") - score1 = parse_score(scores[0].text) if len(scores) > 0 else None - score2 = parse_score(scores[2].text) if len(scores) > 2 else None - return {"status": "live", "home_score": score1, "away_score": score2} - - raise NotImplementedError("Implement fetch_match_live_data(match_id) by yourself") + score_box = soup.find("div", class_="score__container") + if score_box: + scores = score_box.find_all("div", class_="score__item") + home = parse_score(scores[0].text) if len(scores) > 0 else None + away = parse_score(scores[2].text) if len(scores) > 2 else None + else: + home = None + away = None + + live = soup.find("section", class_=lambda c: c and "game--live" in c) + if live: + return {"status": "live", "home_score": home, "away_score": away} + elif not live and home and away: + return {"status": "finished", "home_score": home, "away_score": away} + + return {"status": "scheduled", "home_score": home, "away_score": away} # ========================================================= # ЛОГИКА ЗАПУСКА WATCHER'ОВ diff --git a/static/script.js b/static/script.js index b895fa5..961d693 100644 --- a/static/script.js +++ b/static/script.js @@ -29,8 +29,8 @@ refereeEditorState.pool = refereeEditorState.pool.map(normalizeReferee); const REFEREE_ROLE_OPTIONS = [ "Главный судья", - "Ассистент 1", - "Ассистент 2", + "Ассистент судьи №1", + "Ассистент судьи №2", "Резервный судья", "VAR", "AVAR", @@ -2285,9 +2285,57 @@ function convertPitchYToPan(y) { } +// function buildVmixCommandsFromFormation(side) { +// const players = Array.isArray(formationState[side]) ? formationState[side] : []; +// const inputName = getFormationVmixInputName(side); +// const commands = []; +// const playerPrefix = side === "home" ? "HOME" : "AWAY"; + +// if (players.length < 2) { +// return []; +// } + +// // Сбрасываем только свою сторону +// for (let i = 1; i <= 10; i++) { +// commands.push( +// vmixCmd(`Function=ResetInput&Input=${encodeURIComponent(`${playerPrefix} - Игрок${i}`)}`) +// ); +// } + +// commands.push( +// vmixCmd(`Function=ResetInput&Input=${encodeURIComponent(inputName)}`) +// ); + +// // Вратарь players[0], полевые players[1..10] +// players.slice(1, 11).forEach((player, index) => { +// const layer = index + 1; +// const x = convertPitchXToPan(player.x).toFixed(3); +// const y = convertPitchYToPan(player.y).toFixed(3); + +// commands.push( +// vmixCmd(`Function=SetLayer${layer}PanX&Input=${encodeURIComponent(inputName)}&Value=${x}`) +// ); +// commands.push( +// vmixCmd(`Function=SetLayer${layer}PanX&Input=${encodeURIComponent(inputName)}&Value=${x}`) +// ); + +// commands.push( +// vmixCmd(`Function=SetLayer${layer}PanY&Input=${encodeURIComponent(inputName)}&Value=${y}`) +// ); +// commands.push( +// vmixCmd(`Function=SetLayer${layer}PanY&Input=${encodeURIComponent(inputName)}&Value=${y}`) +// ); +// }); + +// commands.push( +// vmixCmd(`Function=PreviewInput&Input=${encodeURIComponent(inputName)}`) +// ); + +// return commands; +// } + function buildVmixCommandsFromFormation(side) { const players = Array.isArray(formationState[side]) ? formationState[side] : []; - const inputName = getFormationVmixInputName(side); const commands = []; const playerPrefix = side === "home" ? "HOME" : "AWAY"; @@ -2295,40 +2343,26 @@ function buildVmixCommandsFromFormation(side) { return []; } - // Сбрасываем только свою сторону - for (let i = 1; i <= 10; i++) { - commands.push( - vmixCmd(`Function=ResetInput&Input=${encodeURIComponent(`${playerPrefix} - Игрок${i}`)}`) - ); - } - - commands.push( - vmixCmd(`Function=ResetInput&Input=${encodeURIComponent(inputName)}`) - ); - // Вратарь players[0], полевые players[1..10] players.slice(1, 11).forEach((player, index) => { - const layer = index + 1; + const playerNumber = index + 1; + const playerInputName = `${playerPrefix} - Игрок${playerNumber}`; + const x = convertPitchXToPan(player.x).toFixed(3); const y = convertPitchYToPan(player.y).toFixed(3); commands.push( - vmixCmd(`Function=SetLayer${layer}PanX&Input=${encodeURIComponent(inputName)}&Value=${x}`) + vmixCmd(`Function=SetPanX&Input=${encodeURIComponent(playerInputName)}&Value=${x}`) ); commands.push( - vmixCmd(`Function=SetLayer${layer}PanY&Input=${encodeURIComponent(inputName)}&Value=${y}`) + vmixCmd(`Function=SetPanY&Input=${encodeURIComponent(playerInputName)}&Value=${y}`) ); }); - commands.push( - vmixCmd(`Function=PreviewInput&Input=${encodeURIComponent(inputName)}`) - ); - return commands; } - async function sendFormationToVmix(side) { syncFormationCaptainsInState(side); @@ -3406,6 +3440,10 @@ async function loadSquadEditorData() { window.MATCH_DATA.homeCoachPool = Array.isArray(data.home_coach_pool) ? data.home_coach_pool : []; window.MATCH_DATA.awayCoachPool = Array.isArray(data.away_coach_pool) ? data.away_coach_pool : []; + if (typeof renderLiveCoachBlocks === "function") { + renderLiveCoachBlocks(); +} + return true; } catch (e) { console.warn("Ошибка загрузки squad editor data", e); diff --git a/vmix.zip b/vmix.zip new file mode 100644 index 0000000..1fde110 Binary files /dev/null and b/vmix.zip differ