fixed youtube's api interaction, fixed discord connection handshake errors
This commit is contained in:
32
bot.py
32
bot.py
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
15
main.py
@@ -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"))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user