diff --git a/get_data.py b/get_data.py
index 44f3a8c..86990a7 100644
--- a/get_data.py
+++ b/get_data.py
@@ -3922,6 +3922,56 @@ async def commentary():
or "—"
)
+ # тайм-ауты (пытаемся аккуратно вытащить, если есть в live-status)
+ home_side = (
+ ls_dict.get("home")
+ or ls_dict.get("team1")
+ or ls_dict.get("homeTeam")
+ or {}
+ )
+ away_side = (
+ ls_dict.get("away")
+ or ls_dict.get("team2")
+ or ls_dict.get("awayTeam")
+ or {}
+ )
+
+ def pick_from(side: dict, top: dict, side_keys, top_keys) -> str:
+ """Берём первое непустое значение из side или из ls_dict."""
+ if isinstance(side, dict):
+ for k in side_keys:
+ v = side.get(k)
+ if v not in (None, "", " "):
+ return str(v)
+ if isinstance(top, dict):
+ for k in top_keys:
+ v = top.get(k)
+ if v not in (None, "", " "):
+ return str(v)
+ return "—"
+ ts = result.get("team_stats", {}) or {}
+
+ timeout_block = ts.get("timeout_left", {}) or {}
+
+ # val1 и val2 — это именно значения для команд 1 и 2
+ timeouts1 = timeout_block.get("val1")
+ timeouts2 = timeout_block.get("val2")
+
+ # если пусто — ставим дефис
+ timeouts1 = str(timeouts1) if timeouts1 not in (None, "") else "—"
+ timeouts2 = str(timeouts2) if timeouts2 not in (None, "") else "—"
+ fouls1 = pick_from(
+ home_side,
+ ls_dict,
+ ["fouls", "fouls1", "foulsA", "teamFouls", "team1Fouls"],
+ ["fouls1", "foulsA", "homeFouls", "team1Fouls"],
+ )
+ fouls2 = pick_from(
+ away_side,
+ ls_dict,
+ ["fouls", "fouls2", "foulsB", "teamFouls", "team2Fouls"],
+ ["fouls2", "foulsB", "awayFouls", "team2Fouls"],
+ )
# счёт по четвертям
quarters = ["Q1", "Q2", "Q3", "Q4", "OT1", "OT2", "OT3", "OT4"]
score_rows = []
@@ -3931,18 +3981,51 @@ async def commentary():
parts = fs_list[i].split(":")
s1, s2 = (parts + ["", ""])[:2]
score_rows.append((q, s1, s2))
+ labels = []
+ for i in range(len(fs_list)):
+ if i < 4:
+ labels.append(f"Q{i+1}")
+ else:
+ labels.append(f"OT{i-3}")
- # как в /team1 и /team2
+ score_rows = []
+ for label, item in zip(labels, fs_list):
+ parts = item.split(":")
+ s1, s2 = (parts + ["", ""])[:2]
+ score_rows.append((label, s1, s2))
+
+ quarters_html = "".join(
+ f'
{q} '
+ f'{s1} : {s2}
'
+ for (q, s1, s2) in score_rows
+ if s1.strip() != "" and s2.strip() != ""
+ )
+
+ # данные по командам, как в /team1 и /team2
team1_players = await team("team1")
team2_players = await team("team2")
team1_json = json.dumps(team1_players, ensure_ascii=False)
team2_json = json.dumps(team2_players, ensure_ascii=False)
- def render_players_table(players, title, team_key: str) -> str:
- header = """
-
-
+ def render_players_table(players, title: str, team_key: str) -> str:
+ """
+ Рендер таблицы игроков для commentary.
+
+ - Показываем только игроков с startRole == "Player"
+ - Пропускаем строки без имени
+ - Корректно считаем REB (если нет 'reb', используем dreb + oreb)
+ - Вешаем data-on-court для дальнейшей подсветки в JS
+ - Для Foul добавляем классы foul-yellow / foul-red (4 и 5+)
+ - Колонки после MIN помечены .extra-col (прячутся галочкой)
+ """
+
+ # шапка таблицы
+ table_header = f"""
+ {title}
+
+
+
#
Игрок
PTS
@@ -3959,68 +4042,113 @@ async def commentary():
-
+
+
+
"""
- rows = []
- for p in players:
- # только игроки
- if p.get("startRole") not in ("Player", ""):
+
+ rows_html: list[str] = []
+
+ for p in players or []:
+ # 1) Только реальные игроки
+ if p.get("startRole") != "Player":
continue
- num = p.get("num", "")
- pid = p.get("id", "")
- name = p.get("NameGFX") or p.get("name", "")
- pts = p.get("pts", "")
- reb = p.get("reb", (p.get("dreb", 0) or 0) + (p.get("oreb", 0) or 0))
- ast = p.get("ast", "")
- stl = p.get("stl", "")
- blk = p.get("blk", "")
- time_played = p.get("time", "")
+ # 2) Имя (если пустое — пропускаем)
+ name = (p.get("NameGFX") or p.get("name") or "").strip()
+ if not name:
+ continue
- fg = p.get("fg", "")
- pt2 = p.get("pt-2", "")
- pt3 = p.get("pt-3", "")
- pt1 = p.get("pt-1", "")
- to = p.get("to", "")
- foul = p.get("foul", "")
- plus_minus = p.get("plusMinus", "")
- kpi = p.get("kpi", "")
+ # 3) Базовые поля
+ num = p.get("num") or ""
+ pid = p.get("id") or ""
- rows.append(
- f"""
-
- {num}
-
- {name}
-
- {pts}
- {reb}
- {ast}
- {stl}
- {blk}
- {time_played}
-
-
-
-
-
-
-
-
-
+ pts = p.get("pts")
+ pts = "" if pts is None else pts
+
+ # REB: если нет 'reb', то dreb + oreb
+ reb = p.get("reb")
+ if reb in ("", None):
+ dreb = p.get("dreb") or 0
+ oreb = p.get("oreb") or 0
+ reb_val = dreb + oreb
+ else:
+ reb_val = reb
+
+ ast = p.get("ast") or ""
+ stl = p.get("stl") or ""
+ blk = p.get("blk") or ""
+ time_played = p.get("time") or ""
+
+ fg = p.get("fg") or ""
+ pt2 = p.get("pt-2") or ""
+ pt3 = p.get("pt-3") or ""
+ pt1 = p.get("pt-1") or ""
+ to = p.get("to") or ""
+
+ foul_raw = p.get("foul")
+ foul_str = "" if foul_raw is None else str(foul_raw)
+
+ plus_minus = p.get("plusMinus") or ""
+ kpi = p.get("kpi") or ""
+
+ # 4) классы по фолам
+ foul_class = ""
+ try:
+ foul_int = int(foul_str)
+ except (TypeError, ValueError):
+ foul_int = None
+
+ if foul_int == 4:
+ foul_class = "foul-yellow"
+ elif foul_int is not None and foul_int >= 5:
+ foul_class = "foul-red"
+
+ # 5) флаг isOnCourt → в data-атрибут (цвет строки считает JS)
+ on_court = bool(p.get("isOnCourt"))
+ row_attrs = f' data-on-court="{str(on_court).lower()}"'
+
+ row_html = f"""
+
+ {num}
+
+ {name}
+
+ {pts}
+ {reb_val}
+ {ast}
+ {stl}
+ {blk}
+ {time_played}
+
+
+
+
+
+
+
+
+
"""
+
+ rows_html.append(row_html)
+
+ # Если вдруг ни одного игрока не прошло фильтр – покажем заглушку
+ if not rows_html:
+ rows_html.append(
+ 'Нет данных по игрокам. '
)
- return f"""
- {title}
- {header}
- {''.join(rows)}
+ table_footer = """
+
"""
- # (pbp можно добавить сюда, я его пока опустил, чтобы не раздувать код)
+ return table_header + "".join(rows_html) + table_footer
+
+ # пока без PBP, при необходимости подставишь сюда pbp_html
pbp_html = ""
game_time_str = GAME_START_DT.strftime("%d.%m.%Y %H:%M") if GAME_START_DT else "N/A"
@@ -4030,8 +4158,41 @@ async def commentary():
Комментаторский дашборд
-
- {team1_name} vs {team2_name}
-
- Счёт: {score_now or "—"} • Статус: {live_status} • Начало: {game_time_str}
-
+
@@ -4145,32 +4395,220 @@ async def commentary():
team1: {team1_json},
team2: {team2_json}
}};
+
+ // === обновление счёта / фолов / тайм-аутов из /live_status ===
+ function updateHeaderFromLiveStatus() {{
+ fetch('/live_status')
+ .then(function (r) {{ return r.json(); }})
+ .then(function (data) {{
+ if (!Array.isArray(data) || !data.length) return;
+ var s = data[0] || {{}};
- // === ГАЛОЧКА "показать всю статистику" с localStorage ===
- (function() {{
- var checkbox = document.getElementById("toggle-all-stats");
- function apply() {{
+ function pick() {{
+ for (var i = 0; i < arguments.length; i++) {{
+ var v = arguments[i];
+ if (v !== undefined && v !== null && v !== "") {{
+ return v;
+ }}
+ }}
+ return null;
+ }}
+
+ var scoreA = pick(s.scoreA, s.score1, s.homeScore, s.team1Score);
+ var scoreB = pick(s.scoreB, s.score2, s.awayScore, s.team2Score);
+ var elScore = document.getElementById('live-score');
+ if (elScore && scoreA !== null && scoreB !== null) {{
+ elScore.textContent = scoreA + " : " + scoreB;
+ }}
+
+ var fouls1 = pick(s.foulsA, s.fouls1, s.homeFouls, s.team1Fouls);
+ var fouls2 = pick(s.foulsB, s.fouls2, s.awayFouls, s.team2Fouls);
+ var elF1 = document.getElementById('fouls-team1');
+ var elF2 = document.getElementById('fouls-team2');
+ if (elF1 && fouls1 !== null) elF1.textContent = fouls1;
+ if (elF2 && fouls2 !== null) elF2.textContent = fouls2;
+
+ var t1 = pick(s.timeoutsA, s.timeouts1, s.homeTimeouts, s.team1Timeouts);
+ var t2 = pick(s.timeoutsB, s.timeouts2, s.awayTimeouts, s.team2Timeouts);
+ if (elT1 && t1 !== null) elT1.textContent = t1;
+ if (elT2 && t2 !== null) elT2.textContent = t2;
+ }})
+ .catch(function () {{ }});
+ }}
+
+ // === обновление тайм-аутов именно из /team_stats (строка name="timeout_left") ===
+ function updateTimeoutsFromTeamStats() {{
+ fetch("/team_stats")
+ .then(function (r) {{ return r.json(); }})
+ .then(function (data) {{
+ if (!Array.isArray(data)) return;
+
+ var row = null;
+ for (var i = 0; i < data.length; i++) {{
+ var item = data[i];
+ if (item && item.name === "timeout_left") {{
+ row = item;
+ break;
+ }}
+ }}
+ if (!row) return;
+
+ var t1 = row.val1;
+ var t2 = row.val2;
+
+ var elT1 = document.getElementById("timeouts-team1");
+ var elT2 = document.getElementById("timeouts-team2");
+
+ if (elT1) {{
+ elT1.textContent =
+ (t1 !== undefined && t1 !== null && t1 !== "") ? t1 : "—";
+ }}
+ if (elT2) {{
+ elT2.textContent =
+ (t2 !== undefined && t2 !== null && t2 !== "") ? t2 : "—";
+ }}
+ }})
+ .catch(function () {{ }});
+ }}
+
+
+ // === обновление счёта по четвертям из /scores ===
+ function updateQuartersFromScores() {{
+ var container = document.getElementById('quarters-strip');
+ if (!container) return;
+
+ fetch('/scores')
+ .then(function (r) {{ return r.json(); }})
+ .then(function (data) {{
+ if (!Array.isArray(data)) return;
+
+ var html = [];
+
+ data.forEach(function (row) {{
+ if (!row) return;
+ var q = row.Q || row.q || "";
+ if (!q) return;
+
+ var s1 = row.score1;
+ var s2 = row.score2;
+
+ var empty1 = (s1 === "" || s1 === null || s1 === undefined);
+ var empty2 = (s2 === "" || s2 === null || s2 === undefined);
+
+ // ❗ если оба пустые — вообще не показываем эту четверть (Q и OT одинаково)
+ if (empty1 && empty2) {{
+ return;
+ }}
+
+ s1 = empty1 ? " " : String(s1);
+ s2 = empty2 ? " " : String(s2);
+
+ html.push(
+ '' +
+ '' + q + ' ' +
+ '' + s1 + ' : ' + s2 + ' ' +
+ '
'
+ );
+ }});
+
+ if (html.length) {{
+ container.innerHTML = html.join("");
+ }} else {{
+ container.innerHTML = 'Пока нет данных по четвертям. ';
+ }}
+ }})
+ .catch(function () {{ }});
+ }}
+
+
+
+ // === функция пересчёта цветов строк и фолов + зелёная подсветка PTS/Foul ===
+ function updateRowStyles() {{
+ ["team1_table", "team2_table"].forEach(function(tableId) {{
+ var table = document.getElementById(tableId);
+ if (!table) return;
+ var tbody = table.querySelector("tbody");
+ if (!tbody) return;
+
+ Array.from(tbody.rows).forEach(function(row) {{
+ var cells = row.cells;
+ if (!cells || cells.length < 14) return;
+
+ // индексы: PTS = 2, Foul = 13
+ var ptsCell = cells[2];
+ var foulCell = cells[13];
+ if (!ptsCell || !foulCell) return;
+
+ // --- 1) зелёная подсветка при изменении PTS / Foul ---
+ [ptsCell, foulCell].forEach(function(cell) {{
+ var currentValue = cell.innerText.trim();
+ var prevValue = cell.getAttribute("data-prev-value");
+
+ if (prevValue !== null && prevValue !== currentValue) {{
+ cell.classList.add("cell-updated");
+ setTimeout(function() {{
+ cell.classList.remove("cell-updated");
+ }}, 5000);
+ }}
+
+ cell.setAttribute("data-prev-value", currentValue);
+ }});
+
+ // --- 2) подсветка строк и ячейки Foul ---
+ var foul = parseInt(foulCell.innerText.trim(), 10) || 0;
+ var onCourt = row.dataset.onCourt === "true";
+
+ row.classList.remove("row-orange", "row-gray");
+ foulCell.classList.remove("foul-yellow", "foul-red");
+
+ if (onCourt) {{
+ row.classList.add("row-orange");
+ }} else if (foul >= 5) {{
+ row.classList.add("row-gray");
+ }}
+
+ if (foul === 4) {{
+ foulCell.classList.add("foul-yellow");
+ }} else if (foul >= 5) {{
+ foulCell.classList.add("foul-red");
+ }}
+ }});
+ }});
+ }}
+
+ // === галочка "Показать всю статистику" с localStorage ===
+ (function () {{
+ function applyFullStatsColumns() {{
+ var checkbox = document.getElementById("toggle-all-stats");
var show = checkbox && checkbox.checked;
var els = document.querySelectorAll(".extra-col");
+
for (var i = 0; i < els.length; i++) {{
- // ВАЖНО: явное 'table-cell', а не пустая строка
els[i].style.display = show ? "table-cell" : "none";
}}
+
if (checkbox) {{
localStorage.setItem("commentary_full_stats", show ? "1" : "0");
}}
}}
- if (checkbox) {{
+
+ window.applyFullStatsColumns = applyFullStatsColumns;
+
+ document.addEventListener("DOMContentLoaded", function () {{
+ var checkbox = document.getElementById("toggle-all-stats");
+ if (!checkbox) return;
+
var saved = localStorage.getItem("commentary_full_stats");
if (saved === "1") {{
checkbox.checked = true;
}}
- apply();
- checkbox.addEventListener("change", apply);
- }}
+
+ applyFullStatsColumns();
+ checkbox.addEventListener("change", applyFullStatsColumns);
+ }});
}})();
- // === КЛИК ПО ИГРОКУ + уплотнённая таблица 4 строки ===
+ // === клик по игроку + компактные таблицы сезон/карьера ===
(function() {{
var details = document.getElementById("player-details");
if (!details) return;
@@ -4339,7 +4777,6 @@ async def commentary():
// === автообновление статистики без перезагрузки страницы ===
function updateTeamTable(teamKey, newPlayers) {{
- // делаем map id -> player
var map = {{}};
for (var i = 0; i < newPlayers.length; i++) {{
var p = newPlayers[i];
@@ -4350,7 +4787,6 @@ async def commentary():
return (v === undefined || v === null) ? "" : v;
}}
- // все ячейки с игроками этой команды
var cells = document.querySelectorAll('.player-name[data-team="' + teamKey + '"]');
cells.forEach(function(cell) {{
@@ -4358,13 +4794,16 @@ async def commentary():
var newP = map[String(pid)];
if (!newP) return;
- var row = cell.parentElement; //
+ var row = cell.parentElement;
var tds = row.children;
- // список полей: индекс TD -> ключ в объекте игрока
+ // обновляем data-on-court
+ var onCourt = !!newP.isOnCourt;
+ row.dataset.onCourt = onCourt ? "true" : "false";
+
var fields = [
[2, "pts"],
- [3, "reb"], // обработаем отдельно
+ [3, "reb"],
[4, "ast"],
[5, "stl"],
[6, "blk"],
@@ -4379,24 +4818,14 @@ async def commentary():
[15, "kpi"]
];
- // считаем total rebounds, если нет поля reb
var newReb = newP.reb;
if (newReb === undefined) {{
newReb = (newP.dreb || 0) + (newP.oreb || 0);
}}
- // применяем обновление + подсветку, если значение изменилось
function applyUpdate(td, newVal) {{
newVal = safe(newVal);
- var oldVal = td.textContent.trim();
- if (String(oldVal) !== String(newVal)) {{
- td.textContent = newVal;
-
- td.classList.add("flash-update");
- setTimeout(function() {{
- td.classList.remove("flash-update");
- }}, 1000);
- }}
+ td.textContent = newVal;
}}
fields.forEach(function(item) {{
@@ -4404,10 +4833,11 @@ async def commentary():
var key = item[1];
var td = tds[index];
+ if (!td) return;
+
if (key === "reb") {{
applyUpdate(td, newReb);
}} else {{
- // обращаемся к полю, включая "pt-2" / "pt-3"
applyUpdate(td, newP[key]);
}}
}});
@@ -4432,7 +4862,11 @@ async def commentary():
window.PLAYER_DATA.team1 = data;
updateTeamTable('team1', data);
refreshSelected();
- }}).catch(function() {{}});
+ updateRowStyles();
+ if (window.applyFullStatsColumns){{ {{
+ window.applyFullStatsColumns();
+ }}}}
+ }}).catch(function(){ {}});
fetch('/team2').then(function(r) {{
return r.json();
@@ -4440,537 +4874,149 @@ async def commentary():
window.PLAYER_DATA.team2 = data;
updateTeamTable('team2', data);
refreshSelected();
+ updateRowStyles();
+ if (window.applyFullStatsColumns) {{
+ window.applyFullStatsColumns();
+ }}
}}).catch(function() {{}});
+
+ // 🔁 обновляем шапку и четверти
+ updateHeaderFromLiveStatus(); // счёт + фолы из live_status
+ updateQuartersFromScores(); // счёт по четвертям из /scores
+
}}, 1000);
+ updateTimeoutsFromTeamStats(); // тайм-ауты строго из team_stats.timeout_left
+
}})();
+
+ // === сортировка таблиц по клику по заголовку (только tbody) ===
+ document.addEventListener("DOMContentLoaded", function() {{
+
+ // Обновление счёта и счёта по четвертям онлайн
+ function updateScoreAndQuarters() {{
+ fetch("/scores")
+ .then(function (resp) {{ return resp.json(); }})
+ .then(function (data) {{
+ if (!Array.isArray(data)) return;
+
+ var quartersStrip = document.getElementById("quarters-strip");
+ var scoreSpan = document.getElementById("live-score");
+ if (!quartersStrip && !scoreSpan) return;
+
+ var htmlPills = [];
+ var total1 = 0;
+ var total2 = 0;
+ var anyScores = false;
+
+ data.forEach(function (row) {{
+ if (!row) return;
+
+ var q = row.Q || row.q || "";
+ if (!q) return;
+
+ var s1_raw = row.score1;
+ var s2_raw = row.score2;
+
+ // ❗ показываем четверть ТОЛЬКО если оба значения не пустые
+ var hasBoth = s1_raw !== "" && s1_raw != null &&
+ s2_raw !== "" && s2_raw != null;
+
+ if (!hasBoth) {{
+ // и для Q1–Q4, и для OT1–OT4 — просто не рисуем эту пилюлю
+ return;
+ }}
+
+ var s1 = parseInt(s1_raw || 0, 10) || 0;
+ var s2 = parseInt(s2_raw || 0, 10) || 0;
+
+ if (s1 > 0 || s2 > 0) {{
+ total1 += s1;
+ total2 += s2;
+ anyScores = true;
+ }}
+
+ var display1 = String(s1_raw);
+ var display2 = String(s2_raw);
+
+ htmlPills.push(
+ '' +
+ '' + q + ' ' +
+ '' + display1 + ' : ' + display2 + ' ' +
+ '
'
+ );
+ }});
+
+ if (quartersStrip) {{
+ if (htmlPills.length > 0) {{
+ quartersStrip.innerHTML = htmlPills.join("");
+ }} else {{
+ quartersStrip.innerHTML = 'Пока нет данных по четвертям. ';
+ }}
+ }}
+
+ if (scoreSpan && anyScores) {{
+ scoreSpan.textContent = total1 + " : " + total2;
+ }}
+ }})
+ .catch(function (err) {{
+ if (console && console.warn) {{
+ console.warn("updateScoreAndQuarters error", err);
+ }}
+ }});
+ }}
+
+ function makeTableSortable(tableId) {{
+ var table = document.getElementById(tableId);
+ if (!table) return;
+
+ var headers = table.querySelectorAll("thead th");
+ var tbody = table.querySelector("tbody");
+ if (!tbody) return;
+
+ headers.forEach(function(header, index) {{
+ header.style.cursor = "pointer";
+ var descending = true; // первый клик — от большего к меньшему
+
+ header.addEventListener("click", function() {{
+ var rows = Array.from(tbody.rows);
+
+ rows.sort(function(a, b) {{
+ var A = a.children[index].innerText.trim();
+ var B = b.children[index].innerText.trim();
+
+ var numA = parseFloat(A.replace(",", "."));
+ var numB = parseFloat(B.replace(",", "."));
+
+ if (!isNaN(numA) && !isNaN(numB)) {{
+ return descending ? numB - numA : numA - numB;
+ }}
+
+ return descending ? B.localeCompare(A) : A.localeCompare(B);
+ }});
+
+ descending = !descending;
+ rows.forEach(function(r) {{ tbody.appendChild(r); }});
+ }});
+ }});
+ }}
+ // запустим автообновление счёта и четвертей
+ updateScoreAndQuarters();
+ setInterval(updateScoreAndQuarters, 1000);
+
+ makeTableSortable("team1_table");
+ makeTableSortable("team2_table");
+
+ // первый старт стилей сразу
+ updateRowStyles();
+ // и периодически обновляем (на случай любых внешних изменений)
+ setInterval(updateRowStyles, 1000);
+ }});