From fa2afbdd9984ac05e737e3b69a947ba6c6a13579 Mon Sep 17 00:00:00 2001 From: Top1055 <123alexfeetham@gmail.com> Date: Thu, 27 Nov 2025 23:04:01 +0000 Subject: [PATCH] new help command, fixed queue and loop display, thumbnails and urls added to database --- bot.py | 15 +- cogs/music/queue.py | 505 ++++++++++++-------------------------------- cogs/music/util.py | 236 ++++++++++++--------- help.py | 177 ++++++++++------ main.py | 5 +- 5 files changed, 393 insertions(+), 545 deletions(-) diff --git a/bot.py b/bot.py index 5e4e1d8..4456810 100644 --- a/bot.py +++ b/bot.py @@ -1,15 +1,22 @@ from discord.ext import commands from discord.ext import tasks -import config from cogs.music.main import music +from help import GroovyHelp # Import the new Help Cog cogs = [ - music - ] + music, + GroovyHelp +] -class Astro(commands.Bot): +class Groovy(commands.Bot): + def __init__(self, *args, **kwargs): + # We force help_command to None because we are using a custom Cog for it + # But we pass all other args (like command_prefix) to the parent + super().__init__(*args, help_command=None, **kwargs) async def on_ready(self): + import config # Imported here to avoid circular dependencies if any + # Set status await self.change_presence(activity=config.get_status()) diff --git a/cogs/music/queue.py b/cogs/music/queue.py index eb34e54..62f9266 100644 --- a/cogs/music/queue.py +++ b/cogs/music/queue.py @@ -1,6 +1,7 @@ from http import server import sqlite3 import random +import time import discord import asyncio @@ -109,67 +110,55 @@ def initialize_tables(): # Create servers table if it doesn't exist cursor.execute('''CREATE TABLE IF NOT EXISTS servers ( - server_id TEXT PRIMARY KEY, - is_playing INTEGER DEFAULT 0, - song_name TEXT, - loop_mode TEXT DEFAULT 'off', - volume INTEGER DEFAULT 100, - effect TEXT DEFAULT 'none', - song_start_time REAL DEFAULT 0, - song_duration INTEGER DEFAULT 0 - );''') + server_id TEXT PRIMARY KEY, + is_playing INTEGER DEFAULT 0, + song_name TEXT, + song_url TEXT, + song_thumbnail TEXT, + loop_mode TEXT DEFAULT 'off', + volume INTEGER DEFAULT 100, + effect TEXT DEFAULT 'none', + song_start_time REAL DEFAULT 0, + song_duration INTEGER DEFAULT 0 + );''') # Set all to not playing cursor.execute("UPDATE servers SET is_playing = 0;") # Add new columns if they don't exist (for existing databases) - try: - cursor.execute("ALTER TABLE servers ADD COLUMN loop_mode TEXT DEFAULT 'off';") - except sqlite3.OperationalError: - pass # Column already exists + # Migrations for existing databases + columns = [ + ("loop_mode", "TEXT DEFAULT 'off'"), + ("volume", "INTEGER DEFAULT 100"), + ("effect", "TEXT DEFAULT 'none'"), + ("song_start_time", "REAL DEFAULT 0"), + ("song_duration", "INTEGER DEFAULT 0"), + ("song_thumbnail", "TEXT DEFAULT ''"), + ("song_url", "TEXT DEFAULT ''") # NEW + ] - try: - cursor.execute("ALTER TABLE servers ADD COLUMN volume INTEGER DEFAULT 100;") - except sqlite3.OperationalError: - pass # Column already exists + for col_name, col_type in columns: + try: + cursor.execute(f"ALTER TABLE servers ADD COLUMN {col_name} {col_type};") + except sqlite3.OperationalError: + pass - try: - cursor.execute("ALTER TABLE servers ADD COLUMN effect TEXT DEFAULT 'none';") - except sqlite3.OperationalError: - pass # Column already exists - - try: - cursor.execute("ALTER TABLE servers ADD COLUMN song_start_time REAL DEFAULT 0;") - except sqlite3.OperationalError: - pass # Column already exists - - try: - cursor.execute("ALTER TABLE servers ADD COLUMN song_duration INTEGER DEFAULT 0;") - except sqlite3.OperationalError: - pass # Column already exists - - # Create queue table if it doesn't exist cursor.execute('''CREATE TABLE IF NOT EXISTS songs ( server_id TEXT NOT NULL, song_link TEXT, queued_by TEXT, position INTEGER NOT NULL, - title TEXT, thumbnail TEXT, duration INTEGER, - PRIMARY KEY (position), FOREIGN KEY (server_id) REFERENCES servers(server_id) );''') - # Clear all entries - cursor.execute("DELETE FROM songs;") - # Commit the changes and close the connection + cursor.execute("DELETE FROM songs;") conn.commit() conn.close() - # Queue a song in the db async def add_song(server_id, details, queued_by): # Connect to db @@ -181,39 +170,13 @@ async def add_song(server_id, details, queued_by): max_order_num = await get_max(server_id, cursor) + 1 if isinstance(details, str): - cursor.execute(""" - INSERT INTO songs (server_id, - song_link, - queued_by, - position, - title, - thumbnail, - duration) - VALUES (?, ?, ?, ?, ?, ?, ?) - """, (server_id, - "Not grabbed", - queued_by, - max_order_num, - details, - "Unkown", - "Unkown")) + # Fallback for raw strings + cursor.execute("""INSERT INTO songs VALUES (?, ?, ?, ?, ?, ?, ?)""", + (server_id, "Not grabbed", queued_by, max_order_num, details, "Unknown", 0)) else: - cursor.execute(""" - INSERT INTO songs (server_id, - song_link, - queued_by, - position, - title, - thumbnail, - duration) - VALUES (?, ?, ?, ?, ?, ?, ?) - """, (server_id, - details['url'], - queued_by, - max_order_num, - details['title'], - details['thumbnail'], - details['duration'])) + # Save exact duration and thumbnail from the start + cursor.execute("""INSERT INTO songs VALUES (?, ?, ?, ?, ?, ?, ?)""", + (server_id, details['url'], queued_by, max_order_num, details['title'], details['thumbnail'], details['duration'])) conn.commit() conn.close() @@ -235,39 +198,35 @@ async def pop(server_id, ignore=False, skip_mode=False): # JUST INCASE! await add_server(server_id, cursor, conn) - cursor.execute('''SELECT * - FROM songs - WHERE server_id = ? - ORDER BY position - LIMIT 1;''', (server_id,)) + # Fetch info: link(1), title(4), thumbnail(5), duration(6) + cursor.execute('''SELECT * FROM songs WHERE server_id = ? ORDER BY position LIMIT 1;''', (server_id,)) result = cursor.fetchone() - conn.commit() conn.close() - if result == None: + if result is None: return None elif ignore: await mark_song_as_finished(server_id, result[3]) return None elif result[1] == "Not grabbed": - # Fetch song info - song = await search_song(result[4]) - if song == []: + # Lazy load logic + song_list = await search_song(result[4]) + if not song_list: return None - else: - song = song[0] + song = song_list[0] - await set_current_song(server_id, song['title'], song.get('duration', 0)) + await set_current_song(server_id, song['title'], song.get('thumbnail', ''), song.get('duration', 0)) # Check loop mode before removing loop_mode = await get_loop_mode(server_id) if loop_mode != 'song': # Only remove if not looping song await mark_song_as_finished(server_id, result[3]) - return song['url'] - await set_current_song(server_id, result[4], result[6]) # result[6] is duration + # Pre-grabbed logic (Standard) + # result[1] is url, result[5] is thumbnail, result[6] is duration + await set_current_song(server_id, result[4], result[1], result[5], result[6]) # Check loop mode before removing loop_mode = await get_loop_mode(server_id) @@ -276,21 +235,12 @@ async def pop(server_id, ignore=False, skip_mode=False): return result[1] - # Add server to db if first time queuing async def add_server(server_id, cursor, conn): - # Check if the server exists - cursor.execute('''SELECT COUNT(*) - FROM servers - WHERE server_id = ?''', (server_id,)) - - result = cursor.fetchone() - server_exists = result[0] > 0 - - # If the server doesn't exist, add it - if not server_exists: - cursor.execute('''INSERT INTO servers (server_id, loop_mode, volume, effect) - VALUES (?, 'off', 100, 'none')''', (server_id,)) + cursor.execute('SELECT COUNT(*) FROM servers WHERE server_id = ?', (server_id,)) + if cursor.fetchone()[0] == 0: + cursor.execute('''INSERT INTO servers (server_id, loop_mode, volume, effect, song_thumbnail, song_url) + VALUES (?, 'off', 100, 'none', '', '')''', (server_id,)) conn.commit() @@ -311,375 +261,215 @@ async def mark_song_as_finished(server_id, order_num): # set the current playing song of the server -async def set_current_song(server_id, title, duration=0): - # Connect to the database +async def set_current_song(server_id, title, url, thumbnail="", duration=0): conn = sqlite3.connect(db_path) cursor = conn.cursor() - - import time start_time = time.time() - cursor.execute(''' UPDATE servers - SET song_name = ?, song_start_time = ?, song_duration = ? - WHERE server_id = ?''', - (title, start_time, duration, server_id)) + # Ensure duration is an integer + try: + duration = int(duration) + except: + duration = 0 + + cursor.execute(''' UPDATE servers + SET song_name = ?, song_url = ?, song_thumbnail = ?, song_start_time = ?, song_duration = ? + WHERE server_id = ?''', + (title, url, thumbnail, start_time, duration, server_id)) - # Close connection conn.commit() conn.close() - +# Returns dictionary with title and thumbnail async def get_current_song(server_id): - # Connect to the database conn = sqlite3.connect(db_path) cursor = conn.cursor() - cursor.execute(''' SELECT song_name - FROM servers - WHERE server_id = ? - LIMIT 1;''', - (server_id,)) - + cursor.execute(''' SELECT song_name, song_thumbnail, song_url FROM servers WHERE server_id = ? LIMIT 1;''', (server_id,)) result = cursor.fetchone() - - # Close connection conn.commit() conn.close() - return result[0] if result else "Nothing" - + if result: + return {'title': result[0], 'thumbnail': result[1], 'url': result[2]} + return {'title': "Nothing", 'thumbnail': None, 'url': ''} async def get_current_progress(server_id): - """Get current playback progress (elapsed, duration, percentage)""" conn = sqlite3.connect(db_path) cursor = conn.cursor() - - cursor.execute('''SELECT song_start_time, song_duration, is_playing - FROM servers - WHERE server_id = ? - LIMIT 1;''', - (server_id,)) - + cursor.execute('''SELECT song_start_time, song_duration, is_playing FROM servers WHERE server_id = ? LIMIT 1;''', (server_id,)) result = cursor.fetchone() - conn.close() + conn.close() # Close quickly - if not result or result[2] == 0: # Not playing + if not result or result[2] == 0: return 0, 0, 0.0 start_time, duration, _ = result - if duration == 0: + if duration is None or duration == 0: return 0, 0, 0.0 - import time elapsed = int(time.time() - start_time) - elapsed = min(elapsed, duration) # Cap at duration + elapsed = min(elapsed, duration) percentage = (elapsed / duration) * 100 if duration > 0 else 0 return elapsed, duration, percentage - return result[0] if result else "Nothing" - - -# Grab max order from server async def get_max(server_id, cursor): - cursor.execute(f""" - SELECT MAX(position) - FROM songs - WHERE server_id = ? - """, (server_id,)) + cursor.execute("SELECT MAX(position) FROM songs WHERE server_id = ?", (server_id,)) result = cursor.fetchone() + return result[0] if result[0] is not None else -1 - # Highnest number or 0 - max_order_num = result[0] if result[0] is not None else -1 - - return max_order_num - - -# Sets the playing variable in a server to true or false -async def update_server(server_id, playing: bool): - # Connect to database +async def update_server(server_id, playing): conn = sqlite3.connect(db_path) cursor = conn.cursor() - - # add server to db if not present await add_server(server_id, cursor, conn) - - value = 1 if playing else 0 - - # Update field - cursor.execute("""UPDATE servers - SET is_playing = ? - WHERE server_id = ? - """, (value, server_id)) - - # Close connection + val = 1 if playing else 0 + cursor.execute("UPDATE servers SET is_playing = ? WHERE server_id = ?", (val, server_id)) conn.commit() conn.close() - async def is_server_playing(server_id): - # Connect to db conn = sqlite3.connect(db_path) cursor = conn.cursor() - - # add server to db if not present await add_server(server_id, cursor, conn) - - cursor.execute("""SELECT is_playing - FROM servers - WHERE server_id = ?""", - (server_id,)) - - result = cursor.fetchone() - - conn.commit() + cursor.execute("SELECT is_playing FROM servers WHERE server_id = ?", (server_id,)) + res = cursor.fetchone() conn.close() + return True if res[0] == 1 else False - return True if result[0] == 1 else False - - -# Delete all songs from a server async def clear(server_id): - # Connect to db conn = sqlite3.connect(db_path) cursor = conn.cursor() - await add_server(server_id, cursor, conn) await update_server(server_id, False) - - # Delete all songs from the server - cursor.execute('''DELETE FROM songs WHERE server_id = ?''', (server_id,)) - + cursor.execute("DELETE FROM songs WHERE server_id = ?", (server_id,)) conn.commit() conn.close() - -# Grabs all songs from a server for display purposes async def grab_songs(server_id): - # Connect to db conn = sqlite3.connect(db_path) cursor = conn.cursor() - await add_server(server_id, cursor, conn) - - # Grabs all songs from the server - cursor.execute('''SELECT title, duration, queued_by - FROM songs - WHERE server_id = ? - ORDER BY position - LIMIT 10''', (server_id,)) + cursor.execute("SELECT title, duration, queued_by FROM songs WHERE server_id = ? ORDER BY position LIMIT 10", (server_id,)) songs = cursor.fetchall() - max = await get_max(server_id, cursor) - - conn.commit() + max_pos = await get_max(server_id, cursor) conn.close() + return max_pos, songs - return max, songs - - -# ============= LOOP/SHUFFLE/VOLUME FEATURES ============= - -# Get/Set loop mode +# --- Effects/Loop/Shuffle/Volume (Simplified Paste) --- async def get_loop_mode(server_id): - """Get the current loop mode: 'off', 'song', or 'queue'""" conn = sqlite3.connect(db_path) cursor = conn.cursor() - await add_server(server_id, cursor, conn) - - cursor.execute("""SELECT loop_mode - FROM servers - WHERE server_id = ?""", - (server_id,)) - result = cursor.fetchone() - - conn.commit() + cursor.execute("SELECT loop_mode FROM servers WHERE server_id = ?", (server_id,)) + res = cursor.fetchone() conn.close() - - return result[0] if result else 'off' - + return res[0] if res else 'off' async def set_loop_mode(server_id, mode): - """Set loop mode: 'off', 'song', or 'queue'""" - if mode not in ['off', 'song', 'queue']: - return False - conn = sqlite3.connect(db_path) cursor = conn.cursor() - await add_server(server_id, cursor, conn) - - cursor.execute("""UPDATE servers - SET loop_mode = ? - WHERE server_id = ?""", - (mode, server_id)) - + cursor.execute("UPDATE servers SET loop_mode = ? WHERE server_id = ?", (mode, server_id)) conn.commit() conn.close() - return True - -# Get/Set volume async def get_volume(server_id): - """Get the current volume (0-200)""" conn = sqlite3.connect(db_path) cursor = conn.cursor() - await add_server(server_id, cursor, conn) - - cursor.execute("""SELECT volume - FROM servers - WHERE server_id = ?""", - (server_id,)) - result = cursor.fetchone() - - conn.commit() + cursor.execute("SELECT volume FROM servers WHERE server_id = ?", (server_id,)) + res = cursor.fetchone() conn.close() + return res[0] if res else 100 - return result[0] if result else 100 - - -async def set_volume(server_id, volume): - """Set volume (0-200)""" - volume = max(0, min(200, volume)) # Clamp between 0-200 - +async def set_volume(server_id, vol): conn = sqlite3.connect(db_path) cursor = conn.cursor() - await add_server(server_id, cursor, conn) - - cursor.execute("""UPDATE servers - SET volume = ? - WHERE server_id = ?""", - (volume, server_id)) - + cursor.execute("UPDATE servers SET volume = ? WHERE server_id = ?", (vol, server_id)) conn.commit() conn.close() - return volume + return vol - -# Shuffle the queue async def shuffle_queue(server_id): - """Randomize the order of songs in the queue""" conn = sqlite3.connect(db_path) cursor = conn.cursor() - await add_server(server_id, cursor, conn) - - # Get all songs - cursor.execute('''SELECT position, song_link, queued_by, title, thumbnail, duration - FROM songs - WHERE server_id = ? - ORDER BY position''', (server_id,)) + cursor.execute("SELECT position, song_link, queued_by, title, thumbnail, duration FROM songs WHERE server_id = ? ORDER BY position", (server_id,)) songs = cursor.fetchall() - if len(songs) <= 1: conn.close() - return False # Nothing to shuffle - - # Shuffle the songs (keep positions but randomize order) + return False random.shuffle(songs) - - # Delete all current songs - cursor.execute('''DELETE FROM songs WHERE server_id = ?''', (server_id,)) - - # Re-insert in shuffled order - for i, song in enumerate(songs): - cursor.execute("""INSERT INTO songs (server_id, song_link, queued_by, position, title, thumbnail, duration) - VALUES (?, ?, ?, ?, ?, ?, ?)""", - (server_id, song[1], song[2], i, song[3], song[4], song[5])) - + cursor.execute("DELETE FROM songs WHERE server_id = ?", (server_id,)) + for i, s in enumerate(songs): + cursor.execute("INSERT INTO songs VALUES (?, ?, ?, ?, ?, ?, ?)", (server_id, s[1], s[2], i, s[3], s[4], s[5])) conn.commit() conn.close() return True - -# ============= AUDIO EFFECTS FEATURES ============= - async def get_effect(server_id): - """Get the current audio effect""" conn = sqlite3.connect(db_path) cursor = conn.cursor() - await add_server(server_id, cursor, conn) - - cursor.execute("""SELECT effect - FROM servers - WHERE server_id = ?""", - (server_id,)) - result = cursor.fetchone() - - conn.commit() + cursor.execute("SELECT effect FROM servers WHERE server_id = ?", (server_id,)) + res = cursor.fetchone() conn.close() + return res[0] if res else 'none' - return result[0] if result else 'none' - - -async def set_effect(server_id, effect_name): - """Set the audio effect""" +async def set_effect(server_id, fx): conn = sqlite3.connect(db_path) cursor = conn.cursor() - await add_server(server_id, cursor, conn) - - cursor.execute("""UPDATE servers - SET effect = ? - WHERE server_id = ?""", - (effect_name, server_id)) - + cursor.execute("UPDATE servers SET effect = ? WHERE server_id = ?", (fx, server_id)) conn.commit() conn.close() - return True - def list_all_effects(): - """Return a list of all available effects""" return [ 'none', 'bassboost', 'nightcore', 'slowed', 'earrape', 'deepfry', 'distortion', 'reverse', 'chipmunk', 'demonic', 'underwater', 'robot', '8d', 'vibrato', 'tremolo', 'echo', 'phone', 'megaphone' ] - def get_effect_emoji(effect_name): - """Get emoji representation for each effect""" + # Short list of emoji mappings emojis = { - 'none': 'šŸ”Š', - 'bassboost': 'šŸ”‰šŸ’„', - 'nightcore': 'āš”šŸŽ€', - 'slowed': 'šŸŒšŸ’¤', - 'earrape': 'šŸ’€šŸ“¢', - 'deepfry': 'šŸŸšŸ’„', - 'distortion': 'āš”šŸ”Š', - 'reverse': 'āŖšŸ”„', + 'none': '✨', # Changed to generic Sparkles + 'bassboost': 'šŸ’„', + 'nightcore': '⚔', + 'slowed': '🐢', + 'earrape': 'šŸ’€', + 'deepfry': 'šŸŸ', + 'distortion': 'ć€°ļø', + 'reverse': 'āŖ', 'chipmunk': 'šŸæļø', - 'demonic': 'šŸ˜ˆšŸ”„', - 'underwater': 'šŸŒŠšŸ’¦', + 'demonic': '😈', + 'underwater': '🫧', 'robot': 'šŸ¤–', - '8d': 'šŸŽ§šŸŒ€', + '8d': 'šŸŽ§', 'vibrato': 'ć€°ļø', 'tremolo': 'šŸ“³', - 'echo': 'šŸ—£ļøšŸ’­', + 'echo': 'šŸ—£ļø', 'phone': 'šŸ“ž', - 'megaphone': 'šŸ“¢šŸ“£' + 'megaphone': 'šŸ“£' } - return emojis.get(effect_name, 'šŸ”Š') - + return emojis.get(effect_name, '✨') def get_effect_description(effect_name): - """Get user-friendly description for each effect""" descriptions = { 'none': 'Normal audio', 'bassboost': 'MAXIMUM BASS šŸ”Š', 'nightcore': 'Speed + pitch up (anime vibes)', - 'slowed': 'Slowed + reverb (TikTok aesthetic)', - 'earrape': 'āš ļø Aggressive compression + distortion + clipping āš ļø', - 'deepfry': 'šŸŸ EXTREME bitcrushing + bass (meme audio) šŸŸ', - 'distortion': 'Heavy bitcrushing distortion', + 'slowed': 'Slowed + reverb', + 'earrape': 'āš ļø Loud volume & distortion', + 'deepfry': 'Bits crushed + Bass', + 'distortion': 'Heavy distortion', 'reverse': 'Plays audio BACKWARDS', - 'chipmunk': 'High pitched and fast (Alvin mode)', - 'demonic': 'Low pitched and slow (cursed)', + 'chipmunk': 'High pitched and fast', + 'demonic': 'Low pitched and slow', 'underwater': 'Muffled underwater sound', 'robot': 'Robotic vocoder', '8d': 'Panning audio (use headphones!)', @@ -691,59 +481,40 @@ def get_effect_description(effect_name): } return descriptions.get(effect_name, 'Unknown effect') - -# Play and loop songs in server async def play(ctx): - """Main playback loop - plays songs from queue sequentially with effects""" server_id = ctx.guild.id voice_client = ctx.voice_client - - # Safety check if voice_client is None: await update_server(server_id, False) return - # Wait until current song finishes while voice_client.is_playing(): await asyncio.sleep(0.5) - # Get next song url = await pop(server_id) - - # If no songs left, update status and return if url is None: await update_server(server_id, False) return try: - # Get volume and effect settings - volume_percent = await get_volume(server_id) - volume = volume_percent / 100.0 # Convert to 0.0-2.0 range + vol = await get_volume(server_id) / 100.0 + fx = await get_effect(server_id) + opts = get_effect_options(fx) - current_effect = await get_effect(server_id) - ffmpeg_opts = get_effect_options(current_effect) + src = discord.FFmpegPCMAudio(url, **opts) + src = discord.PCMVolumeTransformer(src, volume=vol) - # Create audio source with effect and volume control - audio_source = discord.FFmpegPCMAudio(url, **ffmpeg_opts) - audio_source = discord.PCMVolumeTransformer(audio_source, volume=volume) - - # Play with callback to continue queue - def after_playing(error): - if error: - print(f"Player error: {error}") - # Schedule the next song in the event loop - if voice_client and not voice_client.is_connected(): - return + def after(e): + if e: print(e) + if voice_client and not voice_client.is_connected(): return coro = play(ctx) fut = asyncio.run_coroutine_threadsafe(coro, ctx.bot.loop) - try: - fut.result() - except Exception as e: - print(f"Error playing next song: {e}") - - voice_client.play(audio_source, after=after_playing) + try: fut.result() + except: pass + voice_client.play(src, after=after) except Exception as e: - print(f"Error starting playback: {e}") - # Try to continue with next song + print(f"Play error: {e}") await play(ctx) + + diff --git a/cogs/music/util.py b/cogs/music/util.py index 9942af3..846064c 100644 --- a/cogs/music/util.py +++ b/cogs/music/util.py @@ -107,72 +107,160 @@ def update_activity(guild_id): # Interactive buttons for queue control class QueueControls(View): def __init__(self, ctx): - super().__init__(timeout=300) # 5 minute timeout + super().__init__(timeout=None) # No timeout allows buttons to stay active longer self.ctx = ctx + async def refresh_message(self, interaction: discord.Interaction): + """Helper to regenerate the embed and edit the message""" + try: + # Generate new embed + embed, view = await generate_queue_ui(self.ctx) + await interaction.response.edit_message(embed=embed, view=view) + except Exception as e: + # Fallback if edit fails + if not interaction.response.is_done(): + await interaction.response.send_message("Refreshed, but something went wrong updating the display.", ephemeral=True) + @discord.ui.button(label="ā­ļø Skip", style=discord.ButtonStyle.primary) async def skip_button(self, interaction: discord.Interaction, button: Button): - if interaction.user != self.ctx.author: - await interaction.response.send_message("āŒ Only the person who requested the queue can use these buttons!", ephemeral=True) + if interaction.user not in self.ctx.voice_client.channel.members: + await interaction.response.send_message("āŒ You must be in the voice channel!", ephemeral=True) return - if self.ctx.voice_client and self.ctx.voice_client.is_playing(): + # Loop logic check + loop_mode = await queue.get_loop_mode(self.ctx.guild.id) + + # Logic mimics the command + if loop_mode == 'song': + # Just restart current song effectively but here we assume standard skip behavior for button + pass + + # Perform the skip + await queue.pop(self.ctx.guild.id, True, skip_mode=True) + if self.ctx.voice_client: self.ctx.voice_client.stop() - await interaction.response.send_message("ā­ļø Skipped!", ephemeral=True) - else: - await interaction.response.send_message("āŒ Nothing is playing!", ephemeral=True) + + # Refresh UI + await self.refresh_message(interaction) @discord.ui.button(label="šŸ”€ Shuffle", style=discord.ButtonStyle.secondary) async def shuffle_button(self, interaction: discord.Interaction, button: Button): - if interaction.user != self.ctx.author: - await interaction.response.send_message("āŒ Only the person who requested the queue can use these buttons!", ephemeral=True) - return - - success = await queue.shuffle_queue(self.ctx.guild.id) - if success: - await interaction.response.send_message("šŸ”€ Queue shuffled!", ephemeral=True) - else: - await interaction.response.send_message("āŒ Not enough songs to shuffle!", ephemeral=True) + await queue.shuffle_queue(self.ctx.guild.id) + await self.refresh_message(interaction) @discord.ui.button(label="šŸ” Loop", style=discord.ButtonStyle.secondary) async def loop_button(self, interaction: discord.Interaction, button: Button): - if interaction.user != self.ctx.author: - await interaction.response.send_message("āŒ Only the person who requested the queue can use these buttons!", ephemeral=True) - return - current_mode = await queue.get_loop_mode(self.ctx.guild.id) - - # Cycle through modes - if current_mode == 'off': - new_mode = 'song' - elif current_mode == 'song': - new_mode = 'queue' - else: - new_mode = 'off' - + new_mode = 'song' if current_mode == 'off' else ('queue' if current_mode == 'song' else 'off') await queue.set_loop_mode(self.ctx.guild.id, new_mode) - - emojis = {'off': 'ā¹ļø', 'song': 'šŸ”‚', 'queue': 'šŸ”'} - messages = { - 'off': 'Loop disabled', - 'song': 'Looping current song šŸ”‚', - 'queue': 'Looping entire queue šŸ”' - } - - await interaction.response.send_message(f"{emojis[new_mode]} {messages[new_mode]}", ephemeral=True) + await self.refresh_message(interaction) @discord.ui.button(label="šŸ—‘ļø Clear", style=discord.ButtonStyle.danger) async def clear_button(self, interaction: discord.Interaction, button: Button): - if interaction.user != self.ctx.author: - await interaction.response.send_message("āŒ Only the person who requested the queue can use these buttons!", ephemeral=True) - return - await queue.clear(self.ctx.guild.id) if self.ctx.voice_client and self.ctx.voice_client.is_playing(): self.ctx.voice_client.stop() + await self.refresh_message(interaction) - await interaction.response.send_message("šŸ—‘ļø Queue cleared!", ephemeral=True) + @discord.ui.button(label="šŸ”„ Refresh", style=discord.ButtonStyle.gray) + async def refresh_button(self, interaction: discord.Interaction, button: Button): + await self.refresh_message(interaction) +async def generate_queue_ui(ctx: Context): + guild_id = ctx.guild.id + server = ctx.guild + + # Fetch all data + n, songs = await queue.grab_songs(guild_id) + current = await queue.get_current_song(guild_id) # Returns title, thumbnail, url + loop_mode = await queue.get_loop_mode(guild_id) + volume = await queue.get_volume(guild_id) + effect = await queue.get_effect(guild_id) + elapsed, duration, percentage = await queue.get_current_progress(guild_id) + + # Configs + effect_emoji = queue.get_effect_emoji(effect) + + # Map loop mode to nicer text + loop_map = { + 'off': {'emoji': 'ā¹ļø', 'text': 'Off'}, + 'song': {'emoji': 'šŸ”‚', 'text': 'Song'}, + 'queue': {'emoji': 'šŸ”', 'text': 'Queue'} + } + loop_info = loop_map.get(loop_mode, loop_map['off']) + loop_emoji = loop_info['emoji'] + loop_text = loop_info['text'] + + # Build Embed + embed = discord.Embed(color=discord.Color.from_rgb(43, 45, 49)) + embed.set_author(name=f"{server.name}'s Queue", icon_url=server.icon.url if server.icon else None) + + # Progress Bar Logic + progress_bar = "" + # Only show bar if duration > 0 (prevents weird 00:00 bars) + if duration > 0: + bar_length = 16 + filled = int((percentage / 100) * bar_length) + # Ensure filled isn't bigger than length + filled = min(filled, bar_length) + bar_str = 'ā–¬' * filled + 'šŸ”˜' + 'ā–¬' * (bar_length - filled) + progress_bar = f"\n`{format_time(elapsed)}` {bar_str} `{format_time(duration)}`" + + # Now Playing Header + title = current.get('title', 'Nothing Playing') + thumb = current.get('thumbnail') + url = current.get('url', '') + + if title == 'Nothing': + description = "## šŸ’¤ Nothing is playing\nUse `/play` to start the party!" + else: + # Create Hyperlink [Title](URL) + # If no URL exists, link to Discord homepage as fallback or just bold + if url and url.startswith("http"): + song_link = f"[{title}]({url})" + else: + song_link = f"**{title}**" + + # CLEARER STATUS LINE: + # Loop: Mode | Effect: Name | Vol: % + description = ( + f"## šŸ’æ Now Playing\n" + f"### {song_link}\n" + f"{loop_emoji} **Loop: {loop_text}** | {effect_emoji} **Effect: {effect}** | šŸ”Š **{volume}%**" + f"{progress_bar}" + ) + + embed.description = description + + # Queue List + if len(songs) > 0: + queue_text = "" + for i, song in enumerate(songs[:10]): + dur = '' if isinstance(song[1], str) else f" | `{format_time(song[1])}`" + queue_text += f"**{i+1}.** {song[0]}{dur}\n" + + embed.add_field(name="ā³ Up Next", value=queue_text, inline=False) + + remaining = (n) - 9 # Approx calculation based on your grabbing logic + if remaining > 0: + embed.set_footer(text=f"Waitlist: {remaining} more songs...") + else: + embed.add_field(name="ā³ Up Next", value="*The queue is empty.*") + + # Set Thumbnail safely + if thumb and isinstance(thumb, str) and thumb.startswith("http"): + embed.set_thumbnail(url=thumb) + elif server.icon: + # Fallback to server icon + embed.set_thumbnail(url=server.icon.url) + + view = QueueControls(ctx) + return embed, view + +# The command entry point calls this +async def display_server_queue(ctx: Context, songs, n): + embed, view = await generate_queue_ui(ctx) + await ctx.send(embed=embed, view=view) # Build a display message for queuing a new song async def queue_message(ctx: Context, data: dict): @@ -188,68 +276,6 @@ async def queue_message(ctx: Context, data: dict): await ctx.send(embed=msg) - -# Build an embed message that shows the queue -async def display_server_queue(ctx: Context, songs, n): - server = ctx.guild - - # Get current settings - current_song = await queue.get_current_song(ctx.guild.id) - loop_mode = await queue.get_loop_mode(ctx.guild.id) - volume = await queue.get_volume(ctx.guild.id) - effect = await queue.get_effect(ctx.guild.id) - elapsed, duration, percentage = await queue.get_current_progress(ctx.guild.id) - - # Build a beautiful embed - embed = discord.Embed( - title=f"šŸŽµ {server.name}'s Queue", - color=discord.Color.blue() - ) - - # Add loop emoji based on mode - loop_emojis = {'off': '', 'song': 'šŸ”‚', 'queue': 'šŸ”'} - loop_emoji = loop_emojis.get(loop_mode, '') - effect_emoji = queue.get_effect_emoji(effect) - - # Progress bar - using Unicode block characters for smooth look - progress_bar = "" - if duration > 0: - bar_length = 20 # Increased from 15 for smoother display - filled = int((percentage / 100) * bar_length) - - # Use block characters: ā–ˆ for filled, ā–‘ for empty - progress_bar = f"\n{'ā–ˆ' * filled}{'ā–‘' * (bar_length - filled)} `{format_time(elapsed)} / {format_time(duration)}`" - - # Now playing section - now_playing = f"### šŸ”Š Now Playing\n**{current_song}** {loop_emoji}{progress_bar}\n" - embed.add_field(name="", value=now_playing, inline=False) - - # Settings section - settings = f"šŸ”Š Volume: **{volume}%** | šŸ” Loop: **{loop_mode}** | {effect_emoji} Effect: **{effect}**" - embed.add_field(name="āš™ļø Settings", value=settings, inline=False) - - # Queue section - if len(songs) > 0: - queue_text = "" - for i, song in enumerate(songs[:10]): # Show max 10 - time = '' if isinstance(song[1], str) else format_time(song[1]) - queue_text += f"`{i + 1}.` **{song[0]}**\nā”” ā±ļø {time} • Queued by {song[2]}\n" - - embed.add_field(name="šŸ“œ Up Next", value=queue_text, inline=False) - - if n > 10: - embed.set_footer(text=f"...and {n - 10} more songs in queue") - else: - embed.add_field(name="šŸ“œ Queue", value="*Queue is empty*", inline=False) - - # Add thumbnail - embed.set_thumbnail(url=server.icon.url if server.icon else None) - - # Send with interactive buttons - view = QueueControls(ctx) - await ctx.send(embed=embed, view=view) - - # Converts seconds into more readable format def format_time(seconds): try: diff --git a/help.py b/help.py index 51f9537..930624a 100644 --- a/help.py +++ b/help.py @@ -1,81 +1,126 @@ -from collections.abc import Mapping -from typing import List import discord -from discord.app_commands import Command from discord.ext import commands -from discord.ext.commands.cog import Cog +from discord import app_commands import config -class AstroHelp(commands.MinimalHelpCommand): +class HelpView(discord.ui.View): + def __init__(self, mapping, ctx): + super().__init__(timeout=180) + self.ctx = ctx + self.mapping = mapping + self.add_item(HelpSelect(mapping, ctx)) - def __init__(self): - super().__init__() - self.command_attrs = { - 'name': "help", - 'aliases': ["commands", "?"], - 'cooldown': commands.CooldownMapping.from_cooldown(2, 5.0, commands.BucketType.user) - } +class HelpSelect(discord.ui.Select): + def __init__(self, mapping, ctx): + self.mapping = mapping + self.ctx = ctx + + options = [ + discord.SelectOption( + label='Home', + description='Back to main menu', + emoji='šŸ ', + value='home' + ) + ] + + # Dynamically add categories (Cogs) + for cog, cmds in mapping.items(): + if not cmds: continue + + # Use attributes safely + cog_name = getattr(cog, "name", "Other").replace("šŸŽ¶ ", "") + emoji = getattr(cog, "emoji", "šŸ“„") + + options.append(discord.SelectOption( + label=cog_name, + description=f"{len(cmds)} commands available", + emoji=emoji, + value=cog_name + )) + super().__init__(placeholder="Select a category...", min_values=1, max_values=1, options=options) - # Called when using help no args - async def send_bot_help(self, mapping: Mapping[Cog, List[Command]]): + async def callback(self, interaction: discord.Interaction): + if interaction.user != self.ctx.author: + return await interaction.response.send_message("Create your own help command with /help", ephemeral=True) - # Our embed message + value = self.values[0] + + if value == 'home': + await interaction.response.edit_message(embed=get_home_embed(self.ctx), view=self.view) + return + + # Find the selected cog + selected_cog = None + selected_commands = [] + + for cog, cmds in self.mapping.items(): + cog_name_clean = getattr(cog, "name", "Other").replace("šŸŽ¶ ", "") + if cog_name_clean == value: + selected_cog = cog + selected_commands = cmds + break + embed = discord.Embed( - title="Help", - color=config.get_color("main")) - embed.add_field(name="", - value="Use `help ` or `help ` for more details", - inline=False) + title=f"{getattr(selected_cog, 'emoji', '')} {value} Commands", + color=config.get_color("main") if hasattr(config, 'get_color') else discord.Color.blue() + ) - embed.set_footer(text=f"Prefix: {self.context.prefix}") + for cmd in selected_commands: + # Get description + desc = cmd.short_doc or cmd.description or "No description provided." + embed.add_field( + name=f"/{cmd.name}", + value=desc, + inline=False + ) + + await interaction.response.edit_message(embed=embed, view=self.view) - # grabs iterable of (Cog, list[Command]) - for cog, commands in mapping.items(): +def get_home_embed(ctx): + embed = discord.Embed( + title="šŸ¤– Bot Help Menu", + description=f"Hello **{ctx.author.name}**! Select a category below to see available commands.", + color=discord.Color.purple() + ) + if ctx.bot.user.avatar: + embed.set_thumbnail(url=ctx.bot.user.avatar.url) + embed.add_field(name="ā„¹ļø How to use", value="Use the dropdown menu below to navigate categories.\nMost commands work as `/command` or `=command`.", inline=False) + return embed - # Grab commands only the user can access - # Safe to ignore warning - filtered = await self.filter_commands(commands, sort=True) +class GroovyHelp(commands.Cog): + def __init__(self, client): + self.client = client + self.name = "Help" + self.emoji = "šŸ†˜" - # For each command we grab the signature - command_signatures = [ - # Rmove prefix and format command name - f"``{self.get_command_signature(c)[1:]}``" for c in filtered] + @commands.hybrid_command(name="help", description="Show the help menu") + async def help(self, ctx: commands.Context): + bot = ctx.bot + mapping = {} + + for cog_name in bot.cogs: + cog = bot.get_cog(cog_name) + + # --- FIXED FILTERING LOGIC --- + visible_cmds = [] + for cmd in cog.get_commands(): + if cmd.hidden: + continue + try: + # Check if user has permission to run this command + if await cmd.can_run(ctx): + visible_cmds.append(cmd) + except commands.CommandError: + continue + # ----------------------------- - # Check if cog has any commands - if command_signatures: + if visible_cmds: + # Sort alphabetically + visible_cmds.sort(key=lambda x: x.name) + mapping[cog] = visible_cmds - # Use get incase cog is None - cog_name = getattr(cog, "name", "No Category") - - # Add cog section to help message - embed.add_field( - name=f"{cog_name}", - value="\n".join(command_signatures), - inline=True) - - # Display message - channel = self.get_destination() - await channel.send(embed=embed) - - - # Help for specific command - async def send_command_help(self, command): - - embed = discord.Embed( - title=self.get_command_signature(command)[1:], - color=config.get_color("main")) - embed.set_footer(text=f"Prefix: {self.context.prefix}") - embed.add_field(name="Description", value=command.help) - - alias = command.aliases - if alias: - embed.add_field(name="Aliases", value=", ".join(alias), inline=False) - - channel = self.get_destination() - await channel.send(embed=embed) - -# TODO add error support see -# https://gist.github.com/InterStella0/b78488fb28cadf279dfd3164b9f0cf96 -# and -# https://gist.github.com/EvieePy/7822af90858ef65012ea500bcecf1612 \ No newline at end of file + embed = get_home_embed(ctx) + view = HelpView(mapping, ctx) + await ctx.send(embed=embed, view=view) diff --git a/main.py b/main.py index 3ddbd97..8b4cac8 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,9 @@ import discord -from bot import Astro +from bot import Groovy import config import help -client = Astro(command_prefix=config.get_prefix(), intents=discord.Intents.all()) -client.help_command = help.AstroHelp() +client = Groovy(command_prefix=config.get_prefix(), intents=discord.Intents.all()) @client.event async def on_voice_state_update(member, before, after): -- 2.49.1