fixed youtube's api interaction, fixed discord connection handshake errors

This commit is contained in:
2025-11-27 12:46:46 +00:00
parent 669e9c19fc
commit c7e033acb6
6 changed files with 217 additions and 79 deletions

32
bot.py
View File

@@ -1,4 +1,5 @@
from discord.ext import commands from discord.ext import commands
from discord.ext import tasks
import config import config
from cogs.music.main import music from cogs.music.main import music
@@ -8,11 +9,34 @@ cogs = [
class Astro(commands.Bot): class Astro(commands.Bot):
# Once the bot is up and running
async def on_ready(self): async def on_ready(self):
# Set the status # Set status
await self.change_presence(activity=config.get_status()) await self.change_presence(activity=config.get_status())
# Setup commands # Load cogs
print(f"Loading {len(cogs)} cogs...")
for cog in 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()

View File

@@ -213,7 +213,7 @@ async def get_current_song(server_id):
conn.commit() conn.commit()
conn.close() conn.close()
return result[0] return result[0] if result else "Nothing"
# Grab max order from server # Grab max order from server
@@ -312,34 +312,51 @@ async def grab_songs(server_id):
return max, songs 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 # Play and loop songs in server
async def play(ctx): async def play(ctx):
"""Main playback loop - plays songs from queue sequentially"""
server_id = ctx.guild.id server_id = ctx.guild.id
voice_client = ctx.voice_client
# Wait until song is stopped playing fully # Safety check
while ctx.voice_client.is_playing(): if voice_client is None:
await asyncio.sleep(1) 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) 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: if url is None:
await update_server(server_id, False) await update_server(server_id, False)
return return
# else play next song and call play again try:
ctx.voice_client.play( # Create audio source
AstroPlayer(ctx, url, FFMPEG_OPTS)) 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)

View File

@@ -3,11 +3,23 @@
import yt_dlp as ytdlp import yt_dlp as ytdlp
import spotipy import spotipy
# Updated yt-dlp options to handle current YouTube changes
ydl_opts = { ydl_opts = {
'format': 'bestaudio/best', 'format': 'bestaudio/best',
'quiet': True, 'quiet': True,
'default_search': 'ytsearch', 'no_warnings': False, # Show warnings for debugging
'ignoreerrors': True, '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): async def main(url, sp):
@@ -45,16 +57,28 @@ async def search_song(search):
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)
except: except Exception as e:
print(f"Error searching for '{search}': {e}")
return [] return []
if info is None: if info is None:
return [] return []
info = info['entries'][0] # Get audio stream URL if 'entries' not in info or len(info['entries']) == 0:
data = {'url': info['url'], return []
'title': info['title'],
'thumbnail': info['thumbnail'], info = info['entries'][0] # Get first search result
'duration': info['duration']} # Grab data
# 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] return [data]
@@ -123,15 +147,29 @@ async def song_download(url):
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)
except: except Exception as e:
print(f"Error downloading '{url}': {e}")
return [] return []
if info is None: if info is None:
return [] return []
data = {'url': info['url'], # Handle both direct videos and playlists with single entry
'title': info['title'], if 'entries' in info:
'thumbnail': info['thumbnail'], if len(info['entries']) == 0:
'duration': info['duration']} # Grab data 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] return [data]
@@ -139,19 +177,26 @@ async def playlist_download(url):
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)
except: except Exception as e:
print(f"Error downloading playlist '{url}': {e}")
return [] return []
if info is None: if info is None:
return [] return []
info = info['entries'] # Grabbing all songs in playlist info = info['entries'] # Grabbing all songs in playlist
urls = [] urls = []
for song in info: for song in info:
data = {'url': song['url'], if song is None or 'url' not in song:
'title': song['title'], continue
'thumbnail': song['thumbnail'],
'duration': song['duration']} # Grab data data = {
'url': song['url'],
'title': song.get('title', 'Unknown'),
'thumbnail': song.get('thumbnail', ''),
'duration': song.get('duration', 0)
}
urls.append(data) urls.append(data)
return urls return urls

View File

@@ -3,6 +3,10 @@ from discord.ext.commands.context import Context
from discord.ext.commands.converter import CommandError from discord.ext.commands.converter import CommandError
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):
@@ -20,11 +24,14 @@ 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 = await vc.connect(reconnect=True, timeout=60.0)
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,8 +52,60 @@ 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"""
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 # 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!", title=f"{server.name}'s Queue!",
color=config.get_color("main")) 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): for i, song in enumerate(songs):
# If text is not avaialable do not display # If text is not avaialable do not display

15
main.py
View File

@@ -1,5 +1,4 @@
import discord import discord
from discord.ext import tasks
from bot import Astro from bot import Astro
import config import config
import help import help
@@ -9,14 +8,22 @@ client.help_command = help.AstroHelp()
@client.event @client.event
async def on_voice_state_update(member, before, after): async def on_voice_state_update(member, before, after):
"""Handle voice state changes - auto-disconnect when alone"""
if member == client.user: if member == client.user:
return #ignore self actions return # ignore self actions
# get the vc # get the vc
voice_client = discord.utils.get(client.voice_clients, guild=member.guild) 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: 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")) client.run(config.get_login("live"))

View File

@@ -1,28 +1,12 @@
# Core bot framework
discord.py==2.6.4
aiohttp==3.8.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 PyNaCl==1.5.0
pytz==2023.3
redis==4.5.5
requests==2.30.0
six==1.16.0
spotipy==2.23.0 spotipy==2.23.0
urllib3==2.0.2
websockets==11.0.3 # YouTube extractor
yarl==1.9.2 yt-dlp>=2025.10.14
yt-dlp
spotipy # System dependencies
PyAudio==0.2.13
mutagen==1.46.0