From bd5e8143069752939ea2ecfd3f1af693ae9d37fe Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 22 Jan 2025 22:50:06 -0500 Subject: [PATCH] Fixed progress bar issues, queue issues, and redid the playback buttons. --- audio.py | 279 +++++++++++++++++++++++-------------------------------- 1 file changed, 116 insertions(+), 163 deletions(-) diff --git a/audio.py b/audio.py index 60c2417..893e402 100644 --- a/audio.py +++ b/audio.py @@ -16,6 +16,7 @@ current_tracks = {} # Currently playing tracks volumes = {} # Volume levels per guild repeat_modes = {} # Tracks repeat mode per guild: "one", "all", or "off" now_playing_messages = {} # Tracks the "Now Playing" embed per guild +progress_tasks = {} # Progress bar tasks per guild default_volume = 0.05 # Default volume (5%) USER_DATA_FILE = 'user_data.json' YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY") # Replace with your API key @@ -66,7 +67,7 @@ async def join_voice(interaction: discord.Interaction): async def play_audio(interaction: discord.Interaction, query: str): guild_id = interaction.guild.id - # Defer the interaction if not already responded + # Immediate acknowledgment to prevent "Amber is thinking" if not interaction.response.is_done(): await interaction.response.defer() @@ -77,6 +78,13 @@ async def play_audio(interaction: discord.Interaction, query: str): await interaction.followup.send("❌ **Error:** Could not join the voice channel.") return + # Start a background task to fetch and queue the song + asyncio.create_task(fetch_and_queue_song(interaction, query)) + + +async def fetch_and_queue_song(interaction: discord.Interaction, query: str): + guild_id = interaction.guild.id + # Search for the song on YouTube ydl_opts = { 'format': 'bestaudio/best', @@ -92,7 +100,7 @@ async def play_audio(interaction: discord.Interaction, query: str): song_url = info['url'] title = info.get('title', 'Unknown Title') artist = info.get('uploader', 'Unknown Artist') - duration = info.get('duration', 0) # Duration in seconds + duration = info.get('duration', 0) # Log the song in the user's history update_user_history(interaction.user.id, title, artist) @@ -102,47 +110,18 @@ async def play_audio(interaction: discord.Interaction, query: str): music_queues[guild_id] = [] music_queues[guild_id].append((song_url, title, duration)) + await interaction.followup.send(f"βœ… **Added to Queue:** {title}") # Start playback if nothing is playing - voice_client = voice_clients[guild_id] - if not voice_client.is_playing(): + if not voice_clients[guild_id].is_playing(): await play_next(interaction) - # Use the existing embed or create a new one - if guild_id in now_playing_messages and now_playing_messages[guild_id]: - try: - message = await interaction.channel.fetch_message(now_playing_messages[guild_id]) - embed = discord.Embed( - title="🎡 Now Playing", - description=f"**{title}** by {artist}\n\n`00:00 / {time.strftime('%M:%S', time.gmtime(duration))}`", - color=discord.Color.blue() - ) - view = PlaybackControls(interaction, guild_id) - await message.edit(embed=embed, view=view) - return - except discord.NotFound: - pass # If the original embed is deleted, send a new one - - # Send the "Now Playing" embed - embed = discord.Embed( - title="🎡 Now Playing", - description=f"**{title}** by {artist}\n\n`00:00 / {time.strftime('%M:%S', time.gmtime(duration))}`", - color=discord.Color.blue() - ) - view = PlaybackControls(interaction, guild_id) - message = await interaction.followup.send(embed=embed, view=view) - now_playing_messages[guild_id] = message.id # Track the message ID - - # Start updating the progress bar - start_time = time.time() - asyncio.create_task(update_progress_bar(interaction, duration, start_time)) - async def play_next(interaction: discord.Interaction): guild_id = interaction.guild.id + # If no songs are left in the queue if guild_id not in music_queues or not music_queues[guild_id]: - # If no songs are left in the queue, update the embed to reflect this if guild_id in now_playing_messages and now_playing_messages[guild_id]: try: message = await interaction.channel.fetch_message(now_playing_messages[guild_id]) @@ -153,7 +132,6 @@ async def play_next(interaction: discord.Interaction): ) await message.edit(embed=embed, view=None) # Remove playback controls except discord.NotFound: - # If the message was deleted, do nothing pass return @@ -161,6 +139,10 @@ async def play_next(interaction: discord.Interaction): song_url, title, duration = music_queues[guild_id].pop(0) current_tracks[guild_id] = (song_url, title) + # Stop any existing progress bar task + if guild_id in progress_tasks: + progress_tasks[guild_id].cancel() + # Prepare FFmpeg options ffmpeg_options = { 'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', @@ -187,28 +169,57 @@ async def play_next(interaction: discord.Interaction): description=f"**{title}**\n\n`00:00 / {time.strftime('%M:%S', time.gmtime(duration))}`", color=discord.Color.green() ) + view = PlaybackControls(interaction, guild_id) - # Check if the "Now Playing" message already exists if guild_id in now_playing_messages and now_playing_messages[guild_id]: try: message = await interaction.channel.fetch_message(now_playing_messages[guild_id]) - await message.edit(embed=embed) + await message.edit(embed=embed, view=view) except discord.NotFound: - # If the message was deleted, send a new one - message = await interaction.followup.send(embed=embed) + message = await interaction.followup.send(embed=embed, view=view) now_playing_messages[guild_id] = message.id else: - # Send a new "Now Playing" embed - message = await interaction.followup.send(embed=embed) + message = await interaction.followup.send(embed=embed, view=view) now_playing_messages[guild_id] = message.id - # Start updating the progress bar with the song duration + + # Start the progress bar task start_time = time.time() - asyncio.create_task(update_progress_bar(interaction, duration, start_time)) + progress_tasks[guild_id] = asyncio.create_task(update_progress_bar(interaction, duration, start_time)) except Exception as e: await interaction.followup.send(f"❌ **Error:** Could not play the next song. {str(e)}") +async def update_progress_bar(interaction: discord.Interaction, duration: int, start_time: float): + guild_id = interaction.guild.id + + try: + while guild_id in voice_clients and voice_clients[guild_id].is_playing(): + elapsed_time = time.time() - start_time + progress = min(elapsed_time / duration, 1.0) + progress_blocks = int(progress * 20) + progress_bar = "β–¬" * progress_blocks + "πŸ”˜" + "β–¬" * (20 - progress_blocks) + elapsed_time_str = time.strftime("%M:%S", time.gmtime(elapsed_time)) + duration_str = time.strftime("%M:%S", time.gmtime(duration)) + + embed = discord.Embed( + title="🎡 Now Playing", + description=f"**{current_tracks[guild_id][1]}**\n\n{progress_bar} `{elapsed_time_str} / {duration_str}`", + color=discord.Color.green() + ) + + if guild_id in now_playing_messages and now_playing_messages[guild_id]: + message_id = now_playing_messages[guild_id] + try: + message = await interaction.channel.fetch_message(message_id) + await message.edit(embed=embed) + except discord.NotFound: + break + await asyncio.sleep(2) + except asyncio.CancelledError: + pass + + async def stop_audio(interaction: discord.Interaction): guild_id = interaction.guild.id @@ -288,145 +299,87 @@ async def leave_voice(interaction: discord.Interaction): class PlaybackControls(discord.ui.View): - def __init__(self, interaction, guild_id): - super().__init__(timeout=None) # Persistent buttons + def __init__(self, interaction: discord.Interaction, guild_id: int): + super().__init__(timeout=None) self.interaction = interaction self.guild_id = guild_id - @discord.ui.button(label="⏸️ Pause", style=discord.ButtonStyle.primary) - async def pause_button(self, interaction: discord.Interaction, button: discord.ui.Button): + @discord.ui.button(label="Pause", style=discord.ButtonStyle.primary) + async def pause(self, interaction: discord.Interaction, button: discord.ui.Button): if self.guild_id in voice_clients and voice_clients[self.guild_id].is_playing(): voice_clients[self.guild_id].pause() - button.label = "▢️ Resume" - await interaction.response.edit_message(content="⏸️ **Paused playback.**", view=self) - elif self.guild_id in voice_clients and voice_clients[self.guild_id].is_paused(): - voice_clients[self.guild_id].resume() - button.label = "⏸️ Pause" - await interaction.response.edit_message(content="▢️ **Resumed playback.**", view=self) - - @discord.ui.button(label="⏭️ Skip", style=discord.ButtonStyle.secondary) - async def skip_button(self, interaction: discord.Interaction, button: discord.ui.Button): - if self.guild_id in voice_clients and voice_clients[self.guild_id].is_playing(): - voice_clients[self.guild_id].stop() # Stop triggers `play_next` - await interaction.response.send_message("⏭️ **Skipped to the next song.**", ephemeral=True) + await interaction.response.send_message("⏸️ **Playback paused.**", ephemeral=True) else: - await interaction.response.send_message("❌ **No song is currently playing to skip.**", ephemeral=True) + await interaction.response.send_message("⚠️ **No audio is playing.**", ephemeral=True) - @discord.ui.button(label="πŸ›‘ Stop", style=discord.ButtonStyle.danger) - async def stop_button(self, interaction: discord.Interaction, button: discord.ui.Button): - if self.guild_id in voice_clients and voice_clients[self.guild_id].is_playing(): + @discord.ui.button(label="Resume", style=discord.ButtonStyle.green) + async def resume(self, interaction: discord.Interaction, button: discord.ui.Button): + if self.guild_id in voice_clients and voice_clients[self.guild_id].is_paused(): + voice_clients[self.guild_id].resume() + await interaction.response.send_message("▢️ **Playback resumed.**", ephemeral=True) + else: + await interaction.response.send_message("⚠️ **No audio is paused.**", ephemeral=True) + + @discord.ui.button(label="Stop", style=discord.ButtonStyle.danger) + async def stop(self, interaction: discord.Interaction, button: discord.ui.Button): + if self.guild_id in voice_clients: voice_clients[self.guild_id].stop() music_queues[self.guild_id] = [] # Clear the queue - await interaction.response.send_message("πŸ›‘ **Stopped playback and cleared the queue.**", ephemeral=True) + await interaction.response.send_message("⏹️ **Playback stopped and queue cleared.**", ephemeral=True) else: - await interaction.response.send_message("❌ **No song is currently playing to stop.**", ephemeral=True) + await interaction.response.send_message("⚠️ **No audio is playing.**", ephemeral=True) - @discord.ui.button(label="πŸ”„ Repeat: Off", style=discord.ButtonStyle.success) - async def repeat_button(self, interaction: discord.Interaction, button: discord.ui.Button): + @discord.ui.button(label="Skip", style=discord.ButtonStyle.secondary) + async def skip(self, interaction: discord.Interaction, button: discord.ui.Button): + if self.guild_id in voice_clients and voice_clients[self.guild_id].is_playing(): + voice_clients[self.guild_id].stop() # Trigger play_next + await interaction.response.send_message("⏭️ **Skipped to the next track.**", ephemeral=True) + else: + await interaction.response.send_message("⚠️ **No audio is playing.**", ephemeral=True) + + @discord.ui.button(label="Repeat: Off", style=discord.ButtonStyle.success) + async def repeat(self, interaction: discord.Interaction, button: discord.ui.Button): current_mode = repeat_modes.get(self.guild_id, "off") new_mode = "one" if current_mode == "off" else "all" if current_mode == "one" else "off" repeat_modes[self.guild_id] = new_mode - button.label = f"πŸ”„ Repeat: {new_mode.capitalize()}" - await interaction.response.edit_message(content=f"πŸ”„ **Repeat mode set to:** {new_mode.capitalize()}", view=self) + button.label = f"Repeat: {'One' if new_mode == 'one' else 'All' if new_mode == 'all' else 'Off'}" + await interaction.response.edit_message(view=self) - @discord.ui.button(label="πŸ”ˆ Volume Down", style=discord.ButtonStyle.secondary) - async def volume_down(self, interaction: discord.Interaction, button: discord.ui.Button): - current_volume = volumes.get(self.guild_id, default_volume) - new_volume = max(0.0, current_volume - 0.1) # Decrease volume by 10% - volumes[self.guild_id] = new_volume - - # Apply volume to the current source - if self.guild_id in voice_clients and voice_clients[self.guild_id].is_playing(): - source = voice_clients[self.guild_id].source - if isinstance(source, discord.PCMVolumeTransformer): - source.volume = new_volume - - await interaction.response.send_message(f"πŸ”ˆ **Volume decreased to {int(new_volume * 100)}%.**", ephemeral=True) - - @discord.ui.button(label="πŸ”Š Volume Up", style=discord.ButtonStyle.primary) + @discord.ui.button(label="Volume Up", style=discord.ButtonStyle.primary) async def volume_up(self, interaction: discord.Interaction, button: discord.ui.Button): - current_volume = volumes.get(self.guild_id, default_volume) - new_volume = min(1.0, current_volume + 0.1) # Increase volume by 10% - volumes[self.guild_id] = new_volume - - # Apply volume to the current source - if self.guild_id in voice_clients and voice_clients[self.guild_id].is_playing(): - source = voice_clients[self.guild_id].source - if isinstance(source, discord.PCMVolumeTransformer): - source.volume = new_volume - - await interaction.response.send_message(f"πŸ”Š **Volume increased to {int(new_volume * 100)}%.**", ephemeral=True) - - @discord.ui.button(label="πŸ”‡ Mute/Unmute", style=discord.ButtonStyle.danger) - async def mute_toggle(self, interaction: discord.Interaction, button: discord.ui.Button): - current_volume = volumes.get(self.guild_id, default_volume) - - # Toggle mute - if current_volume > 0: - volumes[self.guild_id] = 0.0 - button.label = "πŸ”Š Unmute" + if self.guild_id in voice_clients: + volume = volumes.get(self.guild_id, default_volume) + new_volume = min(volume + 0.1, 2.0) # Max volume 200% + volumes[self.guild_id] = new_volume + voice_clients[self.guild_id].source.volume = new_volume + await interaction.response.send_message(f"πŸ”Š **Volume increased to {int(new_volume * 100)}%.**", ephemeral=True) else: - volumes[self.guild_id] = default_volume - button.label = "πŸ”‡ Mute" + await interaction.response.send_message("⚠️ **No audio is playing.**", ephemeral=True) - # Apply volume to the current source - if self.guild_id in voice_clients and voice_clients[self.guild_id].is_playing(): - source = voice_clients[self.guild_id].source - if isinstance(source, discord.PCMVolumeTransformer): - source.volume = volumes[self.guild_id] + @discord.ui.button(label="Volume Down", style=discord.ButtonStyle.primary) + async def volume_down(self, interaction: discord.Interaction, button: discord.ui.Button): + if self.guild_id in voice_clients: + volume = volumes.get(self.guild_id, default_volume) + new_volume = max(volume - 0.1, 0.1) # Min volume 10% + volumes[self.guild_id] = new_volume + voice_clients[self.guild_id].source.volume = new_volume + await interaction.response.send_message(f"πŸ”‰ **Volume decreased to {int(new_volume * 100)}%.**", ephemeral=True) + else: + await interaction.response.send_message("⚠️ **No audio is playing.**", ephemeral=True) - status = "muted" if volumes[self.guild_id] == 0.0 else "unmuted" - await interaction.response.edit_message(content=f"πŸ”‡ **Volume {status}.**", view=self) - - -async def update_progress_bar(interaction: discord.Interaction, duration: int, start_time: float): - """Updates the progress bar in the 'Now Playing' embed.""" - guild_id = interaction.guild.id - - while guild_id in voice_clients and voice_clients[guild_id].is_playing(): - elapsed_time = time.time() - start_time - - # Calculate progress - progress = min(elapsed_time / duration, 1.0) - progress_blocks = int(progress * 20) # 20 blocks for the bar - progress_bar = "β–¬" * progress_blocks + "πŸ”˜" + "β–¬" * (20 - progress_blocks) - - elapsed_time_str = time.strftime("%M:%S", time.gmtime(elapsed_time)) - duration_str = time.strftime("%M:%S", time.gmtime(duration)) - - embed = discord.Embed( - title="🎡 Now Playing", - description=f"**{current_tracks[guild_id][1]}**\n\n{progress_bar} `{elapsed_time_str} / {duration_str}`", - color=discord.Color.green() - ) - - # Update the original "Now Playing" message - try: - message_id = now_playing_messages.get(guild_id) - if message_id: - message = await interaction.channel.fetch_message(message_id) - await message.edit(embed=embed) - else: - break - except discord.NotFound: - break # Stop if the message is deleted or invalid - - await asyncio.sleep(2) # Update every 2 seconds - - # When playback ends, clear the progress bar - if not voice_clients[guild_id].is_playing(): - try: - message_id = now_playing_messages.pop(guild_id, None) - if message_id: - message = await interaction.channel.fetch_message(message_id) - embed = discord.Embed( - title="🎡 Queue Finished", - description="Amber is now idle. Add more songs to the queue to keep the music going!", - color=discord.Color.red() - ) - await message.edit(embed=embed, view=None) # Remove playback controls - except discord.NotFound: - pass + @discord.ui.button(label="Mute/Unmute", style=discord.ButtonStyle.danger) + async def mute(self, interaction: discord.Interaction, button: discord.ui.Button): + if self.guild_id in voice_clients: + muted = volumes.get(self.guild_id) == 0.0 + new_volume = default_volume if muted else 0.0 + volumes[self.guild_id] = new_volume + voice_clients[self.guild_id].source.volume = new_volume + await interaction.response.send_message( + "πŸ”‡ **Muted.**" if new_volume == 0.0 else "πŸ”ˆ **Unmuted.**", + ephemeral=True, + ) + else: + await interaction.response.send_message("⚠️ **No audio is playing.**", ephemeral=True) # ---------- Recommendations ----------