added type hints all over the project, updated mypy to ignore missing types from libraries
This commit is contained in:
25
bot.py
25
bot.py
@@ -1,20 +1,30 @@
|
|||||||
|
"""
|
||||||
|
Groovy-Zilean Bot Class
|
||||||
|
Main bot implementation with cog loading and background tasks
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord.ext import tasks
|
from discord.ext import tasks
|
||||||
from cogs.music.main import music
|
from cogs.music.main import music
|
||||||
from help import GroovyHelp # Import the new Help Cog
|
from help import GroovyHelp
|
||||||
|
|
||||||
cogs = [
|
# List of cogs to load on startup
|
||||||
|
cogs: list[type[commands.Cog]] = [
|
||||||
music,
|
music,
|
||||||
GroovyHelp
|
GroovyHelp
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class Groovy(commands.Bot):
|
class Groovy(commands.Bot):
|
||||||
def __init__(self, *args, **kwargs):
|
"""Custom bot class with automatic cog loading and inactivity checking"""
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
# We force help_command to None because we are using a custom Cog for it
|
# 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
|
# But we pass all other args (like command_prefix) to the parent
|
||||||
super().__init__(*args, help_command=None, **kwargs)
|
super().__init__(*args, help_command=None, **kwargs)
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self) -> None:
|
||||||
import config # Imported here to avoid circular dependencies if any
|
import config # Imported here to avoid circular dependencies if any
|
||||||
|
|
||||||
# Set status
|
# Set status
|
||||||
@@ -47,11 +57,12 @@ class Groovy(commands.Bot):
|
|||||||
print(f"✅ {self.user} is ready and online!")
|
print(f"✅ {self.user} is ready and online!")
|
||||||
|
|
||||||
@tasks.loop(seconds=30)
|
@tasks.loop(seconds=30)
|
||||||
async def inactivity_checker(self):
|
async def inactivity_checker(self) -> None:
|
||||||
"""Check for inactive voice connections"""
|
"""Check for inactive voice connections every 30 seconds"""
|
||||||
from cogs.music import util
|
from cogs.music import util
|
||||||
await util.check_inactivity(self)
|
await util.check_inactivity(self)
|
||||||
|
|
||||||
@inactivity_checker.before_loop
|
@inactivity_checker.before_loop
|
||||||
async def before_inactivity_checker(self):
|
async def before_inactivity_checker(self) -> None:
|
||||||
|
"""Wait for bot to be ready before starting inactivity checker"""
|
||||||
await self.wait_until_ready()
|
await self.wait_until_ready()
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ class music(commands.Cog):
|
|||||||
app_commands.Choice(name="Song", value="song"),
|
app_commands.Choice(name="Song", value="song"),
|
||||||
app_commands.Choice(name="Queue", value="queue")
|
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 = 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
|
||||||
|
|
||||||
@@ -391,7 +391,7 @@ class music(commands.Cog):
|
|||||||
description="Set playback volume",
|
description="Set playback volume",
|
||||||
aliases=['vol', 'v'])
|
aliases=['vol', 'v'])
|
||||||
@app_commands.describe(level="Volume level (0-200%, default shows current)")
|
@app_commands.describe(level="Volume level (0-200%, default shows current)")
|
||||||
async def volume(self, ctx: Context, level: int = None):
|
async def volume(self, ctx: Context, level: int | None = None):
|
||||||
"""Set or display the current volume"""
|
"""Set or display the current volume"""
|
||||||
server = ctx.guild
|
server = ctx.guild
|
||||||
|
|
||||||
@@ -435,7 +435,7 @@ class music(commands.Cog):
|
|||||||
description="Apply audio effects to playback",
|
description="Apply audio effects to playback",
|
||||||
aliases=['fx', 'filter'])
|
aliases=['fx', 'filter'])
|
||||||
@app_commands.describe(effect_name="The audio effect to apply (leave empty to see list)")
|
@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 = None):
|
||||||
"""Apply or list audio effects"""
|
"""Apply or list audio effects"""
|
||||||
server = ctx.guild
|
server = ctx.guild
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# Handles translating urls and search terms
|
"""
|
||||||
|
URL and search query handling for Groovy-Zilean
|
||||||
|
Translates YouTube, Spotify, SoundCloud URLs and search queries into playable audio
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
import yt_dlp as ytdlp
|
import yt_dlp as ytdlp
|
||||||
import spotipy
|
import spotipy
|
||||||
|
|
||||||
@@ -22,7 +26,7 @@ ydl_opts = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
async def main(url, sp):
|
async def main(url: str, sp: spotipy.Spotify) -> list[dict[str, Any] | str]:
|
||||||
|
|
||||||
#url = url.lower()
|
#url = url.lower()
|
||||||
|
|
||||||
@@ -60,7 +64,7 @@ async def main(url, sp):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
async def search_song(search):
|
async def search_song(search: str) -> list[dict[str, Any]]:
|
||||||
with ytdlp.YoutubeDL(ydl_opts) as ydl:
|
with ytdlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
try:
|
try:
|
||||||
info = ydl.extract_info(f"ytsearch1:{search}", download=False)
|
info = ydl.extract_info(f"ytsearch1:{search}", download=False)
|
||||||
@@ -89,7 +93,7 @@ async def search_song(search):
|
|||||||
return [data]
|
return [data]
|
||||||
|
|
||||||
|
|
||||||
async def spotify_song(url, sp):
|
async def spotify_song(url: str, sp: spotipy.Spotify) -> list[dict[str, Any]]:
|
||||||
track = sp.track(url.split("/")[-1].split("?")[0])
|
track = sp.track(url.split("/")[-1].split("?")[0])
|
||||||
search = ""
|
search = ""
|
||||||
|
|
||||||
@@ -106,7 +110,11 @@ async def spotify_song(url, sp):
|
|||||||
return await search_song(query)
|
return await search_song(query)
|
||||||
|
|
||||||
|
|
||||||
async def spotify_playlist(url, sp):
|
async def spotify_playlist(url: str, sp: spotipy.Spotify) -> list[str | dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Get songs from a Spotify playlist
|
||||||
|
Returns a mixed list where first item is dict, rest are search strings
|
||||||
|
"""
|
||||||
# Get the playlist uri code
|
# Get the playlist uri code
|
||||||
code = url.split("/")[-1].split("?")[0]
|
code = url.split("/")[-1].split("?")[0]
|
||||||
|
|
||||||
@@ -116,41 +124,35 @@ async def spotify_playlist(url, sp):
|
|||||||
except spotipy.exceptions.SpotifyException:
|
except spotipy.exceptions.SpotifyException:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Go through the tracks
|
# Go through the tracks and build search queries
|
||||||
songs = []
|
songs: list[str | dict[str, Any]] = [] # Explicit type for mypy
|
||||||
for track in results:
|
for track in results:
|
||||||
search = ""
|
search = ""
|
||||||
|
|
||||||
# Fetch all artists
|
# Fetch all artists
|
||||||
for artist in track['track']['artists']:
|
for artist in track['track']['artists']:
|
||||||
|
|
||||||
# Add all artists to search
|
# Add all artists to search
|
||||||
search += f"{artist['name']}, "
|
search += f"{artist['name']}, "
|
||||||
|
|
||||||
# Remove last column
|
# Remove last comma
|
||||||
search = search[:-2]
|
search = search[:-2]
|
||||||
search += f" - {track['track']['name']}"
|
search += f" - {track['track']['name']}"
|
||||||
songs.append(search)
|
songs.append(search)
|
||||||
|
|
||||||
#searched_result = search_song(search)
|
# Fetch first song's full data
|
||||||
#if searched_result == []:
|
|
||||||
#continue
|
|
||||||
|
|
||||||
#songs.append(searched_result[0])
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
search_result = await search_song(songs[0])
|
search_result = await search_song(songs[0]) # type: ignore
|
||||||
if search_result == []:
|
if search_result == []:
|
||||||
songs.pop(0)
|
songs.pop(0)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
songs[0] = search_result[0]
|
songs[0] = search_result[0] # Replace string with dict
|
||||||
break
|
break
|
||||||
|
|
||||||
return songs
|
return songs
|
||||||
|
|
||||||
|
|
||||||
async def song_download(url):
|
async def song_download(url: str) -> list[dict[str, Any]]:
|
||||||
with ytdlp.YoutubeDL(ydl_opts) as ydl:
|
with ytdlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
try:
|
try:
|
||||||
info = ydl.extract_info(url, download=False)
|
info = ydl.extract_info(url, download=False)
|
||||||
@@ -180,7 +182,7 @@ async def song_download(url):
|
|||||||
return [data]
|
return [data]
|
||||||
|
|
||||||
|
|
||||||
async def playlist_download(url):
|
async def playlist_download(url: str) -> list[dict[str, Any]]:
|
||||||
with ytdlp.YoutubeDL(ydl_opts) as ydl:
|
with ytdlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
try:
|
try:
|
||||||
info = ydl.extract_info(url, download=False)
|
info = ydl.extract_info(url, download=False)
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
"""
|
||||||
|
Utility functions for Groovy-Zilean music bot
|
||||||
|
Handles voice channel operations, queue display, and inactivity tracking
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
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
|
||||||
@@ -7,15 +13,29 @@ from . import queue
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
# Track last activity time for each server
|
# Track last activity time for each server
|
||||||
last_activity = {}
|
last_activity: dict[int, float] = {}
|
||||||
|
|
||||||
# Joining/moving to the user's vc in a guild
|
|
||||||
async def join_vc(ctx: Context):
|
|
||||||
|
|
||||||
|
# ===================================
|
||||||
|
# Voice Channel Management
|
||||||
|
# ===================================
|
||||||
|
|
||||||
|
async def join_vc(ctx: Context) -> discord.VoiceClient:
|
||||||
|
"""
|
||||||
|
Join or move to the user's voice channel
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctx: Command context
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The voice client connection
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
CommandError: If user is not in a voice channel
|
||||||
|
"""
|
||||||
# Get the user's vc
|
# Get the user's vc
|
||||||
author_voice = getattr(ctx.author, "voice")
|
author_voice = getattr(ctx.author, "voice")
|
||||||
if author_voice is None:
|
if author_voice is None:
|
||||||
# Raise exception if user is not in vc
|
|
||||||
raise CommandError("User is not in voice channel")
|
raise CommandError("User is not in voice channel")
|
||||||
|
|
||||||
# Get user's vc
|
# Get user's vc
|
||||||
@@ -25,19 +45,26 @@ async def join_vc(ctx: Context):
|
|||||||
|
|
||||||
# Join or move to the user's vc
|
# Join or move to the user's vc
|
||||||
if ctx.voice_client is None:
|
if ctx.voice_client is None:
|
||||||
vc = await vc.connect()
|
vc_client = await vc.connect()
|
||||||
else:
|
else:
|
||||||
# Safe to ignore type error for now
|
vc_client = await ctx.voice_client.move_to(vc)
|
||||||
vc = await ctx.voice_client.move_to(vc)
|
|
||||||
|
|
||||||
# Update last activity
|
# Update last activity
|
||||||
last_activity[ctx.guild.id] = asyncio.get_event_loop().time()
|
last_activity[ctx.guild.id] = asyncio.get_event_loop().time()
|
||||||
|
|
||||||
return vc
|
return vc_client
|
||||||
|
|
||||||
|
|
||||||
# Leaving the voice channel of a user
|
async def leave_vc(ctx: Context) -> None:
|
||||||
async def leave_vc(ctx: Context):
|
"""
|
||||||
|
Leave the voice channel and clean up
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctx: Command context
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
CommandError: If bot is not in VC or user is not in same VC
|
||||||
|
"""
|
||||||
# If the bot is not in a vc of this server
|
# If the bot is not in a vc of this server
|
||||||
if ctx.voice_client is None:
|
if ctx.voice_client is None:
|
||||||
raise CommandError("I am not in a voice channel")
|
raise CommandError("I am not in a voice channel")
|
||||||
@@ -73,9 +100,18 @@ async def leave_vc(ctx: Context):
|
|||||||
del last_activity[ctx.guild.id]
|
del last_activity[ctx.guild.id]
|
||||||
|
|
||||||
|
|
||||||
# Auto-disconnect if inactive
|
# ===================================
|
||||||
async def check_inactivity(bot):
|
# Inactivity Management
|
||||||
"""Background task to check for inactive voice connections"""
|
# ===================================
|
||||||
|
|
||||||
|
async def check_inactivity(bot: discord.Client) -> None:
|
||||||
|
"""
|
||||||
|
Background task to check for inactive voice connections
|
||||||
|
Auto-disconnects after 5 minutes of inactivity
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot: The Discord bot instance
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
current_time = asyncio.get_event_loop().time()
|
current_time = asyncio.get_event_loop().time()
|
||||||
|
|
||||||
@@ -98,20 +134,34 @@ async def check_inactivity(bot):
|
|||||||
print(f"Error in inactivity checker: {e}")
|
print(f"Error in inactivity checker: {e}")
|
||||||
|
|
||||||
|
|
||||||
# Update activity timestamp when playing
|
def update_activity(guild_id: int) -> None:
|
||||||
def update_activity(guild_id):
|
"""
|
||||||
"""Call this when a song starts playing"""
|
Update activity timestamp when a song starts playing
|
||||||
|
|
||||||
|
Args:
|
||||||
|
guild_id: Discord guild/server ID
|
||||||
|
"""
|
||||||
last_activity[guild_id] = asyncio.get_event_loop().time()
|
last_activity[guild_id] = asyncio.get_event_loop().time()
|
||||||
|
|
||||||
|
|
||||||
# Interactive buttons for queue control
|
# ===================================
|
||||||
|
# Queue Display & Controls
|
||||||
|
# ===================================
|
||||||
|
|
||||||
class QueueControls(View):
|
class QueueControls(View):
|
||||||
def __init__(self, ctx):
|
"""Interactive buttons for queue control"""
|
||||||
super().__init__(timeout=None) # No timeout allows buttons to stay active longer
|
|
||||||
|
def __init__(self, ctx: Context) -> None:
|
||||||
|
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):
|
async def refresh_message(self, interaction: discord.Interaction) -> None:
|
||||||
"""Helper to regenerate the embed and edit the message"""
|
"""
|
||||||
|
Helper to regenerate the embed and edit the message
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interaction: Discord interaction from button press
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
# Generate new embed
|
# Generate new embed
|
||||||
embed, view = await generate_queue_ui(self.ctx)
|
embed, view = await generate_queue_ui(self.ctx)
|
||||||
@@ -119,10 +169,13 @@ class QueueControls(View):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Fallback if edit fails
|
# Fallback if edit fails
|
||||||
if not interaction.response.is_done():
|
if not interaction.response.is_done():
|
||||||
await interaction.response.send_message("Refreshed, but something went wrong updating the display.", ephemeral=True)
|
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) -> None:
|
||||||
if interaction.user not in self.ctx.voice_client.channel.members:
|
if interaction.user not in self.ctx.voice_client.channel.members:
|
||||||
await interaction.response.send_message("❌ You must be in the voice channel!", ephemeral=True)
|
await interaction.response.send_message("❌ You must be in the voice channel!", ephemeral=True)
|
||||||
return
|
return
|
||||||
@@ -130,11 +183,6 @@ class QueueControls(View):
|
|||||||
# Loop logic check
|
# Loop logic check
|
||||||
loop_mode = await queue.get_loop_mode(self.ctx.guild.id)
|
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
|
# Perform the skip
|
||||||
await queue.pop(self.ctx.guild.id, True, skip_mode=True)
|
await queue.pop(self.ctx.guild.id, True, skip_mode=True)
|
||||||
if self.ctx.voice_client:
|
if self.ctx.voice_client:
|
||||||
@@ -144,35 +192,45 @@ class QueueControls(View):
|
|||||||
await self.refresh_message(interaction)
|
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) -> None:
|
||||||
await queue.shuffle_queue(self.ctx.guild.id)
|
await queue.shuffle_queue(self.ctx.guild.id)
|
||||||
await self.refresh_message(interaction)
|
await self.refresh_message(interaction)
|
||||||
|
|
||||||
@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) -> None:
|
||||||
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')
|
new_mode = 'song' if current_mode == 'off' else ('queue' if current_mode == 'song' else 'off')
|
||||||
await queue.set_loop_mode(self.ctx.guild.id, new_mode)
|
await queue.set_loop_mode(self.ctx.guild.id, new_mode)
|
||||||
await self.refresh_message(interaction)
|
await self.refresh_message(interaction)
|
||||||
|
|
||||||
@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) -> None:
|
||||||
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 self.refresh_message(interaction)
|
||||||
|
|
||||||
@discord.ui.button(label="🔄 Refresh", style=discord.ButtonStyle.gray)
|
@discord.ui.button(label="🔄 Refresh", style=discord.ButtonStyle.gray)
|
||||||
async def refresh_button(self, interaction: discord.Interaction, button: Button):
|
async def refresh_button(self, interaction: discord.Interaction, button: Button) -> None:
|
||||||
await self.refresh_message(interaction)
|
await self.refresh_message(interaction)
|
||||||
|
|
||||||
async def generate_queue_ui(ctx: Context):
|
|
||||||
|
async def generate_queue_ui(ctx: Context) -> tuple[discord.Embed, QueueControls]:
|
||||||
|
"""
|
||||||
|
Generate the queue embed and controls
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctx: Command context
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (embed, view) for displaying queue
|
||||||
|
"""
|
||||||
guild_id = ctx.guild.id
|
guild_id = ctx.guild.id
|
||||||
server = ctx.guild
|
server = ctx.guild
|
||||||
|
|
||||||
# Fetch all data
|
# Fetch all data
|
||||||
n, songs = await queue.grab_songs(guild_id)
|
n, songs = await queue.grab_songs(guild_id)
|
||||||
current = await queue.get_current_song(guild_id) # Returns title, thumbnail, url
|
current = await queue.get_current_song(guild_id)
|
||||||
loop_mode = await queue.get_loop_mode(guild_id)
|
loop_mode = await queue.get_loop_mode(guild_id)
|
||||||
volume = await queue.get_volume(guild_id)
|
volume = await queue.get_volume(guild_id)
|
||||||
effect = await queue.get_effect(guild_id)
|
effect = await queue.get_effect(guild_id)
|
||||||
@@ -183,10 +241,10 @@ async def generate_queue_ui(ctx: Context):
|
|||||||
|
|
||||||
# Map loop mode to nicer text
|
# Map loop mode to nicer text
|
||||||
loop_map = {
|
loop_map = {
|
||||||
'off': {'emoji': '⏹️', 'text': 'Off'},
|
'off': {'emoji': '⏹️', 'text': 'Off'},
|
||||||
'song': {'emoji': '🔂', 'text': 'Song'},
|
'song': {'emoji': '🔂', 'text': 'Song'},
|
||||||
'queue': {'emoji': '🔁', 'text': 'Queue'}
|
'queue': {'emoji': '🔁', 'text': 'Queue'}
|
||||||
}
|
}
|
||||||
loop_info = loop_map.get(loop_mode, loop_map['off'])
|
loop_info = loop_map.get(loop_mode, loop_map['off'])
|
||||||
loop_emoji = loop_info['emoji']
|
loop_emoji = loop_info['emoji']
|
||||||
loop_text = loop_info['text']
|
loop_text = loop_info['text']
|
||||||
@@ -197,11 +255,9 @@ async def generate_queue_ui(ctx: Context):
|
|||||||
|
|
||||||
# Progress Bar Logic
|
# Progress Bar Logic
|
||||||
progress_bar = ""
|
progress_bar = ""
|
||||||
# Only show bar if duration > 0 (prevents weird 00:00 bars)
|
|
||||||
if duration > 0:
|
if duration > 0:
|
||||||
bar_length = 16
|
bar_length = 16
|
||||||
filled = int((percentage / 100) * bar_length)
|
filled = int((percentage / 100) * bar_length)
|
||||||
# Ensure filled isn't bigger than length
|
|
||||||
filled = min(filled, bar_length)
|
filled = min(filled, bar_length)
|
||||||
bar_str = '▬' * filled + '🔘' + '▬' * (bar_length - filled)
|
bar_str = '▬' * filled + '🔘' + '▬' * (bar_length - filled)
|
||||||
progress_bar = f"\n`{format_time(elapsed)}` {bar_str} `{format_time(duration)}`"
|
progress_bar = f"\n`{format_time(elapsed)}` {bar_str} `{format_time(duration)}`"
|
||||||
@@ -215,14 +271,11 @@ async def generate_queue_ui(ctx: Context):
|
|||||||
description = "## 💤 Nothing is playing\nUse `/play` to start the party!"
|
description = "## 💤 Nothing is playing\nUse `/play` to start the party!"
|
||||||
else:
|
else:
|
||||||
# Create Hyperlink [Title](URL)
|
# Create Hyperlink [Title](URL)
|
||||||
# If no URL exists, link to Discord homepage as fallback or just bold
|
|
||||||
if url and url.startswith("http"):
|
if url and url.startswith("http"):
|
||||||
song_link = f"[{title}]({url})"
|
song_link = f"[{title}]({url})"
|
||||||
else:
|
else:
|
||||||
song_link = f"**{title}**"
|
song_link = f"**{title}**"
|
||||||
|
|
||||||
# CLEARER STATUS LINE:
|
|
||||||
# Loop: Mode | Effect: Name | Vol: %
|
|
||||||
description = (
|
description = (
|
||||||
f"## 💿 Now Playing\n"
|
f"## 💿 Now Playing\n"
|
||||||
f"### {song_link}\n"
|
f"### {song_link}\n"
|
||||||
@@ -241,7 +294,7 @@ async def generate_queue_ui(ctx: Context):
|
|||||||
|
|
||||||
embed.add_field(name="⏳ Up Next", value=queue_text, inline=False)
|
embed.add_field(name="⏳ Up Next", value=queue_text, inline=False)
|
||||||
|
|
||||||
remaining = (n) - 9 # Approx calculation based on your grabbing logic
|
remaining = n - 9
|
||||||
if remaining > 0:
|
if remaining > 0:
|
||||||
embed.set_footer(text=f"Waitlist: {remaining} more songs...")
|
embed.set_footer(text=f"Waitlist: {remaining} more songs...")
|
||||||
else:
|
else:
|
||||||
@@ -251,23 +304,38 @@ async def generate_queue_ui(ctx: Context):
|
|||||||
if thumb and isinstance(thumb, str) and thumb.startswith("http"):
|
if thumb and isinstance(thumb, str) and thumb.startswith("http"):
|
||||||
embed.set_thumbnail(url=thumb)
|
embed.set_thumbnail(url=thumb)
|
||||||
elif server.icon:
|
elif server.icon:
|
||||||
# Fallback to server icon
|
|
||||||
embed.set_thumbnail(url=server.icon.url)
|
embed.set_thumbnail(url=server.icon.url)
|
||||||
|
|
||||||
view = QueueControls(ctx)
|
view = QueueControls(ctx)
|
||||||
return embed, view
|
return embed, view
|
||||||
|
|
||||||
# The command entry point calls this
|
|
||||||
async def display_server_queue(ctx: Context, songs, n):
|
async def display_server_queue(ctx: Context, songs: list, n: int) -> None:
|
||||||
|
"""
|
||||||
|
Display the server's queue with interactive controls
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctx: Command context
|
||||||
|
songs: List of songs in queue
|
||||||
|
n: Total number of songs
|
||||||
|
"""
|
||||||
embed, view = await generate_queue_ui(ctx)
|
embed, view = await generate_queue_ui(ctx)
|
||||||
await ctx.send(embed=embed, view=view)
|
await ctx.send(embed=embed, view=view)
|
||||||
|
|
||||||
# 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[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Display a message when a song is queued
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctx: Command context
|
||||||
|
data: Song data dictionary
|
||||||
|
"""
|
||||||
msg = discord.Embed(
|
msg = discord.Embed(
|
||||||
title="🎵 Song Queued",
|
title="🎵 Song Queued",
|
||||||
description=f"**{data['title']}**",
|
description=f"**{data['title']}**",
|
||||||
color=discord.Color.green())
|
color=discord.Color.green()
|
||||||
|
)
|
||||||
|
|
||||||
msg.set_thumbnail(url=data['thumbnail'])
|
msg.set_thumbnail(url=data['thumbnail'])
|
||||||
msg.add_field(name="⏱️ Duration", value=format_time(data['duration']), inline=True)
|
msg.add_field(name="⏱️ Duration", value=format_time(data['duration']), inline=True)
|
||||||
@@ -276,9 +344,23 @@ async def queue_message(ctx: Context, data: dict):
|
|||||||
|
|
||||||
await ctx.send(embed=msg)
|
await ctx.send(embed=msg)
|
||||||
|
|
||||||
# Converts seconds into more readable format
|
|
||||||
def format_time(seconds):
|
# ===================================
|
||||||
|
# Utility Functions
|
||||||
|
# ===================================
|
||||||
|
|
||||||
|
def format_time(seconds: int | float) -> str:
|
||||||
|
"""
|
||||||
|
Convert seconds into readable time format (MM:SS or HH:MM:SS)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seconds: Time in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted time string
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
|
seconds = int(seconds)
|
||||||
minutes, seconds = divmod(seconds, 60)
|
minutes, seconds = divmod(seconds, 60)
|
||||||
hours, minutes = divmod(minutes, 60)
|
hours, minutes = divmod(minutes, 60)
|
||||||
|
|
||||||
|
|||||||
31
mypy.ini
Normal file
31
mypy.ini
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# mypy configuration for groovy-zilean
|
||||||
|
# Type checking configuration that's practical for a Discord bot
|
||||||
|
|
||||||
|
[mypy]
|
||||||
|
# Python version
|
||||||
|
python_version = 3.13
|
||||||
|
|
||||||
|
# Ignore missing imports for libraries without type stubs
|
||||||
|
# Discord.py, spotipy, yt-dlp don't have complete type stubs
|
||||||
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
# Be strict about our own code
|
||||||
|
# Start lenient, can tighten later
|
||||||
|
disallow_untyped_defs = False
|
||||||
|
check_untyped_defs = True
|
||||||
|
# Too noisy with discord.py
|
||||||
|
warn_return_any = False
|
||||||
|
warn_unused_configs = True
|
||||||
|
|
||||||
|
# Exclude patterns
|
||||||
|
exclude = venv/
|
||||||
|
|
||||||
|
# Per-module overrides
|
||||||
|
[mypy-discord.*]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
[mypy-spotipy.*]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
[mypy-yt_dlp.*]
|
||||||
|
ignore_missing_imports = True
|
||||||
Reference in New Issue
Block a user