status bar in queue and cleaned up embeds, slash command on help
This commit is contained in:
8
bot.py
8
bot.py
@@ -25,6 +25,14 @@ class Astro(commands.Bot):
|
|||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# Sync slash commands with Discord
|
||||||
|
print("🔄 Syncing slash commands...")
|
||||||
|
try:
|
||||||
|
synced = await self.tree.sync()
|
||||||
|
print(f"✅ Synced {len(synced)} slash command(s)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Failed to sync commands: {e}")
|
||||||
|
|
||||||
# Start inactivity checker
|
# Start inactivity checker
|
||||||
if not self.inactivity_checker.is_running():
|
if not self.inactivity_checker.is_running():
|
||||||
self.inactivity_checker.start()
|
self.inactivity_checker.start()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord.ext.commands.context import Context
|
from discord.ext.commands.context import Context
|
||||||
|
from discord import app_commands
|
||||||
|
import discord
|
||||||
|
|
||||||
import cogs.music.util as util
|
import cogs.music.util as util
|
||||||
import cogs.music.queue as queue
|
import cogs.music.queue as queue
|
||||||
@@ -74,31 +76,42 @@ class music(commands.Cog):
|
|||||||
await util.leave_vc(ctx)
|
await util.leave_vc(ctx)
|
||||||
await ctx.message.add_reaction('👍')
|
await ctx.message.add_reaction('👍')
|
||||||
|
|
||||||
@commands.command(
|
|
||||||
help="Queues a song into the bot",
|
# HYBRID COMMAND - works as both =play and /play
|
||||||
aliases=['p'])
|
@commands.hybrid_command(
|
||||||
async def play(self, ctx: Context, *, url=None):
|
name="play",
|
||||||
if url is None:
|
description="Queue a song to play")
|
||||||
raise commands.CommandError("Must provide a link or search query")
|
@app_commands.describe(query="YouTube URL, Spotify link, or search query")
|
||||||
elif ctx.guild is None:
|
async def play(self, ctx: Context, *, query: str):
|
||||||
raise commands.CommandError("Command must be issued in a server")
|
"""Queues a song into the bot"""
|
||||||
|
if ctx.guild is None:
|
||||||
|
await ctx.send("❌ This command must be used in a server!", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
server = ctx.guild.id
|
server = ctx.guild.id
|
||||||
|
|
||||||
|
# For slash commands, defer the response since fetching takes time
|
||||||
|
if ctx.interaction:
|
||||||
|
await ctx.defer()
|
||||||
|
|
||||||
await util.join_vc(ctx)
|
await util.join_vc(ctx)
|
||||||
await ctx.message.add_reaction('👍')
|
|
||||||
|
# Different responses for slash vs prefix
|
||||||
|
if not ctx.interaction:
|
||||||
|
await ctx.message.add_reaction('👍')
|
||||||
|
|
||||||
msg = await ctx.send("Fetching song(s)...")
|
msg = await ctx.send("Fetching song(s)...")
|
||||||
async with ctx.typing():
|
|
||||||
#TODO potentially save requests before getting stream link
|
#TODO potentially save requests before getting stream link
|
||||||
# Grab video details such as title thumbnail duration
|
# Grab video details such as title thumbnail duration
|
||||||
audio = await translate.main(url, self.sp)
|
audio = await translate.main(query, self.sp)
|
||||||
|
|
||||||
await msg.delete()
|
await msg.delete()
|
||||||
|
|
||||||
if len(audio) == 0:
|
if len(audio) == 0:
|
||||||
await ctx.message.add_reaction('🚫')
|
if not ctx.interaction:
|
||||||
await ctx.send("Failed to find song!")
|
await ctx.message.add_reaction('🚫')
|
||||||
|
await ctx.send("❌ Failed to find song!")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@@ -128,16 +141,61 @@ class music(commands.Cog):
|
|||||||
|
|
||||||
|
|
||||||
@commands.command(
|
@commands.command(
|
||||||
help="Display the current music queue",
|
help="Queue a song to play next (top of queue)",
|
||||||
aliases=['q', 'songs'])
|
aliases=['pt', 'pn', 'playnext'])
|
||||||
async def queue(self, ctx: Context):
|
async def playtop(self, ctx: Context, *, url=None):
|
||||||
|
"""Queue a song at the top of the queue (plays next)"""
|
||||||
|
if url is None:
|
||||||
|
raise commands.CommandError("Must provide a link or search query")
|
||||||
|
elif ctx.guild is None:
|
||||||
|
raise commands.CommandError("Command must be issued in a server")
|
||||||
|
|
||||||
|
server = ctx.guild.id
|
||||||
|
|
||||||
|
await util.join_vc(ctx)
|
||||||
|
await ctx.message.add_reaction('⏭️')
|
||||||
|
|
||||||
|
msg = await ctx.send("Fetching song(s) to play next...")
|
||||||
|
async with ctx.typing():
|
||||||
|
audio = await translate.main(url, self.sp)
|
||||||
|
|
||||||
|
await msg.delete()
|
||||||
|
|
||||||
|
if len(audio) == 0:
|
||||||
|
await ctx.message.add_reaction('🚫')
|
||||||
|
await ctx.send("Failed to find song!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add songs to TOP of queue (position 0, then 1, then 2...)
|
||||||
|
for i, song in enumerate(audio):
|
||||||
|
await queue.add_song(
|
||||||
|
server,
|
||||||
|
song,
|
||||||
|
ctx.author.display_name,
|
||||||
|
position=i) # Insert at top
|
||||||
|
|
||||||
|
# Show first song that was added
|
||||||
|
audio[0]['position'] = 0
|
||||||
|
await util.queue_message(ctx, audio[0])
|
||||||
|
|
||||||
|
if await queue.is_server_playing(server):
|
||||||
|
return
|
||||||
|
|
||||||
|
await queue.update_server(server, True)
|
||||||
|
await queue.play(ctx)
|
||||||
|
|
||||||
|
|
||||||
|
@commands.hybrid_command(
|
||||||
|
name="queue",
|
||||||
|
description="Display the current music queue")
|
||||||
|
async def queue_cmd(self, ctx: Context):
|
||||||
|
"""Display the current music queue"""
|
||||||
server = ctx.guild
|
server = ctx.guild
|
||||||
|
|
||||||
# Perform usual checks
|
# Perform usual checks
|
||||||
if server is None:
|
if server is None:
|
||||||
raise commands.CommandError("Command must be issued in a server")
|
await ctx.send("❌ This command must be used in a server!", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
# Grab all songs from this server
|
# Grab all songs from this server
|
||||||
n, songs = await queue.grab_songs(server.id)
|
n, songs = await queue.grab_songs(server.id)
|
||||||
@@ -151,42 +209,62 @@ class music(commands.Cog):
|
|||||||
await util.display_server_queue(ctx, songs, n)
|
await util.display_server_queue(ctx, songs, n)
|
||||||
|
|
||||||
|
|
||||||
@commands.command(
|
@commands.hybrid_command(
|
||||||
help="Skips the current song that is playing, can include number to skip more songs",
|
name="skip",
|
||||||
aliases=['s'])
|
description="Skip the current song")
|
||||||
async def skip(self, ctx: Context, n='1'):
|
@app_commands.describe(count="Number of songs to skip (default: 1)")
|
||||||
|
async def skip(self, ctx: Context, count: int = 1):
|
||||||
|
"""Skips the current song that is playing"""
|
||||||
server = ctx.guild
|
server = ctx.guild
|
||||||
|
|
||||||
if server is None:
|
if server is None:
|
||||||
raise commands.CommandError("Command must be issued in a server")
|
await ctx.send("❌ This command must be used in a server!", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
if ctx.voice_client is None:
|
if ctx.voice_client is None:
|
||||||
raise commands.CommandError("I'm not in a voice channel")
|
await ctx.send("❌ I'm not in a voice channel!", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
if not n.isdigit():
|
if count <= 0:
|
||||||
raise commands.CommandError("Please enter a number to skip")
|
await ctx.send("❌ Please enter a positive number!", ephemeral=True)
|
||||||
n = int(n)
|
return
|
||||||
|
|
||||||
if n <= 0:
|
# Check loop mode
|
||||||
raise commands.CommandError("Please enter a positive number")
|
loop_mode = await queue.get_loop_mode(server.id)
|
||||||
|
|
||||||
# Skip specificed number of songs
|
if loop_mode == 'song' and count == 1:
|
||||||
for _ in range(n-1):
|
# When looping song and skipping once, just restart it
|
||||||
await queue.pop(server.id, True)
|
ctx.voice_client.stop()
|
||||||
|
await ctx.send(f"⏭️ Restarting song (loop mode active)")
|
||||||
# Safe to ignore error for now
|
else:
|
||||||
ctx.voice_client.stop()
|
# Skip specified number of songs
|
||||||
|
for _ in range(count-1):
|
||||||
|
await queue.pop(server.id, True, skip_mode=True)
|
||||||
|
|
||||||
|
# Last song gets skip_mode=True to handle loop properly
|
||||||
|
if loop_mode != 'song':
|
||||||
|
await queue.pop(server.id, True, skip_mode=True)
|
||||||
|
|
||||||
|
ctx.voice_client.stop()
|
||||||
|
await ctx.send(f"⏭️ Skipped {count} song(s)")
|
||||||
|
|
||||||
|
|
||||||
@commands.command(
|
@commands.hybrid_command(
|
||||||
help="Toggle loop mode: off -> song -> queue -> off",
|
name="loop",
|
||||||
aliases=['l', 'repeat'])
|
description="Toggle loop mode")
|
||||||
|
@app_commands.describe(mode="Loop mode: off, song, or queue")
|
||||||
|
@app_commands.choices(mode=[
|
||||||
|
app_commands.Choice(name="Off", value="off"),
|
||||||
|
app_commands.Choice(name="Song", value="song"),
|
||||||
|
app_commands.Choice(name="Queue", value="queue")
|
||||||
|
])
|
||||||
async def loop(self, ctx: Context, mode: str = None):
|
async def loop(self, ctx: Context, mode: str = None):
|
||||||
"""Toggle between loop modes or set a specific mode"""
|
"""Toggle between loop modes or set a specific mode"""
|
||||||
server = ctx.guild
|
server = ctx.guild
|
||||||
|
|
||||||
if server is None:
|
if server is None:
|
||||||
raise commands.CommandError("Command must be issued in a server")
|
await ctx.send("❌ This command must be used in a server!", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
current_mode = await queue.get_loop_mode(server.id)
|
current_mode = await queue.get_loop_mode(server.id)
|
||||||
|
|
||||||
@@ -202,7 +280,8 @@ class music(commands.Cog):
|
|||||||
# Set specific mode
|
# Set specific mode
|
||||||
mode = mode.lower()
|
mode = mode.lower()
|
||||||
if mode not in ['off', 'song', 'queue']:
|
if mode not in ['off', 'song', 'queue']:
|
||||||
raise commands.CommandError("Loop mode must be: off, song, or queue")
|
await ctx.send("❌ Loop mode must be: off, song, or queue", ephemeral=True)
|
||||||
|
return
|
||||||
new_mode = mode
|
new_mode = mode
|
||||||
|
|
||||||
await queue.set_loop_mode(server.id, new_mode)
|
await queue.set_loop_mode(server.id, new_mode)
|
||||||
@@ -211,7 +290,7 @@ class music(commands.Cog):
|
|||||||
emojis = {'off': '⏹️', 'song': '🔂', 'queue': '🔁'}
|
emojis = {'off': '⏹️', 'song': '🔂', 'queue': '🔁'}
|
||||||
messages = {
|
messages = {
|
||||||
'off': 'Loop disabled',
|
'off': 'Loop disabled',
|
||||||
'song': 'Looping current song 🔂',
|
'song': 'Looping current song 🔂 (skip to restart)',
|
||||||
'queue': 'Looping entire queue 🔁'
|
'queue': 'Looping entire queue 🔁'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,31 +316,30 @@ class music(commands.Cog):
|
|||||||
await ctx.send("🔀 Queue shuffled!")
|
await ctx.send("🔀 Queue shuffled!")
|
||||||
|
|
||||||
|
|
||||||
@commands.command(
|
@commands.hybrid_command(
|
||||||
help="Set playback volume (0-200%)",
|
name="volume",
|
||||||
aliases=['vol', 'v'])
|
description="Set playback volume")
|
||||||
async def volume(self, ctx: Context, vol: str = None):
|
@app_commands.describe(level="Volume level (0-200%, default shows current)")
|
||||||
|
async def volume(self, ctx: Context, level: int = None):
|
||||||
"""Set or display the current volume"""
|
"""Set or display the current volume"""
|
||||||
server = ctx.guild
|
server = ctx.guild
|
||||||
|
|
||||||
if server is None:
|
if server is None:
|
||||||
raise commands.CommandError("Command must be issued in a server")
|
await ctx.send("❌ This command must be used in a server!", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
if vol is None:
|
if level is None:
|
||||||
# Display current volume
|
# Display current volume
|
||||||
current_vol = await queue.get_volume(server.id)
|
current_vol = await queue.get_volume(server.id)
|
||||||
await ctx.send(f"🔊 Current volume: {current_vol}%")
|
await ctx.send(f"🔊 Current volume: {current_vol}%")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Set volume
|
# Set volume
|
||||||
if not vol.isdigit():
|
if level < 0 or level > 200:
|
||||||
raise commands.CommandError("Volume must be a number (0-200)")
|
await ctx.send("❌ Volume must be between 0 and 200!", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
vol = int(vol)
|
new_vol = await queue.set_volume(server.id, level)
|
||||||
if vol < 0 or vol > 200:
|
|
||||||
raise commands.CommandError("Volume must be between 0 and 200")
|
|
||||||
|
|
||||||
new_vol = await queue.set_volume(server.id, vol)
|
|
||||||
|
|
||||||
# Update the current playing song's volume if something is playing
|
# Update the current playing song's volume if something is playing
|
||||||
if ctx.voice_client and ctx.voice_client.source:
|
if ctx.voice_client and ctx.voice_client.source:
|
||||||
@@ -280,15 +358,17 @@ class music(commands.Cog):
|
|||||||
await ctx.send(f"{emoji} Volume set to {new_vol}%")
|
await ctx.send(f"{emoji} Volume set to {new_vol}%")
|
||||||
|
|
||||||
|
|
||||||
@commands.command(
|
@commands.hybrid_command(
|
||||||
help="Apply audio effects to playback",
|
name="effect",
|
||||||
aliases=['fx', 'filter'])
|
description="Apply audio effects to playback")
|
||||||
|
@app_commands.describe(effect_name="The audio effect to apply (leave empty to see list)")
|
||||||
async def effect(self, ctx: Context, effect_name: str = None):
|
async def effect(self, ctx: Context, effect_name: str = None):
|
||||||
"""Apply or list audio effects"""
|
"""Apply or list audio effects"""
|
||||||
server = ctx.guild
|
server = ctx.guild
|
||||||
|
|
||||||
if server is None:
|
if server is None:
|
||||||
raise commands.CommandError("Command must be issued in a server")
|
await ctx.send("❌ This command must be used in a server!", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
# If no effect specified, show current effect and list options
|
# If no effect specified, show current effect and list options
|
||||||
if effect_name is None:
|
if effect_name is None:
|
||||||
@@ -298,18 +378,18 @@ class music(commands.Cog):
|
|||||||
|
|
||||||
effects_list = '\n'.join([
|
effects_list = '\n'.join([
|
||||||
f"`{e}` - {queue.get_effect_description(e)}"
|
f"`{e}` - {queue.get_effect_description(e)}"
|
||||||
for e in queue.list_all_effects()[:7] # Show first 7
|
for e in queue.list_all_effects()[:9] # Show first 9
|
||||||
])
|
])
|
||||||
more_effects = '\n'.join([
|
more_effects = '\n'.join([
|
||||||
f"`{e}` - {queue.get_effect_description(e)}"
|
f"`{e}` - {queue.get_effect_description(e)}"
|
||||||
for e in queue.list_all_effects()[7:] # Show rest
|
for e in queue.list_all_effects()[9:] # Show rest
|
||||||
])
|
])
|
||||||
|
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
f"{emoji} **Current effect:** {current} - {desc}\n\n"
|
f"{emoji} **Current effect:** {current} - {desc}\n\n"
|
||||||
f"**Available effects:**\n{effects_list}\n\n"
|
f"**Available effects:**\n{effects_list}\n\n"
|
||||||
f"**More effects:**\n{more_effects}\n\n"
|
f"**More effects:**\n{more_effects}\n\n"
|
||||||
f"Use `=effect <name>` to apply an effect!"
|
f"Use `=effect <name>` or `/effect <name>` to apply!"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -317,21 +397,24 @@ class music(commands.Cog):
|
|||||||
effect_name = effect_name.lower()
|
effect_name = effect_name.lower()
|
||||||
|
|
||||||
if effect_name not in queue.list_all_effects():
|
if effect_name not in queue.list_all_effects():
|
||||||
raise commands.CommandError(
|
await ctx.send(
|
||||||
f"Unknown effect! Use `=effect` to see available effects."
|
f"❌ Unknown effect! Use `/effect` to see available effects.",
|
||||||
|
ephemeral=True
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
await queue.set_effect(server.id, effect_name)
|
await queue.set_effect(server.id, effect_name)
|
||||||
|
|
||||||
emoji = queue.get_effect_emoji(effect_name)
|
emoji = queue.get_effect_emoji(effect_name)
|
||||||
desc = queue.get_effect_description(effect_name)
|
desc = queue.get_effect_description(effect_name)
|
||||||
|
|
||||||
# Special warning for earrape
|
# Special warning for earrape/deepfry
|
||||||
if effect_name == 'earrape':
|
if effect_name in ['earrape', 'deepfry']:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
f"⚠️ **EARRAPE MODE ACTIVATED** ⚠️\n"
|
f"⚠️ **{effect_name.upper()} MODE ACTIVATED** ⚠️\n"
|
||||||
f"RIP your eardrums. Effect will apply to next song.\n"
|
f"{desc}\n"
|
||||||
f"Use `=effect none` to disable."
|
f"Effect will apply to next song.\n"
|
||||||
|
f"Use `/effect none` to disable."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
@@ -342,4 +425,4 @@ class music(commands.Cog):
|
|||||||
|
|
||||||
# If something is currently playing, notify about skip
|
# If something is currently playing, notify about skip
|
||||||
if ctx.voice_client and ctx.voice_client.is_playing():
|
if ctx.voice_client and ctx.voice_client.is_playing():
|
||||||
await ctx.send("💡 Tip: Use `=skip` to apply effect to current song immediately!")
|
await ctx.send("💡 Tip: Use `/skip` to apply effect immediately!")
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ BASE_FFMPEG_OPTS = {
|
|||||||
# Audio effects configurations
|
# Audio effects configurations
|
||||||
def get_effect_options(effect_name):
|
def get_effect_options(effect_name):
|
||||||
"""Get FFmpeg options for a specific audio effect"""
|
"""Get FFmpeg options for a specific audio effect"""
|
||||||
|
|
||||||
effects = {
|
effects = {
|
||||||
'none': {
|
'none': {
|
||||||
**BASE_FFMPEG_OPTS
|
**BASE_FFMPEG_OPTS
|
||||||
@@ -35,20 +35,10 @@ def get_effect_options(effect_name):
|
|||||||
**BASE_FFMPEG_OPTS,
|
**BASE_FFMPEG_OPTS,
|
||||||
'options': '-vn -af "atempo=0.8,asetrate=48000*0.8,aecho=0.8:0.9:1000:0.3"'
|
'options': '-vn -af "atempo=0.8,asetrate=48000*0.8,aecho=0.8:0.9:1000:0.3"'
|
||||||
},
|
},
|
||||||
'earrape': {
|
|
||||||
**BASE_FFMPEG_OPTS,
|
|
||||||
# Aggressive compression + hard clipping + bitcrushing for maximum distortion
|
|
||||||
'options': '-vn -af "volume=8,acompressor=threshold=0.001:ratio=30:attack=0.1:release=5,acrusher=bits=8:mix=0.7,volume=2,alimiter=limit=0.8"'
|
|
||||||
},
|
|
||||||
'deepfry': {
|
|
||||||
**BASE_FFMPEG_OPTS,
|
|
||||||
# Extreme bitcrushing + bass boost + compression (meme audio effect)
|
|
||||||
'options': '-vn -af "acrusher=bits=4:mode=log:aa=1,bass=g=15,acompressor=threshold=0.001:ratio=20,volume=3"'
|
|
||||||
},
|
|
||||||
'distortion': {
|
'distortion': {
|
||||||
**BASE_FFMPEG_OPTS,
|
**BASE_FFMPEG_OPTS,
|
||||||
# Pure bitcrushing distortion
|
# Pure bitcrushing distortion
|
||||||
'options': '-vn -af "acrusher=bits=6:mix=0.9,acompressor=threshold=0.01:ratio=15,volume=2"'
|
'options': '-vn -af "acrusher=bits=6:mix=0.9,acompressor=threshold=0.01:ratio=15"'
|
||||||
},
|
},
|
||||||
'reverse': {
|
'reverse': {
|
||||||
**BASE_FFMPEG_OPTS,
|
**BASE_FFMPEG_OPTS,
|
||||||
@@ -86,18 +76,8 @@ def get_effect_options(effect_name):
|
|||||||
**BASE_FFMPEG_OPTS,
|
**BASE_FFMPEG_OPTS,
|
||||||
'options': '-vn -af "aecho=0.8:0.88:60:0.4"'
|
'options': '-vn -af "aecho=0.8:0.88:60:0.4"'
|
||||||
},
|
},
|
||||||
'phone': {
|
|
||||||
**BASE_FFMPEG_OPTS,
|
|
||||||
# Sounds like a phone call (bandpass filter)
|
|
||||||
'options': '-vn -af "bandpass=f=1500:width_type=h:w=1000,volume=2"'
|
|
||||||
},
|
|
||||||
'megaphone': {
|
|
||||||
**BASE_FFMPEG_OPTS,
|
|
||||||
# Megaphone/radio effect
|
|
||||||
'options': '-vn -af "highpass=f=300,lowpass=f=3000,volume=2,acompressor=threshold=0.1:ratio=8"'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return effects.get(effect_name, effects['none'])
|
return effects.get(effect_name, effects['none'])
|
||||||
|
|
||||||
|
|
||||||
@@ -114,28 +94,40 @@ def initialize_tables():
|
|||||||
song_name TEXT,
|
song_name TEXT,
|
||||||
loop_mode TEXT DEFAULT 'off',
|
loop_mode TEXT DEFAULT 'off',
|
||||||
volume INTEGER DEFAULT 100,
|
volume INTEGER DEFAULT 100,
|
||||||
effect TEXT DEFAULT 'none'
|
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:
|
try:
|
||||||
cursor.execute("ALTER TABLE servers ADD COLUMN loop_mode TEXT DEFAULT 'off';")
|
cursor.execute("ALTER TABLE servers ADD COLUMN loop_mode TEXT DEFAULT 'off';")
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
pass # Column already exists
|
pass # Column already exists
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cursor.execute("ALTER TABLE servers ADD COLUMN volume INTEGER DEFAULT 100;")
|
cursor.execute("ALTER TABLE servers ADD COLUMN volume INTEGER DEFAULT 100;")
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
pass # Column already exists
|
pass # Column already exists
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cursor.execute("ALTER TABLE servers ADD COLUMN effect TEXT DEFAULT 'none';")
|
cursor.execute("ALTER TABLE servers ADD COLUMN effect TEXT DEFAULT 'none';")
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
pass # Column already exists
|
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
|
# 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,
|
||||||
@@ -241,17 +233,17 @@ async def pop(server_id, ignore=False):
|
|||||||
else:
|
else:
|
||||||
song = song[0]
|
song = song[0]
|
||||||
|
|
||||||
await set_current_song(server_id, song['title'])
|
await set_current_song(server_id, song['title'], 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])
|
await set_current_song(server_id, result[4], result[6]) # result[6] is duration
|
||||||
|
|
||||||
# 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
|
||||||
@@ -294,15 +286,18 @@ 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):
|
async def set_current_song(server_id, title, duration=0):
|
||||||
# Connect to the database
|
# 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()
|
||||||
|
|
||||||
cursor.execute(''' UPDATE servers
|
cursor.execute(''' UPDATE servers
|
||||||
SET song_name = ?
|
SET song_name = ?, song_start_time = ?, song_duration = ?
|
||||||
WHERE server_id = ?''',
|
WHERE server_id = ?''',
|
||||||
(title, server_id))
|
(title, start_time, duration, server_id))
|
||||||
|
|
||||||
# Close connection
|
# Close connection
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -319,7 +314,7 @@ async def get_current_song(server_id):
|
|||||||
WHERE server_id = ?
|
WHERE server_id = ?
|
||||||
LIMIT 1;''',
|
LIMIT 1;''',
|
||||||
(server_id,))
|
(server_id,))
|
||||||
|
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
# Close connection
|
# Close connection
|
||||||
@@ -329,6 +324,38 @@ async def get_current_song(server_id):
|
|||||||
return result[0] if result else "Nothing"
|
return result[0] if result else "Nothing"
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_progress(server_id):
|
||||||
|
"""Get current playback progress (elapsed, duration, percentage)"""
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute('''SELECT song_start_time, song_duration, is_playing
|
||||||
|
FROM servers
|
||||||
|
WHERE server_id = ?
|
||||||
|
LIMIT 1;''',
|
||||||
|
(server_id,))
|
||||||
|
|
||||||
|
result = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not result or result[2] == 0: # Not playing
|
||||||
|
return 0, 0, 0.0
|
||||||
|
|
||||||
|
start_time, duration, _ = result
|
||||||
|
|
||||||
|
if duration == 0:
|
||||||
|
return 0, 0, 0.0
|
||||||
|
|
||||||
|
import time
|
||||||
|
elapsed = int(time.time() - start_time)
|
||||||
|
elapsed = min(elapsed, duration) # Cap at duration
|
||||||
|
percentage = (elapsed / duration) * 100 if duration > 0 else 0
|
||||||
|
|
||||||
|
return elapsed, duration, percentage
|
||||||
|
|
||||||
|
return result[0] if result else "Nothing"
|
||||||
|
|
||||||
|
|
||||||
# Grab max order from server
|
# Grab max order from server
|
||||||
async def get_max(server_id, cursor):
|
async def get_max(server_id, cursor):
|
||||||
cursor.execute(f"""
|
cursor.execute(f"""
|
||||||
@@ -433,18 +460,18 @@ async def get_loop_mode(server_id):
|
|||||||
"""Get the current loop mode: 'off', 'song', or 'queue'"""
|
"""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
|
cursor.execute("""SELECT loop_mode
|
||||||
FROM servers
|
FROM servers
|
||||||
WHERE server_id = ?""",
|
WHERE server_id = ?""",
|
||||||
(server_id,))
|
(server_id,))
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return result[0] if result else 'off'
|
return result[0] if result else 'off'
|
||||||
|
|
||||||
|
|
||||||
@@ -452,17 +479,17 @@ async def set_loop_mode(server_id, mode):
|
|||||||
"""Set loop mode: 'off', 'song', or 'queue'"""
|
"""Set loop mode: 'off', 'song', or 'queue'"""
|
||||||
if mode not in ['off', 'song', 'queue']:
|
if mode not in ['off', 'song', 'queue']:
|
||||||
return False
|
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
|
cursor.execute("""UPDATE servers
|
||||||
SET loop_mode = ?
|
SET loop_mode = ?
|
||||||
WHERE server_id = ?""",
|
WHERE server_id = ?""",
|
||||||
(mode, server_id))
|
(mode, server_id))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return True
|
return True
|
||||||
@@ -473,35 +500,35 @@ async def get_volume(server_id):
|
|||||||
"""Get the current volume (0-200)"""
|
"""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
|
cursor.execute("""SELECT volume
|
||||||
FROM servers
|
FROM servers
|
||||||
WHERE server_id = ?""",
|
WHERE server_id = ?""",
|
||||||
(server_id,))
|
(server_id,))
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return result[0] if result else 100
|
return result[0] if result else 100
|
||||||
|
|
||||||
|
|
||||||
async def set_volume(server_id, volume):
|
async def set_volume(server_id, volume):
|
||||||
"""Set volume (0-200)"""
|
"""Set volume (0-200)"""
|
||||||
volume = max(0, min(200, volume)) # Clamp between 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
|
cursor.execute("""UPDATE servers
|
||||||
SET volume = ?
|
SET volume = ?
|
||||||
WHERE server_id = ?""",
|
WHERE server_id = ?""",
|
||||||
(volume, server_id))
|
(volume, server_id))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return volume
|
return volume
|
||||||
@@ -512,32 +539,32 @@ async def shuffle_queue(server_id):
|
|||||||
"""Randomize the order of songs in the queue"""
|
"""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)
|
||||||
|
|
||||||
# Get all songs
|
# Get all songs
|
||||||
cursor.execute('''SELECT position, song_link, queued_by, title, thumbnail, duration
|
cursor.execute('''SELECT position, song_link, queued_by, title, thumbnail, duration
|
||||||
FROM songs
|
FROM songs
|
||||||
WHERE server_id = ?
|
WHERE server_id = ?
|
||||||
ORDER BY position''', (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 # Nothing to shuffle
|
||||||
|
|
||||||
# Shuffle the songs (keep positions but randomize order)
|
# Shuffle the songs (keep positions but randomize order)
|
||||||
random.shuffle(songs)
|
random.shuffle(songs)
|
||||||
|
|
||||||
# Delete all current songs
|
# Delete all current songs
|
||||||
cursor.execute('''DELETE FROM songs WHERE server_id = ?''', (server_id,))
|
cursor.execute('''DELETE FROM songs WHERE server_id = ?''', (server_id,))
|
||||||
|
|
||||||
# Re-insert in shuffled order
|
# Re-insert in shuffled order
|
||||||
for i, song in enumerate(songs):
|
for i, song in enumerate(songs):
|
||||||
cursor.execute("""INSERT INTO songs (server_id, song_link, queued_by, position, title, thumbnail, duration)
|
cursor.execute("""INSERT INTO songs (server_id, song_link, queued_by, position, title, thumbnail, duration)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
||||||
(server_id, song[1], song[2], i, song[3], song[4], song[5]))
|
(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
|
||||||
@@ -549,18 +576,18 @@ async def get_effect(server_id):
|
|||||||
"""Get the current audio effect"""
|
"""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
|
cursor.execute("""SELECT effect
|
||||||
FROM servers
|
FROM servers
|
||||||
WHERE server_id = ?""",
|
WHERE server_id = ?""",
|
||||||
(server_id,))
|
(server_id,))
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return result[0] if result else 'none'
|
return result[0] if result else 'none'
|
||||||
|
|
||||||
|
|
||||||
@@ -568,14 +595,14 @@ async def set_effect(server_id, effect_name):
|
|||||||
"""Set the audio effect"""
|
"""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
|
cursor.execute("""UPDATE servers
|
||||||
SET effect = ?
|
SET effect = ?
|
||||||
WHERE server_id = ?""",
|
WHERE server_id = ?""",
|
||||||
(effect_name, server_id))
|
(effect_name, server_id))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return True
|
return True
|
||||||
@@ -597,8 +624,6 @@ def get_effect_emoji(effect_name):
|
|||||||
'bassboost': '🔉💥',
|
'bassboost': '🔉💥',
|
||||||
'nightcore': '⚡🎀',
|
'nightcore': '⚡🎀',
|
||||||
'slowed': '🐌💤',
|
'slowed': '🐌💤',
|
||||||
'earrape': '💀📢',
|
|
||||||
'deepfry': '🍟💥',
|
|
||||||
'distortion': '⚡🔊',
|
'distortion': '⚡🔊',
|
||||||
'reverse': '⏪🔄',
|
'reverse': '⏪🔄',
|
||||||
'chipmunk': '🐿️',
|
'chipmunk': '🐿️',
|
||||||
@@ -609,8 +634,6 @@ def get_effect_emoji(effect_name):
|
|||||||
'vibrato': '〰️',
|
'vibrato': '〰️',
|
||||||
'tremolo': '📳',
|
'tremolo': '📳',
|
||||||
'echo': '🗣️💭',
|
'echo': '🗣️💭',
|
||||||
'phone': '📞',
|
|
||||||
'megaphone': '📢📣'
|
|
||||||
}
|
}
|
||||||
return emojis.get(effect_name, '🔊')
|
return emojis.get(effect_name, '🔊')
|
||||||
|
|
||||||
@@ -622,8 +645,6 @@ def get_effect_description(effect_name):
|
|||||||
'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 (TikTok aesthetic)',
|
||||||
'earrape': '⚠️ Aggressive compression + distortion + clipping ⚠️',
|
|
||||||
'deepfry': '🍟 EXTREME bitcrushing + bass (meme audio) 🍟',
|
|
||||||
'distortion': 'Heavy bitcrushing distortion',
|
'distortion': 'Heavy bitcrushing distortion',
|
||||||
'reverse': 'Plays audio BACKWARDS',
|
'reverse': 'Plays audio BACKWARDS',
|
||||||
'chipmunk': 'High pitched and fast (Alvin mode)',
|
'chipmunk': 'High pitched and fast (Alvin mode)',
|
||||||
@@ -634,8 +655,6 @@ def get_effect_description(effect_name):
|
|||||||
'vibrato': 'Warbling pitch effect',
|
'vibrato': 'Warbling pitch effect',
|
||||||
'tremolo': 'Volume oscillation',
|
'tremolo': 'Volume oscillation',
|
||||||
'echo': 'Echo/reverb effect',
|
'echo': 'Echo/reverb effect',
|
||||||
'phone': 'Sounds like a phone call',
|
|
||||||
'megaphone': 'Megaphone/radio effect'
|
|
||||||
}
|
}
|
||||||
return descriptions.get(effect_name, 'Unknown effect')
|
return descriptions.get(effect_name, 'Unknown effect')
|
||||||
|
|
||||||
@@ -667,14 +686,14 @@ async def play(ctx):
|
|||||||
# Get volume and effect settings
|
# Get volume and effect settings
|
||||||
volume_percent = await get_volume(server_id)
|
volume_percent = await get_volume(server_id)
|
||||||
volume = volume_percent / 100.0 # Convert to 0.0-2.0 range
|
volume = volume_percent / 100.0 # Convert to 0.0-2.0 range
|
||||||
|
|
||||||
current_effect = await get_effect(server_id)
|
current_effect = await get_effect(server_id)
|
||||||
ffmpeg_opts = get_effect_options(current_effect)
|
ffmpeg_opts = get_effect_options(current_effect)
|
||||||
|
|
||||||
# Create audio source with effect and volume control
|
# Create audio source with effect and volume control
|
||||||
audio_source = discord.FFmpegPCMAudio(url, **ffmpeg_opts)
|
audio_source = discord.FFmpegPCMAudio(url, **ffmpeg_opts)
|
||||||
audio_source = discord.PCMVolumeTransformer(audio_source, volume=volume)
|
audio_source = discord.PCMVolumeTransformer(audio_source, volume=volume)
|
||||||
|
|
||||||
# Play with callback to continue queue
|
# Play with callback to continue queue
|
||||||
def after_playing(error):
|
def after_playing(error):
|
||||||
if error:
|
if error:
|
||||||
@@ -690,7 +709,7 @@ async def play(ctx):
|
|||||||
print(f"Error playing next song: {e}")
|
print(f"Error playing next song: {e}")
|
||||||
|
|
||||||
voice_client.play(audio_source, after=after_playing)
|
voice_client.play(audio_source, after=after_playing)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error starting playback: {e}")
|
print(f"Error starting playback: {e}")
|
||||||
# Try to continue with next song
|
# Try to continue with next song
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext.commands.context import Context
|
from discord.ext.commands.context import Context
|
||||||
from discord.ext.commands.converter import CommandError
|
from discord.ext.commands.converter import CommandError
|
||||||
|
from discord.ui import Button, View
|
||||||
import config
|
import config
|
||||||
from . import queue
|
from . import queue
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
# Track last activity time for each server
|
||||||
|
last_activity = {}
|
||||||
|
|
||||||
# Joining/moving to the user's vc in a guild
|
# Joining/moving to the user's vc in a guild
|
||||||
async def join_vc(ctx: Context):
|
async def join_vc(ctx: Context):
|
||||||
@@ -24,7 +29,10 @@ async def join_vc(ctx: Context):
|
|||||||
else:
|
else:
|
||||||
# Safe to ignore type error for now
|
# Safe to ignore type error for now
|
||||||
vc = await ctx.voice_client.move_to(vc)
|
vc = await ctx.voice_client.move_to(vc)
|
||||||
|
|
||||||
|
# Update last activity
|
||||||
|
last_activity[ctx.guild.id] = asyncio.get_event_loop().time()
|
||||||
|
|
||||||
return vc
|
return vc
|
||||||
|
|
||||||
|
|
||||||
@@ -45,20 +53,138 @@ async def leave_vc(ctx: Context):
|
|||||||
if author_vc is None or vc != author_vc:
|
if author_vc is None or vc != author_vc:
|
||||||
raise CommandError("You are not in this voice channel")
|
raise CommandError("You are not in this voice channel")
|
||||||
|
|
||||||
# Disconnect
|
# Clear the queue for this server
|
||||||
await ctx.voice_client.disconnect(force=False)
|
await queue.clear(ctx.guild.id)
|
||||||
|
|
||||||
|
# Stop any currently playing audio
|
||||||
|
if ctx.voice_client.is_playing():
|
||||||
|
ctx.voice_client.stop()
|
||||||
|
|
||||||
|
# Disconnect with force to ensure it actually leaves
|
||||||
|
try:
|
||||||
|
await ctx.voice_client.disconnect(force=True)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error disconnecting: {e}")
|
||||||
|
# If regular disconnect fails, try cleanup
|
||||||
|
await ctx.voice_client.cleanup()
|
||||||
|
|
||||||
|
# Remove from activity tracker
|
||||||
|
if ctx.guild.id in last_activity:
|
||||||
|
del last_activity[ctx.guild.id]
|
||||||
|
|
||||||
|
|
||||||
|
# Auto-disconnect if inactive
|
||||||
|
async def check_inactivity(bot):
|
||||||
|
"""Background task to check for inactive voice connections"""
|
||||||
|
try:
|
||||||
|
current_time = asyncio.get_event_loop().time()
|
||||||
|
|
||||||
|
for guild_id, last_time in list(last_activity.items()):
|
||||||
|
# If inactive for more than 5 minutes
|
||||||
|
if current_time - last_time > 300: # 300 seconds = 5 minutes
|
||||||
|
# Find the guild and voice client
|
||||||
|
guild = bot.get_guild(guild_id)
|
||||||
|
if guild and guild.voice_client:
|
||||||
|
# Check if not playing
|
||||||
|
if not guild.voice_client.is_playing():
|
||||||
|
print(f"Auto-disconnecting from {guild.name} due to inactivity")
|
||||||
|
await queue.clear(guild_id)
|
||||||
|
try:
|
||||||
|
await guild.voice_client.disconnect(force=True)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
del last_activity[guild_id]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in inactivity checker: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# Update activity timestamp when playing
|
||||||
|
def update_activity(guild_id):
|
||||||
|
"""Call this when a song starts playing"""
|
||||||
|
last_activity[guild_id] = asyncio.get_event_loop().time()
|
||||||
|
|
||||||
|
|
||||||
|
# Interactive buttons for queue control
|
||||||
|
class QueueControls(View):
|
||||||
|
def __init__(self, ctx):
|
||||||
|
super().__init__(timeout=300) # 5 minute timeout
|
||||||
|
self.ctx = ctx
|
||||||
|
|
||||||
|
@discord.ui.button(label="⏭️ Skip", style=discord.ButtonStyle.primary)
|
||||||
|
async def skip_button(self, interaction: discord.Interaction, button: Button):
|
||||||
|
if interaction.user != self.ctx.author:
|
||||||
|
await interaction.response.send_message("❌ Only the person who requested the queue can use these buttons!", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.ctx.voice_client and self.ctx.voice_client.is_playing():
|
||||||
|
self.ctx.voice_client.stop()
|
||||||
|
await interaction.response.send_message("⏭️ Skipped!", ephemeral=True)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message("❌ Nothing is playing!", ephemeral=True)
|
||||||
|
|
||||||
|
@discord.ui.button(label="🔀 Shuffle", style=discord.ButtonStyle.secondary)
|
||||||
|
async def shuffle_button(self, interaction: discord.Interaction, button: Button):
|
||||||
|
if interaction.user != self.ctx.author:
|
||||||
|
await interaction.response.send_message("❌ Only the person who requested the queue can use these buttons!", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
success = await queue.shuffle_queue(self.ctx.guild.id)
|
||||||
|
if success:
|
||||||
|
await interaction.response.send_message("🔀 Queue shuffled!", ephemeral=True)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message("❌ Not enough songs to shuffle!", ephemeral=True)
|
||||||
|
|
||||||
|
@discord.ui.button(label="🔁 Loop", style=discord.ButtonStyle.secondary)
|
||||||
|
async def loop_button(self, interaction: discord.Interaction, button: Button):
|
||||||
|
if interaction.user != self.ctx.author:
|
||||||
|
await interaction.response.send_message("❌ Only the person who requested the queue can use these buttons!", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
current_mode = await queue.get_loop_mode(self.ctx.guild.id)
|
||||||
|
|
||||||
|
# Cycle through modes
|
||||||
|
if current_mode == 'off':
|
||||||
|
new_mode = 'song'
|
||||||
|
elif current_mode == 'song':
|
||||||
|
new_mode = 'queue'
|
||||||
|
else:
|
||||||
|
new_mode = 'off'
|
||||||
|
|
||||||
|
await queue.set_loop_mode(self.ctx.guild.id, new_mode)
|
||||||
|
|
||||||
|
emojis = {'off': '⏹️', 'song': '🔂', 'queue': '🔁'}
|
||||||
|
messages = {
|
||||||
|
'off': 'Loop disabled',
|
||||||
|
'song': 'Looping current song 🔂',
|
||||||
|
'queue': 'Looping entire queue 🔁'
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.response.send_message(f"{emojis[new_mode]} {messages[new_mode]}", ephemeral=True)
|
||||||
|
|
||||||
|
@discord.ui.button(label="🗑️ Clear", style=discord.ButtonStyle.danger)
|
||||||
|
async def clear_button(self, interaction: discord.Interaction, button: Button):
|
||||||
|
if interaction.user != self.ctx.author:
|
||||||
|
await interaction.response.send_message("❌ Only the person who requested the queue can use these buttons!", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
await queue.clear(self.ctx.guild.id)
|
||||||
|
if self.ctx.voice_client and self.ctx.voice_client.is_playing():
|
||||||
|
self.ctx.voice_client.stop()
|
||||||
|
|
||||||
|
await interaction.response.send_message("🗑️ Queue cleared!", ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
# 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):
|
||||||
msg = discord.Embed(
|
msg = discord.Embed(
|
||||||
title=f"{ctx.author.display_name} queued a song!",
|
title="🎵 Song Queued",
|
||||||
color=config.get_color("main"))
|
description=f"**{data['title']}**",
|
||||||
|
color=discord.Color.green())
|
||||||
|
|
||||||
msg.set_thumbnail(url=data['thumbnail'])
|
msg.set_thumbnail(url=data['thumbnail'])
|
||||||
msg.add_field(name=data['title'],
|
msg.add_field(name="⏱️ Duration", value=format_time(data['duration']), inline=True)
|
||||||
value=f"Duration: {format_time(data['duration'])}" + '\n'
|
msg.add_field(name="📍 Position", value=f"#{data['position']}", inline=True)
|
||||||
+ f"Position: {data['position']}")
|
msg.set_footer(text=f"Queued by {ctx.author.display_name}", icon_url=ctx.author.display_avatar.url)
|
||||||
|
|
||||||
await ctx.send(embed=msg)
|
await ctx.send(embed=msg)
|
||||||
|
|
||||||
@@ -67,37 +193,59 @@ async def queue_message(ctx: Context, data: dict):
|
|||||||
async def display_server_queue(ctx: Context, songs, n):
|
async def display_server_queue(ctx: Context, songs, n):
|
||||||
server = ctx.guild
|
server = ctx.guild
|
||||||
|
|
||||||
msg = discord.Embed(
|
# Get current settings
|
||||||
title=f"{server.name}'s Queue!",
|
|
||||||
color=config.get_color("main"))
|
|
||||||
|
|
||||||
current_song = await queue.get_current_song(ctx.guild.id)
|
current_song = await queue.get_current_song(ctx.guild.id)
|
||||||
loop_mode = await queue.get_loop_mode(ctx.guild.id)
|
loop_mode = await queue.get_loop_mode(ctx.guild.id)
|
||||||
volume = await queue.get_volume(ctx.guild.id)
|
volume = await queue.get_volume(ctx.guild.id)
|
||||||
effect = await queue.get_effect(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
|
# Add loop emoji based on mode
|
||||||
loop_emojis = {'off': '', 'song': '🔂', 'queue': '🔁'}
|
loop_emojis = {'off': '', 'song': '🔂', 'queue': '🔁'}
|
||||||
loop_emoji = loop_emojis.get(loop_mode, '')
|
loop_emoji = loop_emojis.get(loop_mode, '')
|
||||||
effect_emoji = queue.get_effect_emoji(effect)
|
effect_emoji = queue.get_effect_emoji(effect)
|
||||||
|
|
||||||
display = f"🔊 Currently playing: ``{current_song}`` {loop_emoji}\n"
|
|
||||||
display += f"Volume: {volume}% | Loop: {loop_mode} | Effect: {effect_emoji} {effect}\n\n"
|
|
||||||
|
|
||||||
for i, song in enumerate(songs):
|
|
||||||
|
|
||||||
# If text is not avaialable do not display
|
# Progress bar (━ for filled, ─ for empty)
|
||||||
time = '' if isinstance(song[1], str) else format_time(song[1])
|
progress_bar = ""
|
||||||
|
if duration > 0:
|
||||||
|
bar_length = 15
|
||||||
|
filled = int((percentage / 100) * bar_length)
|
||||||
|
progress_bar = f"\n{'━' * filled}{'─' * (bar_length - filled)} `{format_time(elapsed)} / {format_time(duration)}`"
|
||||||
|
|
||||||
display += f"``{i + 1}.`` {song[0]} - {time} Queued by {song[2]}\n"
|
# 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)
|
||||||
|
|
||||||
msg.add_field(name="Songs:",
|
# Settings section
|
||||||
value=display,
|
settings = f"🔊 Volume: **{volume}%** | 🔁 Loop: **{loop_mode}** | {effect_emoji} Effect: **{effect}**"
|
||||||
inline=True)
|
embed.add_field(name="⚙️ Settings", value=settings, inline=False)
|
||||||
if n > 10:
|
|
||||||
msg.set_footer(text=f"and {n - 10} more!..")
|
|
||||||
|
|
||||||
await ctx.send(embed=msg)
|
# 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
|
||||||
@@ -111,6 +259,6 @@ def format_time(seconds):
|
|||||||
elif minutes > 0:
|
elif minutes > 0:
|
||||||
return f"{minutes}:{seconds:02d}"
|
return f"{minutes}:{seconds:02d}"
|
||||||
else:
|
else:
|
||||||
return f"{seconds} seconds"
|
return f"0:{seconds:02d}"
|
||||||
except:
|
except:
|
||||||
return ""
|
return "Unknown"
|
||||||
|
|||||||
Reference in New Issue
Block a user