aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cogs/music/audio_playback.py12
-rw-r--r--cogs/vclogs.py10
-rw-r--r--cogs/voice.py122
3 files changed, 111 insertions, 33 deletions
diff --git a/cogs/music/audio_playback.py b/cogs/music/audio_playback.py
index 1313d01..0c885c3 100644
--- a/cogs/music/audio_playback.py
+++ b/cogs/music/audio_playback.py
@@ -173,6 +173,18 @@ class AudioPlaybackManager:
173 vc = await target_channel.connect() 173 vc = await target_channel.connect()
174 except discord.ClientException: 174 except discord.ClientException:
175 vc = discord.utils.get(ctx.bot.voice_clients, guild=guild) 175 vc = discord.utils.get(ctx.bot.voice_clients, guild=guild)
176 elif vc.channel == target_channel:
177 from cogs.voice import is_voice_client_alive
178 if not is_voice_client_alive(vc, guild):
179 logger.warning("Stale voice client, reconnecting...")
180 try:
181 await vc.disconnect()
182 except Exception:
183 pass
184 try:
185 vc = await target_channel.connect()
186 except discord.ClientException:
187 vc = discord.utils.get(ctx.bot.voice_clients, guild=guild)
176 elif vc.channel != target_channel: 188 elif vc.channel != target_channel:
177 logger.info(f"Moving to voice channel: {target_channel.name}") 189 logger.info(f"Moving to voice channel: {target_channel.name}")
178 await vc.move_to(target_channel) 190 await vc.move_to(target_channel)
diff --git a/cogs/vclogs.py b/cogs/vclogs.py
index e5ca202..b538265 100644
--- a/cogs/vclogs.py
+++ b/cogs/vclogs.py
@@ -129,6 +129,16 @@ class VcLogs(commands.Cog):
129 is_connected = voice_status[1] if voice_status else False 129 is_connected = voice_status[1] if voice_status else False
130 is_playing = voice_status[2] if voice_status else False 130 is_playing = voice_status[2] if voice_status else False
131 131
132 if is_connected and not is_playing:
133 from cogs.voice import is_voice_client_alive
134 if not is_voice_client_alive(voice_client, channel.guild):
135 logger.warning("Stale voice client in play_tts, reconnecting")
136 try:
137 await voice_client.disconnect()
138 except Exception:
139 pass
140 is_connected = False
141
132 if not is_connected: 142 if not is_connected:
133 voice_client = await voice_mgr.join_channel(channel) 143 voice_client = await voice_mgr.join_channel(channel)
134 voice_status = await voice_mgr.get_voice_status(channel.guild) 144 voice_status = await voice_mgr.get_voice_status(channel.guild)
diff --git a/cogs/voice.py b/cogs/voice.py
index 046c2a8..6f9a269 100644
--- a/cogs/voice.py
+++ b/cogs/voice.py
@@ -24,60 +24,116 @@ def get_voice_manager(bot) -> "VoiceManager":
24 return _voice_managers[bot_id] 24 return _voice_managers[bot_id]
25 25
26 26
27def is_voice_client_alive(voice_client, guild):
28 """Check if a voice client connection is healthy.
29
30 Verifies the client is connected, the channel still exists,
31 and the underlying voice socket is still open.
32 """
33 if voice_client is None:
34 return False
35 if not voice_client.is_connected():
36 return False
37 if voice_client.channel is None:
38 return False
39 if guild.get_channel(voice_client.channel.id) is None:
40 return False
41
42 try:
43 ws = voice_client._connection
44 s = ws.socket
45 if s is None or s.fileno() == -1:
46 return False
47 except Exception:
48 return False
49
50 return True
51
52
27@dataclass 53@dataclass
28class VoiceManager: 54class VoiceManager:
29 bot: Any # type: ignore[no-any-import] 55 bot: Any # type: ignore[no-any-import]
30 _voice_lock: asyncio.Lock = field(default_factory=asyncio.Lock, init=False) 56 _voice_lock: asyncio.Lock = field(default_factory=asyncio.Lock, init=False)
57 _connecting: set[int] = field(default_factory=set, init=False)
31 58
32 async def join_channel(self, channel): 59 async def join_channel(self, channel):
33 """Join a voice channel by channel object.""" 60 """Join a voice channel by channel object."""
34 async with self._voice_lock: 61 async with self._voice_lock:
35 guild = channel.guild 62 guild = channel.guild
63 guild_id = guild.id
64
65 if guild_id in self._connecting:
66 logger.info(f"Connection already in progress for guild {guild_id}, skipping")
67 return None
68
36 voice_client = discord.utils.get(self.bot.voice_clients, guild=guild) 69 voice_client = discord.utils.get(self.bot.voice_clients, guild=guild)
37 70
38 if voice_client and voice_client.is_connected(): 71 if voice_client and voice_client.is_connected():
39 if voice_client.channel == channel: 72 if voice_client.channel == channel:
40 logger.info(f"Already connected to {channel}") 73 if is_voice_client_alive(voice_client, guild):
41 return voice_client 74 logger.info(f"Already connected to {channel}")
42 try: 75 return voice_client
43 await voice_client.move_to(channel) 76 logger.warning(f"Stale connection to {channel}, reconnecting")
44 logger.info(f"Moved to voice channel: {channel}")
45 return voice_client
46 except Exception as e:
47 logger.error(f"Failed to move voice client: {e}")
48 logger.info("Disconnecting after failed move attempt")
49 try: 77 try:
50 await voice_client.disconnect() 78 await voice_client.disconnect()
51 except Exception: 79 except Exception:
52 pass 80 pass
53 81 else:
54 await asyncio.sleep(0.3) 82 try:
55 try: 83 await voice_client.move_to(channel)
56 voice_client = await channel.connect(timeout=30.0, reconnect=False) 84 logger.info(f"Moved to voice channel: {channel}")
57 logger.info(f"Connected to voice channel: {channel}") 85 return voice_client
58 return voice_client 86 except Exception as e:
59 except discord.ClientException as e: 87 logger.error(f"Failed to move voice client: {e}")
60 if "Already connected" in str(e): 88 logger.info("Disconnecting after failed move attempt")
61 logger.warning(f"Stale voice state detected, forcing disconnect and reconnect: {e}")
62 voice_client = discord.utils.get(self.bot.voice_clients, guild=guild)
63 if voice_client:
64 try: 89 try:
65 await voice_client.disconnect() 90 await voice_client.disconnect()
66 except Exception: 91 except Exception:
67 pass 92 pass
68 await asyncio.sleep(0.3) 93 elif voice_client and not voice_client.is_connected():
69 try: 94 logger.info("Voice client exists but disconnected, cleaning up stale entry")
70 voice_client = await channel.connect(timeout=30.0, reconnect=False) 95 try:
71 logger.info(f"Reconnected to voice channel: {channel}") 96 await voice_client.disconnect()
72 return voice_client 97 except Exception:
73 except Exception as e2: 98 pass
74 logger.error(f"Failed to reconnect after stale cleanup: {e2}") 99 voice_client = None
75 return None 100
76 logger.error(f"Failed to connect to voice channel: {e}") 101 self._connecting.add(guild_id)
77 return None 102 try:
78 except Exception as e: 103 await asyncio.sleep(0.3)
79 logger.error(f"Failed to connect to voice channel: {e}") 104 try:
80 return None 105 voice_client = await channel.connect(timeout=30.0, reconnect=False)
106 logger.info(f"Connected to voice channel: {channel}")
107 return voice_client
108 except discord.ClientException as e:
109 if "Already connected" in str(e):
110 logger.warning(f"Stale voice state detected, forcing disconnect and reconnect: {e}")
111 voice_client = discord.utils.get(self.bot.voice_clients, guild=guild)
112 if voice_client:
113 try:
114 await voice_client.disconnect()
115 except Exception:
116 pass
117 else:
118 try:
119 channel._state._remove_voice_client(guild.id)
120 except Exception:
121 pass
122 await asyncio.sleep(0.3)
123 try:
124 voice_client = await channel.connect(timeout=30.0, reconnect=False)
125 logger.info(f"Reconnected to voice channel: {channel}")
126 return voice_client
127 except Exception as e2:
128 logger.error(f"Failed to reconnect after stale cleanup: {e2}")
129 return None
130 logger.error(f"Failed to connect to voice channel: {e}")
131 return None
132 except Exception as e:
133 logger.error(f"Failed to connect to voice channel: {e}")
134 return None
135 finally:
136 self._connecting.discard(guild_id)
81 137
82 async def join_by_channel_id(self, channel_id): 138 async def join_by_channel_id(self, channel_id):
83 """Join a voice channel by channel ID.""" 139 """Join a voice channel by channel ID."""