Merge pull request 'new help command, fixed queue and loop display, thumbnails and urls added to database' (#1) from google-help into main

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2025-11-27 23:06:21 +00:00
5 changed files with 393 additions and 545 deletions

15
bot.py
View File

@@ -1,15 +1,22 @@
from discord.ext import commands from discord.ext import commands
from discord.ext import tasks from discord.ext import tasks
import config
from cogs.music.main import music from cogs.music.main import music
from help import GroovyHelp # Import the new Help Cog
cogs = [ 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): async def on_ready(self):
import config # Imported here to avoid circular dependencies if any
# Set status # Set status
await self.change_presence(activity=config.get_status()) await self.change_presence(activity=config.get_status())

View File

@@ -1,6 +1,7 @@
from http import server from http import server
import sqlite3 import sqlite3
import random import random
import time
import discord import discord
import asyncio import asyncio
@@ -109,67 +110,55 @@ def initialize_tables():
# Create servers table if it doesn't exist # Create servers table if it doesn't exist
cursor.execute('''CREATE TABLE IF NOT EXISTS servers ( cursor.execute('''CREATE TABLE IF NOT EXISTS servers (
server_id TEXT PRIMARY KEY, server_id TEXT PRIMARY KEY,
is_playing INTEGER DEFAULT 0, is_playing INTEGER DEFAULT 0,
song_name TEXT, song_name TEXT,
loop_mode TEXT DEFAULT 'off', song_url TEXT,
volume INTEGER DEFAULT 100, song_thumbnail TEXT,
effect TEXT DEFAULT 'none', loop_mode TEXT DEFAULT 'off',
song_start_time REAL DEFAULT 0, volume INTEGER DEFAULT 100,
song_duration INTEGER DEFAULT 0 effect TEXT DEFAULT 'none',
);''') song_start_time REAL DEFAULT 0,
song_duration INTEGER DEFAULT 0
);''')
# Set all to not playing # Set all to not playing
cursor.execute("UPDATE servers SET is_playing = 0;") cursor.execute("UPDATE servers SET is_playing = 0;")
# Add new columns if they don't exist (for existing databases) # Add new columns if they don't exist (for existing databases)
try: # Migrations for existing databases
cursor.execute("ALTER TABLE servers ADD COLUMN loop_mode TEXT DEFAULT 'off';") columns = [
except sqlite3.OperationalError: ("loop_mode", "TEXT DEFAULT 'off'"),
pass # Column already exists ("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: for col_name, col_type in columns:
cursor.execute("ALTER TABLE servers ADD COLUMN volume INTEGER DEFAULT 100;") try:
except sqlite3.OperationalError: cursor.execute(f"ALTER TABLE servers ADD COLUMN {col_name} {col_type};")
pass # Column already exists 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 ( cursor.execute('''CREATE TABLE IF NOT EXISTS songs (
server_id TEXT NOT NULL, server_id TEXT NOT NULL,
song_link TEXT, song_link TEXT,
queued_by TEXT, queued_by TEXT,
position INTEGER NOT NULL, position INTEGER NOT NULL,
title TEXT, title TEXT,
thumbnail TEXT, thumbnail TEXT,
duration INTEGER, duration INTEGER,
PRIMARY KEY (position), PRIMARY KEY (position),
FOREIGN KEY (server_id) REFERENCES servers(server_id) 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.commit()
conn.close() conn.close()
# Queue a song in the db # Queue a song in the db
async def add_song(server_id, details, queued_by): async def add_song(server_id, details, queued_by):
# Connect to db # 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 max_order_num = await get_max(server_id, cursor) + 1
if isinstance(details, str): if isinstance(details, str):
cursor.execute(""" # Fallback for raw strings
INSERT INTO songs (server_id, cursor.execute("""INSERT INTO songs VALUES (?, ?, ?, ?, ?, ?, ?)""",
song_link, (server_id, "Not grabbed", queued_by, max_order_num, details, "Unknown", 0))
queued_by,
position,
title,
thumbnail,
duration)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (server_id,
"Not grabbed",
queued_by,
max_order_num,
details,
"Unkown",
"Unkown"))
else: else:
cursor.execute(""" # Save exact duration and thumbnail from the start
INSERT INTO songs (server_id, cursor.execute("""INSERT INTO songs VALUES (?, ?, ?, ?, ?, ?, ?)""",
song_link, (server_id, details['url'], queued_by, max_order_num, details['title'], details['thumbnail'], details['duration']))
queued_by,
position,
title,
thumbnail,
duration)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (server_id,
details['url'],
queued_by,
max_order_num,
details['title'],
details['thumbnail'],
details['duration']))
conn.commit() conn.commit()
conn.close() conn.close()
@@ -235,39 +198,35 @@ async def pop(server_id, ignore=False, skip_mode=False):
# JUST INCASE! # JUST INCASE!
await add_server(server_id, cursor, conn) await add_server(server_id, cursor, conn)
cursor.execute('''SELECT * # Fetch info: link(1), title(4), thumbnail(5), duration(6)
FROM songs cursor.execute('''SELECT * FROM songs WHERE server_id = ? ORDER BY position LIMIT 1;''', (server_id,))
WHERE server_id = ?
ORDER BY position
LIMIT 1;''', (server_id,))
result = cursor.fetchone() result = cursor.fetchone()
conn.commit() conn.commit()
conn.close() conn.close()
if result == None: if result is None:
return None return None
elif ignore: elif ignore:
await mark_song_as_finished(server_id, result[3]) await mark_song_as_finished(server_id, result[3])
return None return None
elif result[1] == "Not grabbed": elif result[1] == "Not grabbed":
# Fetch song info # Lazy load logic
song = await search_song(result[4]) song_list = await search_song(result[4])
if song == []: if not song_list:
return None return None
else: song = song_list[0]
song = song[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 # Check loop mode before removing
loop_mode = await get_loop_mode(server_id) loop_mode = await get_loop_mode(server_id)
if loop_mode != 'song': # Only remove if not looping song if loop_mode != 'song': # Only remove if not looping song
await mark_song_as_finished(server_id, result[3]) await mark_song_as_finished(server_id, result[3])
return song['url'] 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 # Check loop mode before removing
loop_mode = await get_loop_mode(server_id) 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] return result[1]
# Add server to db if first time queuing # Add server to db if first time queuing
async def add_server(server_id, cursor, conn): async def add_server(server_id, cursor, conn):
# Check if the server exists cursor.execute('SELECT COUNT(*) FROM servers WHERE server_id = ?', (server_id,))
cursor.execute('''SELECT COUNT(*) if cursor.fetchone()[0] == 0:
FROM servers cursor.execute('''INSERT INTO servers (server_id, loop_mode, volume, effect, song_thumbnail, song_url)
WHERE server_id = ?''', (server_id,)) VALUES (?, 'off', 100, 'none', '', '')''', (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,))
conn.commit() conn.commit()
@@ -311,375 +261,215 @@ async def mark_song_as_finished(server_id, order_num):
# set the current playing song of the server # set the current playing song of the server
async def set_current_song(server_id, title, duration=0): async def set_current_song(server_id, title, url, thumbnail="", duration=0):
# Connect to the database
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
import time
start_time = time.time() start_time = time.time()
cursor.execute(''' UPDATE servers # Ensure duration is an integer
SET song_name = ?, song_start_time = ?, song_duration = ? try:
WHERE server_id = ?''', duration = int(duration)
(title, start_time, duration, server_id)) 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.commit()
conn.close() conn.close()
# Returns dictionary with title and thumbnail
async def get_current_song(server_id): async def get_current_song(server_id):
# Connect to the database
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(''' SELECT song_name cursor.execute(''' SELECT song_name, song_thumbnail, song_url FROM servers WHERE server_id = ? LIMIT 1;''', (server_id,))
FROM servers
WHERE server_id = ?
LIMIT 1;''',
(server_id,))
result = cursor.fetchone() result = cursor.fetchone()
# Close connection
conn.commit() conn.commit()
conn.close() 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): async def get_current_progress(server_id):
"""Get current playback progress (elapsed, duration, percentage)"""
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() 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() 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 return 0, 0, 0.0
start_time, duration, _ = result start_time, duration, _ = result
if duration == 0: if duration is None or duration == 0:
return 0, 0, 0.0 return 0, 0, 0.0
import time
elapsed = int(time.time() - start_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 percentage = (elapsed / duration) * 100 if duration > 0 else 0
return elapsed, duration, percentage return elapsed, duration, percentage
return result[0] if result else "Nothing"
# Grab max order from server
async def get_max(server_id, cursor): async def get_max(server_id, cursor):
cursor.execute(f""" cursor.execute("SELECT MAX(position) FROM songs WHERE server_id = ?", (server_id,))
SELECT MAX(position)
FROM songs
WHERE server_id = ?
""", (server_id,))
result = cursor.fetchone() result = cursor.fetchone()
return result[0] if result[0] is not None else -1
# Highnest number or 0 async def update_server(server_id, playing):
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
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
# add server to db if not present
await add_server(server_id, cursor, conn) await add_server(server_id, cursor, conn)
val = 1 if playing else 0
value = 1 if playing else 0 cursor.execute("UPDATE servers SET is_playing = ? WHERE server_id = ?", (val, server_id))
# Update field
cursor.execute("""UPDATE servers
SET is_playing = ?
WHERE server_id = ?
""", (value, server_id))
# Close connection
conn.commit() conn.commit()
conn.close() conn.close()
async def is_server_playing(server_id): async def is_server_playing(server_id):
# Connect to db
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
# add server to db if not present
await add_server(server_id, cursor, conn) await add_server(server_id, cursor, conn)
cursor.execute("SELECT is_playing FROM servers WHERE server_id = ?", (server_id,))
cursor.execute("""SELECT is_playing res = cursor.fetchone()
FROM servers
WHERE server_id = ?""",
(server_id,))
result = cursor.fetchone()
conn.commit()
conn.close() 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): async def clear(server_id):
# Connect to db
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
await add_server(server_id, cursor, conn) await add_server(server_id, cursor, conn)
await update_server(server_id, False) await update_server(server_id, False)
cursor.execute("DELETE FROM songs WHERE server_id = ?", (server_id,))
# Delete all songs from the server
cursor.execute('''DELETE FROM songs WHERE server_id = ?''', (server_id,))
conn.commit() conn.commit()
conn.close() conn.close()
# Grabs all songs from a server for display purposes
async def grab_songs(server_id): async def grab_songs(server_id):
# Connect to db
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
await add_server(server_id, cursor, conn) await add_server(server_id, cursor, conn)
cursor.execute("SELECT title, duration, queued_by FROM songs WHERE server_id = ? ORDER BY position LIMIT 10", (server_id,))
# 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,))
songs = cursor.fetchall() songs = cursor.fetchall()
max = await get_max(server_id, cursor) max_pos = await get_max(server_id, cursor)
conn.commit()
conn.close() conn.close()
return max_pos, songs
return max, songs # --- Effects/Loop/Shuffle/Volume (Simplified Paste) ---
# ============= LOOP/SHUFFLE/VOLUME FEATURES =============
# Get/Set loop mode
async def get_loop_mode(server_id): async def get_loop_mode(server_id):
"""Get the current loop mode: 'off', 'song', or 'queue'"""
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
await add_server(server_id, cursor, conn) await add_server(server_id, cursor, conn)
cursor.execute("SELECT loop_mode FROM servers WHERE server_id = ?", (server_id,))
cursor.execute("""SELECT loop_mode res = cursor.fetchone()
FROM servers
WHERE server_id = ?""",
(server_id,))
result = cursor.fetchone()
conn.commit()
conn.close() conn.close()
return res[0] if res else 'off'
return result[0] if result else 'off'
async def set_loop_mode(server_id, mode): 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) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
await add_server(server_id, cursor, conn) 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.commit()
conn.close() conn.close()
return True
# Get/Set volume
async def get_volume(server_id): async def get_volume(server_id):
"""Get the current volume (0-200)"""
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
await add_server(server_id, cursor, conn) await add_server(server_id, cursor, conn)
cursor.execute("SELECT volume FROM servers WHERE server_id = ?", (server_id,))
cursor.execute("""SELECT volume res = cursor.fetchone()
FROM servers
WHERE server_id = ?""",
(server_id,))
result = cursor.fetchone()
conn.commit()
conn.close() conn.close()
return res[0] if res else 100
return result[0] if result else 100 async def set_volume(server_id, vol):
async def set_volume(server_id, volume):
"""Set volume (0-200)"""
volume = max(0, min(200, volume)) # Clamp between 0-200
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
await add_server(server_id, cursor, conn) await add_server(server_id, cursor, conn)
cursor.execute("UPDATE servers SET volume = ? WHERE server_id = ?", (vol, server_id))
cursor.execute("""UPDATE servers
SET volume = ?
WHERE server_id = ?""",
(volume, server_id))
conn.commit() conn.commit()
conn.close() conn.close()
return volume return vol
# Shuffle the queue
async def shuffle_queue(server_id): async def shuffle_queue(server_id):
"""Randomize the order of songs in the queue"""
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
await add_server(server_id, cursor, conn) await add_server(server_id, cursor, conn)
cursor.execute("SELECT position, song_link, queued_by, title, thumbnail, duration FROM songs WHERE server_id = ? ORDER BY position", (server_id,))
# Get all songs
cursor.execute('''SELECT position, song_link, queued_by, title, thumbnail, duration
FROM songs
WHERE server_id = ?
ORDER BY position''', (server_id,))
songs = cursor.fetchall() songs = cursor.fetchall()
if len(songs) <= 1: if len(songs) <= 1:
conn.close() conn.close()
return False # Nothing to shuffle return False
# Shuffle the songs (keep positions but randomize order)
random.shuffle(songs) random.shuffle(songs)
cursor.execute("DELETE FROM songs WHERE server_id = ?", (server_id,))
# Delete all current songs for i, s in enumerate(songs):
cursor.execute('''DELETE FROM songs WHERE server_id = ?''', (server_id,)) cursor.execute("INSERT INTO songs VALUES (?, ?, ?, ?, ?, ?, ?)", (server_id, s[1], s[2], i, s[3], s[4], s[5]))
# 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]))
conn.commit() conn.commit()
conn.close() conn.close()
return True return True
# ============= AUDIO EFFECTS FEATURES =============
async def get_effect(server_id): async def get_effect(server_id):
"""Get the current audio effect"""
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
await add_server(server_id, cursor, conn) await add_server(server_id, cursor, conn)
cursor.execute("SELECT effect FROM servers WHERE server_id = ?", (server_id,))
cursor.execute("""SELECT effect res = cursor.fetchone()
FROM servers
WHERE server_id = ?""",
(server_id,))
result = cursor.fetchone()
conn.commit()
conn.close() conn.close()
return res[0] if res else 'none'
return result[0] if result else 'none' async def set_effect(server_id, fx):
async def set_effect(server_id, effect_name):
"""Set the audio effect"""
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
await add_server(server_id, cursor, conn) await add_server(server_id, cursor, conn)
cursor.execute("UPDATE servers SET effect = ? WHERE server_id = ?", (fx, server_id))
cursor.execute("""UPDATE servers
SET effect = ?
WHERE server_id = ?""",
(effect_name, server_id))
conn.commit() conn.commit()
conn.close() conn.close()
return True
def list_all_effects(): def list_all_effects():
"""Return a list of all available effects"""
return [ return [
'none', 'bassboost', 'nightcore', 'slowed', 'earrape', 'deepfry', 'distortion', 'none', 'bassboost', 'nightcore', 'slowed', 'earrape', 'deepfry', 'distortion',
'reverse', 'chipmunk', 'demonic', 'underwater', 'robot', 'reverse', 'chipmunk', 'demonic', 'underwater', 'robot',
'8d', 'vibrato', 'tremolo', 'echo', 'phone', 'megaphone' '8d', 'vibrato', 'tremolo', 'echo', 'phone', 'megaphone'
] ]
def get_effect_emoji(effect_name): def get_effect_emoji(effect_name):
"""Get emoji representation for each effect""" # Short list of emoji mappings
emojis = { emojis = {
'none': '🔊', 'none': '', # Changed to generic Sparkles
'bassboost': '🔉💥', 'bassboost': '💥',
'nightcore': '🎀', 'nightcore': '',
'slowed': '🐌💤', 'slowed': '🐢',
'earrape': '💀📢', 'earrape': '💀',
'deepfry': '🍟💥', 'deepfry': '🍟',
'distortion': '⚡🔊', 'distortion': '〰️',
'reverse': '🔄', 'reverse': '',
'chipmunk': '🐿️', 'chipmunk': '🐿️',
'demonic': '😈🔥', 'demonic': '😈',
'underwater': '🌊💦', 'underwater': '🫧',
'robot': '🤖', 'robot': '🤖',
'8d': '🎧🌀', '8d': '🎧',
'vibrato': '〰️', 'vibrato': '〰️',
'tremolo': '📳', 'tremolo': '📳',
'echo': '🗣️💭', 'echo': '🗣️',
'phone': '📞', 'phone': '📞',
'megaphone': '📢📣' 'megaphone': '📣'
} }
return emojis.get(effect_name, '🔊') return emojis.get(effect_name, '')
def get_effect_description(effect_name): def get_effect_description(effect_name):
"""Get user-friendly description for each effect"""
descriptions = { descriptions = {
'none': 'Normal audio', 'none': 'Normal audio',
'bassboost': 'MAXIMUM BASS 🔊', 'bassboost': 'MAXIMUM BASS 🔊',
'nightcore': 'Speed + pitch up (anime vibes)', 'nightcore': 'Speed + pitch up (anime vibes)',
'slowed': 'Slowed + reverb (TikTok aesthetic)', 'slowed': 'Slowed + reverb',
'earrape': '⚠️ Aggressive compression + distortion + clipping ⚠️', 'earrape': '⚠️ Loud volume & distortion',
'deepfry': '🍟 EXTREME bitcrushing + bass (meme audio) 🍟', 'deepfry': 'Bits crushed + Bass',
'distortion': 'Heavy bitcrushing distortion', 'distortion': 'Heavy distortion',
'reverse': 'Plays audio BACKWARDS', 'reverse': 'Plays audio BACKWARDS',
'chipmunk': 'High pitched and fast (Alvin mode)', 'chipmunk': 'High pitched and fast',
'demonic': 'Low pitched and slow (cursed)', 'demonic': 'Low pitched and slow',
'underwater': 'Muffled underwater sound', 'underwater': 'Muffled underwater sound',
'robot': 'Robotic vocoder', 'robot': 'Robotic vocoder',
'8d': 'Panning audio (use headphones!)', '8d': 'Panning audio (use headphones!)',
@@ -691,59 +481,40 @@ def get_effect_description(effect_name):
} }
return descriptions.get(effect_name, 'Unknown effect') return descriptions.get(effect_name, 'Unknown effect')
# Play and loop songs in server
async def play(ctx): async def play(ctx):
"""Main playback loop - plays songs from queue sequentially with effects"""
server_id = ctx.guild.id server_id = ctx.guild.id
voice_client = ctx.voice_client voice_client = ctx.voice_client
# Safety check
if voice_client is None: if voice_client is None:
await update_server(server_id, False) await update_server(server_id, False)
return return
# Wait until current song finishes
while voice_client.is_playing(): while voice_client.is_playing():
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
# Get next song
url = await pop(server_id) url = await pop(server_id)
# If no songs left, update status and return
if url is None: if url is None:
await update_server(server_id, False) await update_server(server_id, False)
return return
try: try:
# Get volume and effect settings vol = await get_volume(server_id) / 100.0
volume_percent = await get_volume(server_id) fx = await get_effect(server_id)
volume = volume_percent / 100.0 # Convert to 0.0-2.0 range opts = get_effect_options(fx)
current_effect = await get_effect(server_id) src = discord.FFmpegPCMAudio(url, **opts)
ffmpeg_opts = get_effect_options(current_effect) src = discord.PCMVolumeTransformer(src, volume=vol)
# Create audio source with effect and volume control def after(e):
audio_source = discord.FFmpegPCMAudio(url, **ffmpeg_opts) if e: print(e)
audio_source = discord.PCMVolumeTransformer(audio_source, volume=volume) if voice_client and not voice_client.is_connected(): return
# 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
coro = play(ctx) coro = play(ctx)
fut = asyncio.run_coroutine_threadsafe(coro, ctx.bot.loop) fut = asyncio.run_coroutine_threadsafe(coro, ctx.bot.loop)
try: try: fut.result()
fut.result() except: pass
except Exception as e:
print(f"Error playing next song: {e}")
voice_client.play(audio_source, after=after_playing)
voice_client.play(src, after=after)
except Exception as e: except Exception as e:
print(f"Error starting playback: {e}") print(f"Play error: {e}")
# Try to continue with next song
await play(ctx) await play(ctx)

View File

@@ -107,72 +107,160 @@ def update_activity(guild_id):
# Interactive buttons for queue control # Interactive buttons for queue control
class QueueControls(View): class QueueControls(View):
def __init__(self, ctx): 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 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) @discord.ui.button(label="⏭️ Skip", style=discord.ButtonStyle.primary)
async def skip_button(self, interaction: discord.Interaction, button: Button): async def skip_button(self, interaction: discord.Interaction, button: Button):
if interaction.user != self.ctx.author: if interaction.user not in self.ctx.voice_client.channel.members:
await interaction.response.send_message("Only the person who requested the queue can use these buttons!", ephemeral=True) await interaction.response.send_message("You must be in the voice channel!", ephemeral=True)
return 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() self.ctx.voice_client.stop()
await interaction.response.send_message("⏭️ Skipped!", ephemeral=True)
else: # Refresh UI
await interaction.response.send_message("❌ Nothing is playing!", ephemeral=True) await self.refresh_message(interaction)
@discord.ui.button(label="🔀 Shuffle", style=discord.ButtonStyle.secondary) @discord.ui.button(label="🔀 Shuffle", style=discord.ButtonStyle.secondary)
async def shuffle_button(self, interaction: discord.Interaction, button: Button): async def shuffle_button(self, interaction: discord.Interaction, button: Button):
if interaction.user != self.ctx.author: await queue.shuffle_queue(self.ctx.guild.id)
await interaction.response.send_message("❌ Only the person who requested the queue can use these buttons!", ephemeral=True) await self.refresh_message(interaction)
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)
@discord.ui.button(label="🔁 Loop", style=discord.ButtonStyle.secondary) @discord.ui.button(label="🔁 Loop", style=discord.ButtonStyle.secondary)
async def loop_button(self, interaction: discord.Interaction, button: Button): 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) current_mode = await queue.get_loop_mode(self.ctx.guild.id)
new_mode = 'song' if current_mode == 'off' else ('queue' if current_mode == 'song' else 'off')
# Cycle through modes
if current_mode == 'off':
new_mode = 'song'
elif current_mode == 'song':
new_mode = 'queue'
else:
new_mode = 'off'
await queue.set_loop_mode(self.ctx.guild.id, new_mode) await queue.set_loop_mode(self.ctx.guild.id, new_mode)
await self.refresh_message(interaction)
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)
@discord.ui.button(label="🗑️ Clear", style=discord.ButtonStyle.danger) @discord.ui.button(label="🗑️ Clear", style=discord.ButtonStyle.danger)
async def clear_button(self, interaction: discord.Interaction, button: Button): 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) await queue.clear(self.ctx.guild.id)
if self.ctx.voice_client and self.ctx.voice_client.is_playing(): if self.ctx.voice_client and self.ctx.voice_client.is_playing():
self.ctx.voice_client.stop() 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 # Build a display message for queuing a new song
async def queue_message(ctx: Context, data: dict): 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) 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 # Converts seconds into more readable format
def format_time(seconds): def format_time(seconds):
try: try:

177
help.py
View File

@@ -1,81 +1,126 @@
from collections.abc import Mapping
from typing import List
import discord import discord
from discord.app_commands import Command
from discord.ext import commands from discord.ext import commands
from discord.ext.commands.cog import Cog from discord import app_commands
import config 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): class HelpSelect(discord.ui.Select):
super().__init__() def __init__(self, mapping, ctx):
self.command_attrs = { self.mapping = mapping
'name': "help", self.ctx = ctx
'aliases': ["commands", "?"],
'cooldown': commands.CooldownMapping.from_cooldown(2, 5.0, commands.BucketType.user) 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 callback(self, interaction: discord.Interaction):
async def send_bot_help(self, mapping: Mapping[Cog, List[Command]]): 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( embed = discord.Embed(
title="Help", title=f"{getattr(selected_cog, 'emoji', '')} {value} Commands",
color=config.get_color("main")) color=config.get_color("main") if hasattr(config, 'get_color') else discord.Color.blue()
embed.add_field(name="", )
value="Use `help <command>` or `help <category>` for more details",
inline=False)
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]) def get_home_embed(ctx):
for cog, commands in mapping.items(): 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 class GroovyHelp(commands.Cog):
# Safe to ignore warning def __init__(self, client):
filtered = await self.filter_commands(commands, sort=True) self.client = client
self.name = "Help"
self.emoji = "🆘"
# For each command we grab the signature @commands.hybrid_command(name="help", description="Show the help menu")
command_signatures = [ async def help(self, ctx: commands.Context):
# Rmove prefix and format command name bot = ctx.bot
f"``{self.get_command_signature(c)[1:]}``" for c in filtered] 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 visible_cmds:
if command_signatures: # Sort alphabetically
visible_cmds.sort(key=lambda x: x.name)
mapping[cog] = visible_cmds
# Use get incase cog is None embed = get_home_embed(ctx)
cog_name = getattr(cog, "name", "No Category") view = HelpView(mapping, ctx)
await ctx.send(embed=embed, view=view)
# 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

View File

@@ -1,10 +1,9 @@
import discord import discord
from bot import Astro from bot import Groovy
import config import config
import help import help
client = Astro(command_prefix=config.get_prefix(), intents=discord.Intents.all()) client = Groovy(command_prefix=config.get_prefix(), intents=discord.Intents.all())
client.help_command = help.AstroHelp()
@client.event @client.event
async def on_voice_state_update(member, before, after): async def on_voice_state_update(member, before, after):