предварительный коммит shotmap
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,4 +2,5 @@
|
|||||||
/TestJson
|
/TestJson
|
||||||
/logs/*
|
/logs/*
|
||||||
*.venv
|
*.venv
|
||||||
*.env
|
*.env
|
||||||
|
/shotmaps/*
|
||||||
183
get_data.py
183
get_data.py
@@ -16,6 +16,10 @@ import nasio
|
|||||||
import io, os, platform, time
|
import io, os, platform, time
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
import re
|
import re
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
import base64
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
@@ -87,6 +91,18 @@ SYNO_URL = os.getenv("SYNO_URL")
|
|||||||
SYNO_USERNAME = os.getenv("SYNO_USERNAME")
|
SYNO_USERNAME = os.getenv("SYNO_USERNAME")
|
||||||
SYNO_PASSWORD = os.getenv("SYNO_PASSWORD")
|
SYNO_PASSWORD = os.getenv("SYNO_PASSWORD")
|
||||||
SYNO_PATH_VMIX = os.getenv("SYNO_PATH_VMIX")
|
SYNO_PATH_VMIX = os.getenv("SYNO_PATH_VMIX")
|
||||||
|
SYNO_FONT_PATH = os.getenv("SYNO_FONT_PATH")
|
||||||
|
|
||||||
|
|
||||||
|
SHOTMAP_SUBDIR = "shotmaps"
|
||||||
|
# SHOTMAP_DIR = (
|
||||||
|
# os.path.join(SYNO_PATH_VMIX, SHOTMAP_SUBDIR)
|
||||||
|
# if SYNO_PATH_VMIX
|
||||||
|
# else os.path.join(os.getcwd(), SHOTMAP_SUBDIR)
|
||||||
|
# )
|
||||||
|
# os.makedirs(SHOTMAP_DIR, exist_ok=True)
|
||||||
|
SHOTMAP_DIR = os.path.join(os.getcwd(), "shotmaps")
|
||||||
|
os.makedirs(SHOTMAP_DIR, exist_ok=True)
|
||||||
|
|
||||||
CALENDAR = None
|
CALENDAR = None
|
||||||
|
|
||||||
@@ -602,14 +618,10 @@ def results_consumer():
|
|||||||
and GAME_START_DT.date() == datetime.now().date()
|
and GAME_START_DT.date() == datetime.now().date()
|
||||||
):
|
):
|
||||||
globals()["STATUS"] = "finished_wait"
|
globals()["STATUS"] = "finished_wait"
|
||||||
globals()[
|
globals()["CLEAR_OUTPUT_FOR_VMIX"] = False
|
||||||
"CLEAR_OUTPUT_FOR_VMIX"
|
|
||||||
] = False # 👈 включаем режим "пустых" данных
|
|
||||||
else:
|
else:
|
||||||
globals()["STATUS"] = "finished_wait"
|
globals()["STATUS"] = "finished_wait"
|
||||||
globals()[
|
globals()["CLEAR_OUTPUT_FOR_VMIX"] = False
|
||||||
"CLEAR_OUTPUT_FOR_VMIX"
|
|
||||||
] = False # 👈 включаем режим "пустых" данных
|
|
||||||
|
|
||||||
human_time = datetime.fromtimestamp(switch_at).strftime(
|
human_time = datetime.fromtimestamp(switch_at).strftime(
|
||||||
"%H:%M:%S"
|
"%H:%M:%S"
|
||||||
@@ -2238,6 +2250,8 @@ async def team(who: str):
|
|||||||
"Career_TPlayedTime": format_time(car_T_sec),
|
"Career_TPlayedTime": format_time(car_T_sec),
|
||||||
"Career_TGameCount": car_T_gms,
|
"Career_TGameCount": car_T_gms,
|
||||||
"Career_TStartCount": car_T_starts,
|
"Career_TStartCount": car_T_starts,
|
||||||
|
"startNum": stats.get("startNum"),
|
||||||
|
"photoShotMapGFX": "",
|
||||||
}
|
}
|
||||||
team_rows.append(row)
|
team_rows.append(row)
|
||||||
|
|
||||||
@@ -2295,6 +2309,59 @@ async def team(who: str):
|
|||||||
key=lambda x: role_priority.get(x.get("startRole", 99), 99),
|
key=lambda x: role_priority.get(x.get("startRole", 99), 99),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# --- 👇 ДОБАВЛЯЕМ КАРТЫ БРОСКОВ ПО startNum --- #
|
||||||
|
# Берём play-by-play из текущего game, если он есть
|
||||||
|
plays = result.get("plays") or []
|
||||||
|
|
||||||
|
# Собираем множество startNum текущей команды, чтобы не ловить чужих игроков
|
||||||
|
team_startnums = {
|
||||||
|
p.get("startNum")
|
||||||
|
for p in payload.get("starts", [])
|
||||||
|
if p.get("startRole") == "Player"
|
||||||
|
}
|
||||||
|
|
||||||
|
# startNum -> список (x, y, is_made)
|
||||||
|
shots_by_startnum: dict[int, list[tuple[float, float, bool]]] = {}
|
||||||
|
|
||||||
|
for ev in plays:
|
||||||
|
play_code = ev.get("play")
|
||||||
|
if play_code not in (2, 3, 5, 6):
|
||||||
|
continue
|
||||||
|
|
||||||
|
sn = ev.get("startNum")
|
||||||
|
if sn not in team_startnums:
|
||||||
|
continue
|
||||||
|
|
||||||
|
x = ev.get("x")
|
||||||
|
y = ev.get("y")
|
||||||
|
if x is None or y is None:
|
||||||
|
continue
|
||||||
|
sec = ev.get("sec")
|
||||||
|
period = ev.get("period")
|
||||||
|
|
||||||
|
is_made = play_code in (2, 3) # 2,3 — точные, 5,6 — промахи
|
||||||
|
shots_by_startnum.setdefault(sn, []).append((x, y, is_made, sec, period))
|
||||||
|
|
||||||
|
# Рендерим по картинке на каждого startNum
|
||||||
|
shotmap_paths: dict[int, str] = {}
|
||||||
|
for sn, points in shots_by_startnum.items():
|
||||||
|
# timestamp/версия — просто количество бросков этого игрока.
|
||||||
|
# Увеличилось кол-во бросков → поменялся путь → vMix не возьмёт картинку из кеша.
|
||||||
|
version = str(len(points))
|
||||||
|
bib = str(sn) # можно заменить на номер игрока, если нужно
|
||||||
|
|
||||||
|
path = get_image(points, bib, version)
|
||||||
|
shotmap_paths[sn] = path
|
||||||
|
|
||||||
|
# Присоединяем пути к игрокам по startNum
|
||||||
|
for row in sorted_team:
|
||||||
|
sn = row.get("startNum")
|
||||||
|
if sn in shotmap_paths:
|
||||||
|
row["photoShotMapGFX"] = f"{shotmap_paths[sn]}"
|
||||||
|
else:
|
||||||
|
# если бросков не было — оставляем пустую строку
|
||||||
|
row["photoShotMapGFX"] = ""
|
||||||
|
|
||||||
return sorted_team
|
return sorted_team
|
||||||
|
|
||||||
|
|
||||||
@@ -3245,6 +3312,110 @@ async def games_online():
|
|||||||
return todays_games
|
return todays_games
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_image(points, bib, count_point):
|
||||||
|
"""
|
||||||
|
points: список кортежей (x, y, is_made)
|
||||||
|
x, y — координаты из API, где (0,0) = центр кольца
|
||||||
|
is_made — True (play 2,3) или False (play 5,6)
|
||||||
|
bib: startNum/номер игрока
|
||||||
|
count_point: строка-версия (анти-кэш vMix)
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not points:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# # --- подложка ---
|
||||||
|
# try:
|
||||||
|
# base_image = Image.open(COURT_IMAGE_PATH).convert("RGBA")
|
||||||
|
# except Exception as e:
|
||||||
|
# logger.warning(
|
||||||
|
# f"[shotmap] не удалось открыть COURT_IMAGE_PATH={COURT_IMAGE_PATH}: {e}"
|
||||||
|
# )
|
||||||
|
base_image = Image.new("RGBA", (1500, 2800), (0, 0, 0, 0))
|
||||||
|
|
||||||
|
width, height = base_image.size # ожидаем 1500 x 2800
|
||||||
|
draw = ImageDraw.Draw(base_image)
|
||||||
|
|
||||||
|
# === ДИАПАЗОН КООРДИНАТ ИЗ API ===
|
||||||
|
COURT_WIDTH_UNITS = 150.0 # по X
|
||||||
|
COURT_LENGTH_UNITS = 280.0 # по Y
|
||||||
|
|
||||||
|
# масштаб: пиксели на 1 API-юнит
|
||||||
|
scale_x = height / COURT_LENGTH_UNITS
|
||||||
|
scale_y = width / COURT_WIDTH_UNITS
|
||||||
|
|
||||||
|
# === ЦЕНТР КОЛЬЦА В PILLOW-КООРДИНАТАХ (ОТ ВЕРХНЕГО ЛЕВОГО УГЛА) ===
|
||||||
|
HOOP_X_PX = 750
|
||||||
|
HOOP_Y_PX = 157.5
|
||||||
|
|
||||||
|
def to_px(x, y):
|
||||||
|
"""
|
||||||
|
(0,0) из API → (HOOP_X_PX, HOOP_Y_PX) на картинке.
|
||||||
|
x > 0 — вправо, y > 0 — вниз (вглубь площадки).
|
||||||
|
Если нужно, чтобы y шёл вверх — поменяй + на -.
|
||||||
|
"""
|
||||||
|
px = int(HOOP_X_PX - x * scale_x)
|
||||||
|
py = int(HOOP_Y_PX + y * scale_y)
|
||||||
|
return px, py
|
||||||
|
|
||||||
|
point_radius = 10
|
||||||
|
|
||||||
|
# try:
|
||||||
|
nasio_font = nasio.load_bio(
|
||||||
|
user=SYNO_USERNAME,
|
||||||
|
password=SYNO_PASSWORD,
|
||||||
|
nas_ip=SYNO_URL,
|
||||||
|
nas_port="443",
|
||||||
|
path=SYNO_FONT_PATH,
|
||||||
|
)
|
||||||
|
if nasio_font:
|
||||||
|
font = ImageFont.truetype(nasio_font, 18)
|
||||||
|
# except Exception:
|
||||||
|
# font = ImageFont.load_default()
|
||||||
|
|
||||||
|
for x_raw, y_raw, is_made, sec, period in points:
|
||||||
|
try:
|
||||||
|
x = float(x_raw)
|
||||||
|
y = float(y_raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
px, py = to_px(x, y)
|
||||||
|
|
||||||
|
# кружок
|
||||||
|
bbox = (
|
||||||
|
px - point_radius,
|
||||||
|
py - point_radius,
|
||||||
|
px + point_radius,
|
||||||
|
py + point_radius,
|
||||||
|
)
|
||||||
|
color = (0, 255, 0, 255) if is_made else (255, 0, 0, 255)
|
||||||
|
draw.ellipse(bbox, fill=color)
|
||||||
|
|
||||||
|
# подпись координат (для отладки)
|
||||||
|
label = f"{x:.2f}; {y:.2f}"
|
||||||
|
text_x = px + point_radius + 4
|
||||||
|
text_y = py - point_radius - 4
|
||||||
|
label = f"Q{period}"
|
||||||
|
|
||||||
|
# тень
|
||||||
|
draw.text((text_x + 1, text_y + 1), label, font=font, fill=(0, 0, 0, 255))
|
||||||
|
# текст
|
||||||
|
draw.text((text_x, text_y), label, font=font, fill=(255, 255, 255, 255))
|
||||||
|
|
||||||
|
filename = f"shots_{bib}_{count_point}.png"
|
||||||
|
path = os.path.join(SHOTMAP_DIR, filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
base_image.save(path, "PNG")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[shotmap] не удалось сохранить {path}: {e}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
"get_data:app", host="0.0.0.0", port=8000, reload=True, log_level="debug"
|
"get_data:app", host="0.0.0.0", port=8000, reload=True, log_level="debug"
|
||||||
|
|||||||
Reference in New Issue
Block a user