Refactor bot to use Discord app commands and modernize structure

Replaced old commands-based approach with Discord app commands for slash command functionality. Simplified error handling, removed redundant code, and improved voice channel management. Updated bot's initialization and ensured proper command syncing for global or guild-specific contexts.
This commit is contained in:
mmacierzanka committed 2025-04-28 03:56:57 +02:00
1 parent 78cacc3a17
commit cd0a738fdf
1 file changed
+207 -142
+207 -142
View File
@@ -1,48 +1,15 @@
import discord import discord
from discord import app_commands
from discord.ext import commands from discord.ext import commands
import yt_dlp as youtube_dl import yt_dlp as youtube_dl
import asyncio import asyncio
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
import traceback
import ctypes.util
# Add these debug lines after the imports
print("Searching for opus library...")
opus_path = ctypes.util.find_library('opus')
print(f"Found opus path: {opus_path}")
# Try multiple known opus library locations
possible_opus_paths = [
'/opt/homebrew/lib/libopus.dylib', # Apple Silicon
'/usr/local/lib/libopus.dylib', # Intel Mac
'opus', # Default system path
'libopus', # Alternative name
'libopus.0.dylib', # Another possible name
opus_path if opus_path else '' # System detected path
]
opus_loaded = False
for path in possible_opus_paths:
if not path:
continue
try:
print(f"Attempting to load opus from: {path}")
discord.opus.load_opus(path)
print(f"Successfully loaded opus from {path}")
opus_loaded = True
break
except Exception as e:
print(f"Failed to load opus from {path}: {str(e)}")
if not opus_loaded:
print("Could not load opus from any location")
raise RuntimeError('Opus failed to load')
# Rest of your code remains the same
# Load environment variables # Load environment variables
load_dotenv() load_dotenv()
TOKEN = os.getenv('DISCORD_TOKEN') TOKEN = os.getenv('DISCORD_TOKEN')
GUILD_ID = os.getenv('GUILD_ID') # Optional: For testing
# Suppress noise about console usage from errors # Suppress noise about console usage from errors
youtube_dl.utils.bug_reports_message = lambda: '' youtube_dl.utils.bug_reports_message = lambda: ''
@@ -58,7 +25,7 @@ ytdl_format_options = {
'quiet': True, 'quiet': True,
'no_warnings': True, 'no_warnings': True,
'default_search': 'auto', 'default_search': 'auto',
'source_address': '0.0.0.0', # bind to ipv4 since ipv6 addresses cause issues sometimes 'source_address': '0.0.0.0',
} }
ffmpeg_options = { ffmpeg_options = {
@@ -79,21 +46,13 @@ class YTDLSource(discord.PCMVolumeTransformer):
@classmethod @classmethod
async def from_url(cls, url, *, loop=None, stream=False): async def from_url(cls, url, *, loop=None, stream=False):
loop = loop or asyncio.get_event_loop() loop = loop or asyncio.get_event_loop()
try: data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=not stream))
data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=not stream))
if 'entries' in data: if 'entries' in data:
# Take the first item from a playlist data = data['entries'][0]
data = data['entries'][0]
filename = data['url'] if stream else ytdl.prepare_filename(data) filename = data['url'] if stream else ytdl.prepare_filename(data)
try: return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data)
return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data)
except Exception as e:
raise Exception(f"Error creating FFmpeg audio: {str(e)}. Make sure FFmpeg is installed and accessible.")
except Exception as e:
raise Exception(f"Error extracting info from URL: {str(e)}")
class MusicBot(commands.Cog): class MusicBot(commands.Cog):
@@ -103,143 +62,249 @@ class MusicBot(commands.Cog):
self.current = None self.current = None
self.volume = 0.5 self.volume = 0.5
@commands.command(name='join', help='Joins the voice channel') async def ensure_voice(self, interaction):
async def join(self, ctx): if not interaction.user.voice:
if not ctx.message.author.voice: await interaction.response.send_message(
await ctx.send("You are not connected to a voice channel") "You are not connected to a voice channel",
return None ephemeral=True
)
return False
channel = ctx.message.author.voice.channel if interaction.guild.voice_client is None:
if ctx.voice_client is not None: await interaction.user.voice.channel.connect()
return await ctx.voice_client.move_to(channel) elif interaction.guild.voice_client.channel != interaction.user.voice.channel:
await interaction.response.send_message(
"I'm already in another voice channel",
ephemeral=True
)
return False
await channel.connect() return True
return None
@commands.command(name='play', help='Plays a song from YouTube') @app_commands.command(name="play", description="Play a song from YouTube")
async def play(self, ctx, *, url): @app_commands.describe(query="YouTube URL or search query")
if not ctx.message.author.voice: async def play(self, interaction: discord.Interaction, query: str):
await ctx.send("You are not connected to a voice channel") """Play a song from YouTube"""
if not await self.ensure_voice(interaction):
return return
if ctx.voice_client is None: await interaction.response.defer()
await ctx.invoke(self.join)
async with ctx.typing(): try:
try: player = await YTDLSource.from_url(query, loop=self.bot.loop, stream=True)
player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True)
if ctx.voice_client.is_playing(): if interaction.guild.voice_client.is_playing():
self.queue.append(player) self.queue.append(player)
await ctx.send(f"Added to queue: {player.title}") await interaction.followup.send(
else: f"Added to queue: {player.title}",
self.current = player ephemeral=False
ctx.voice_client.play(player, after=lambda e: self.play_next(ctx)) )
await ctx.send(f"Now playing: {player.title}") else:
except Exception as e: self.current = player
error_msg = f"An error occurred: {str(e)}\n```{traceback.format_exc()}```" interaction.guild.voice_client.play(
print(error_msg) # Print to console for debugging player,
await ctx.send(error_msg[:1900]) # Discord has a 2000 character limit after=lambda e: asyncio.run_coroutine_threadsafe(
self.play_next(interaction),
self.bot.loop
)
)
await interaction.followup.send(
f"Now playing: {player.title}",
ephemeral=False
)
except Exception as e:
await interaction.followup.send(
f"An error occurred: {e}",
ephemeral=True
)
def play_next(self, ctx): async def play_next(self, interaction):
if len(self.queue) > 0: if len(self.queue) > 0:
self.current = self.queue.pop(0) self.current = self.queue.pop(0)
ctx.voice_client.play(self.current, after=lambda e: self.play_next(ctx)) interaction.guild.voice_client.play(
asyncio.run_coroutine_threadsafe(ctx.send(f"Now playing: {self.current.title}"), self.bot.loop) self.current,
after=lambda e: asyncio.run_coroutine_threadsafe(
self.play_next(interaction),
self.bot.loop
)
)
await interaction.followup.send(
f"Now playing: {self.current.title}",
ephemeral=False
)
else: else:
self.current = None self.current = None
asyncio.run_coroutine_threadsafe(ctx.send("Queue is empty"), self.bot.loop) await interaction.followup.send(
"Queue is empty",
ephemeral=False
)
@commands.command(name='queue', help='Shows the current queue') @app_commands.command(name="queue", description="Show the current queue")
async def show_queue(self, ctx): async def show_queue(self, interaction: discord.Interaction):
"""Show the current queue"""
if not self.queue: if not self.queue:
await ctx.send("The queue is empty") await interaction.response.send_message(
"The queue is empty",
ephemeral=True
)
return return
queue_list = "\n".join([f"{i + 1}. {item.title}" for i, item in enumerate(self.queue)]) queue_list = "\n".join([f"{i + 1}. {item.title}" for i, item in enumerate(self.queue)])
await ctx.send(f"Current queue:\n{queue_list}") await interaction.response.send_message(
f"Current queue:\n{queue_list}",
ephemeral=False
)
@commands.command(name='skip', help='Skips the current song') @app_commands.command(name="skip", description="Skip the current song")
async def skip(self, ctx): async def skip(self, interaction: discord.Interaction):
if ctx.voice_client.is_playing(): """Skip the current song"""
ctx.voice_client.stop() if interaction.guild.voice_client.is_playing():
await ctx.send("Skipped current song") interaction.guild.voice_client.stop()
await interaction.response.send_message(
"Skipped current song",
ephemeral=False
)
else: else:
await ctx.send("Not playing anything") await interaction.response.send_message(
"Not playing anything",
ephemeral=True
)
@commands.command(name='pause', help='Pauses the current song') @app_commands.command(name="pause", description="Pause the current song")
async def pause(self, ctx): async def pause(self, interaction: discord.Interaction):
if ctx.voice_client.is_playing(): """Pause the current song"""
ctx.voice_client.pause() if interaction.guild.voice_client.is_playing():
await ctx.send("Paused") interaction.guild.voice_client.pause()
await interaction.response.send_message(
"Paused",
ephemeral=False
)
else: else:
await ctx.send("Not playing anything") await interaction.response.send_message(
"Not playing anything",
ephemeral=True
)
@commands.command(name='resume', help='Resumes the current song') @app_commands.command(name="resume", description="Resume the current song")
async def resume(self, ctx): async def resume(self, interaction: discord.Interaction):
if ctx.voice_client.is_paused(): """Resume the current song"""
ctx.voice_client.resume() if interaction.guild.voice_client.is_paused():
await ctx.send("Resumed") interaction.guild.voice_client.resume()
await interaction.response.send_message(
"Resumed",
ephemeral=False
)
else: else:
await ctx.send("Not paused") await interaction.response.send_message(
"Not paused",
ephemeral=True
)
@commands.command(name='stop', help='Stops the bot and clears the queue') @app_commands.command(name="stop", description="Stop playback and clear queue")
async def stop(self, ctx): async def stop(self, interaction: discord.Interaction):
if ctx.voice_client.is_playing(): """Stop playback and clear queue"""
ctx.voice_client.stop() if interaction.guild.voice_client.is_playing():
interaction.guild.voice_client.stop()
self.queue.clear() self.queue.clear()
self.current = None self.current = None
await ctx.send("Stopped and cleared queue") await interaction.response.send_message(
"Stopped and cleared queue",
ephemeral=False
)
@commands.command(name='volume', help='Changes the volume (0-100)') @app_commands.command(name="volume", description="Change the player volume")
async def volume(self, ctx, volume: int): @app_commands.describe(volume="The volume level (1-100)")
if ctx.voice_client is None: async def volume(self, interaction: discord.Interaction, volume: int):
return await ctx.send("Not connected to a voice channel.") """Change the player volume"""
if interaction.guild.voice_client is None:
return await interaction.response.send_message(
"Not connected to a voice channel",
ephemeral=True
)
if 0 < volume <= 100: if 0 < volume <= 100:
self.volume = volume / 100 self.volume = volume / 100
ctx.voice_client.source.volume = self.volume interaction.guild.voice_client.source.volume = self.volume
await ctx.send(f"Changed volume to {volume}%") await interaction.response.send_message(
f"Changed volume to {volume}%",
ephemeral=False
)
return None return None
else: else:
await ctx.send("Please enter a value between 1 and 100") await interaction.response.send_message(
"Please enter a value between 1 and 100",
ephemeral=True
)
return None return None
@commands.command(name='nowplaying', help='Shows the currently playing song') @app_commands.command(name="nowplaying", description="Show the currently playing song")
async def now_playing(self, ctx): async def now_playing(self, interaction: discord.Interaction):
"""Show the currently playing song"""
if self.current: if self.current:
await ctx.send(f"Now playing: {self.current.title}") await interaction.response.send_message(
f"Now playing: {self.current.title}",
ephemeral=False
)
else: else:
await ctx.send("Nothing is currently playing") await interaction.response.send_message(
"Nothing is currently playing",
ephemeral=True
)
@commands.command(name='leave', help='Leaves the voice channel') @app_commands.command(name="leave", description="Leave the voice channel")
async def leave(self, ctx): async def leave(self, interaction: discord.Interaction):
if ctx.voice_client is not None: """Leave the voice channel"""
await ctx.voice_client.disconnect() if interaction.guild.voice_client is not None:
await interaction.guild.voice_client.disconnect()
self.queue.clear() self.queue.clear()
self.current = None self.current = None
await ctx.send("Left the voice channel") await interaction.response.send_message(
"Left the voice channel",
ephemeral=False
)
else: else:
await ctx.send("I'm not in a voice channel") await interaction.response.send_message(
"I'm not in a voice channel",
@play.before_invoke ephemeral=True
async def ensure_voice(self, ctx): )
if ctx.voice_client is None:
if ctx.author.voice:
await ctx.author.voice.channel.connect()
else:
await ctx.send("You are not connected to a voice channel.")
raise commands.CommandError("Author not connected to a voice channel.")
bot = commands.Bot(command_prefix='!', intents=discord.Intents.all()) class MyBot(commands.Bot):
def __init__(self):
intents = discord.Intents.default()
intents.message_content = True
intents.voice_states = True
super().__init__(command_prefix="!", intents=intents)
async def setup_hook(self):
await self.add_cog(MusicBot(self))
if GUILD_ID:
guild = discord.Object(id=int(GUILD_ID))
self.tree.copy_global_to(guild=guild)
await self.tree.sync(guild=guild)
print(f"Synced commands to guild {GUILD_ID}")
else:
await self.tree.sync()
print("Synced commands globally")
async def on_ready(self):
print(f'Logged in as {self.user.name} ({self.user.id})')
print('------')
bot = MyBot()
@bot.event @bot.event
async def on_ready(): async def on_command_error(ctx, error):
print(f'Logged in as {bot.user.name} ({bot.user.id})') print(f"Error: {error}")
print('------')
await bot.add_cog(MusicBot(bot))
bot.run(TOKEN) async def main():
async with bot:
await bot.start(TOKEN)
if __name__ == "__main__":
asyncio.run(main())