Implement naming modal for watch parties and temporary voice channel support with auto-cleanup
This commit is contained in:
parent
fcb03ad8c2
commit
f95c9c81b6
3 changed files with 133 additions and 11 deletions
|
|
@ -205,6 +205,49 @@ class DiscordGatewayManager:
|
|||
async def on_disconnect(self) -> None:
|
||||
self.manager.ready.clear()
|
||||
|
||||
async def on_voice_state_update(self, member: Any, before: Any, after: Any) -> None:
|
||||
if before.channel is not None and before.channel != after.channel:
|
||||
try:
|
||||
from .db import connect_db
|
||||
from .watchparty import end_watch_party_session
|
||||
|
||||
selfbot_user_id = None
|
||||
from .worker_client import worker_enabled, worker_health
|
||||
if worker_enabled():
|
||||
try:
|
||||
health = worker_health()
|
||||
selfbot_user_id = str(health.get("discordUserId") or "")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
with connect_db() as connection:
|
||||
row = connection.execute(
|
||||
"SELECT id, is_temp_channel FROM watch_party_sessions WHERE voice_channel_id = ? AND status != 'stopped'",
|
||||
(str(before.channel.id),),
|
||||
).fetchone()
|
||||
|
||||
if row is not None:
|
||||
session_id = int(row["id"])
|
||||
is_temp = bool(row["is_temp_channel"])
|
||||
|
||||
humans = [
|
||||
m for m in before.channel.members
|
||||
if not m.bot and str(m.id) != selfbot_user_id
|
||||
]
|
||||
|
||||
if len(humans) == 0:
|
||||
print(f"[on_voice_state_update] Voice channel {before.channel.name} ({before.channel.id}) is empty of humans. Ending session {session_id}.", flush=True)
|
||||
await asyncio.to_thread(end_watch_party_session, session_id)
|
||||
|
||||
if is_temp:
|
||||
print(f"[on_voice_state_update] Deleting temporary voice channel {before.channel.name} ({before.channel.id}).", flush=True)
|
||||
try:
|
||||
await before.channel.delete()
|
||||
except Exception as exc:
|
||||
print(f"Failed to delete temporary channel {before.channel.id}: {exc}", flush=True)
|
||||
except Exception as err:
|
||||
print(f"Error in on_voice_state_update handling: {err}", flush=True)
|
||||
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
self.client = GatewayClient(self)
|
||||
|
|
@ -317,20 +360,43 @@ class DiscordGatewayManager:
|
|||
await interaction.response.defer(ephemeral=True)
|
||||
await action()
|
||||
|
||||
async def do_create(interaction: Any, title: str | None = None) -> None:
|
||||
async def do_create(interaction: Any, title: str | None = None, is_temp_channel: bool = False) -> None:
|
||||
voice_channel = ensure_voice_context(interaction)
|
||||
session = session_for_voice(interaction)
|
||||
if session is None or session.get("status") in {"stopped", "error"}:
|
||||
title_str = (title or f"{voice_channel.name} Watch Party").strip()
|
||||
target_vc = voice_channel
|
||||
|
||||
if is_temp_channel:
|
||||
try:
|
||||
target_vc = await interaction.guild.create_voice_channel(
|
||||
name=title_str,
|
||||
category=voice_channel.category
|
||||
)
|
||||
members_to_move = list(voice_channel.members)
|
||||
for m in members_to_move:
|
||||
try:
|
||||
await m.move_to(target_vc)
|
||||
except Exception as exc:
|
||||
print(f"Failed to move member {m.name}: {exc}", flush=True)
|
||||
except Exception as exc:
|
||||
print(f"Failed to create temporary voice channel: {exc}", flush=True)
|
||||
target_vc = voice_channel
|
||||
is_temp_channel = False
|
||||
|
||||
payload = create_watch_party_session(
|
||||
runtime,
|
||||
guild_id=str(interaction.guild.id),
|
||||
voice_channel_id=str(voice_channel.id),
|
||||
voice_channel_id=str(target_vc.id),
|
||||
text_channel_id=str(interaction.channel_id),
|
||||
owner_user_id=str(interaction.user.id),
|
||||
title=(title or f"{voice_channel.name} Watch Party").strip(),
|
||||
title=title_str,
|
||||
is_temp_channel=is_temp_channel,
|
||||
)
|
||||
session = payload["session"]
|
||||
message = f"Created `{session['title']}` for <#{session['voiceChannelId']}>."
|
||||
if is_temp_channel:
|
||||
message += " Created a temporary voice channel and moved members."
|
||||
else:
|
||||
message = f"Using existing session `{session['title']}` for <#{session['voiceChannelId']}>."
|
||||
await respond(interaction, message)
|
||||
|
|
@ -390,12 +456,24 @@ class DiscordGatewayManager:
|
|||
|
||||
async def do_end_session(interaction: Any) -> None:
|
||||
session = ensure_session(interaction, create_if_missing=False)
|
||||
end_watch_party_session(int(session["id"]))
|
||||
session_id = int(session["id"])
|
||||
is_temp = session.get("isTempChannel", False)
|
||||
vc_id = session.get("voiceChannelId")
|
||||
|
||||
await asyncio.to_thread(end_watch_party_session, session_id)
|
||||
await respond(
|
||||
interaction,
|
||||
f"Ended the watch-party session `{session['title']}`.",
|
||||
)
|
||||
|
||||
if is_temp and vc_id:
|
||||
try:
|
||||
channel = interaction.guild.get_channel(int(vc_id))
|
||||
if channel:
|
||||
await channel.delete()
|
||||
except Exception as exc:
|
||||
print(f"Failed to delete temporary channel {vc_id}: {exc}", flush=True)
|
||||
|
||||
class WatchPartyAddModal(discord.ui.Modal, title="Add To Watch Party"):
|
||||
query = discord.ui.TextInput(label="Title", placeholder="Alien", required=True, max_length=120)
|
||||
media_type = discord.ui.TextInput(
|
||||
|
|
@ -412,6 +490,35 @@ class DiscordGatewayManager:
|
|||
except Exception as exc:
|
||||
await respond(interaction, str(exc))
|
||||
|
||||
class WatchPartyCreateModal(discord.ui.Modal, title="Create Watch Party"):
|
||||
def __init__(self, default_title: str) -> None:
|
||||
super().__init__()
|
||||
self.party_title = discord.ui.TextInput(
|
||||
label="Watch Party Name",
|
||||
placeholder="e.g. Movie Night",
|
||||
default=default_title,
|
||||
required=True,
|
||||
max_length=100
|
||||
)
|
||||
self.add_item(self.party_title)
|
||||
|
||||
self.temp_vc_opt = discord.ui.TextInput(
|
||||
label="Create temporary Voice Channel?",
|
||||
placeholder="yes or no (default: yes)",
|
||||
default="yes",
|
||||
required=True,
|
||||
max_length=5
|
||||
)
|
||||
self.add_item(self.temp_vc_opt)
|
||||
|
||||
async def on_submit(self, interaction: Any) -> None:
|
||||
try:
|
||||
title_val = str(self.party_title.value).strip()
|
||||
is_temp = str(self.temp_vc_opt.value).strip().lower() in {"yes", "y", "true", "1"}
|
||||
await defer_then(interaction, lambda: do_create(interaction, title_val, is_temp))
|
||||
except Exception as exc:
|
||||
await respond(interaction, str(exc))
|
||||
|
||||
class WatchPartyPanelView(discord.ui.View):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(timeout=None)
|
||||
|
|
@ -419,7 +526,10 @@ class DiscordGatewayManager:
|
|||
@discord.ui.button(label="Create", style=discord.ButtonStyle.primary, custom_id=WATCH_PARTY_PANEL_CREATE_ID)
|
||||
async def create_button(self, interaction: Any, _button: Any) -> None:
|
||||
try:
|
||||
await defer_then(interaction, lambda: do_create(interaction, None))
|
||||
voice_channel = ensure_voice_context(interaction)
|
||||
await interaction.response.send_modal(
|
||||
WatchPartyCreateModal(default_title=f"{voice_channel.name} Watch Party")
|
||||
)
|
||||
except Exception as exc:
|
||||
await respond(interaction, str(exc))
|
||||
|
||||
|
|
@ -486,10 +596,10 @@ class DiscordGatewayManager:
|
|||
group = discord.app_commands.Group(name="watchparty", description="Control Jellyfin watch parties in Discord")
|
||||
|
||||
@group.command(name="create", description="Create a watch-party session for your current voice channel")
|
||||
@discord.app_commands.describe(title="Optional session title")
|
||||
async def create_command(interaction: Any, title: str | None = None) -> None:
|
||||
@discord.app_commands.describe(title="Optional session title", temp_vc="Create a temporary voice channel and move members")
|
||||
async def create_command(interaction: Any, title: str | None = None, temp_vc: bool = False) -> None:
|
||||
try:
|
||||
await do_create(interaction, title)
|
||||
await do_create(interaction, title, temp_vc)
|
||||
except Exception as exc:
|
||||
await respond(interaction, str(exc))
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ class WatchPartySession:
|
|||
status: str
|
||||
worker_session_id: str
|
||||
current_queue_entry_id: int | None
|
||||
is_temp_channel: bool
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
|
|
@ -51,11 +52,18 @@ def initialize_watchparty_schema() -> None:
|
|||
status TEXT NOT NULL,
|
||||
worker_session_id TEXT NOT NULL DEFAULT '',
|
||||
current_queue_entry_id INTEGER,
|
||||
is_temp_channel INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
import sqlite3
|
||||
try:
|
||||
connection.execute("ALTER TABLE watch_party_sessions ADD COLUMN is_temp_channel INTEGER NOT NULL DEFAULT 0")
|
||||
connection.commit()
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
connection.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS watch_party_queue (
|
||||
|
|
@ -134,6 +142,7 @@ def watch_party_session_from_row(row: Any) -> WatchPartySession:
|
|||
status=str(row["status"]),
|
||||
worker_session_id=str(row["worker_session_id"] or ""),
|
||||
current_queue_entry_id=int(row["current_queue_entry_id"]) if row["current_queue_entry_id"] is not None else None,
|
||||
is_temp_channel=bool(row["is_temp_channel"]) if "is_temp_channel" in row.keys() else False,
|
||||
created_at=str(row["created_at"]),
|
||||
updated_at=str(row["updated_at"]),
|
||||
)
|
||||
|
|
@ -150,6 +159,7 @@ def session_to_jsonable(session: WatchPartySession) -> dict[str, Any]:
|
|||
"status": session.status,
|
||||
"workerSessionId": session.worker_session_id,
|
||||
"currentQueueEntryId": session.current_queue_entry_id,
|
||||
"isTempChannel": session.is_temp_channel,
|
||||
"createdAt": session.created_at,
|
||||
"updatedAt": session.updated_at,
|
||||
}
|
||||
|
|
@ -234,6 +244,7 @@ def create_watch_party_session(
|
|||
text_channel_id: str,
|
||||
owner_user_id: str,
|
||||
title: str,
|
||||
is_temp_channel: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
del runtime
|
||||
initialize_watchparty_schema()
|
||||
|
|
@ -251,11 +262,11 @@ def create_watch_party_session(
|
|||
"""
|
||||
INSERT INTO watch_party_sessions(
|
||||
guild_id, voice_channel_id, text_channel_id, owner_user_id,
|
||||
title, status, worker_session_id, current_queue_entry_id, created_at, updated_at
|
||||
title, status, worker_session_id, current_queue_entry_id, is_temp_channel, created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, 'draft', '', NULL, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, 'draft', '', NULL, ?, ?, ?)
|
||||
""",
|
||||
(guild_id.strip(), voice_channel_id.strip(), text_channel_id.strip(), owner_user_id.strip(), title.strip(), now, now),
|
||||
(guild_id.strip(), voice_channel_id.strip(), text_channel_id.strip(), owner_user_id.strip(), title.strip(), 1 if is_temp_channel else 0, now, now),
|
||||
)
|
||||
session_id = int(cursor.lastrowid)
|
||||
connection.execute(
|
||||
|
|
|
|||
|
|
@ -511,6 +511,7 @@ const server = http.createServer(async (request, response) => {
|
|||
streamingError: streamingStack.available ? "" : streamingStack.error,
|
||||
discordConfigured: Boolean(DISCORD_USER_TOKEN),
|
||||
discordReady: runtime.ready,
|
||||
discordUserId: runtime.client && runtime.client.user ? runtime.client.user.id : null,
|
||||
sessions: sessions.size,
|
||||
activeSessionId: runtime.activeSessionId,
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue