41
41
from .exceptions import (
42
42
ChannelTimeoutException ,
43
43
InvalidChannelStateException ,
44
+ InvalidNodeException ,
44
45
LavalinkException ,
45
46
LavalinkLoadException ,
46
47
QueueEmpty ,
73
74
TrackStartEventPayload ,
74
75
)
75
76
from .types .request import Request as RequestPayload
76
- from .types .state import PlayerVoiceState , VoiceState
77
+ from .types .state import PlayerBasicState , PlayerVoiceState , VoiceState
77
78
78
79
VocalGuildChannel = discord .VoiceChannel | discord .StageChannel
79
80
@@ -168,6 +169,26 @@ def __init__(
168
169
self ._inactivity_task : asyncio .Task [bool ] | None = None
169
170
self ._inactivity_wait : int | None = self ._node ._inactive_player_timeout
170
171
172
+ self ._should_wait : int = 10
173
+ self ._reconnecting : asyncio .Event = asyncio .Event ()
174
+ self ._reconnecting .set ()
175
+
176
+ async def _disconnected_wait (self , code : int , by_remote : bool ) -> None :
177
+ if code != 4014 or not by_remote :
178
+ return
179
+
180
+ self ._connected = False
181
+
182
+ if self ._reconnecting .is_set ():
183
+ await asyncio .sleep (self ._should_wait )
184
+ else :
185
+ await self ._reconnecting .wait ()
186
+
187
+ if self ._connected :
188
+ return
189
+
190
+ await self ._destroy ()
191
+
171
192
def _inactivity_task_callback (self , task : asyncio .Task [bool ]) -> None :
172
193
cancelled : bool = False
173
194
@@ -425,6 +446,89 @@ async def _search(query: str | None) -> T_a:
425
446
logger .info ('Player "%s" could not load any songs via AutoPlay.' , self .guild .id )
426
447
self ._inactivity_start ()
427
448
449
+ @property
450
+ def state (self ) -> PlayerBasicState :
451
+ """Property returning a dict of the current basic state of the player.
452
+
453
+ This property includes the ``voice_state`` received via Discord.
454
+
455
+ Returns
456
+ -------
457
+ PlayerBasicState
458
+
459
+ .. versionadded:: 3.5.0
460
+ """
461
+ data : PlayerBasicState = {
462
+ "voice_state" : self ._voice_state .copy (),
463
+ "position" : self .position ,
464
+ "connected" : self .connected ,
465
+ "current" : self .current ,
466
+ "paused" : self .paused ,
467
+ "volume" : self .volume ,
468
+ "filters" : self .filters ,
469
+ }
470
+ return data
471
+
472
+ async def switch_node (self , new_node : wavelink .Node , / ) -> None :
473
+ """Method which attempts to switch the current node of the player.
474
+
475
+ This method initiates a live switch, and all player state will be moved from the current node to the provided
476
+ node.
477
+
478
+ .. warning::
479
+
480
+ Caution should be used when using this method. If this method fails, your player might be left in a stale
481
+ state. Consider handling cases where the player is unable to connect to the new node. To avoid stale state
482
+ in both wavelink and discord.py, it is recommended to disconnect the player when a RuntimeError occurs.
483
+
484
+ Parameters
485
+ ----------
486
+ new_node: :class:`wavelink.Node`
487
+ A positional only argument of a :class:`wavelink.Node`, which is the new node the player will attempt to
488
+ switch to. This must not be the same as the current node.
489
+
490
+ Raises
491
+ ------
492
+ InvalidNodeException
493
+ The provided node was identical to the players current node.
494
+ RuntimeError
495
+ The player was unable to connect properly to the new node. At this point your player might be in a stale
496
+ state. Consider trying another node, or :meth:`disconnect` the player.
497
+
498
+
499
+ .. versionadded:: 3.5.0
500
+ """
501
+ assert self ._guild
502
+
503
+ if new_node .identifier == self .node .identifier :
504
+ msg : str = f"Player '{ self ._guild .id } ' current node is identical to the passed node: { new_node !r} "
505
+ raise InvalidNodeException (msg )
506
+
507
+ await self ._destroy (with_invalidate = False )
508
+ self ._node = new_node
509
+
510
+ await self ._dispatch_voice_update ()
511
+ if not self .connected :
512
+ raise RuntimeError (f"Switching Node on player '{ self ._guild .id } ' failed. Failed to switch voice_state." )
513
+
514
+ self .node ._players [self ._guild .id ] = self
515
+
516
+ if not self ._current :
517
+ await self .set_filters (self .filters )
518
+ await self .set_volume (self .volume )
519
+ await self .pause (self .paused )
520
+ return
521
+
522
+ await self .play (
523
+ self ._current ,
524
+ replace = True ,
525
+ start = self .position ,
526
+ volume = self .volume ,
527
+ filters = self .filters ,
528
+ paused = self .paused ,
529
+ )
530
+ logger .debug ("Switching nodes for player: '%s' was successful. New Node: %r" , self ._guild .id , self .node )
531
+
428
532
@property
429
533
def inactive_channel_tokens (self ) -> int | None :
430
534
"""A settable property which returns the token limit as an ``int`` of the amount of tracks to play before firing
@@ -695,6 +799,7 @@ async def _dispatch_voice_update(self) -> None:
695
799
except LavalinkException :
696
800
await self .disconnect ()
697
801
else :
802
+ self ._connected = True
698
803
self ._connection_event .set ()
699
804
700
805
logger .debug ("Player %s is dispatching VOICE_UPDATE." , self .guild .id )
@@ -772,6 +877,7 @@ async def move_to(
772
877
raise InvalidChannelStateException ("Player tried to move without a valid guild." )
773
878
774
879
self ._connection_event .clear ()
880
+ self ._reconnecting .clear ()
775
881
voice : discord .VoiceState | None = self .guild .me .voice
776
882
777
883
if self_deaf is None and voice :
@@ -786,6 +892,7 @@ async def move_to(
786
892
await self .guild .change_voice_state (channel = channel , self_mute = self_mute , self_deaf = self_deaf )
787
893
788
894
if channel is None :
895
+ self ._reconnecting .set ()
789
896
return
790
897
791
898
try :
@@ -794,6 +901,8 @@ async def move_to(
794
901
except (asyncio .TimeoutError , asyncio .CancelledError ):
795
902
msg = f"Unable to connect to { channel } as it exceeded the timeout of { timeout } seconds."
796
903
raise ChannelTimeoutException (msg )
904
+ finally :
905
+ self ._reconnecting .set ()
797
906
798
907
async def play (
799
908
self ,
@@ -1103,17 +1212,19 @@ def _invalidate(self) -> None:
1103
1212
except (AttributeError , KeyError ):
1104
1213
pass
1105
1214
1106
- async def _destroy (self ) -> None :
1215
+ async def _destroy (self , with_invalidate : bool = True ) -> None :
1107
1216
assert self .guild
1108
1217
1109
- self ._invalidate ()
1218
+ if with_invalidate :
1219
+ self ._invalidate ()
1220
+
1110
1221
player : Player | None = self .node ._players .pop (self .guild .id , None )
1111
1222
1112
1223
if player :
1113
1224
try :
1114
1225
await self .node ._destroy_player (self .guild .id )
1115
- except LavalinkException :
1116
- pass
1226
+ except Exception as e :
1227
+ logger . debug ( "Disregarding. Failed to send 'destroy_player' payload to Lavalink: %s" , e )
1117
1228
1118
1229
def _add_to_previous_seeds (self , seed : str ) -> None :
1119
1230
# Helper method to manage previous seeds.
0 commit comments