diff --git a/.gitignore b/.gitignore index 8a3b79a..f9ad92c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env .vscode /venv -/downloads \ No newline at end of file +/downloads +.idea diff --git a/bot.py b/bot.py index 6422307..1083151 100644 --- a/bot.py +++ b/bot.py @@ -1,11 +1,6 @@ import discord -import yt_dlp import os -import asyncio -import subprocess -from pprint import pprint -import uuid -import random +import commands # Replace with your actual bot token TOKEN = os.environ['DISCORD_911BOT_TOKEN'] @@ -14,205 +9,32 @@ intents = discord.Intents.all() client = discord.Client(intents=intents) - -def print_object_properties(obj): - for attr in dir(obj): - # Filter out special attributes and methods - if not attr.startswith('__'): - value = getattr(obj, attr) - print(f"{attr}: {value}") - @client.event async def on_ready(): print(f'Logged in as {client.user}') - client.loop.create_task(attend_to_song_queue()) - -song_queue = [] -current_ffmpeg_process = None -paused = False - -async def attend_to_song_queue(): - global song_queue - global current_ffmpeg_process - global paused - while True: - if len(song_queue) > 0 and not paused: - filename, voice_channel, guildname, txtchannel, volume = song_queue.pop(0) - voice = discord.utils.get(client.voice_clients, guild=guildname) - - realvolume = 1.0 - try: - if isinstance(float(volume), float): - realvolume = float(volume) - except ValueError: - print("Invalid value queued for volume") - - if not voice or not voice.is_connected(): - try: - voice = await voice_channel.connect() - except Exception as e: - print(f"Error connecting to voice channel: {e}") - continue - - try: - current_ffmpeg_process = discord.FFmpegPCMAudio(executable="ffmpeg", source=filename, options=f"-filter:a \"volume={realvolume}\"") - voice.play(current_ffmpeg_process) - await txtchannel.send(f"Now playing: {filename}") - while voice.is_playing(): - await asyncio.sleep(1) - except Exception as e: - print(f"Error playing audio: {e}") - finally: - if current_ffmpeg_process: - current_ffmpeg_process.cleanup() - current_ffmpeg_process = None - # Cleanup after playing - os.remove(filename) - else: - await asyncio.sleep(1) # Sleep for a short time when the queue is empty - + client.loop.create_task(commands.attend_to_song_queue()) @client.event async def on_message(message): - global song_queue - global current_ffmpeg_process - global paused - if message.author == client.user: + if not message.content.startswith('!'): return - - texts = message.content.split(" ") - - commandlength = len(texts[0]) - - message.content = message.content[0:commandlength].lower() + message.content[commandlength:len(message.content)] + if message.author == client.user: + return print("Message content: \" " + message.content + " \" ") - # command to join the voice channel - if message.content.startswith('!join'): - channel = message.author.voice.channel - - - if not channel: - await message.channel.send("You're not connected to any vc!") - else: - voice = discord.utils.get(client.voice_clients, guild=message.guild) - if voice and voice.is_connected(): - await voice.move_to(channel) - else: - voice = await channel.connect() - await message.channel.send("Joined channel!") - - if message.content.startswith('!stop') or message.content.startswith('!pause'): - voice = discord.utils.get(client.voice_clients, guild=message.guild) - if voice and voice.is_playing(): - voice.stop() - if current_ffmpeg_process: - current_ffmpeg_process.cleanup() - current_ffmpeg_process = None - paused = True - await message.channel.send("Playback stopped. Type !resume to resume the queue, or !play a link to start a fresh one. If you !resume, then further calls to !play will keep adding to the existing queue.") - else: - await message.channel.send("Nothing is currently playing.") - - if message.content.startswith('!resume'): - paused = False - - - - if message.content.startswith('!skip'): - voice = discord.utils.get(client.voice_clients, guild=message.guild) - - if voice and voice.is_playing(): - voice.stop() - if current_ffmpeg_process: - current_ffmpeg_process.cleanup() - current_ffmpeg_process = None - await message.channel.send("Song skipped.") - else: - await message.channel.send("Nothing is currently playing.") - - if message.content.startswith('!clear'): - song_queue = [] - paused = False - await message.channel.send("Cleared song queue.") - - - if message.content.startswith('!play'): - if paused: - paused = False - song_queue = [] - channel = message.author.voice.channel - - if not channel: - await message.channel.send("You're not connected to any vc!") - else: - voice = discord.utils.get(client.voice_clients, guild=message.guild) - if voice and voice.is_connected(): - await voice.move_to(channel) - else: - voice = await channel.connect() - await message.channel.send("Joined channel!") - - url = message.content[len('!play '):].strip() - - if not url: - await message.channel.send("Please provide a valid YouTube URL.") - return - - realvolume = 1.0 - - try: - x = message.content.rsplit("v:") - - if len(x) > 1: - try: - #clamp the value to PREVENT ABUSE - realvolume = max(0.0, min(float(x[1]), 4.0)) - except: - await message.channel.send(f"Invalid volume argument, using 1.0") - else: - print("No volume argument found.") - except: - print("No volume argument found.") - - - # filename to be downloaded - filename = f'downloads/{uuid.uuid4()}.mp3' - - await message.channel.send(f"Downloading to {filename}...") - - # command to download MP3 using yt-dlp - command = ['yt-dlp', '-x', '--audio-format', 'mp3', '--audio-quality', '0', '-o', filename, url] - - try: - # run the command - subprocess.run(command, check=True) - # have to do this before i actually append otherwise the other thread is ON IT! - await message.channel.send(f"Added {filename} to queue.") - # append the filename to the queued files to play - song_queue.append((filename, message.author.voice.channel, message.guild, message.channel, realvolume)) - - - except subprocess.CalledProcessError as e: - await message.channel.send(f"An error occurred while downloading: {str(e)}") - except Exception as e: - await message.channel.send(f"An unexpected error occurred: {str(e)}") - - # command to leavae the voice channel - if message.content.startswith('!leave'): - channel = message.author.voice.channel - voice = discord.utils.get(client.voice_clients, guild=message.guild) + message_parts = message.content[1:].split(" ", 1) + cmd, args = message_parts[0].lower(), None + if len(message_parts) == 2: + args = message_parts[1] - if voice: - if voice.is_playing(): - voice.stop() # Stop the audio playback first - await voice.disconnect() - await message.channel.send("Disconnected from the voice channel.") - else: - await message.channel.send("I'm not in a voice channel.") + cmd_func = commands.command_map.get(cmd) + if cmd_func: + ctx = commands.Context(message, client, args) + return cmd_func(ctx) + await commands.send_message(message, f"unknown command: '!{cmd}'") # Run the bot with your token -client.run(TOKEN) \ No newline at end of file +client.run(TOKEN) diff --git a/commands.py b/commands.py new file mode 100644 index 0000000..9ce3325 --- /dev/null +++ b/commands.py @@ -0,0 +1,199 @@ +import discord +import asyncio +import subprocess +import uuid +import os + +song_queue = [] +current_ffmpeg_process = None +paused = False + +class Context: + def __init__(self, message, client, args): + self.message = message + self.client = client + self.args = args + self.volume = 1 + self.filename = None + self.url = None + +async def join(ctx): + channel = ctx.message.author.voice.channel + + if not channel: + return await send_message(ctx, "You're not connected to any vc!") + + voice = get_voice(ctx) + if voice and voice.is_connected(): + await voice.move_to(channel) + else: + await channel.connect() + await send_message(ctx, "Joined channel!") + + +async def leave(ctx): + voice = get_voice(ctx) + + if not voice: + return await send_message(ctx, "I'm not in a voice channel.") + + if voice.is_playing(): + voice.stop() # Stop the audio playback first + await voice.disconnect() + await send_message(ctx, "Disconnected from the voice channel.") + + +async def play(ctx): + global current_ffmpeg_process + global paused + global song_queue + if paused: + paused = False + song_queue = [] + + await join(ctx) + + parse_args(ctx) + + if not ctx.url: + return await send_message(ctx, "Please provide a valid YouTube URL.") + + # filename to be downloaded + filename = f'downloads/{uuid.uuid4()}.mp3' + ctx.filename = filename + + await send_message(ctx, f"Downloading to {filename}...") + + # command to download MP3 using yt-dlp + # todo we should be able to have yt-dlp save the file as the video title, so we can leave identify the songs being + # played, instead of uuids. We would have to parse the output from subprocess below. + command = ['yt-dlp', '-x', '--audio-format', 'mp3', '--audio-quality', '0', '-o', filename, ctx.url] + + try: + # run the command + subprocess.run(command, check=True) + # have to do this before i actually append otherwise the other thread is ON IT! + await send_message(ctx, f"Added {filename} to queue.") + # append the filename to the queued files to play + song_queue.append(ctx) + + except subprocess.CalledProcessError as e: + await send_message(ctx, f"An error occurred while downloading: {str(e)}") + except Exception as e: + await send_message(ctx, f"An unexpected error occurred: {str(e)}") + + +async def pause(ctx): + global paused + global current_ffmpeg_process + + voice = get_voice(ctx) + if not voice or not voice.is_playing(): + return await send_message(ctx, "Nothing is currently playing.") + + voice.stop() + ffmpeg_clean() + paused = True + await send_message(ctx, + "Playback stopped. Type !resume to resume the queue, or !play a link to start a fresh one. If you !resume, then further calls to !play will keep adding to the existing queue.") + +async def resume(ctx): + global paused + paused = False + + +async def skip(ctx): + global current_ffmpeg_process + + voice = get_voice(ctx) + if not voice or not voice.is_playing(): + return await send_message(ctx, "Nothing is currently playing.") + + voice.stop() + ffmpeg_clean() + await send_message(ctx, "Song skipped.") + + +async def clear(ctx): + global paused + global song_queue + song_queue = [] + paused = False + await send_message(ctx, "Cleared song queue.") + +command_map = { + "join": join, + "leave": leave, + "play": play, + "queue": play, + "pause": pause, + "stop": pause, + "resume": resume, + "skip": skip, + "clear": clear, +} + +def parse_volume(ctx, volume): + volume = volume[2:] # remove v: + try: + ctx.volume = max(0.0, min(float(volume), 4.0)) + except ValueError: + return None + +def parse_url(ctx, url): + ctx.url = url + +def parse_args(ctx): + for arg in ctx.args.split(" "): + arg_split = arg.split(":",1) + arg_letter = "" and len(arg_split) == 1 or arg_split[0] + arg_cmd = arg_map.get(arg_letter) + if arg_cmd: + arg_cmd(ctx, arg) + +arg_map = { + "v": parse_volume, + "": parse_url, +} + +async def attend_to_song_queue(): + global song_queue + global current_ffmpeg_process + global paused + while True: + if len(song_queue) == 0 or paused: + await asyncio.sleep(1) # Sleep for a short time when the queue is empty + continue + + ctx = song_queue.pop(0) + voice = get_voice(ctx) + + if not voice or not voice.is_connected(): + await join(ctx) + + filename = ctx.filename + try: + current_ffmpeg_process = discord.FFmpegPCMAudio(executable="ffmpeg", source=filename, + options=f"-filter:a \"volume={ctx.volume}\"") + voice.play(current_ffmpeg_process) + await send_message(ctx,f"Now playing: {filename}") + while voice.is_playing(): + await asyncio.sleep(1) + except Exception as e: + print(f"Error playing audio: {e}") + finally: + ffmpeg_clean() + # Cleanup after playing + os.remove(filename) + +async def send_message(ctx, msg): + await ctx.message.channel.send(msg) + +def ffmpeg_clean(): + global current_ffmpeg_process + if current_ffmpeg_process: + current_ffmpeg_process.cleanup() + current_ffmpeg_process = None + +def get_voice(ctx): + return discord.utils.get(ctx.client.voice_clients, guild=ctx.message.guild)