diff --git a/get_data.py b/get_data.py index 350a354..ae93666 100644 --- a/get_data.py +++ b/get_data.py @@ -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", "") @@ -3839,8 +3846,8 @@ async def last_5_games(): # 🆕 формируем английскую строку dt = best["dt"] weekday_en = WEEKDAYS_EN[dt.weekday()] # monday..sunday - month_en = MONTHS_EN[dt.month - 1] # january..december - day = dt.day # 1..31 + month_en = MONTHS_EN[dt.month - 1] # january..december + day = dt.day # 1..31 place_en = "home" if best["place"] == "home" else "away" formatted = ( @@ -3850,9 +3857,9 @@ async def last_5_games(): return { "opponent": best["opp"], "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" + "place": best["place"], # "home" / "away" + "place_ru": place_ru, # "дома" / "в гостях" + "formatted": formatted, # "wednesday, march 26, at home against astana" } # последние 5 игр и результаты @@ -4031,6 +4038,8 @@ async def commentary(): Игрок PTS REB + OREB + DREB AST STL BLK @@ -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 @@ -4089,6 +4100,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(): {pts} {reb_val} + {oreb_val} + {dreb_val} {ast} {stl} {blk} @@ -4139,7 +4156,7 @@ async def commentary(): # Если вдруг ни одного игрока не прошло фильтр – покажем заглушку if not rows_html: rows_html.append( - 'Нет данных по игрокам.' + 'Нет данных по игрокам.' ) 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; + }} @@ -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 += "

" + safe(p.NameGFX || p.name) + "

"; + + 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 += "

" + + safe(p.NameGFX || p.name) + + (p.flag ? ("") : "") + + "

"; + + if (metaParts.length) {{ + html += "
" + safe(metaParts.join(" · ")) + "
"; + }} + html += "

Сезон / карьера — компактно

"; + html += ""; html += ""; 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 += "
"; + + // левая часть: текст + html += "
"; + html += "
"; + html += " " + safe((p && (p.NameGFX || p.name)) || "") + ""; + if (p && p.flag) {{ + var title = safe(p.country || "").replace(/\"/g, """); + html += " "; + }} + html += "
"; + html += " "; + html += "
"; + + // правая часть: фото из p.photo_site (если есть) + if (p && p.photo_site) {{ + html += "
"; + html += " " + safe((p && (p.NameGFX || p.name)) || \"\") + ""; + html += "
"; + }} + + html += "
"; + return html; +}} + + var html = ""; + + html += "

Сравнение игроков

"; + html += "
"; + html += renderPlayerHeader(leftP || {{}}); + html += renderPlayerHeader(rightP || {{}}); + html += "
"; + + // --- СТАРАЯ таблица: 4 колонки слева + 4 справа --- + html += "
"; + + // шапка таблицы + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + + html += ""; + + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + + html += ""; + + // строки таблицы — одна строка = один показатель + for (var i = 0; i < base.length; i++) {{ + var L = LM[i] || {{}}; + var R = RM[i] || {{}}; + + html += ""; + + // левый + html += ""; + html += ""; + html += ""; + html += ""; + + // показатель + html += ""; + + // правый + html += ""; + html += ""; + html += ""; + html += ""; + + html += ""; + }} + + html += "
Левый игрокПравый игрок
Сезон (сред.)Сезон (тотал)Карьера (сред.)Карьера (тотал)ПоказательСезон (сред.)Сезон (тотал)Карьера (сред.)Карьера (тотал)
" + safe(L.sAvg) + "" + safe(L.sTot) + "" + safe(L.cAvg) + "" + safe(L.cTot) + "" + safe(base[i].label || "") + "" + safe(R.sAvg) + "" + safe(R.sTot) + "" + safe(R.cAvg) + "" + safe(R.cTot) + "
"; + + return html; + }} + + // переотрисовка блока с деталями/сравнением + function rerenderDetails() {{ + if (selectedLeft || selectedRight) {{ + // Всегда рисуем вертикальную сравнительную таблицу. + // Если один из игроков не выбран — его половина будет пустой. + details.innerHTML = renderPlayersComparison(selectedLeft, selectedRight); + }} else {{ + details.innerHTML = '
Кликни по фамилии игрока, чтобы показать сезон/карьеру.
'; + }} + }} + + // выбор/снятие выбора игрока 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 {{ @@ -4775,7 +5136,7 @@ async def commentary(): }} }} catch (e) {{}} }} - + // === автообновление статистики без перезагрузки страницы === function updateTeamTable(teamKey, newPlayers) {{ var map = {{}}; @@ -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"] + [2, "pts"], + [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]; - + 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); - }} - }} catch (e) {{}} - }} + + + + +function refreshSelected() {{ + // если никого не выбрали – нечего обновлять + if (!selectedLeft && !selectedRight) {{ + return; + }} + + 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); }}); @@ -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);