diff --git a/getDevices.py b/getDevices.py index 44964f5..89380a2 100644 --- a/getDevices.py +++ b/getDevices.py @@ -3,6 +3,7 @@ from NoiseGatev2 import AudioStream print('Getting a list of devices') list_of_devices = AudioStream().list_devices() print("----- INPUT DEVICES -----") +print("----- *You will likely want to pick from one of these devices* -----") for inputDevice in list_of_devices['Input']: print(f"{inputDevice}\t-\t{list_of_devices['Input'][inputDevice]}") diff --git a/main.py b/main.py index e0388a5..9f928d0 100644 --- a/main.py +++ b/main.py @@ -1,301 +1,213 @@ -import argparse, platform, os -from discord import Intents, Client, Member, opus +# Python client file (client.py) +import argparse +import logging +import os +import platform +import socketio +import asyncio +from discord import Intents, opus from discord.ext import commands from NoiseGatev2 import NoiseGate -# Load the proper OPUS library for the device being used -async def load_opus(): - # Check the system type and load the correct library - # Linux ARM AARCH64 running 32bit OS +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +sio = socketio.AsyncClient() +client = None +device_id = None +ng_threshold = None + + +### Core functions +def load_opus(): processor = platform.machine() - print("Processor: ", processor) + logger.info(f"Processor: {processor}") + if os.name == 'nt': - if processor == "AMD64": - print(f"Loaded OPUS library for AMD64") - opus.load_opus('./opus/libopus_amd64.dll') - return "AMD64" + opus_path = './opus/libopus_amd64.dll' if processor == "AMD64" else None else: - if processor == "aarch64": - print(f"Loaded OPUS library for aarch64") - opus.load_opus('./opus/libopus_aarcch64.so') - return "aarch64" - elif processor == "armv7l": - print(f"Loaded OPUS library for armv7l") - opus.load_opus('./opus/libopus_armv7l.so') - return "armv7l" + opus_path = './opus/libopus_aarcch64.so' if processor == "aarch64" else './opus/libopus_armv7l.so' + + if opus_path: + opus.load_opus(opus_path) + logger.info(f"Loaded OPUS library from {opus_path}") + return True + else: + logger.error("Unsupported architecture or OS.") + return False -def main(clientId='OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY', channelId=367396189529833476, NGThreshold=50, deviceId=1): - intents = Intents.default() +async def join_voice_channel(channel_id): + global device_id, ng_threshold + channel = client.get_channel(int(channel_id)) + logger.info(f"Joining voice channel {channel}") + voice_connection = await channel.connect(timeout=60.0, reconnect=True) + if opus.is_loaded(): + logger.info("OPUS library loaded successfully") + stream_handler = NoiseGate( + _input_device_index=device_id, + _voice_connection=voice_connection, + _noise_gate_threshold=ng_threshold + ) + stream_handler.run() + logger.info("Audio stream started") + return True + else: + logger.error("Failed to load OPUS library") + return False + + +async def leave_voice_channel(guild_id): + guild = await client.fetch_guild(guild_id) + logger.info(f"Leaving voice channel in guild {guild}") + voice_client = guild.voice_client + if voice_client: + await voice_client.disconnect() + logger.info("Disconnected from voice channel") + else: + logger.info("Not connected to any voice channel in the guild") + + # Check if the client needs to open + if len(check_for_open_vc_connections()) <= 0: + # Tell the server the client is going to close + return False + + return True + + +def check_for_open_vc_connections(): + return client.voice_clients + + +def check_if_discord_vc_connected(guild_id): + if client: + return any(int(vc.guild.id) == int(guild_id) for vc in client.voice_clients) + + return False + + +async def get_discord_username(guild_id): + try: + guild = await client.fetch_guild(guild_id) + member = await guild.fetch_member(get_discord_id()) + + if member.nick: + print(f"Username: {member.nick}") + return member.nick + + print(f"Username: {client.user.name if client.user else None}") + return client.user.name if client.user else None + except Exception as e: + logging.warning(e) + + return None + + +def check_if_client_is_open(): + if client: + return client.is_ready() + + return False + + +def get_discord_id(): + print(f"ID: {client.user.id if client.user else None}") + return int(client.user.id) if client.user else None + + +async def on_connect(): + logger.info("Connected to WebSocket server") + + +async def on_disconnect(): + logger.info("Disconnected from WebSocket server") + + +### Socket Events +@sio.event +async def connect_error(): + logger.error("Connection to WebSocket server failed") + + +@sio.event +async def join_server(data): + logger.info(f"Received command to join server: {data['channelId']}") + return await join_voice_channel(data['channelId']) + + +@sio.event +async def leave_server(data): + logger.info(f"Received command to leave server: {data['guild_id']}") + return await leave_voice_channel(data['guild_id']) + + +@sio.event +async def check_discord_vc_connected(data): + return check_if_discord_vc_connected(data['guild_id']) + + +@sio.event +async def request_discord_username(data): + return await get_discord_username(data['guild_id']) + + +@sio.event +async def check_client_is_open(): + return check_if_client_is_open() + + +@sio.event +async def request_discord_id(): + return get_discord_id() + + +async def on_ready(): + logger.info(f"We have logged in as {client.user}") + logger.info("Loading OPUS library") + if not load_opus(): + return + + # Send update to socket server + try: + logger.info('Emitting to the server') + await sio.emit('discord_ready', {'message': 'Discord bot is ready'}) + except Exception as e: + logger.error(f"Error emitting to the server: {e}") + logger.info('Server not ready yet') + + +async def main(args): + global client, device_id, ng_threshold + + # Connect to the WebSocket server + await sio.connect('http://127.0.0.1:{}'.format(args.websocket_port), namespaces=['/']) + logger.info("Connecting to WebSocket server...") + + intents = Intents.default() client = commands.Bot(command_prefix='!', intents=intents) - @client.event - async def on_ready(): - print(f'We have logged in as {client.user}') + device_id = args.device_id + ng_threshold = args.ng_threshold - channelIdToJoin = client.get_channel(channelId) - print("Channel", channelIdToJoin) + client.add_listener(on_connect) + client.add_listener(on_disconnect) + client.add_listener(on_ready) - print("Loading opus") - await load_opus() - - if opus.is_loaded(): - print("Joining voice") - channelConnection = await channelIdToJoin.connect(timeout=60.0, reconnect=True) - print("Voice Connected") - streamHandler = NoiseGate( - _input_device_index=deviceId, - _voice_connection=channelConnection, - _noise_gate_threshold=NGThreshold) - # Start the audio stream - streamHandler.run() - print("stream running") + await client.start(args.client_id) - client.run(clientId) - -parser = argparse.ArgumentParser() -parser.add_argument("deviceId", type=int, help="The ID of the audio device to use") -parser.add_argument("channelId", type=int, help="The ID of the voice channel to use") -parser.add_argument("clientId", type=str, help="The discord client ID") -parser.add_argument("-n", "--NGThreshold", type=int, help="Change the noisegate threshold. This defaults to 50") -args = parser.parse_args() - -if (not args.NGThreshold): - args.NGThreshold = 50 - -print("Arguments:", args) - -main( - clientId=args.clientId, - channelId=args.channelId, - NGThreshold=args.NGThreshold, - deviceId=args.deviceId -) +def parse_arguments(): + parser = argparse.ArgumentParser() + parser.add_argument("device_id", type=int, help="The ID of the audio device to use") + parser.add_argument("client_id", type=str, help="The Discord client ID") + parser.add_argument("websocket_port", type=int, help="The port of the WebSocket server") + parser.add_argument("-n", "--ng_threshold", type=int, default=50, + help="Change the noise gate threshold. Defaults to 50") + return parser.parse_args() - - - - - - - - - - - - -#import asyncio -#import functools -#import itertools -#import math -#import random -# -#import discord -#from async_timeout import timeout -#from discord.ext import commands -# -# -#class VoiceError(Exception): -# pass -# -# -#class VoiceState: -# def __init__(self, bot: commands.Bot, ctx: commands.Context): -# self.bot = bot -# self._ctx = ctx -# -# self.current = None -# self.voice = None -# self.next = asyncio.Event() -# self.songs = SongQueue() -# -# self._loop = False -# self._volume = 0.5 -# self.skip_votes = set() -# -# self.audio_player = bot.loop.create_task(self.audio_player_task()) -# -# def __del__(self): -# self.audio_player.cancel() -# -# @property -# def loop(self): -# return self._loop -# -# @loop.setter -# def loop(self, value: bool): -# self._loop = value -# -# @property -# def volume(self): -# return self._volume -# -# @volume.setter -# def volume(self, value: float): -# self._volume = value -# -# @property -# def is_playing(self): -# return self.voice and self.current -# -# async def audio_player_task(self): -# while True: -# self.next.clear() -# -# if not self.loop: -# # Try to get the next song within 3 minutes. -# # If no song will be added to the queue in time, -# # the player will disconnect due to performance -# # reasons. -# try: -# async with timeout(180): # 3 minutes -# self.current = await self.songs.get() -# except asyncio.TimeoutError: -# self.bot.loop.create_task(self.stop()) -# return -# -# self.current.source.volume = self._volume -# self.voice.play(self.current.source, after=self.play_next_song) -# await self.current.source.channel.send(embed=self.current.create_embed()) -# -# await self.next.wait() -# -# def play_next_song(self, error=None): -# if error: -# raise VoiceError(str(error)) -# -# self.next.set() -# -# def skip(self): -# self.skip_votes.clear() -# -# if self.is_playing: -# self.voice.stop() -# -# async def stop(self): -# self.songs.clear() -# -# if self.voice: -# await self.voice.disconnect() -# self.voice = None -# -# -#class Music(commands.Cog): -# def __init__(self, bot: commands.Bot): -# self.bot = bot -# self.voice_states = {} -# -# def get_voice_state(self, ctx: commands.Context): -# state = self.voice_states.get(ctx.guild.id) -# if not state: -# state = VoiceState(self.bot, ctx) -# self.voice_states[ctx.guild.id] = state -# -# return state -# -# def cog_unload(self): -# for state in self.voice_states.values(): -# self.bot.loop.create_task(state.stop()) -# -# def cog_check(self, ctx: commands.Context): -# if not ctx.guild: -# raise commands.NoPrivateMessage('This command can\'t be used in DM channels.') -# -# return True -# -# async def cog_before_invoke(self, ctx: commands.Context): -# ctx.voice_state = self.get_voice_state(ctx) -# -# async def cog_command_error(self, ctx: commands.Context, error: commands.CommandError): -# await ctx.send('An error occurred: {}'.format(str(error))) -# -# @commands.command(name='join', invoke_without_subcommand=True) -# async def _join(self, ctx: commands.Context): -# """Joins a voice channel.""" -# -# destination = ctx.author.voice.channel -# if ctx.voice_state.voice: -# await ctx.voice_state.voice.move_to(destination) -# return -# -# ctx.voice_state.voice = await destination.connect() -# -# @commands.command(name='summon') -# @commands.has_permissions(manage_guild=True) -# async def _summon(self, ctx: commands.Context, *, channel: discord.VoiceChannel = None): -# """Summons the bot to a voice channel. -# -# If no channel was specified, it joins your channel. -# """ -# -# if not channel and not ctx.author.voice: -# raise VoiceError('You are neither connected to a voice channel nor specified a channel to join.') -# -# destination = channel or ctx.author.voice.channel -# if ctx.voice_state.voice: -# await ctx.voice_state.voice.move_to(destination) -# return -# -# ctx.voice_state.voice = await destination.connect() -# -# @commands.command(name='leave', aliases=['disconnect']) -# @commands.has_permissions(manage_guild=True) -# async def _leave(self, ctx: commands.Context): -# """Clears the queue and leaves the voice channel.""" -# -# if not ctx.voice_state.voice: -# return await ctx.send('Not connected to any voice channel.') -# -# await ctx.voice_state.stop() -# del self.voice_states[ctx.guild.id] -# -# @commands.command(name='play') -# async def _play(self, ctx: commands.Context, *, search: str): -# """Plays a song. -# -# If there are songs in the queue, this will be queued until the -# other songs finished playing. -# -# This command automatically searches from various sites if no URL is provided. -# A list of these sites can be found here: https://rg3.github.io/youtube-dl/supportedsites.html -# """ -# -# if not ctx.voice_state.voice: -# await ctx.invoke(self._join) -# -# async with ctx.typing(): -# try: -# source = await YTDLSource.create_source(ctx, search, loop=self.bot.loop) -# except YTDLError as e: -# await ctx.send('An error occurred while processing this request: {}'.format(str(e))) -# else: -# song = Song(source) -# -# await ctx.voice_state.songs.put(song) -# await ctx.send('Enqueued {}'.format(str(source))) -# -# @_join.before_invoke -# @_play.before_invoke -# async def ensure_voice_state(self, ctx: commands.Context): -# if not ctx.author.voice or not ctx.author.voice.channel: -# raise commands.CommandError('You are not connected to any voice channel.') -# -# if ctx.voice_client: -# if ctx.voice_client.channel != ctx.author.voice.channel: -# raise commands.CommandError('Bot is already in a voice channel.') -# -#intents = discord.Intents.default() -#intents.message_content = True -# -#bot = commands.Bot('music.', description="Brent's Revenge", intents=intents) -#bot.add_cog(Music(bot)) -# -# -#@bot.event -#async def on_ready(): -# print('Logged in as:\n{0.user.name}\n{0.user.id}'.format(bot)) -# -#bot.run('OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY') \ No newline at end of file +if __name__ == "__main__": + args = parse_arguments() + logger.info("Arguments: %s", args) + asyncio.run(main(args)) diff --git a/requirements.txt b/requirements.txt index 40020b0..e7148f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ -discord^=2.2.3 -PyNaCl^=1.5.0 -pyaudio^=0.2.13 -numpy^=1.24.3 -argparse \ No newline at end of file +discord==2.3.2 +PyNaCl==1.5.0 +pyaudio==0.2.14 +numpy==1.26.4 +argparse +python-socketio[client] \ No newline at end of file