From a3c48cd6512ed0d246e2da7000ee7f65d0dec0a9 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Fri, 1 Aug 2025 23:27:57 -0400 Subject: [PATCH] If it aint broke dont fix it type shit --- app/internal/NoiseGatev2.py | 271 ++++++++++++++++------ app/internal/bot_manager.py | 451 ++++++++++++++++++++++++------------ 2 files changed, 491 insertions(+), 231 deletions(-) diff --git a/app/internal/NoiseGatev2.py b/app/internal/NoiseGatev2.py index 1dc99a8..158937e 100644 --- a/app/internal/NoiseGatev2.py +++ b/app/internal/NoiseGatev2.py @@ -1,98 +1,215 @@ import audioop +import logging import math +import time + import pyaudio -import asyncio -from internal.logger import create_logger +import discord +import numpy -# You need to import the base AudioSource class from your specific library. -# This is a common path, but yours might be different. -from discord import AudioSource +voice_connection = None -LOGGER = create_logger(__name__) - -# Constants for audio processing -SAMPLES_PER_FRAME = 960 -CHANNELS = 2 -SAMPLE_RATE = 48000 -FRAME_SIZE = SAMPLES_PER_FRAME * CHANNELS * 2 # 16-bit PCM -SILENT_FRAME = b'\x00' * FRAME_SIZE +LOGGER = logging.getLogger("Discord_Radio_Bot.NoiseGateV2") -class NoiseGateSource(AudioSource): - def __init__(self, audio_stream, threshold: int): - self.audio_stream = audio_stream - self.threshold = threshold - self.ng_fadeout_count = 0 - self.NG_FADEOUT_FRAMES = 12 # 240ms fadeout time +# noinspection PyUnresolvedReferences +class AudioStream: + def __init__(self, _channels: int = 2, _sample_rate: int = 48000, _frames_per_buffer: int = 1024, + _input_device_index: int = None, _output_device_index: int = None, _input: bool = True, + _output: bool = True, _init_on_startup: bool = True): + self.paInstance_kwargs = { + 'format': pyaudio.paInt16, + 'channels': _channels, + 'rate': _sample_rate, + 'input': _input, + 'output': _output, + 'frames_per_buffer': _frames_per_buffer + } - def read(self) -> bytes: - """ - Reads data from the audio stream, applies the noise gate, - and returns a 20ms audio frame. - """ - try: - # Read a frame's worth of data from the input stream. - pcm_data = self.audio_stream.read(SAMPLES_PER_FRAME, exception_on_overflow=False) + if _input_device_index: + if _input: + self.paInstance_kwargs['input_device_index'] = _input_device_index + else: + LOGGER.warning(f"[AudioStream.__init__]:\tInput was not enabled." + f" Reinitialize with '_input=True'") - # Ensure we have a full frame of data. - if len(pcm_data) != FRAME_SIZE: - return SILENT_FRAME + if _output_device_index: + if _output: + self.paInstance_kwargs['output_device_index'] = _output_device_index + else: + LOGGER.warning(f"[AudioStream.__init__]:\tOutput was not enabled." + f" Reinitialize with '_output=True'") - # Calculate volume to check against the threshold. - rms = audioop.rms(pcm_data, 2) - if rms == 0: - # If there's no volume, check if we're in the fadeout period. - if self.ng_fadeout_count > 0: - self.ng_fadeout_count -= 1 - return pcm_data # Return the (silent) data to complete the fade - return SILENT_FRAME + if _init_on_startup: + # Init PyAudio instance + LOGGER.info("Creating PyAudio instance") + self.paInstance = pyaudio.PyAudio() - db = 20 * math.log10(rms) + # Define and initialize stream object if we have been passed a device ID (pyaudio.open) + self.stream = None - # If volume is above the threshold, send the audio and reset fadeout. - if db >= self.threshold: - self.ng_fadeout_count = self.NG_FADEOUT_FRAMES - return pcm_data + if _output_device_index or _input_device_index: + if _init_on_startup: + LOGGER.info("Init stream") + self.init_stream() - # If below threshold but still in the fadeout period, send the audio. - if self.ng_fadeout_count > 0: - self.ng_fadeout_count -= 1 - return pcm_data + def init_stream(self, _new_output_device_index: int = None, _new_input_device_index: int = None): + # Check what device was asked to be changed (or set) + if _new_input_device_index: + if self.paInstance_kwargs['input']: + self.paInstance_kwargs['input_device_index'] = _new_input_device_index + else: + LOGGER.warning(f"[AudioStream.init_stream]:\tInput was not enabled when initialized." + f" Reinitialize with '_input=True'") - # Otherwise, the gate is closed. Send silence. - return SILENT_FRAME + if _new_output_device_index: + if self.paInstance_kwargs['output']: + self.paInstance_kwargs['output_device_index'] = _new_output_device_index + else: + LOGGER.warning(f"[AudioStream.init_stream]:\tOutput was not enabled when initialized." + f" Reinitialize with '_output=True'") - except Exception as e: - LOGGER.error(f"Error in NoiseGateSource.read: {e}", exc_info=True) - return SILENT_FRAME + self.close_if_open() - def cleanup(self) -> None: - """Called when the player stops.""" - # The AudioStreamManager now handles cleanup. - LOGGER.info("Audio source cleanup called.") - pass + # Open the stream + self.stream = self.paInstance.open(**self.paInstance_kwargs) -class AudioStreamManager: - """Manages the PyAudio instance and input stream.""" - def __init__(self, input_device_index: int): - self.pa = pyaudio.PyAudio() - self.stream = self.pa.open( - format=pyaudio.paInt16, - channels=CHANNELS, - rate=SAMPLE_RATE, - input=True, - frames_per_buffer=SAMPLES_PER_FRAME, - input_device_index=input_device_index - ) + def close_if_open(self): + # Stop the stream if it is started + if self.stream: + if self.stream.is_active(): + self.stream.stop_stream() + self.stream.close() + LOGGER.debug(f"[ReopenStream.close_if_open]:\t Stream was open; It was closed.") + + def list_devices(self, _display_input_devices: bool = True, _display_output_devices: bool = True): + LOGGER.info('Getting a list of the devices connected') + info = self.paInstance.get_host_api_info_by_index(0) + numdevices = info.get('deviceCount') + + devices = { + 'Input': {}, + 'Output': {} + } + for i in range(0, numdevices): + if (self.paInstance.get_device_info_by_host_api_device_index(0, i).get('maxInputChannels')) > 0: + input_device = self.paInstance.get_device_info_by_host_api_device_index(0, i).get('name') + devices['Input'][i] = input_device + if _display_input_devices: + LOGGER.debug(f"Input Device id {i} - {input_device}") + + if (self.paInstance.get_device_info_by_host_api_device_index(0, i).get('maxOutputChannels')) > 0: + output_device = self.paInstance.get_device_info_by_host_api_device_index(0, i).get('name') + devices['Output'][i] = output_device + if _display_output_devices: + LOGGER.debug(f"Output Device id {i} - {output_device}") + + return devices + + async def stop(self): + await voice_connection.disconnect() + self.close_if_open() + self.stream.close() + self.paInstance.terminate() + + +# noinspection PyUnresolvedReferences +class NoiseGate(AudioStream): + def __init__(self, _voice_connection, _noise_gate_threshold: int, **kwargs): + super(NoiseGate, self).__init__(_init_on_startup=True, **kwargs) + global voice_connection + voice_connection = _voice_connection + self.THRESHOLD = _noise_gate_threshold + self.NGStream = NoiseGateStream(self) + self.Voice_Connection_Thread = None + + def run(self) -> None: + global voice_connection + # Start the audio stream + LOGGER.debug(f"Starting stream") self.stream.start_stream() - LOGGER.info(f"Audio stream started on device {input_device_index}") + # Start the stream to discord + self.core() - def get_stream(self): - return self.stream + def core(self, error=None): + if error: + LOGGER.warning(error) - def terminate(self): - if self.stream and self.stream.is_active(): + while not voice_connection.is_connected(): + time.sleep(.2) + + if not voice_connection.is_playing(): + LOGGER.debug(f"Playing stream to discord") + voice_connection.play(self.NGStream, after=self.core) + + async def close(self): + LOGGER.debug(f"Closing") + await voice_connection.disconnect() + if self.stream.is_active: self.stream.stop_stream() - self.stream.close() - self.pa.terminate() - LOGGER.info("PyAudio instance terminated.") \ No newline at end of file + LOGGER.debug(f"Stopping stream") + + +# noinspection PyUnresolvedReferences +class NoiseGateStream(discord.AudioSource): + def __init__(self, _stream): + super(NoiseGateStream, self).__init__() + self.stream = _stream # The actual audio stream object + self.NG_fadeout = 240/20 # Fadeout value used to hold the noisegate after de-triggering + self.NG_fadeout_count = 0 # A count set when the noisegate is triggered and was de-triggered + self.process_set_count = 0 # Counts how many processes have been made + + def read(self): + try: + while voice_connection.is_connected(): + curr_buffer = bytearray(self.stream.stream.read(960)) + buffer_rms = audioop.rms(curr_buffer, 2) + if buffer_rms > 0: + buffer_decibel = 20 * math.log10(buffer_rms) + + if self.process_set_count % 10 == 0: + if buffer_decibel >= self.stream.THRESHOLD: + LOGGER.debug(f"[Noisegate Open] {buffer_decibel} db") + else: + LOGGER.debug(f"[Noisegate Closed] {buffer_decibel} db") + + if buffer_decibel >= self.stream.THRESHOLD: + self.NG_fadeout_count = self.NG_fadeout + self.process_set_count += 1 + if curr_buffer: + return bytes(curr_buffer) + + else: + if self.NG_fadeout_count > 0: + self.NG_fadeout_count -= 1 + LOGGER.debug(f"Frames in fadeout remaining: {self.NG_fadeout_count}") + self.process_set_count += 1 + if curr_buffer: + return bytes(curr_buffer) + + except OSError as e: + LOGGER.warning(e) + pass + + def audio_datalist_set_volume(self, datalist, volume): + """ Change value of list of audio chunks """ + sound_level = (volume / 100.) + + for i in range(len(datalist)): + chunk = numpy.fromstring(datalist[i], numpy.int16) + + chunk = chunk * sound_level + + datalist[i] = chunk.astype(numpy.int16) + + +if __name__ == '__main__': + + input_index = int(input("Input:\t")) + output_index = int(input("Output:\t")) + + ng = NoiseGate(_input_device_index=input_index, _output_device_index=output_index) + + ng.list_devices() + + ng.start() diff --git a/app/internal/bot_manager.py b/app/internal/bot_manager.py index c3d2441..f9ec2a3 100644 --- a/app/internal/bot_manager.py +++ b/app/internal/bot_manager.py @@ -1,181 +1,324 @@ -import asyncio +import argparse import platform import os -from discord import VoiceClient, VoiceChannel, opus, Activity, ActivityType, Intents +import asyncio +import discord from discord.ext import commands -from typing import Optional, Dict -from internal.logger import create_logger -from internal.NoiseGatev2 import AudioStreamManager, NoiseGateSource +from NoiseGatev2 import NoiseGate # Assuming NoiseGatev2.py is in the same directory -LOGGER = create_logger(__name__) +# --- Opus Library Loading --- +def load_opus(): + """Loads the correct opus library for the operating system.""" + try: + if os.name == 'nt': + processor = platform.machine() + if processor == "AMD64": + print("Loaded OPUS library for Windows AMD64") + discord.opus.load_opus('./opus/libopus_amd64.dll') + else: + print(f"Unsupported Windows processor: {processor}. Opus may not work.") + else: + processor = platform.machine() + print(f"Processor: {processor}") + if processor == "aarch64": + print("Loaded OPUS library for aarch64") + discord.opus.load_opus('./opus/libopus_aarcch64.so') + elif processor == "armv7l": + print("Loaded OPUS library for armv7l") + discord.opus.load_opus('./opus/libopus_armv7l.so') + else: + print(f"Attempting to load a generic opus library for {processor}") + discord.opus.load_opus('libopus.so.0') -intents = Intents.default() -intents.voice_states = True -intents.guilds = True + if discord.opus.is_loaded(): + print("Opus library loaded successfully.") + return True + else: + print("Opus library failed to load.") + return False + except Exception as e: + print(f"Error loading opus library: {e}") + return False + +# --- Voice Cog for Multi-Server Management --- +class VoiceCog(commands.Cog): + """Cog to handle all voice-related commands and state management.""" + def __init__(self, bot, device_id, ng_threshold): + self.bot = bot + self.device_id = device_id + self.ng_threshold = ng_threshold + self.voice_states = {} # { guild_id: NoiseGate_instance } + + @commands.Cog.listener() + async def on_ready(self): + print(f'Logged in as {self.bot.user} (ID: {self.bot.user.id})') + print('------') + + # Internal API method to join a voice channel + async def internal_join_voice_channel(self, guild_id: int, channel_id: int): + guild = self.bot.get_guild(guild_id) + if not guild: + print(f"Guild with ID {guild_id} not found.") + return False + + channel = guild.get_channel(channel_id) + if not channel or not isinstance(channel, discord.VoiceChannel): + print(f"Voice channel with ID {channel_id} not found in guild {guild.name}.") + return False + + # Get voice client for this guild + voice_client = discord.utils.get(self.bot.voice_clients, guild=guild) + + if voice_client: + if voice_client.channel == channel: + print(f"Already in channel: {channel.name} in guild {guild.name}.") + else: + await voice_client.move_to(channel) + print(f"Moved to channel: {channel.name} in guild {guild.name}.") + else: + try: + voice_client = await channel.connect(timeout=60.0, reconnect=True) + print(f"Connected to channel: {channel.name} in guild {guild.name}.") + except Exception as e: + print(f"Failed to connect to {channel.name} in guild {guild.name}. Error: {e}") + return False + + if discord.opus.is_loaded(): + # Create and start the NoiseGate audio stream for this server + stream_handler = NoiseGate( + _voice_connection=voice_client, + _noise_gate_threshold=self.ng_threshold, + _input_device_index=self.device_id + ) + self.voice_states[guild.id] = stream_handler + stream_handler.run() + print(f"Started audio stream for server: {guild.name}") + return True + else: + print("Opus library not loaded. Cannot start audio stream.") + await voice_client.disconnect() # Disconnect if opus isn't loaded + return False + + # Internal API method to leave a voice channel + async def internal_leave_voice_channel(self, guild_id: int): + if guild_id not in self.voice_states: + print(f"Not currently in a voice channel on guild ID {guild_id}.") + return False + + guild = self.bot.get_guild(guild_id) + if not guild: + print(f"Guild with ID {guild_id} not found.") + return False + + voice_client = discord.utils.get(self.bot.voice_clients, guild=guild) + + if not voice_client: + print(f"Bot not in a voice channel in guild {guild.name}, but state exists. Cleaning up.") + del self.voice_states[guild.id] + return True + + stream_handler = self.voice_states[guild.id] + await stream_handler.close() # Close the NoiseGate stream + del self.voice_states[guild.id] + print(f"Disconnected and stopped the audio stream for guild: {guild.name}") + return True + + # Discord command for joining (for direct user interaction) + @commands.command(name='join') + async def join_command(self, ctx, *, channel: discord.VoiceChannel = None): + if not channel: + if ctx.author.voice: + channel = ctx.author.voice.channel + else: + await ctx.send("You are not connected to a voice channel. Please specify one to join.") + return + + success = await self.internal_join_voice_channel(ctx.guild.id, channel.id) + if success: + await ctx.send(f"Connected to channel: {channel.name}.") + else: + await ctx.send(f"Failed to connect to {channel.name}.") + + # Discord command for leaving (for direct user interaction) + @commands.command(name='leave') + async def leave_command(self, ctx): + success = await self.internal_leave_voice_channel(ctx.guild.id) + if success: + await ctx.send("Disconnected and stopped the audio stream.") + else: + await ctx.send("I am not currently in a voice channel on this server.") + + @join_command.before_invoke + async def ensure_opus(self, ctx): + if not discord.opus.is_loaded(): + await ctx.send("Opus audio library is not loaded. I cannot join a voice channel.") + raise commands.CommandError("Opus not loaded.") + +# --- Discord Bot Manager Class --- class DiscordBotManager: - def __init__(self): - self.bot: Optional[commands.Bot] = None - self.bot_task: Optional[asyncio.Task] = None - self.voice_connections: Dict[int, Dict] = {} - self.token: Optional[str] = None - self.loop = asyncio.get_event_loop() - self.lock = asyncio.Lock() - self._ready_event = asyncio.Event() - self._voice_ready_event = asyncio.Event() + def __init__(self, client_id: str, device_id: int = 0, ng_threshold: int = 50): + self.client_id = client_id + self.device_id = device_id + self.ng_threshold = ng_threshold + self.bot = None + self.voice_cog = None + self._bot_task = None # To hold the running bot task for graceful stopping - async def start_bot(self, token: str): - async with self.lock: - if self.bot and not self.bot.is_closed(): - raise RuntimeError("Bot is already running.") - if self.bot_task and not self.bot_task.done(): - raise RuntimeError("Bot is already running.") + async def _setup_bot(self): + # Define bot intents + intents = discord.Intents.default() + intents.message_content = True # Required for commands + intents.guilds = True # Required to get guild objects by ID + intents.voice_states = True # Required to get voice channel info - self.token = token - self.bot = commands.Bot(command_prefix="!", intents=intents) + self.bot = commands.Bot(command_prefix='!', intents=intents) + self.voice_cog = VoiceCog(self.bot, self.device_id, self.ng_threshold) + await self.bot.add_cog(self.voice_cog) - @self.bot.event - async def on_ready(): - LOGGER.info(f'Logged in as {self.bot.user}') - self._ready_event.set() + @self.bot.event + async def on_ready(): + print(f'Bot fully ready: {self.bot.user}') + # Set initial presence when the bot is ready + await self.set_presence("Broadcasting...", discord.Game) - @self.bot.event - async def on_voice_state_update(member, before, after): - if member != self.bot.user: return - if before.channel is None and after.channel is not None: - LOGGER.info(f"{member.name} joined voice channel {after.channel.name}") - self._voice_ready_event.set() - elif before.channel is not None and after.channel is not None and before.channel != after.channel: - LOGGER.info(f"{member.name} was moved to voice channel {after.channel.name}") - if not self._voice_ready_event.is_set(): self._voice_ready_event.set() - elif before.channel is not None and after.channel is None: - LOGGER.warning(f"{member.name} left voice channel {before.channel.name}") - # guild_id = before.channel.guild.id - # if guild_id in self.voice_connections: - # LOGGER.warning(f"Bot was disconnected from {guild_id} unexpectedly. Cleaning up...") - # await self.leave_voice_channel(guild_id) - # self._voice_ready_event.clear() + async def start_bot(self): + if self.bot and self.bot.is_ready(): + print("Bot is already running.") + return - @self.bot.event - async def on_disconnect(): - LOGGER.warning("Bot has been disconnected from Discord.") + if not load_opus(): + print("Failed to load Opus library. Bot cannot start voice features.") + return - await self.load_opus() - self.bot_task = self.loop.create_task(self.bot.start(token)) + await self._setup_bot() - LOGGER.info("Waiting for bot to become ready...") + print("Starting bot...") try: - await asyncio.wait_for(self._ready_event.wait(), timeout=60.0) - LOGGER.info("Bot is ready.") - except asyncio.TimeoutError: - LOGGER.error("Timeout waiting for bot to become ready.") - if self.bot_task and not self.bot_task.done(): self.bot_task.cancel() - raise RuntimeError("Bot failed to become ready within timeout.") + # Run the bot in a separate task so we can control it + self._bot_task = asyncio.create_task(self.bot.start(self.client_id)) + # Wait for the bot to connect (optional, useful for ensuring it's ready) + await self.bot.wait_until_ready() + print("Bot started and is ready.") + except discord.LoginFailure: + print("Failed to login to Discord. Check your client ID (token).") + except Exception as e: + print(f"An error occurred while starting the bot: {e}") async def stop_bot(self): - async with self.lock: - if self.bot: - for guild_id in list(self.voice_connections.keys()): - await self.leave_voice_channel(guild_id) - await self.bot.close() - self.bot = None - if self.bot_task: - self.bot_task.cancel() - self.bot_task = None - self.voice_connections.clear() - self._ready_event.clear() - LOGGER.info("Bot has been stopped.") + if not self.bot or not self.bot.is_ready(): + print("Bot is not running or not ready.") + return - async def join_voice_channel(self, guild_id: int, channel_id: int, ng_threshold: int = 50, device_id: int = 4): - if not self.bot: raise RuntimeError("Bot is not running.") - guild = self.bot.get_guild(guild_id) - if not guild: raise ValueError("Guild not found.") - if not opus.is_loaded(): raise RuntimeError("Opus is not loaded.") - channel = guild.get_channel(channel_id) - if not isinstance(channel, VoiceChannel): raise ValueError("Channel is not a voice channel.") - if guild_id in self.voice_connections: raise RuntimeError("Already connected to this guild's voice channel.") + print("Stopping bot...") + # Clean up all active voice connections before stopping + for guild_id in list(self.voice_cog.voice_states.keys()): + await self.voice_cog.internal_leave_voice_channel(guild_id) - try: - self._voice_ready_event.clear() - voice_client = await channel.connect(timeout=60.0, reconnect=True) - await asyncio.wait_for(self._voice_ready_event.wait(), timeout=15.0) + await self.bot.close() + if self._bot_task: + self._bot_task.cancel() # Cancel the bot's running task + try: + await self._bot_task # Await cancellation to ensure cleanup + except asyncio.CancelledError: + pass + print("Bot stopped.") + self.bot = None # Reset bot instance - audio_manager = AudioStreamManager(input_device_index=device_id) - audio_source = NoiseGateSource(audio_manager.get_stream(), threshold=ng_threshold) - - voice_client.play(audio_source, after=lambda e: LOGGER.error(f'Player error: {e}') if e else None) - - self.voice_connections[guild_id] = { - "client": voice_client, - "audio_manager": audio_manager - } - LOGGER.info(f"Joined guild {guild_id} and started audio stream.") - - except Exception as e: - LOGGER.error(f"Failed to connect to voice channel: {e}", exc_info=True) - if guild_id in self.voice_connections: # Cleanup if join fails midway - await self.leave_voice_channel(guild_id) - raise + async def join_voice_channel(self, guild_id: int, channel_id: int): + if not self.bot or not self.bot.is_ready(): + print("Bot is not running or ready. Cannot join voice channel.") + return False + return await self.voice_cog.internal_join_voice_channel(guild_id, channel_id) async def leave_voice_channel(self, guild_id: int): - if not self.bot: raise RuntimeError("Bot is not running.") - - connection_info = self.voice_connections.get(guild_id) - if not connection_info: raise RuntimeError("Not connected to the specified guild's voice channel.") - - voice_client = connection_info.get("client") - if voice_client and voice_client.is_connected(): - voice_client.stop() - await voice_client.disconnect() - - audio_manager = connection_info.get("audio_manager") - if audio_manager: - audio_manager.terminate() - - # Use pop to safely remove the key - self.voice_connections.pop(guild_id, None) - LOGGER.info(f"Left guild {guild_id} voice channel.") - - async def load_opus(self): - if opus.is_loaded(): - LOGGER.info("Opus library is already loaded.") - return - - processor = platform.machine() - script_dir = os.path.dirname(os.path.abspath(__file__)) - - LOGGER.debug(f"Attempting to load Opus. Processor: {processor}, OS: {os.name}") - try: - if os.name == 'nt': - if processor == "AMD64": - opus.load_opus(os.path.join(script_dir, './opus/libopus_amd64.dll')) - LOGGER.info("Loaded OPUS library for AMD64") - else: - if processor == "aarch64": - opus.load_opus(os.path.join(script_dir, './opus/libopus_aarcch64.so')) - LOGGER.info("Loaded OPUS library for aarch64") - elif processor == "armv7l": - opus.load_opus(os.path.join(script_dir, './opus/libopus_armv7l.so')) - LOGGER.info("Loaded OPUS library for armv7l") - else: - opus.load_opus('libopus.so.0') - LOGGER.info(f"Attempted to load system OPUS library for {processor}") - - except Exception as e: - LOGGER.error(f"Failed to load OPUS library: {e}") - raise RuntimeError("Could not load a valid Opus library. Voice functionality will fail.") - - if not opus.is_loaded(): - raise RuntimeError("Opus library could not be loaded. Please ensure it is installed correctly.") - - async def set_presence(self, system_name: str): if not self.bot or not self.bot.is_ready(): - LOGGER.warning("Bot is not ready, cannot set presence.") + print("Bot is not running or ready. Cannot leave voice channel.") + return False + return await self.voice_cog.internal_leave_voice_channel(guild_id) + + async def set_presence(self, name: str, activity_type: discord.Activity = discord.ActivityType.listening): + if not self.bot or not self.bot.is_ready(): + print("Bot is not running or ready. Cannot set presence.") return try: - activity = Activity(type=ActivityType.listening, name=system_name) + if activity_type == discord.Game: + activity = discord.Game(name=name) + elif activity_type == discord.Streaming: + activity = discord.Streaming(name=name, url="https://twitch.tv/your_stream_url_here") # Replace with actual URL + elif activity_type == discord.ActivityType.listening: + activity = discord.Activity(type=discord.ActivityType.listening, name=name) + elif activity_type == discord.ActivityType.watching: + activity = discord.Activity(type=discord.ActivityType.watching, name=name) + else: + print(f"Invalid activity type: {activity_type}. Defaulting to Game.") + activity = discord.Game(name=name) + await self.bot.change_presence(activity=activity) - LOGGER.info(f"Bot presence set to 'Listening to {system_name}'") - except Exception as pe: - LOGGER.error(f"Unable to set presence: '{pe}'") \ No newline at end of file + print(f"Presence set to: {name} ({activity_type.__name__})") + except Exception as e: + print(f"Error setting presence: {e}") + +# --- Example Usage / Main Entry Point --- +async def main_run(): + parser = argparse.ArgumentParser(description="Discord Radio Bot Manager.") + parser.add_argument("clientId", type=str, help="The Discord bot's client token.") + parser.add_argument("deviceId", type=int, help="The ID of the audio input device to use.") + parser.add_argument("-n", "--NGThreshold", type=int, default=50, help="The noise gate threshold (default: 50).") + args = parser.parse_args() + + # Instantiate the bot manager + bot_manager = DiscordBotManager(args.clientId, args.deviceId, args.NGThreshold) + + # --- Start the bot --- + await bot_manager.start_bot() + + # --- Example of how an external system (like your websocket) would interact --- + # You would replace this with your actual websocket logic + print("\nBot is running. You can now use internal_api calls or Discord commands.") + print("Example: Use !join in Discord.") + print("Example: Use !leave in Discord.") + print("Example: Simulating an external request to join a channel after 10 seconds...") + + await asyncio.sleep(10) # Simulate delay + + # Placeholder for guild_id and channel_id from your websocket server + # You would receive these from your websocket server + # For testing, you'll need to manually get a guild ID and channel ID where your bot is. + example_guild_id = 123456789012345678 # <<< REPLACE WITH AN ACTUAL GUILD ID + example_channel_id = 987654321098765432 # <<< REPLACE WITH AN ACTUAL VOICE CHANNEL ID + + print(f"\nAttempting to join voice channel {example_channel_id} in guild {example_guild_id} via internal API...") + success_join = await bot_manager.join_voice_channel(example_guild_id, example_channel_id) + print(f"Internal join successful: {success_join}") + + await asyncio.sleep(15) # Stay in channel for a bit + + print("\nAttempting to leave voice channel via internal API...") + success_leave = await bot_manager.leave_voice_channel(example_guild_id) + print(f"Internal leave successful: {success_leave}") + + await asyncio.sleep(5) + + print("\nChanging bot presence to 'Streaming music'...") + await bot_manager.set_presence("Streaming music", discord.Streaming) + + await asyncio.sleep(5) + + print("\nChanging bot presence back to 'Idle'...") + await bot_manager.set_presence("Idle", discord.Game) + + + # Keep the bot running indefinitely until an external stop command or script exit + try: + while True: + await asyncio.sleep(3600) # Sleep for an hour, or until interrupted + except KeyboardInterrupt: + print("\nKeyboardInterrupt detected. Stopping bot...") + finally: + await bot_manager.stop_bot() + + +if __name__ == "__main__": + asyncio.run(main_run()) \ No newline at end of file