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
from discord import app_commands
from discord.ext import commands
import yt_dlp as youtube_dl
import asyncio
import os
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_dotenv()
TOKEN = os.getenv('DISCORD_TOKEN')
GUILD_ID = os.getenv('GUILD_ID') # Optional: For testing
# Suppress noise about console usage from errors
youtube_dl.utils.bug_reports_message = lambda: ''
@@ -58,7 +25,7 @@ ytdl_format_options = {
'quiet': True,
'no_warnings': True,
'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 = {
@@ -79,21 +46,13 @@ class YTDLSource(discord.PCMVolumeTransformer):
@classmethod
async def from_url(cls, url, *, loop=None, stream=False):
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:
# Take the first item from a playlist
data = data['entries'][0]
if 'entries' in data:
data = data['entries'][0]
filename = data['url'] if stream else ytdl.prepare_filename(data)
try:
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)}")
filename = data['url'] if stream else ytdl.prepare_filename(data)
return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data)
class MusicBot(commands.Cog):
@@ -103,143 +62,249 @@ class MusicBot(commands.Cog):
self.current = None
self.volume = 0.5
@commands.command(name='join', help='Joins the voice channel')
async def join(self, ctx):
if not ctx.message.author.voice:
await ctx.send("You are not connected to a voice channel")
return None
async def ensure_voice(self, interaction):
if not interaction.user.voice:
await interaction.response.send_message(
"You are not connected to a voice channel",
ephemeral=True
)
return False
channel = ctx.message.author.voice.channel
if ctx.voice_client is not None:
return await ctx.voice_client.move_to(channel)
if interaction.guild.voice_client is None:
await interaction.user.voice.channel.connect()
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 None
return True
@commands.command(name='play', help='Plays a song from YouTube')
async def play(self, ctx, *, url):
if not ctx.message.author.voice:
await ctx.send("You are not connected to a voice channel")
@app_commands.command(name="play", description="Play a song from YouTube")
@app_commands.describe(query="YouTube URL or search query")
async def play(self, interaction: discord.Interaction, query: str):
"""Play a song from YouTube"""
if not await self.ensure_voice(interaction):
return
if ctx.voice_client is None:
await ctx.invoke(self.join)
await interaction.response.defer()
async with ctx.typing():
try:
player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True)
try:
player = await YTDLSource.from_url(query, loop=self.bot.loop, stream=True)
if ctx.voice_client.is_playing():
self.queue.append(player)
await ctx.send(f"Added to queue: {player.title}")
else:
self.current = player
ctx.voice_client.play(player, after=lambda e: self.play_next(ctx))
await ctx.send(f"Now playing: {player.title}")
except Exception as e:
error_msg = f"An error occurred: {str(e)}\n```{traceback.format_exc()}```"
print(error_msg) # Print to console for debugging
await ctx.send(error_msg[:1900]) # Discord has a 2000 character limit
if interaction.guild.voice_client.is_playing():
self.queue.append(player)
await interaction.followup.send(
f"Added to queue: {player.title}",
ephemeral=False
)
else:
self.current = player
interaction.guild.voice_client.play(
player,
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:
self.current = self.queue.pop(0)
ctx.voice_client.play(self.current, after=lambda e: self.play_next(ctx))
asyncio.run_coroutine_threadsafe(ctx.send(f"Now playing: {self.current.title}"), self.bot.loop)
interaction.guild.voice_client.play(
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:
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')
async def show_queue(self, ctx):
@app_commands.command(name="queue", description="Show the current queue")
async def show_queue(self, interaction: discord.Interaction):
"""Show the current queue"""
if not self.queue:
await ctx.send("The queue is empty")
await interaction.response.send_message(
"The queue is empty",
ephemeral=True
)
return
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')
async def skip(self, ctx):
if ctx.voice_client.is_playing():
ctx.voice_client.stop()
await ctx.send("Skipped current song")
@app_commands.command(name="skip", description="Skip the current song")
async def skip(self, interaction: discord.Interaction):
"""Skip the current song"""
if interaction.guild.voice_client.is_playing():
interaction.guild.voice_client.stop()
await interaction.response.send_message(
"Skipped current song",
ephemeral=False
)
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')
async def pause(self, ctx):
if ctx.voice_client.is_playing():
ctx.voice_client.pause()
await ctx.send("Paused")
@app_commands.command(name="pause", description="Pause the current song")
async def pause(self, interaction: discord.Interaction):
"""Pause the current song"""
if interaction.guild.voice_client.is_playing():
interaction.guild.voice_client.pause()
await interaction.response.send_message(
"Paused",
ephemeral=False
)
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')
async def resume(self, ctx):
if ctx.voice_client.is_paused():
ctx.voice_client.resume()
await ctx.send("Resumed")
@app_commands.command(name="resume", description="Resume the current song")
async def resume(self, interaction: discord.Interaction):
"""Resume the current song"""
if interaction.guild.voice_client.is_paused():
interaction.guild.voice_client.resume()
await interaction.response.send_message(
"Resumed",
ephemeral=False
)
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')
async def stop(self, ctx):
if ctx.voice_client.is_playing():
ctx.voice_client.stop()
@app_commands.command(name="stop", description="Stop playback and clear queue")
async def stop(self, interaction: discord.Interaction):
"""Stop playback and clear queue"""
if interaction.guild.voice_client.is_playing():
interaction.guild.voice_client.stop()
self.queue.clear()
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)')
async def volume(self, ctx, volume: int):
if ctx.voice_client is None:
return await ctx.send("Not connected to a voice channel.")
@app_commands.command(name="volume", description="Change the player volume")
@app_commands.describe(volume="The volume level (1-100)")
async def volume(self, interaction: discord.Interaction, volume: int):
"""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:
self.volume = volume / 100
ctx.voice_client.source.volume = self.volume
await ctx.send(f"Changed volume to {volume}%")
interaction.guild.voice_client.source.volume = self.volume
await interaction.response.send_message(
f"Changed volume to {volume}%",
ephemeral=False
)
return None
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
@commands.command(name='nowplaying', help='Shows the currently playing song')
async def now_playing(self, ctx):
@app_commands.command(name="nowplaying", description="Show the currently playing song")
async def now_playing(self, interaction: discord.Interaction):
"""Show the currently playing song"""
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:
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')
async def leave(self, ctx):
if ctx.voice_client is not None:
await ctx.voice_client.disconnect()
@app_commands.command(name="leave", description="Leave the voice channel")
async def leave(self, interaction: discord.Interaction):
"""Leave the voice channel"""
if interaction.guild.voice_client is not None:
await interaction.guild.voice_client.disconnect()
self.queue.clear()
self.current = None
await ctx.send("Left the voice channel")
await interaction.response.send_message(
"Left the voice channel",
ephemeral=False
)
else:
await ctx.send("I'm not in a voice channel")
@play.before_invoke
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.")
await interaction.response.send_message(
"I'm not in a voice channel",
ephemeral=True
)
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
async def on_ready():
print(f'Logged in as {bot.user.name} ({bot.user.id})')
print('------')
await bot.add_cog(MusicBot(bot))
async def on_command_error(ctx, error):
print(f"Error: {error}")
bot.run(TOKEN)
async def main():
async with bot:
await bot.start(TOKEN)
if __name__ == "__main__":
asyncio.run(main())