added type hints all over the project, updated mypy to ignore missing types from libraries

This commit is contained in:
2025-11-29 18:56:44 +00:00
parent 7c6249b120
commit 4ef4bdf309
5 changed files with 211 additions and 85 deletions

View File

@@ -328,7 +328,7 @@ class music(commands.Cog):
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 = None):
"""Toggle between loop modes or set a specific mode"""
server = ctx.guild
@@ -391,7 +391,7 @@ class music(commands.Cog):
description="Set playback volume",
aliases=['vol', 'v'])
@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"""
server = ctx.guild
@@ -435,7 +435,7 @@ class music(commands.Cog):
description="Apply audio effects to playback",
aliases=['fx', 'filter'])
@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"""
server = ctx.guild

View File

@@ -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 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()
@@ -60,7 +64,7 @@ async def main(url, sp):
return []
async def search_song(search):
async def search_song(search: str) -> list[dict[str, Any]]:
with ytdlp.YoutubeDL(ydl_opts) as ydl:
try:
info = ydl.extract_info(f"ytsearch1:{search}", download=False)
@@ -89,7 +93,7 @@ async def search_song(search):
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])
search = ""
@@ -106,7 +110,11 @@ async def spotify_song(url, sp):
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
code = url.split("/")[-1].split("?")[0]
@@ -115,42 +123,36 @@ async def spotify_playlist(url, sp):
results = sp.playlist_tracks(code)['items']
except spotipy.exceptions.SpotifyException:
return []
# Go through the tracks
songs = []
# Go through the tracks and build search queries
songs: list[str | dict[str, Any]] = [] # Explicit type for mypy
for track in results:
search = ""
# Fetch all artists
for artist in track['track']['artists']:
# Add all artists to search
search += f"{artist['name']}, "
# Remove last column
# Remove last comma
search = search[:-2]
search += f" - {track['track']['name']}"
songs.append(search)
#searched_result = search_song(search)
#if searched_result == []:
#continue
#songs.append(searched_result[0])
# Fetch first song's full data
while True:
search_result = await search_song(songs[0])
search_result = await search_song(songs[0]) # type: ignore
if search_result == []:
songs.pop(0)
continue
else:
songs[0] = search_result[0]
songs[0] = search_result[0] # Replace string with dict
break
return songs
async def song_download(url):
async def song_download(url: str) -> list[dict[str, Any]]:
with ytdlp.YoutubeDL(ydl_opts) as ydl:
try:
info = ydl.extract_info(url, download=False)
@@ -180,7 +182,7 @@ async def song_download(url):
return [data]
async def playlist_download(url):
async def playlist_download(url: str) -> list[dict[str, Any]]:
with ytdlp.YoutubeDL(ydl_opts) as ydl:
try:
info = ydl.extract_info(url, download=False)

View File

@@ -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
from discord.ext.commands.context import Context
from discord.ext.commands.converter import CommandError
@@ -7,15 +13,29 @@ from . import queue
import asyncio
# 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
author_voice = getattr(ctx.author, "voice")
if author_voice is None:
# Raise exception if user is not in vc
raise CommandError("User is not in voice channel")
# Get user's vc
@@ -25,19 +45,26 @@ async def join_vc(ctx: Context):
# Join or move to the user's vc
if ctx.voice_client is None:
vc = await vc.connect()
vc_client = await vc.connect()
else:
# Safe to ignore type error for now
vc = await ctx.voice_client.move_to(vc)
vc_client = await ctx.voice_client.move_to(vc)
# Update last activity
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):
async def leave_vc(ctx: Context) -> None:
"""
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 ctx.voice_client is None:
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]
# Auto-disconnect if inactive
async def check_inactivity(bot):
"""Background task to check for inactive voice connections"""
# ===================================
# Inactivity Management
# ===================================
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:
current_time = asyncio.get_event_loop().time()
@@ -98,20 +134,34 @@ async def check_inactivity(bot):
print(f"Error in inactivity checker: {e}")
# Update activity timestamp when playing
def update_activity(guild_id):
"""Call this when a song starts playing"""
def update_activity(guild_id: int) -> None:
"""
Update activity timestamp when a song starts playing
Args:
guild_id: Discord guild/server ID
"""
last_activity[guild_id] = asyncio.get_event_loop().time()
# Interactive buttons for queue control
# ===================================
# Queue Display & Controls
# ===================================
class QueueControls(View):
def __init__(self, ctx):
super().__init__(timeout=None) # No timeout allows buttons to stay active longer
"""Interactive buttons for queue control"""
def __init__(self, ctx: Context) -> None:
super().__init__(timeout=None) # No timeout allows buttons to stay active longer
self.ctx = ctx
async def refresh_message(self, interaction: discord.Interaction):
"""Helper to regenerate the embed and edit the message"""
async def refresh_message(self, interaction: discord.Interaction) -> None:
"""
Helper to regenerate the embed and edit the message
Args:
interaction: Discord interaction from button press
"""
try:
# Generate new embed
embed, view = await generate_queue_ui(self.ctx)
@@ -119,10 +169,13 @@ class QueueControls(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)
await interaction.response.send_message(
"Refreshed, but something went wrong updating the display.",
ephemeral=True
)
@discord.ui.button(label="⏭️ Skip", style=discord.ButtonStyle.primary)
async def skip_button(self, interaction: discord.Interaction, button: Button):
async def skip_button(self, interaction: discord.Interaction, button: Button) -> None:
if interaction.user not in self.ctx.voice_client.channel.members:
await interaction.response.send_message("❌ You must be in the voice channel!", ephemeral=True)
return
@@ -130,11 +183,6 @@ class QueueControls(View):
# 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:
@@ -144,35 +192,45 @@ class QueueControls(View):
await self.refresh_message(interaction)
@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 self.refresh_message(interaction)
@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)
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 self.refresh_message(interaction)
@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)
if self.ctx.voice_client and self.ctx.voice_client.is_playing():
self.ctx.voice_client.stop()
await self.refresh_message(interaction)
@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)
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
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
current = await queue.get_current_song(guild_id)
loop_mode = await queue.get_loop_mode(guild_id)
volume = await queue.get_volume(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
loop_map = {
'off': {'emoji': '⏹️', 'text': 'Off'},
'song': {'emoji': '🔂', 'text': 'Song'},
'queue': {'emoji': '🔁', 'text': 'Queue'}
}
'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']
@@ -197,11 +255,9 @@ async def generate_queue_ui(ctx: Context):
# 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)}`"
@@ -215,14 +271,11 @@ async def generate_queue_ui(ctx: Context):
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"
@@ -241,7 +294,7 @@ async def generate_queue_ui(ctx: Context):
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:
embed.set_footer(text=f"Waitlist: {remaining} more songs...")
else:
@@ -251,23 +304,38 @@ async def generate_queue_ui(ctx: Context):
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):
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)
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(
title="🎵 Song Queued",
description=f"**{data['title']}**",
color=discord.Color.green())
title="🎵 Song Queued",
description=f"**{data['title']}**",
color=discord.Color.green()
)
msg.set_thumbnail(url=data['thumbnail'])
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)
# 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:
seconds = int(seconds)
minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)