From e43fe1cdf5e9b9d7718a299e01c67201ed1eba03 Mon Sep 17 00:00:00 2001 From: Eviee Py Date: Fri, 26 Jul 2024 04:32:37 +1000 Subject: [PATCH] Add switch_node method to Player. --- wavelink/player.py | 96 ++++++++++++++++++++++++++++++++++++++--- wavelink/types/state.py | 33 ++++++++++++++ 2 files changed, 124 insertions(+), 5 deletions(-) diff --git a/wavelink/player.py b/wavelink/player.py index ac34f069..02ad3e9f 100644 --- a/wavelink/player.py +++ b/wavelink/player.py @@ -41,6 +41,7 @@ from .exceptions import ( ChannelTimeoutException, InvalidChannelStateException, + InvalidNodeException, LavalinkException, LavalinkLoadException, QueueEmpty, @@ -73,7 +74,7 @@ TrackStartEventPayload, ) from .types.request import Request as RequestPayload - from .types.state import PlayerVoiceState, VoiceState + from .types.state import PlayerBasicState, PlayerVoiceState, VoiceState VocalGuildChannel = discord.VoiceChannel | discord.StageChannel @@ -445,6 +446,89 @@ async def _search(query: str | None) -> T_a: logger.info('Player "%s" could not load any songs via AutoPlay.', self.guild.id) self._inactivity_start() + @property + def state(self) -> PlayerBasicState: + """Property returning a dict of the current basic state of the player. + + This property includes the ``voice_state`` received via Discord. + + Returns + ------- + PlayerBasicState + + .. versionadded:: 3.5.0 + """ + data: PlayerBasicState = { + "voice_state": self._voice_state.copy(), + "position": self.position, + "connected": self.connected, + "current": self.current, + "paused": self.paused, + "volume": self.volume, + "filters": self.filters, + } + return data + + async def switch_node(self, new_node: wavelink.Node, /) -> None: + """Method which attempts to switch the current node of the player. + + This method initiates a live switch, and all player state will be moved from the current node to the provided + node. + + .. warning:: + + Caution should be used when using this method. If this method fails, your player might be left in a stale + state. Consider handling cases where the player is unable to connect to the new node. To avoid stale state + in both wavelink and discord.py, it is recommended to disconnect the player when a RuntimeError occurs. + + Parameters + ---------- + new_node: :class:`wavelink.Node` + A positional only argument of a :class:`wavelink.Node`, which is the new node the player will attempt to + switch to. This must not be the same as the current node. + + Raises + ------ + InvalidNodeException + The provided node was identical to the players current node. + RuntimeError + The player was unable to connect properly to the new node. At this point your player might be in a stale + state. Consider trying another node, or :meth:`disconnect` the player. + + + .. versionadded:: 3.5.0 + """ + assert self._guild + + if new_node.identifier == self.node.identifier: + msg: str = f"Player '{self._guild.id}' current node is identical to the passed node: {new_node!r}" + raise InvalidNodeException(msg) + + await self._destroy(with_invalidate=False) + self._node = new_node + + await self._dispatch_voice_update() + if not self.connected: + raise RuntimeError(f"Switching Node on player '{self._guild.id}' failed. Failed to switch voice_state.") + + self.node._players[self._guild.id] = self + + if not self._current: + await self.set_filters(self.filters) + await self.set_volume(self.volume) + await self.pause(self.paused) + return + + await self.play( + self._current, + replace=True, + start=self.position, + volume=self.volume, + filters=self.filters, + paused=self.paused, + ) + logger.debug("Switching nodes for player: '%s' was successful. New Node: %r", self._guild.id, self.node) + @property def inactive_channel_tokens(self) -> int | None: """A settable property which returns the token limit as an ``int`` of the amount of tracks to play before firing @@ -1128,17 +1212,19 @@ def _invalidate(self) -> None: except (AttributeError, KeyError): pass - async def _destroy(self) -> None: + async def _destroy(self, with_invalidate: bool = True) -> None: assert self.guild - self._invalidate() + if with_invalidate: + self._invalidate() + player: Player | None = self.node._players.pop(self.guild.id, None) if player: try: await self.node._destroy_player(self.guild.id) - except LavalinkException: - pass + except Exception as e: + logger.debug("Disregarding. Failed to send 'destroy_player' payload to Lavalink: %s", e) def _add_to_previous_seeds(self, seed: str) -> None: # Helper method to manage previous seeds. diff --git a/wavelink/types/state.py b/wavelink/types/state.py index 2e819dd7..683e6f1d 100644 --- a/wavelink/types/state.py +++ b/wavelink/types/state.py @@ -26,6 +26,9 @@ from typing_extensions import NotRequired +from ..filters import Filters +from ..tracks import Playable + class PlayerState(TypedDict): time: int @@ -45,3 +48,33 @@ class PlayerVoiceState(TypedDict): channel_id: NotRequired[str] track: NotRequired[str] position: NotRequired[int] + + +class PlayerBasicState(TypedDict): + """A dictionary of basic state for the Player. + + Attributes + ---------- + voice_state: :class:`PlayerVoiceState` + The voice state received via Discord. Includes the voice connection ``token``, ``endpoint`` and ``session_id``. + position: int + The player position. + connected: bool + Whether the player is currently connected to a channel. + current: :class:`~wavelink.Playable` | None + The currently playing track or `None` if no track is playing. + paused: bool + The players paused state. + volume: int + The players current volume. + filters: :class:`~wavelink.Filters` + The filters currently assigned to the Player. + """ + + voice_state: PlayerVoiceState + position: int + connected: bool + current: Playable | None + paused: bool + volume: int + filters: Filters