new help command, fixed queue and loop display, thumbnails and urls added to database
This commit is contained in:
15
bot.py
15
bot.py
@@ -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())
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
177
help.py
@@ -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
|
|
||||||
|
|||||||
5
main.py
5
main.py
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user