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
|
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())
|
||||||
Reference in new issue
Block a user