From 82e7387379aa62dccc13c528e21539d93cf28791 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 22 Jan 2025 22:36:19 -0500 Subject: [PATCH] Fixed up the embeds so it's a single embed now, added a progress bar that tracks the song progress down to two seconds. --- audio.py | 208 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 148 insertions(+), 60 deletions(-) diff --git a/audio.py b/audio.py index 9f7954b..60c2417 100644 --- a/audio.py +++ b/audio.py @@ -5,6 +5,7 @@ import os import json import random import requests +import time from dotenv import load_dotenv load_dotenv() @@ -14,6 +15,7 @@ music_queues = {} # Per-guild song queue 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 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 @@ -44,54 +46,53 @@ def update_user_history(user_id, song_title, artist): # ---------- Playback Functions ---------- 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 None - - channel = interaction.user.voice.channel + """Joins the user's voice channel.""" guild_id = interaction.guild.id + channel = interaction.user.voice.channel - if guild_id not in voice_clients: - voice_clients[guild_id] = await channel.connect() + if guild_id in voice_clients and voice_clients[guild_id].channel == channel: + # Already in the same channel + return True - # Ensure the response is completed - if interaction.response.is_done(): - await interaction.followup.send(f"✅ **Joined {channel.name}.**") - else: - await interaction.response.send_message(f"✅ **Joined {channel.name}.**") - return voice_clients[guild_id] + try: + voice_client = await channel.connect() + voice_clients[guild_id] = voice_client + return True + except Exception as e: + await interaction.followup.send(f"❌ **Error:** Could not join the voice channel. {str(e)}") + return False async def play_audio(interaction: discord.Interaction, query: str): guild_id = interaction.guild.id - # Defer the response if not already done + # Defer the interaction if not already responded if not interaction.response.is_done(): await interaction.response.defer() # Ensure Amber is connected to a voice channel if guild_id not in voice_clients or not voice_clients[guild_id].is_connected(): - voice_client = await join_voice(interaction) - if voice_client is None: # If join failed + joined = await join_voice(interaction) + if not joined: + await interaction.followup.send("❌ **Error:** Could not join the voice channel.") return - else: - voice_client = voice_clients[guild_id] # Search for the song on YouTube ydl_opts = { 'format': 'bestaudio/best', 'quiet': True, } - with yt_dlp.YoutubeDL(ydl_opts) as ydl: - try: + try: + with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(f"ytsearch:{query}", download=False)['entries'][0] - except Exception as e: - await interaction.followup.send(f"❌ **Error:** Could not find or play the requested audio. {str(e)}") - return + except Exception as e: + await interaction.followup.send(f"❌ **Error:** Could not find or play the requested audio. {str(e)}") + return song_url = info['url'] title = info.get('title', 'Unknown Title') artist = info.get('uploader', 'Unknown Artist') + duration = info.get('duration', 0) # Duration in seconds # Log the song in the user's history update_user_history(interaction.user.id, title, artist) @@ -100,47 +101,64 @@ async def play_audio(interaction: discord.Interaction, query: str): 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}") + music_queues[guild_id].append((song_url, title, duration)) - # If nothing is playing, start playback + # Start playback if nothing is playing + voice_client = voice_clients[guild_id] if not voice_client.is_playing(): await play_next(interaction) - # Send "Now Playing" embed with controls + # 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}", + 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 interaction.followup.send(embed=embed, view=view) + 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 guild_id not in music_queues: - music_queues[guild_id] = [] - - # Handle Repeat One - if repeat_modes.get(guild_id) == "one" and current_tracks.get(guild_id): - song_url, title = current_tracks[guild_id] - music_queues[guild_id].insert(0, (song_url, title)) # Re-add the current song to the front of the queue - # Skip sending an alert for the repeated song - voice_client = voice_clients[guild_id] - elif repeat_modes.get(guild_id) == "all" and not music_queues[guild_id]: - # Recycle the current track into the queue - if current_tracks.get(guild_id): - music_queues[guild_id].append(current_tracks[guild_id]) - - # If no songs are left in the queue - if not music_queues[guild_id]: - await interaction.followup.send("❌ **No more songs 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]) + 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: + # If the message was deleted, do nothing + pass return # Get the next song from the queue - song_url, title = music_queues[guild_id].pop(0) + song_url, title, duration = music_queues[guild_id].pop(0) current_tracks[guild_id] = (song_url, title) # Prepare FFmpeg options @@ -163,16 +181,32 @@ async def play_next(interaction: discord.Interaction): ), ) - # Only alert if the song is new - if repeat_modes.get(guild_id) != "one": - embed = discord.Embed( - title="🎵 Now Playing", - description=f"**{title}**", - color=discord.Color.green() - ) - await interaction.followup.send(embed=embed) + # Update or create the "Now Playing" embed + embed = discord.Embed( + title="🎵 Now Playing", + description=f"**{title}**\n\n`00:00 / {time.strftime('%M:%S', time.gmtime(duration))}`", + color=discord.Color.green() + ) + + # 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) + except discord.NotFound: + # If the message was deleted, send a new one + message = await interaction.followup.send(embed=embed) + now_playing_messages[guild_id] = message.id + else: + # Send a new "Now Playing" embed + message = await interaction.followup.send(embed=embed) + now_playing_messages[guild_id] = message.id + + # Start updating the progress bar with the song duration + start_time = time.time() + asyncio.create_task(update_progress_bar(interaction, duration, start_time)) except Exception as e: - await interaction.followup.send(f"❌ **Error:** Failed to play the next song. {str(e)}") + await interaction.followup.send(f"❌ **Error:** Could not play the next song. {str(e)}") async def stop_audio(interaction: discord.Interaction): @@ -182,11 +216,15 @@ async def stop_audio(interaction: discord.Interaction): 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.**") + await interaction.response.send_message("⏹️ **Playback stopped.**") + else: + await interaction.response.send_message("⚠️ **Nothing is playing.**") + + # Clear the queue but do not disconnect + music_queues[guild_id] = [] + await interaction.followup.send("🗑️ **Queue cleared. Amber will remain idle in the voice channel.**") else: - await interaction.response.send_message("❌ **No music is playing.**") + await interaction.response.send_message("⚠️ **Amber is not connected to a voice channel.**") async def pause_audio(interaction: discord.Interaction): @@ -341,6 +379,56 @@ class PlaybackControls(discord.ui.View): 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 + + # ---------- Recommendations ---------- def fetch_related_songs(song_title, artist): """