обновил поиск следующей игры для титра и поправил commentary

This commit is contained in:
2025-12-10 15:07:36 +03:00
parent 5e42fd69cd
commit d75042ca7a

View File

@@ -1,6 +1,5 @@
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import Response, HTMLResponse, StreamingResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.responses import Response, HTMLResponse, StreamingResponse
from contextlib import asynccontextmanager
import requests, uvicorn, json
import threading, queue
@@ -16,7 +15,7 @@ import nasio
import io, os, platform, time
import xml.etree.ElementTree as ET
import re
from PIL import Image, ImageDraw, ImageFont
from PIL import Image, ImageDraw
from io import BytesIO
import warnings
@@ -2263,6 +2262,8 @@ async def team(who: str):
)
+ ".svg"
),
"country": (item.get("countryName") or "").strip(),
"photo_site": (item.get("photo") or "").strip(),
"photoGFX": (
os.path.join(
"D:\\Photos",
@@ -3766,6 +3767,7 @@ async def last_5_games():
return wl_list[::-1]
# ищем СЛЕДУЮЩИЙ матч (ближайший в будущем) для команды
# ищем СЛЕДУЮЩИЙ матч (ближайший в будущем) для команды
def find_next_game(team_id: int):
if not CALENDAR or "items" not in CALENDAR:
@@ -3806,6 +3808,11 @@ async def last_5_games():
if team_id not in (t1.get("teamId"), t2.get("teamId")):
continue
# 👇 пропускаем ТЕКУЩИЙ матч (тот, что в GAME_ID)
gid = game_info.get("id")
if gid is not None and GAME_ID is not None and str(gid) == str(GAME_ID):
continue
dt_str = game_info.get("defaultZoneDateTime") or ""
try:
dt = datetime.fromisoformat(dt_str)
@@ -3815,11 +3822,11 @@ async def last_5_games():
# убираем tzinfo, чтобы можно было сравнивать с now
if dt.tzinfo is not None:
dt = dt.replace(tzinfo=None)
# только будущие матчи
# только матчи ПОСЛЕ текущего момента
if dt <= now:
print(dt, now)
continue
print(item)
# определяем соперника и место
if team_id == t1.get("teamId"):
opp_name = t2.get("name", "")
@@ -3852,7 +3859,7 @@ async def last_5_games():
"date": best["dt"].strftime("%Y-%m-%d %H:%M"),
"place": best["place"], # "home" / "away"
"place_ru": place_ru, # "дома" / "в гостях"
"formatted": formatted, # 🆕 "wednesday, march 26, at home against astana"
"formatted": formatted, # "wednesday, march 26, at home against astana"
}
# последние 5 игр и результаты
@@ -4031,6 +4038,8 @@ async def commentary():
<th>Игрок</th>
<th>PTS</th>
<th>REB</th>
<th class="extra-col">OREB</th>
<th class="extra-col">DREB</th>
<th>AST</th>
<th>STL</th>
<th>BLK</th>
@@ -4069,9 +4078,11 @@ async def commentary():
# REB: если нет 'reb', то dreb + oreb
reb = p.get("reb")
dreb_raw = p.get("dreb")
oreb_raw = p.get("oreb")
if reb in ("", None):
dreb = p.get("dreb") or 0
oreb = p.get("oreb") or 0
dreb = dreb_raw or 0
oreb = oreb_raw or 0
reb_val = dreb + oreb
else:
reb_val = reb
@@ -4090,6 +4101,10 @@ async def commentary():
foul_raw = p.get("foul")
foul_str = "" if foul_raw is None else str(foul_raw)
# значения для отдельных колонок
oreb_val = "" if oreb_raw in (None, "") else oreb_raw
dreb_val = "" if dreb_raw in (None, "") else dreb_raw
plus_minus = p.get("plusMinus") or ""
kpi = p.get("kpi") or ""
@@ -4119,6 +4134,8 @@ async def commentary():
</td>
<td>{pts}</td>
<td>{reb_val}</td>
<td class="extra-col">{oreb_val}</td>
<td class="extra-col">{dreb_val}</td>
<td>{ast}</td>
<td>{stl}</td>
<td>{blk}</td>
@@ -4139,7 +4156,7 @@ async def commentary():
# Если вдруг ни одного игрока не прошло фильтр покажем заглушку
if not rows_html:
rows_html.append(
'<tr><td colspan="16" class="meta">Нет данных по игрокам.</td></tr>'
'<tr><td colspan="18" class="meta">Нет данных по игрокам.</td></tr>'
)
table_footer = """
@@ -4324,6 +4341,19 @@ async def commentary():
cursor: pointer;
text-decoration: underline;
}}
.player-name.selected-player-left,
.player-name.selected-player-right {{
text-decoration: none;
font-weight: 600;
background: rgba(59,130,246,0.12);
border-radius: 4px;
padding: 0 4px;
}}
.player-name.selected-player-right {{
background: rgba(16,185,129,0.12); /* можно оставить одинаковый, если не хочется различать */
}}
.extra-col {{
display: none; /* по умолчанию скрыто */
}}
@@ -4337,6 +4367,75 @@ async def commentary():
#player-details table {{
margin-top: 5px;
}}
.compare-header {{
display: flex;
gap: 16px;
justify-content: space-between;
flex-wrap: wrap;
margin-bottom: 10px;
}}
/* делаем блок игрока flex-контейнером: слева текст, справа фото */
.compare-player-block {{
flex: 1 1 0;
padding: 8px 10px;
border-radius: 10px;
background: #141414;
border: 1px solid #333;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
}}
/* левая часть — текст (имя, амплуа и т.п.) */
.compare-player-info {{
flex: 1 1 auto;
min-width: 0;
}}
/* правая часть — фото */
.compare-player-photo {{
flex: 0 0 auto;
width: 72px;
height: 72px;
border-radius: 8px;
overflow: hidden;
background: #111;
border: 1px solid #333;
}}
.compare-player-photo img {{
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}}
.compare-name-row {{
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
}}
.compare-name {{
font-size: 16px;
font-weight: 600;
}}
.compare-flag {{
width: 20px;
height: 14px;
border-radius: 2px;
object-fit: cover;
box-shadow: 0 0 0 1px rgba(0,0,0,0.4);
}}
.compare-meta-list {{
list-style: none;
padding: 0;
margin: 0;
font-size: 12px;
color: #ccc;
line-height: 1.4;
}}
</style>
</head>
<body>
@@ -4528,16 +4627,39 @@ async def commentary():
["team1_table", "team2_table"].forEach(function(tableId) {{
var table = document.getElementById(tableId);
if (!table) return;
var tbody = table.querySelector("tbody");
if (!tbody) return;
// Находим индексы нужных колонок по заголовкам
var headers = table.querySelectorAll("thead th");
var ptsIndex = -1;
var foulIndex = -1;
headers.forEach(function(th, idx) {{
var text = (th.innerText || th.textContent || "").trim().toLowerCase();
if (text === "pts") {{
ptsIndex = idx;
}}
// если вдруг подпишешь по-русски — можно добавить варианты
if (text === "foul" || text === "фол" || text === "фолы") {{
foulIndex = idx;
}}
}});
if (ptsIndex < 0 || foulIndex < 0) {{
// не нашли нужные колонки тихо выходим
return;
}}
Array.from(tbody.rows).forEach(function(row) {{
var cells = row.cells;
if (!cells || cells.length < 14) return;
if (!cells) return;
if (cells.length <= ptsIndex || cells.length <= foulIndex) return;
// индексы: PTS = 2, Foul = 13
var ptsCell = cells[2];
var foulCell = cells[13];
var ptsCell = cells[ptsIndex];
var foulCell = cells[foulIndex];
if (!ptsCell || !foulCell) return;
// --- 1) зелёная подсветка при изменении PTS / Foul ---
@@ -4609,7 +4731,7 @@ async def commentary():
}});
}})();
// === клик по игроку + компактные таблицы сезон/карьера ===
// === клик по игроку + компактные таблицы сезон/карьера + сравнение левого/правого ===
(function() {{
var details = document.getElementById("player-details");
if (!details) return;
@@ -4620,6 +4742,8 @@ async def commentary():
function buildMetrics(p) {{
var seasonAvg = [
["Игры", p.TGameCount],
["В старте", p.TStartCount],
["Очки", p.AvgPoints],
["Передачи", p.AvgAssist],
["Подборы", p.AvgRebound],
@@ -4635,6 +4759,8 @@ async def commentary():
["С игры, %", p.Shot23Percent]
];
var seasonTot = [
["Игры", p.TGameCount],
["В старте", p.TStartCount],
["Очки", p.TPoints],
["Передачи", p.TAssist],
["Подборы", p.TRebound],
@@ -4650,6 +4776,8 @@ async def commentary():
["С игры (goal/shot)", p.TShots23]
];
var careerAvg = [
["Игры", p.Career_TGameCount],
["В старте", p.Career_TStartCount],
["Очки", p.Career_AvgPoints],
["Передачи", p.Career_AvgAssist],
["Подборы", p.Career_AvgRebound],
@@ -4665,6 +4793,8 @@ async def commentary():
["С игры, %", p.Career_Shot23Percent]
];
var careerTot = [
["Игры", p.Career_TGameCount],
["В старте", p.Career_TStartCount],
["Очки", p.Career_TPoints],
["Передачи", p.Career_TAssist],
["Подборы", p.Career_TRebound],
@@ -4694,12 +4824,32 @@ async def commentary():
return metrics;
}}
// выбранные игроки слева/справа
var selectedLeft = null;
var selectedRight = null;
// одиночное отображение (как было раньше) — НЕ трогаем таблицу, тут всё ок
function renderPlayerDetails(p) {{
if (!p) return "";
var metrics = buildMetrics(p);
var html = "";
html += "<h2>" + safe(p.NameGFX || p.name) + "</h2>";
var metaParts = [];
if (p.age) metaParts.push(p.age + " лет");
if (p.height) metaParts.push(p.height.height || p.height);
if (p.weight) metaParts.push(p.weight);
html += "<h2 style='display:flex;align-items:center;gap:6px;'>"
+ safe(p.NameGFX || p.name)
+ (p.flag ? ("<img src='" + p.flag + "' style='width:22px;height:16px;border-radius:3px;'>") : "")
+ "</h2>";
if (metaParts.length) {{
html += "<div class='player-meta'>" + safe(metaParts.join(" · ")) + "</div>";
}}
html += "<h3>Сезон / карьера — компактно</h3>";
html += "<table><thead><tr>";
html += "<th></th>";
for (var j = 0; j < metrics.length; j++) {{
@@ -4735,6 +4885,156 @@ async def commentary():
return html;
}}
// === Сравнение игроков: шапка + СТАРАЯ вертикальная таблица с отдельными колонками ===
function renderPlayersComparison(leftP, rightP) {{
// если вообще никого нет — ничего не показываем
if (!leftP && !rightP) {{
return "";
}}
// --- метрики ---
var LM = leftP ? buildMetrics(leftP) : null;
var RM = rightP ? buildMetrics(rightP) : null;
// базовый набор строк (под один из игроков)
var base = LM || RM || [];
// если одного игрока нет — делаем для него пустой набор метрик
function makeEmptyMetrics(from) {{
return from.map(function (m) {{
return {{
label: m.label,
sAvg: "",
sTot: "",
cAvg: "",
cTot: ""
}};
}});
}}
if (!LM) LM = makeEmptyMetrics(base);
if (!RM) RM = makeEmptyMetrics(base);
function metaList(p) {{
if (!p) return [];
var arr = [];
if (p.roleFull || p.role) arr.push("Амплуа: " + (p.roleFull || p.role));
if (p.age) arr.push("Возраст: " + p.age);
if (p.height) arr.push("Рост: " + p.height);
if (p.weight) arr.push("Вес: " + p.weight);
if (p.teamName) arr.push("Команда: " + p.teamName);
return arr;
}}
function renderPlayerHeader(p) {{
var html = "";
html += "<div class='compare-player-block'>";
// левая часть: текст
html += " <div class='compare-player-info'>";
html += " <div class='compare-name-row'>";
html += " <span class='compare-name'>" + safe((p && (p.NameGFX || p.name)) || "") + "</span>";
if (p && p.flag) {{
var title = safe(p.country || "").replace(/\"/g, "&quot;");
html += " <img src='" + p.flag + "' class='compare-flag' title='" + title + "'>";
}}
html += " </div>";
html += " <ul class='compare-meta-list'>";
metaList(p || {{}}).forEach(function (txt) {{
html += " <li>" + safe(txt) + "</li>";
}});
html += " </ul>";
html += " </div>";
// правая часть: фото из p.photo_site (если есть)
if (p && p.photo_site) {{
html += " <div class='compare-player-photo'>";
html += " <img src='" + safe(p.photo_site) + "' alt='" + safe((p && (p.NameGFX || p.name)) || \"\") + "'>";
html += " </div>";
}}
html += "</div>";
return html;
}}
var html = "";
html += "<h2>Сравнение игроков</h2>";
html += "<div class='compare-header'>";
html += renderPlayerHeader(leftP || {{}});
html += renderPlayerHeader(rightP || {{}});
html += "</div>";
// --- СТАРАЯ таблица: 4 колонки слева + 4 справа ---
html += "<table class='details-wide-table' style='width:100%;border-collapse:collapse;'>";
// шапка таблицы
html += "<thead>";
html += "<tr>";
html += "<th colspan='4' style='text-align:center;border-bottom:2px solid #333;'>Левый игрок</th>";
html += "<th></th>";
html += "<th colspan='4' style='text-align:center;border-bottom:2px solid #333;'>Правый игрок</th>";
html += "</tr>";
html += "<tr>";
html += "<th>Сезон (сред.)</th>";
html += "<th>Сезон (тотал)</th>";
html += "<th>Карьера (сред.)</th>";
html += "<th>Карьера (тотал)</th>";
html += "<th>Показатель</th>";
html += "<th>Сезон (сред.)</th>";
html += "<th>Сезон (тотал)</th>";
html += "<th>Карьера (сред.)</th>";
html += "<th>Карьера (тотал)</th>";
html += "</tr>";
html += "</thead>";
html += "<tbody>";
// строки таблицы — одна строка = один показатель
for (var i = 0; i < base.length; i++) {{
var L = LM[i] || {{}};
var R = RM[i] || {{}};
html += "<tr>";
// левый
html += "<td style='text-align:center;'>" + safe(L.sAvg) + "</td>";
html += "<td style='text-align:center;'>" + safe(L.sTot) + "</td>";
html += "<td style='text-align:center;'>" + safe(L.cAvg) + "</td>";
html += "<td style='text-align:center;'>" + safe(L.cTot) + "</td>";
// показатель
html += "<td style='text-align:center;font-weight:600;'>" + safe(base[i].label || "") + "</td>";
// правый
html += "<td style='text-align:center;'>" + safe(R.sAvg) + "</td>";
html += "<td style='text-align:center;'>" + safe(R.sTot) + "</td>";
html += "<td style='text-align:center;'>" + safe(R.cAvg) + "</td>";
html += "<td style='text-align:center;'>" + safe(R.cTot) + "</td>";
html += "</tr>";
}}
html += "</tbody></table>";
return html;
}}
// переотрисовка блока с деталями/сравнением
function rerenderDetails() {{
if (selectedLeft || selectedRight) {{
// Всегда рисуем вертикальную сравнительную таблицу.
// Если один из игроков не выбран — его половина будет пустой.
details.innerHTML = renderPlayersComparison(selectedLeft, selectedRight);
}} else {{
details.innerHTML = '<div class="meta">Кликни по фамилии игрока, чтобы показать сезон/карьеру.</div>';
}}
}}
// выбор/снятие выбора игрока
function selectPlayer(teamKey, pid, noScroll, noSave) {{
var data = window.PLAYER_DATA || {{}};
var list = data[teamKey] || [];
@@ -4745,16 +5045,77 @@ async def commentary():
break;
}}
}}
// если игрок не найден в данных — просто выходим
if (!found) return;
details.innerHTML = renderPlayerDetails(found);
var pidStr = String(pid || "");
if (teamKey === "team1") {{
// повторный клик по тому же игроку слева — снимаем выбор
if (selectedLeft && String(selectedLeft.id || "") === pidStr) {{
selectedLeft = null;
}} else {{
selectedLeft = found;
}}
}} else if (teamKey === "team2") {{
// повторный клик по тому же игроку справа — снимаем выбор
if (selectedRight && String(selectedRight.id || "") === pidStr) {{
selectedRight = null;
}} else {{
selectedRight = found;
}}
}} else {{
// запасной вариант — считаем как левую сторону
if (selectedLeft && String(selectedLeft.id || "") === pidStr) {{
selectedLeft = null;
}} else {{
selectedLeft = found;
}}
}}
// подсветка выбранных фамилий
var allCells = document.querySelectorAll(".player-name");
allCells.forEach(function (cell) {{
cell.classList.remove("selected-player-left", "selected-player-right");
var cPid = cell.getAttribute("data-player-id");
var cTeam = cell.getAttribute("data-team");
if (selectedLeft && cTeam === "team1" &&
String(selectedLeft.id || "") === String(cPid || "")) {{
cell.classList.add("selected-player-left");
}}
if (selectedRight && cTeam === "team2" &&
String(selectedRight.id || "") === String(cPid || "")) {{
cell.classList.add("selected-player-right");
}}
}});
rerenderDetails();
if (!noSave) {{
localStorage.setItem("commentary_last_player", JSON.stringify({{ team: teamKey, id: pid }}));
try {{
if (selectedLeft || selectedRight) {{
// запоминаем последнего кликнутого
localStorage.setItem(
"commentary_last_player",
JSON.stringify({{ team: teamKey, id: pid }})
);
}} else {{
// если вообще никого не осталось выбранным — чистим
localStorage.removeItem("commentary_last_player");
}}
}} catch (e) {{}}
}}
if (!noScroll) {{
details.scrollIntoView({{ behavior: "smooth", block: "start" }});
}}
}}
window.selectPlayer = selectPlayer;
// клики по фамилиям
var cells = document.querySelectorAll(".player-name");
for (var i = 0; i < cells.length; i++) {{
@@ -4765,7 +5126,7 @@ async def commentary():
}});
}}
// восстановить последнего выбранного
// восстановить последнего выбранного (если был)
var saved = localStorage.getItem("commentary_last_player");
if (saved) {{
try {{
@@ -4788,9 +5149,10 @@ async def commentary():
return (v === undefined || v === null) ? "" : v;
}}
// все ячейки с фамилиями для нужной команды
var cells = document.querySelectorAll('.player-name[data-team="' + teamKey + '"]');
cells.forEach(function(cell) {{
cells.forEach(function (cell) {{
var pid = cell.getAttribute("data-player-id");
var newP = map[String(pid)];
if (!newP) return;
@@ -4798,42 +5160,64 @@ async def commentary():
var row = cell.parentElement;
var tds = row.children;
// обновляем data-on-court
// обновляем data-on-court (для раскраски строк)
var onCourt = !!newP.isOnCourt;
row.dataset.onCourt = onCourt ? "true" : "false";
// Порядок колонок в таблице:
// 0 #
// 1 Игрок
// 2 PTS
// 3 REB
// 4 AST
// 5 STL
// 6 BLK
// 7 MIN
// 8 OREB (extra-col)
// 9 DREB (extra-col)
// 10 FG (extra-col)
// 11 2PT (extra-col)
// 12 3PT (extra-col)
// 13 1PT (extra-col)
// 14 TO (extra-col)
// 15 Foul
// 16 +/-
// 17 KPI
var fields = [
[2, "pts"],
[3, "reb"],
[4, "ast"],
[5, "stl"],
[6, "blk"],
[7, "time"],
[8, "fg"],
[9, "pt-2"],
[10, "pt-3"],
[11, "pt-1"],
[12, "to"],
[13, "foul"],
[14, "plusMinus"],
[15, "kpi"]
[3, "reb"], // пересчёт ниже
[4, "oreb"],
[5, "dreb"],
[6, "ast"],
[7, "stl"],
[8, "blk"],
[9, "time"],
[10, "fg"],
[11, "pt-2"],
[12, "pt-3"],
[13, "pt-1"],
[14, "to"],
[15, "foul"],
[16, "plusMinus"],
[17, "kpi"]
];
// REB: если нет newP.reb, считаем как dreb + oreb
var newReb = newP.reb;
if (newReb === undefined) {{
newReb = (newP.dreb || 0) + (newP.oreb || 0);
if (newReb === undefined || newReb === null || newReb === "") {{
var dreb = newP.dreb || 0;
var oreb = newP.oreb || 0;
newReb = dreb + oreb;
}}
function applyUpdate(td, newVal) {{
newVal = safe(newVal);
td.textContent = newVal;
td.textContent = safe(newVal);
}}
fields.forEach(function(item) {{
fields.forEach(function (item) {{
var index = item[0];
var key = item[1];
var td = tds[index];
if (!td) return;
if (key === "reb") {{
@@ -4845,16 +5229,45 @@ async def commentary():
}});
}}
function refreshSelected() {{
var saved = localStorage.getItem("commentary_last_player");
if (!saved) return;
try {{
var obj = JSON.parse(saved);
if (obj && obj.team && obj.id) {{
selectPlayer(obj.team, obj.id, true, true);
function refreshSelected() {{
// если никого не выбрали нечего обновлять
if (!selectedLeft && !selectedRight) {{
return;
}}
}} catch (e) {{}}
var data = window.PLAYER_DATA || {{}};
var team1 = Array.isArray(data.team1) ? data.team1 : [];
var team2 = Array.isArray(data.team2) ? data.team2 : [];
// обновляем ссылку на объект для левого игрока (team1)
if (selectedLeft) {{
var leftId = String(selectedLeft.id || "");
var updatedLeft = team1.find(function (p) {{
return String(p.id || "") === leftId;
}});
if (updatedLeft) {{
selectedLeft = updatedLeft;
}}
}}
// обновляем ссылку на объект для правого игрока (team2)
if (selectedRight) {{
var rightId = String(selectedRight.id || "");
var updatedRight = team2.find(function (p) {{
return String(p.id || "") === rightId;
}});
if (updatedRight) {{
selectedRight = updatedRight;
}}
}}
// просто перерисовываем блок сравнения, без скролла и без записи в localStorage
rerenderDetails();
}}
setInterval(function() {{
fetch('/team1').then(function(r) {{
@@ -4884,8 +5297,9 @@ async def commentary():
// 🔁 обновляем шапку и четверти
updateHeaderFromLiveStatus(); // счёт + фолы из live_status
updateQuartersFromScores(); // счёт по четвертям из /scores
updateTimeoutsFromTeamStats(); // тайм-ауты обновление
}}, 1000);
}}, 1500);
updateTimeoutsFromTeamStats(); // тайм-ауты строго из team_stats.timeout_left
}})();
@@ -5002,7 +5416,7 @@ async def commentary():
}}
// запустим автообновление счёта и четвертей
updateScoreAndQuarters();
setInterval(updateScoreAndQuarters, 1000);
setInterval(updateScoreAndQuarters, 1500);
makeTableSortable("team1_table");
makeTableSortable("team2_table");
@@ -5010,7 +5424,7 @@ async def commentary():
// первый старт стилей сразу
updateRowStyles();
// и периодически обновляем (на случай любых внешних изменений)
setInterval(updateRowStyles, 1000);
setInterval(updateRowStyles, 1500);
}});
</script>
</body>
@@ -5563,6 +5977,18 @@ async def milestones_ui():
align-items: center;
gap: 6px;
}
.player-flag {
width: 22px;
height: 16px;
object-fit: cover;
border-radius: 3px;
}
.player-meta,
.player-meta-inline {
font-size: 11px;
color: var(--text-muted);
}
label {
font-size: 12px;
color: var(--text-muted);