From c7e033acb6f310898decc57ce8b4c6739331feec Mon Sep 17 00:00:00 2001 From: Top1055 <123alexfeetham@gmail.com> Date: Thu, 27 Nov 2025 12:46:46 +0000 Subject: [PATCH] fixed youtube's api interaction, fixed discord connection handshake errors --- bot.py | 32 +++++++++++++-- cogs/music/queue.py | 59 ++++++++++++++++++---------- cogs/music/translate.py | 87 +++++++++++++++++++++++++++++++---------- cogs/music/util.py | 69 ++++++++++++++++++++++++++++++-- main.py | 15 +++++-- requirements.txt | 34 +++++----------- 6 files changed, 217 insertions(+), 79 deletions(-) diff --git a/bot.py b/bot.py index 6e508e5..bce61ee 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,5 @@ from discord.ext import commands +from discord.ext import tasks import config from cogs.music.main import music @@ -8,11 +9,34 @@ cogs = [ class Astro(commands.Bot): - # Once the bot is up and running async def on_ready(self): - # Set the status + # Set status await self.change_presence(activity=config.get_status()) - # Setup commands + # Load cogs + print(f"Loading {len(cogs)} cogs...") for cog in cogs: - await self.add_cog(cog(self)) + try: + print(f"Attempting to load: {cog.__name__}") + await self.add_cog(cog(self)) + print(f"✅ Loaded {cog.__name__}") + except Exception as e: + print(f"❌ Failed to load {cog.__name__}: {e}") + import traceback + traceback.print_exc() + + # Start inactivity checker + if not self.inactivity_checker.is_running(): + self.inactivity_checker.start() + + print(f"✅ {self.user} is ready and online!") + + @tasks.loop(seconds=30) + async def inactivity_checker(self): + """Check for inactive voice connections""" + from cogs.music import util + await util.check_inactivity(self) + + @inactivity_checker.before_loop + async def before_inactivity_checker(self): + await self.wait_until_ready() diff --git a/cogs/music/queue.py b/cogs/music/queue.py index ad5f087..34a054e 100644 --- a/cogs/music/queue.py +++ b/cogs/music/queue.py @@ -206,14 +206,14 @@ async def get_current_song(server_id): WHERE server_id = ? LIMIT 1;''', (server_id,)) - + result = cursor.fetchone() # Close connection conn.commit() conn.close() - return result[0] + return result[0] if result else "Nothing" # Grab max order from server @@ -312,34 +312,51 @@ async def grab_songs(server_id): return max, songs -# call play on ffmpeg exit -class AstroPlayer(discord.FFmpegPCMAudio): - def __init__(self, ctx, source, options) -> None: - self.ctx = ctx - super().__init__(source, **options) - - def _kill_process(self): - super()._kill_process() - if self.ctx.voice_client.is_playing(): - return - asyncio.run(play(self.ctx)) # Play and loop songs in server async def play(ctx): + """Main playback loop - plays songs from queue sequentially""" server_id = ctx.guild.id + voice_client = ctx.voice_client - # Wait until song is stopped playing fully - while ctx.voice_client.is_playing(): - await asyncio.sleep(1) + # Safety check + if voice_client is None: + await update_server(server_id, False) + return - # check next song + # Wait until current song finishes + while voice_client.is_playing(): + await asyncio.sleep(0.5) + + # Get next song url = await pop(server_id) - # if no other song update server and return + # If no songs left, update status and return if url is None: await update_server(server_id, False) return - # else play next song and call play again - ctx.voice_client.play( - AstroPlayer(ctx, url, FFMPEG_OPTS)) \ No newline at end of file + try: + # Create audio source + audio_source = discord.FFmpegPCMAudio(url, **FFMPEG_OPTS) + + # Play with callback to continue queue + def after_playing(error): + if error: + print(f"Player error: {error}") + # Schedule the next song in the event loop + if voice_client and not voice_client.is_connected(): + return + coro = play(ctx) + fut = asyncio.run_coroutine_threadsafe(coro, ctx.bot.loop) + try: + fut.result() + except Exception as e: + print(f"Error playing next song: {e}") + + voice_client.play(audio_source, after=after_playing) + + except Exception as e: + print(f"Error starting playback: {e}") + # Try to continue with next song + await play(ctx) diff --git a/cogs/music/translate.py b/cogs/music/translate.py index 25cd858..074d342 100644 --- a/cogs/music/translate.py +++ b/cogs/music/translate.py @@ -3,11 +3,23 @@ import yt_dlp as ytdlp import spotipy +# Updated yt-dlp options to handle current YouTube changes ydl_opts = { - 'format': 'bestaudio/best', - 'quiet': True, - 'default_search': 'ytsearch', - 'ignoreerrors': True, + 'format': 'bestaudio/best', + 'quiet': True, + 'no_warnings': False, # Show warnings for debugging + 'default_search': 'ytsearch', + 'ignoreerrors': True, + 'source_address': '0.0.0.0', # Bind to IPv4 to avoid IPv6 issues + 'extract_flat': False, + 'nocheckcertificate': True, + # Add extractor args to handle YouTube's new requirements + 'extractor_args': { + 'youtube': { + 'player_skip': ['webpage', 'configs'], + 'player_client': ['android', 'web'], + } + }, } async def main(url, sp): @@ -45,16 +57,28 @@ async def search_song(search): with ytdlp.YoutubeDL(ydl_opts) as ydl: try: info = ydl.extract_info(f"ytsearch1:{search}", download=False) - except: + except Exception as e: + print(f"Error searching for '{search}': {e}") return [] if info is None: return [] - info = info['entries'][0] # Get audio stream URL - data = {'url': info['url'], - 'title': info['title'], - 'thumbnail': info['thumbnail'], - 'duration': info['duration']} # Grab data + if 'entries' not in info or len(info['entries']) == 0: + return [] + + info = info['entries'][0] # Get first search result + + # Get the best audio stream URL + if 'url' not in info: + print(f"No URL found for: {search}") + return [] + + data = { + 'url': info['url'], + 'title': info.get('title', 'Unknown'), + 'thumbnail': info.get('thumbnail', ''), + 'duration': info.get('duration', 0) + } return [data] @@ -123,15 +147,29 @@ async def song_download(url): with ytdlp.YoutubeDL(ydl_opts) as ydl: try: info = ydl.extract_info(url, download=False) - except: + except Exception as e: + print(f"Error downloading '{url}': {e}") return [] + if info is None: return [] - data = {'url': info['url'], - 'title': info['title'], - 'thumbnail': info['thumbnail'], - 'duration': info['duration']} # Grab data + # Handle both direct videos and playlists with single entry + if 'entries' in info: + if len(info['entries']) == 0: + return [] + info = info['entries'][0] + + if 'url' not in info: + print(f"No URL found for: {url}") + return [] + + data = { + 'url': info['url'], + 'title': info.get('title', 'Unknown'), + 'thumbnail': info.get('thumbnail', ''), + 'duration': info.get('duration', 0) + } return [data] @@ -139,19 +177,26 @@ async def playlist_download(url): with ytdlp.YoutubeDL(ydl_opts) as ydl: try: info = ydl.extract_info(url, download=False) - except: + except Exception as e: + print(f"Error downloading playlist '{url}': {e}") return [] + if info is None: return [] - info = info['entries'] # Grabbing all songs in playlist + info = info['entries'] # Grabbing all songs in playlist urls = [] for song in info: - data = {'url': song['url'], - 'title': song['title'], - 'thumbnail': song['thumbnail'], - 'duration': song['duration']} # Grab data + if song is None or 'url' not in song: + continue + + data = { + 'url': song['url'], + 'title': song.get('title', 'Unknown'), + 'thumbnail': song.get('thumbnail', ''), + 'duration': song.get('duration', 0) + } urls.append(data) return urls diff --git a/cogs/music/util.py b/cogs/music/util.py index 23a4a75..0009752 100644 --- a/cogs/music/util.py +++ b/cogs/music/util.py @@ -3,6 +3,10 @@ from discord.ext.commands.context import Context from discord.ext.commands.converter import CommandError import config from . import queue +import asyncio + +# Track last activity time for each server +last_activity = {} # Joining/moving to the user's vc in a guild async def join_vc(ctx: Context): @@ -20,11 +24,14 @@ 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 = await vc.connect(reconnect=True, timeout=60.0) else: # Safe to ignore type error for now vc = await ctx.voice_client.move_to(vc) + # Update last activity + last_activity[ctx.guild.id] = asyncio.get_event_loop().time() + return vc @@ -45,8 +52,60 @@ async def leave_vc(ctx: Context): if author_vc is None or vc != author_vc: raise CommandError("You are not in this voice channel") - # Disconnect - await ctx.voice_client.disconnect(force=False) + # Clear the queue for this server + 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""" + while True: + 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] + + # Check every 30 seconds + await asyncio.sleep(30) + except Exception as e: + print(f"Error in inactivity checker: {e}") + await asyncio.sleep(30) + + +# 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() # Build a display message for queuing a new song @@ -71,7 +130,9 @@ async def display_server_queue(ctx: Context, songs, n): title=f"{server.name}'s Queue!", color=config.get_color("main")) - display = f"🔊 Currently playing: ``{await queue.get_current_song(ctx.guild.id)}``\n\n" + current_song = await queue.get_current_song(ctx.guild.id) + display = f"🔊 Currently playing: ``{current_song}``\n\n" + for i, song in enumerate(songs): # If text is not avaialable do not display diff --git a/main.py b/main.py index e82b22e..3ddbd97 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,4 @@ import discord -from discord.ext import tasks from bot import Astro import config import help @@ -9,14 +8,22 @@ client.help_command = help.AstroHelp() @client.event async def on_voice_state_update(member, before, after): + """Handle voice state changes - auto-disconnect when alone""" if member == client.user: - return #ignore self actions + return # ignore self actions # get the vc voice_client = discord.utils.get(client.voice_clients, guild=member.guild) - # if the bot is the only connected member, leave + # if the bot is the only connected member, disconnect if voice_client and len(voice_client.channel.members) == 1: - await voice_client.disconnect() + from cogs.music import queue + # Clean up the queue + await queue.clear(member.guild.id) + await queue.update_server(member.guild.id, False) + try: + await voice_client.disconnect(force=True) + except Exception as e: + print(f"Error auto-disconnecting: {e}") client.run(config.get_login("live")) diff --git a/requirements.txt b/requirements.txt index 6e575d5..4702160 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,28 +1,12 @@ +# Core bot framework +discord.py==2.6.4 aiohttp==3.8.4 -aiosignal==1.3.1 -async-timeout==4.0.2 -attrs==23.1.0 -Brotli==1.0.9 -certifi==2023.5.7 -cffi==1.15.1 -charset-normalizer==3.1.0 -discord==2.2.3 -discord.py==2.2.3 -frozenlist==1.3.3 -idna==3.4 -multidict==6.0.4 -mutagen==1.46.0 -PyAudio==0.2.13 -pycparser==2.21 -pycryptodomex==3.18.0 PyNaCl==1.5.0 -pytz==2023.3 -redis==4.5.5 -requests==2.30.0 -six==1.16.0 spotipy==2.23.0 -urllib3==2.0.2 -websockets==11.0.3 -yarl==1.9.2 -yt-dlp -spotipy + +# YouTube extractor +yt-dlp>=2025.10.14 + +# System dependencies +PyAudio==0.2.13 +mutagen==1.46.0