Skip to content

Commit

Permalink
Add switch_node method to Player.
Browse files Browse the repository at this point in the history
  • Loading branch information
EvieePy committed Jul 25, 2024
1 parent 040af86 commit e43fe1c
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 5 deletions.
96 changes: 91 additions & 5 deletions wavelink/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from .exceptions import (
ChannelTimeoutException,
InvalidChannelStateException,
InvalidNodeException,
LavalinkException,
LavalinkLoadException,
QueueEmpty,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions wavelink/types/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@

from typing_extensions import NotRequired

from ..filters import Filters
from ..tracks import Playable


class PlayerState(TypedDict):
time: int
Expand All @@ -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

0 comments on commit e43fe1c

Please sign in to comment.