This commit is contained in:
Logan
2026-04-06 00:23:33 -04:00
parent 1a9c92b6db
commit 7de55f9885
10 changed files with 189 additions and 44 deletions
+56 -3
View File
@@ -19,7 +19,7 @@ class RadioBot:
f"http://{settings.icecast_host}:{settings.icecast_port}{settings.icecast_mount}"
)
async def join(self, guild_id: int, channel_id: int, token: str) -> bool:
async def join(self, guild_id: int, channel_id: int, token: str, call_active: bool = False) -> bool:
# (Re)start the bot if the token changed or the bot isn't running
if self._current_token != token or not self._is_bot_running():
if not await self._start_bot(token):
@@ -39,8 +39,10 @@ class RadioBot:
if self._voice_client and self._voice_client.is_connected():
await self._voice_client.disconnect(force=True)
self._voice_client = await channel.connect()
self._play_stream()
logger.info(f"Streaming to #{channel.name} in {guild.name}")
# Only start playing immediately if a call is currently active
if call_active:
self._play_stream()
logger.info(f"Joined #{channel.name} in {guild.name} (streaming={'yes' if call_active else 'waiting for call'})")
return True
except Exception as e:
logger.error(f"Failed to join voice channel: {e}")
@@ -49,6 +51,7 @@ class RadioBot:
async def leave(self) -> bool:
if self._voice_client and self._voice_client.is_connected():
try:
self._stop_stream()
await self._voice_client.disconnect(force=True)
self._voice_client = None
logger.info("Disconnected from voice channel.")
@@ -57,6 +60,19 @@ class RadioBot:
logger.error(f"Failed to disconnect: {e}")
return False
def start_stream(self):
"""Called when an OP25 call starts — begin transmitting audio and light the ring."""
if self._voice_client and self._voice_client.is_connected():
if not self._voice_client.is_playing():
self._play_stream()
logger.debug("Stream started (call active).")
def stop_stream(self):
"""Called when an OP25 call ends — stop transmitting so the ring goes dark."""
if self._voice_client and self._voice_client.is_connected():
self._stop_stream()
logger.debug("Stream stopped (call ended).")
async def stop(self):
await self.leave()
if self._task:
@@ -80,11 +96,17 @@ class RadioBot:
after=lambda e: logger.error(f"Stream ended unexpectedly: {e}") if e else None,
)
def _stop_stream(self):
if self._voice_client and self._voice_client.is_playing():
self._voice_client.stop()
async def _start_bot(self, token: str) -> bool:
await self.stop() # clean up any previous instance
intents = discord.Intents.default()
intents.voice_states = True
intents.message_content = True
intents.messages = True
self._bot = commands.Bot(command_prefix="!", intents=intents)
self._ready_event = asyncio.Event()
self._current_token = token
@@ -94,6 +116,37 @@ class RadioBot:
logger.info(f"Discord bot ready: {self._bot.user} ({self._bot.user.id})")
self._ready_event.set()
@self._bot.event
async def on_message(message: discord.Message):
if message.author.bot:
return
if self._bot.user not in message.mentions:
return
content = message.content.lower()
if "leave" in content:
await self.leave()
try:
await message.reply("Disconnected.")
except Exception:
pass
elif "joinme" in content or "join" in content:
member = message.guild.get_member(message.author.id) if message.guild else None
vc = member.voice.channel if member and member.voice else None
if not vc:
try:
await message.reply("You're not in a voice channel.")
except Exception:
pass
return
try:
if self._voice_client and self._voice_client.is_connected():
await self._voice_client.move_to(vc)
else:
self._voice_client = await vc.connect()
await message.reply(f"Joined {vc.name}.")
except Exception as e:
logger.error(f"joinme failed: {e}")
self._task = asyncio.create_task(self._bot.start(token))
try:
@@ -7,8 +7,8 @@ from app.internal.logger import logger
CallbackFn = Callable[[dict], Awaitable[None]]
HANG_THRESHOLD = 3 # polls before declaring a call ended (1 poll/sec3s hang time)
POLL_INTERVAL = 1.0 # seconds
HANG_THRESHOLD = 2 # polls before declaring a call ended (0.5s poll → 1s hang time)
POLL_INTERVAL = 0.5 # seconds
class MetadataWatcher:
+11 -2
View File
@@ -53,9 +53,18 @@ class OP25Client:
"""Poll the OP25 HTTP terminal for current call metadata."""
try:
async with httpx.AsyncClient(timeout=3) as client:
r = await client.get(f"{self.terminal_url}/0/status.json")
r = await client.post(
self.terminal_url,
json=[{"command": "update", "arg1": 0, "arg2": 0}],
)
r.raise_for_status()
return r.json()
messages = r.json()
for msg in messages:
if msg.get("json_type") == "channel_update":
channels = msg.get("channels", [])
if channels:
return msg.get(str(channels[0]), {})
return None
except Exception:
return None