This commit is contained in:
NotSoToothless
2026-06-18 21:04:40 -07:00
committed by GitHub
parent 2f81b8c816
commit 60e91fb4e9
2 changed files with 97 additions and 0 deletions
+96
View File
@@ -33,6 +33,8 @@ _bot: Optional[discord.Client] = None
_sent_channels_by_session: dict[str, set[int]] = {} _sent_channels_by_session: dict[str, set[int]] = {}
# session_id -> lock guarding the one-time PNG render. # session_id -> lock guarding the one-time PNG render.
_render_locks: dict[str, asyncio.Lock] = {} _render_locks: dict[str, asyncio.Lock] = {}
# Cap concurrent MP4 renders (CPU/memory heavy) across all "Generate Video" clicks.
_video_render_sem: asyncio.Semaphore = asyncio.Semaphore(2)
def set_bot(bot: discord.Client) -> None: def set_bot(bot: discord.Client) -> None:
@@ -160,6 +162,14 @@ def build_tss_scoreboard_view(session_id: str) -> discord.ui.View:
url=f"https://tss.pawjob.us/games/{session_id}", emoji="🌐", url=f"https://tss.pawjob.us/games/{session_id}", emoji="🌐",
)) ))
video_btn = discord.ui.Button(label="Generate Video", style=discord.ButtonStyle.blurple, emoji="🎬")
async def _video_cb(interaction: discord.Interaction) -> None:
await handle_view_video(interaction, session_id)
video_btn.callback = _video_cb
view.add_item(video_btn)
chat_log, battle_log = _load_match_logs(session_id) chat_log, battle_log = _load_match_logs(session_id)
battle_btn = discord.ui.Button(label="Battle Log", style=discord.ButtonStyle.green) battle_btn = discord.ui.Button(label="Battle Log", style=discord.ButtonStyle.green)
@@ -184,6 +194,92 @@ def build_tss_scoreboard_view(session_id: str) -> discord.ui.View:
return view return view
def _find_replay_data_path(session_id: str) -> Optional[Path]:
"""Locate a session's stored replay file, preferring the gzipped form."""
session_dir = REPLAYS_TSS_DIR / session_id
for name in ("replay_data.json.gz", "replay_data.json"):
candidate = session_dir / name
if candidate.is_file():
return candidate
return None
async def handle_view_video(interaction: discord.Interaction, session_id: str) -> None:
"""Callback for 'Generate Video' — render the replay to MP4 (once, cached) and
send it ephemerally, falling back to the website link if it can't be uploaded."""
web_url = f"https://tss.pawjob.us/games/{session_id}"
try:
try:
await interaction.response.defer(thinking=True, ephemeral=True)
except discord.HTTPException:
return
replay_path = _find_replay_data_path(session_id)
if replay_path is None:
await interaction.followup.send("No replay data is available for this match.", ephemeral=True)
return
video_path = REPLAYS_TSS_DIR / session_id / "replay_video.mp4"
# Render once and cache on disk; serve the cached file on later clicks.
if not video_path.exists() or video_path.stat().st_size == 0:
if _video_render_sem.locked():
await interaction.followup.send(
"Too many videos are rendering right now — try again in a moment.",
ephemeral=True,
)
return
from .render_replay import load_gob_file, render_gob
def _generate() -> None:
d = load_gob_file(replay_path)
render_gob(d, video_path)
try:
log.info("[TSS-AUTOLOG] video render start %s", session_id)
async with _video_render_sem:
await asyncio.get_event_loop().run_in_executor(None, _generate)
log.info("[TSS-AUTOLOG] video render done %s", session_id)
except Exception as e: # noqa: BLE001 - report any render failure to the user
log.exception("[TSS-AUTOLOG] video render failed %s", session_id)
if video_path.exists():
video_path.unlink(missing_ok=True) # don't cache a broken/partial file
await interaction.followup.send(f"Video generation failed: {str(e)[:1800]}", ephemeral=True)
return
if not video_path.exists() or video_path.stat().st_size == 0:
await interaction.followup.send("Video generation produced no output.", ephemeral=True)
return
guild = interaction.guild
max_size = guild.filesize_limit if guild else 25 * 1_048_576
if video_path.stat().st_size > max_size:
await interaction.followup.send(
f"The rendered video is too large to upload here. Watch it on the website instead: {web_url}",
ephemeral=True,
)
return
try:
await interaction.followup.send(
content=f"Replay video for `{session_id}`. Interactive replay: {web_url}",
file=discord.File(video_path),
ephemeral=True,
)
except discord.HTTPException:
await interaction.followup.send(
f"Couldn't upload the video here. Watch it on the website instead: {web_url}",
ephemeral=True,
)
except Exception: # noqa: BLE001 - never let a button callback raise unhandled
log.exception("[TSS-AUTOLOG] unexpected error in handle_view_video %s", session_id)
try:
await interaction.followup.send("Something went wrong generating the video.", ephemeral=True)
except discord.HTTPException:
pass
async def _render_scoreboard(game: dict[str, Any], session_id: str, bar_color: str, lang_column: str) -> Optional[Path]: async def _render_scoreboard(game: dict[str, Any], session_id: str, bar_color: str, lang_column: str) -> Optional[Path]:
"""Render (once, cached on disk) the scoreboard for a session/color/language.""" """Render (once, cached on disk) the scoreboard for a session/color/language."""
model = transform.build_scoreboard_model(game, lang_column) model = transform.build_scoreboard_model(game, lang_column)
+1
View File
@@ -0,0 +1 @@
FOR TSSBOT WEBSITE, READ ~/GitHub/tssbot.web OR on server ~/tssbot.web, clippi is fucking annoying with this shit