diff --git a/audio.py b/audio.py index fab900a..720a0f7 100644 --- a/audio.py +++ b/audio.py @@ -1,55 +1,34 @@ import discord import yt_dlp +import asyncio voice_clients = {} # Track active voice connections - - -async def join_voice(interaction: discord.Interaction): - if interaction.user.voice is None or interaction.user.voice.channel is None: - await interaction.response.send_message("You must be in a voice channel to use this command.", ephemeral=True) - return - - channel = interaction.user.voice.channel - if interaction.guild.id in voice_clients: - await interaction.response.send_message("Amber is already in a voice channel.") - else: - voice_client = await channel.connect() - voice_clients[interaction.guild.id] = voice_client - await interaction.response.send_message(f"Amber joined {channel.name}!") - - -async def leave_voice(interaction: discord.Interaction): - guild_id = interaction.guild.id - if guild_id not in voice_clients: - await interaction.response.send_message("Amber is not connected to a voice channel.") - return - - voice_client = voice_clients[guild_id] - await voice_client.disconnect() - voice_clients.pop(guild_id, None) - await interaction.response.send_message("Amber has left the voice channel.") +music_queues = {} # Per-guild song queue +current_tracks = {} # Currently playing tracks +volumes = {} # Volume levels per guild +default_volume = 0.5 # Default volume (50%) async def play_audio(interaction: discord.Interaction, query: str): guild_id = interaction.guild.id + if guild_id not in voice_clients: await interaction.response.send_message("Amber is not connected to a voice channel.") return voice_client = voice_clients[guild_id] + if not voice_client.is_connected(): await interaction.response.send_message("Amber is not in a voice channel.") return - await interaction.response.defer() # Indicate the bot is processing the request + await interaction.response.defer() - # yt-dlp options + # Search for the song on YouTube ydl_opts = { 'format': 'bestaudio/best', 'quiet': True, } - - # Search for the query on YouTube with yt_dlp.YoutubeDL(ydl_opts) as ydl: try: info = ydl.extract_info(f"ytsearch:{query}", download=False)['entries'][0] @@ -57,14 +36,116 @@ async def play_audio(interaction: discord.Interaction, query: str): await interaction.followup.send(f"Failed to find or play the requested audio. Error: {str(e)}") return - # Extract the audio URL and metadata - url2 = info['url'] + song_url = info['url'] title = info.get('title', 'Unknown Title') - # Stop any existing audio and play the new one - voice_client.stop() + # Add the song to the queue + if guild_id not in music_queues: + music_queues[guild_id] = [] + + music_queues[guild_id].append((song_url, title)) + await interaction.followup.send(f"✅ **Added to queue:** {title}") + + # If nothing is playing, start playback + if not voice_client.is_playing(): + await play_next(interaction) + + +async def play_next(interaction: discord.Interaction): + guild_id = interaction.guild.id + + if guild_id not in music_queues or not music_queues[guild_id]: + await interaction.followup.send("❌ **No songs in the queue.**") + return + + voice_client = voice_clients[guild_id] + song_url, title = music_queues[guild_id].pop(0) + current_tracks[guild_id] = title + + # Prepare FFmpeg options ffmpeg_options = { + 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', 'options': '-vn', } - voice_client.play(discord.FFmpegPCMAudio(url2, **ffmpeg_options)) - await interaction.followup.send(f"Now playing: {title}") + + try: + source = discord.FFmpegPCMAudio(song_url, **ffmpeg_options) + volume = volumes.get(guild_id, default_volume) + source = discord.PCMVolumeTransformer(source, volume=volume) + + # Play the audio + voice_client.play( + source, + after=lambda e: asyncio.run_coroutine_threadsafe( + play_next(interaction), interaction.client.loop + ), + ) + + await interaction.followup.send(f"🎵 **Now playing:** {title}") + except Exception as e: + await interaction.followup.send(f"Failed to play the next song. Error: {str(e)}") + + +async def stop_audio(interaction: discord.Interaction): + guild_id = interaction.guild.id + + if guild_id in voice_clients: + voice_client = voice_clients[guild_id] + if voice_client.is_playing(): + voice_client.stop() + music_queues[guild_id] = [] # Clear the queue + current_tracks.pop(guild_id, None) # Remove the current track + await interaction.response.send_message("🛑 **Playback stopped and queue cleared.**") + else: + await interaction.response.send_message("❌ **No music is playing.**") + + +async def set_volume(interaction: discord.Interaction, level: int): + guild_id = interaction.guild.id + + if level < 0 or level > 100: + await interaction.response.send_message("❌ **Volume must be between 0 and 100.**") + return + + # Set the volume + volume = level / 100 + volumes[guild_id] = volume + + # Adjust volume for the current source if playing + if guild_id in voice_clients and voice_clients[guild_id].is_playing(): + source = voice_clients[guild_id].source + if isinstance(source, discord.PCMVolumeTransformer): + source.volume = volume + + await interaction.response.send_message(f"🔊 **Volume set to {level}%**.") + + +async def join_voice(interaction: discord.Interaction): + if interaction.user.voice is None or interaction.user.voice.channel is None: + await interaction.response.send_message("You need to be in a voice channel for me to join.") + return + + channel = interaction.user.voice.channel + guild_id = interaction.guild.id + + # Connect to the voice channel + if guild_id not in voice_clients: + voice_clients[guild_id] = await channel.connect() + await interaction.response.send_message(f"✅ **Joined {channel.name}.**") + else: + await interaction.response.send_message("❌ **I am already in a voice channel.**") + + +async def leave_voice(interaction: discord.Interaction): + guild_id = interaction.guild.id + + if guild_id in voice_clients: + voice_client = voice_clients[guild_id] + if voice_client.is_connected(): + await voice_client.disconnect() + voice_clients.pop(guild_id, None) + await interaction.response.send_message("👋 **Left the voice channel.**") + else: + await interaction.response.send_message("❌ **I am not connected to any voice channel.**") + else: + await interaction.response.send_message("❌ **I am not connected to any voice channel.**") diff --git a/commands.py b/commands.py index 09084b5..0ec8a04 100644 --- a/commands.py +++ b/commands.py @@ -1,20 +1,27 @@ import discord from discord import app_commands -from audio import join_voice, leave_voice, play_audio +from audio import play_audio, stop_audio, set_volume, join_voice, leave_voice -async def setup_commands(client): - @client.tree.command(name="join", description="Amber joins the voice channel you're in.") +async def setup_commands(client, guild_id=None): + @client.tree.command(name="join", description="Join the user's current voice channel.") async def join(interaction: discord.Interaction): await join_voice(interaction) - @client.tree.command(name="leave", description="Amber leaves the current voice channel.") + @client.tree.command(name="leave", description="Leave the current voice channel.") async def leave(interaction: discord.Interaction): await leave_voice(interaction) - @client.tree.command(name="play", description="Search and play a song by name or YouTube URL.") - @app_commands.describe(query="The name or YouTube URL of the song.") + @client.tree.command(name="play", description="Play a song by title or artist.") async def play(interaction: discord.Interaction, query: str): await play_audio(interaction, query) + @client.tree.command(name="stop", description="Stop playback and clear the queue.") + async def stop(interaction: discord.Interaction): + await stop_audio(interaction) + + @client.tree.command(name="volume", description="Set playback volume.") + async def volume(interaction: discord.Interaction, level: int): + await set_volume(interaction, level) + await client.tree.sync() diff --git a/main.py b/main.py index 6ef389b..90741bb 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,23 @@ -import discord -from discord.ext import commands -from commands import setup_commands -from dotenv import load_dotenv import os +import logging +import discord +from dotenv import load_dotenv +from commands import setup_commands +# Load environment variables load_dotenv() +TOKEN = os.getenv("DISCORD_TOKEN") +GUILD_ID = os.getenv("GUILD_ID") + +# Validate Guild ID +if GUILD_ID: + try: + GUILD_ID = int(GUILD_ID) + except ValueError: + logging.error("Invalid GUILD_ID in .env file. It must be a numeric value.") + GUILD_ID = None + intents = discord.Intents.default() intents.messages = True intents.message_content = True @@ -18,18 +30,25 @@ class AmberClient(discord.Client): super().__init__(intents=intents) self.tree = discord.app_commands.CommandTree(self) - async def setup_hook(self): - # Register commands - await setup_commands(self) - async def on_ready(self): - print(f"Amber is online as {self.user}") + logging.info(f"Amber is online as {self.user}") + + # Sync commands after the bot is fully ready + if GUILD_ID: + logging.info(f"Setting up commands for guild: {GUILD_ID}") + else: + logging.info("Setting up global commands (no guild ID specified).") + + try: + await setup_commands(self, guild_id=GUILD_ID) + except Exception as e: + logging.error(f"Failed to setup commands: {e}") -# Initialize the bot +logging.basicConfig(level=logging.INFO) client = AmberClient() - -# Run the bot -TOKEN = os.getenv("DISCORD_TOKEN") -client.run(TOKEN) +if TOKEN: + client.run(TOKEN) +else: + logging.error("Bot token not found. Check your .env file.")