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:
1 file changed
+207
-142
@@ -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())
|
||||
Reference in new issue
Block a user