фиксики1
This commit is contained in:
@@ -15,7 +15,7 @@ def get_match_coaches_grouped(
|
|||||||
mc.side,
|
mc.side,
|
||||||
mc.coach_id,
|
mc.coach_id,
|
||||||
COALESCE(c.player, c.name, '') AS coach_name,
|
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
|
FROM match_coaches mc
|
||||||
JOIN coaches c
|
JOIN coaches c
|
||||||
ON c.id = mc.coach_id
|
ON c.id = mc.coach_id
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ def get_match_lineup_for_editor(
|
|||||||
mc.sort_order,
|
mc.sort_order,
|
||||||
c.id AS coach_id,
|
c.id AS coach_id,
|
||||||
COALESCE(c.player, '') AS coach_name,
|
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
|
FROM match_coaches mc
|
||||||
JOIN coaches c
|
JOIN coaches c
|
||||||
ON c.id = mc.coach_id
|
ON c.id = mc.coach_id
|
||||||
@@ -259,7 +259,7 @@ def get_match_lineup_for_vmix(
|
|||||||
mc.sort_order,
|
mc.sort_order,
|
||||||
c.id AS coach_id,
|
c.id AS coach_id,
|
||||||
COALESCE(c.player, '') AS coach_name,
|
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
|
FROM match_coaches mc
|
||||||
JOIN coaches c
|
JOIN coaches c
|
||||||
ON c.id = mc.coach_id
|
ON c.id = mc.coach_id
|
||||||
|
|||||||
56
scheduler.py
56
scheduler.py
@@ -19,13 +19,13 @@ TZ = ZoneInfo("Europe/Moscow")
|
|||||||
MATCH_START_LEAD_MINUTES = 1
|
MATCH_START_LEAD_MINUTES = 1
|
||||||
|
|
||||||
# Как часто обновлять live-матч
|
# Как часто обновлять live-матч
|
||||||
LIVE_MATCH_POLL_SECONDS = 60
|
LIVE_MATCH_POLL_SECONDS = 30
|
||||||
|
|
||||||
# Как часто проверять матчи дня
|
# Как часто проверять матчи дня
|
||||||
MATCHES_LOOP_SECONDS = 60
|
MATCHES_LOOP_SECONDS = 60
|
||||||
|
|
||||||
# Как часто обновлять турнирную таблицу
|
# Как часто обновлять турнирную таблицу
|
||||||
STANDINGS_LOOP_SECONDS = 60
|
STANDINGS_LOOP_SECONDS = 30
|
||||||
|
|
||||||
# Через сколько секунд между попытками после ошибки в worker
|
# Через сколько секунд между попытками после ошибки в worker
|
||||||
WORKER_ERROR_RETRY_SECONDS = 30
|
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:
|
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}")
|
html = fetch_html(f"https://wfl.rfs.ru/match/{match_id}")
|
||||||
soup = BeautifulSoup(html, "html.parser")
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
live = soup.find("section", class_="game game--future game--live js-game-live-label game--shadow game--progress")
|
|
||||||
|
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:
|
if live:
|
||||||
scores = soup.find("div", class_="score__container").find_all("div", class_="score__item")
|
return {"status": "live", "home_score": home, "away_score": away}
|
||||||
score1 = parse_score(scores[0].text) if len(scores) > 0 else None
|
elif not live and home and away:
|
||||||
score2 = parse_score(scores[2].text) if len(scores) > 2 else None
|
return {"status": "finished", "home_score": home, "away_score": away}
|
||||||
return {"status": "live", "home_score": score1, "away_score": score2}
|
|
||||||
|
|
||||||
raise NotImplementedError("Implement fetch_match_live_data(match_id) by yourself")
|
|
||||||
|
|
||||||
|
return {"status": "scheduled", "home_score": home, "away_score": away}
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# ЛОГИКА ЗАПУСКА WATCHER'ОВ
|
# ЛОГИКА ЗАПУСКА WATCHER'ОВ
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ refereeEditorState.pool = refereeEditorState.pool.map(normalizeReferee);
|
|||||||
|
|
||||||
const REFEREE_ROLE_OPTIONS = [
|
const REFEREE_ROLE_OPTIONS = [
|
||||||
"Главный судья",
|
"Главный судья",
|
||||||
"Ассистент 1",
|
"Ассистент судьи №1",
|
||||||
"Ассистент 2",
|
"Ассистент судьи №2",
|
||||||
"Резервный судья",
|
"Резервный судья",
|
||||||
"VAR",
|
"VAR",
|
||||||
"AVAR",
|
"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) {
|
function buildVmixCommandsFromFormation(side) {
|
||||||
const players = Array.isArray(formationState[side]) ? formationState[side] : [];
|
const players = Array.isArray(formationState[side]) ? formationState[side] : [];
|
||||||
const inputName = getFormationVmixInputName(side);
|
|
||||||
const commands = [];
|
const commands = [];
|
||||||
const playerPrefix = side === "home" ? "HOME" : "AWAY";
|
const playerPrefix = side === "home" ? "HOME" : "AWAY";
|
||||||
|
|
||||||
@@ -2295,40 +2343,26 @@ function buildVmixCommandsFromFormation(side) {
|
|||||||
return [];
|
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[0], полевые players[1..10]
|
||||||
players.slice(1, 11).forEach((player, index) => {
|
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 x = convertPitchXToPan(player.x).toFixed(3);
|
||||||
const y = convertPitchYToPan(player.y).toFixed(3);
|
const y = convertPitchYToPan(player.y).toFixed(3);
|
||||||
|
|
||||||
commands.push(
|
commands.push(
|
||||||
vmixCmd(`Function=SetLayer${layer}PanX&Input=${encodeURIComponent(inputName)}&Value=${x}`)
|
vmixCmd(`Function=SetPanX&Input=${encodeURIComponent(playerInputName)}&Value=${x}`)
|
||||||
);
|
);
|
||||||
|
|
||||||
commands.push(
|
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;
|
return commands;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function sendFormationToVmix(side) {
|
async function sendFormationToVmix(side) {
|
||||||
syncFormationCaptainsInState(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.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 : [];
|
window.MATCH_DATA.awayCoachPool = Array.isArray(data.away_coach_pool) ? data.away_coach_pool : [];
|
||||||
|
|
||||||
|
if (typeof renderLiveCoachBlocks === "function") {
|
||||||
|
renderLiveCoachBlocks();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Ошибка загрузки squad editor data", e);
|
console.warn("Ошибка загрузки squad editor data", e);
|
||||||
|
|||||||
Reference in New Issue
Block a user