make dispatching voice updates required
This commit is contained in:
parent
0dccb9b496
commit
c6eae3b3a1
|
|
@ -25,14 +25,7 @@ __all__ = (
|
|||
|
||||
|
||||
class PomiceEvent(ABC):
|
||||
"""The base class for all events dispatched by a node.
|
||||
Every event must be formatted within your bot's code as a listener.
|
||||
i.e: If you want to listen for when a track starts, the event would be:
|
||||
```py
|
||||
@bot.listen
|
||||
async def on_pomice_track_start(self, event):
|
||||
```
|
||||
"""
|
||||
"""The base class for all events dispatched by a node."""
|
||||
|
||||
name = "event"
|
||||
handler_args: Tuple
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ __all__ = (
|
|||
"NodeRestException",
|
||||
"NodeNotAvailable",
|
||||
"NoNodesAvailable",
|
||||
"PlayerCreationError",
|
||||
"TrackInvalidPosition",
|
||||
"TrackLoadError",
|
||||
"FilterInvalidArgument",
|
||||
|
|
@ -61,6 +62,12 @@ class NoNodesAvailable(PomiceException):
|
|||
pass
|
||||
|
||||
|
||||
class PlayerCreationError(PomiceException):
|
||||
"""There was a problem while creating the player."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TrackInvalidPosition(PomiceException):
|
||||
"""An invalid position was chosen for a track."""
|
||||
|
||||
|
|
|
|||
104
pomice/player.py
104
pomice/player.py
|
|
@ -121,19 +121,19 @@ class Filters:
|
|||
|
||||
class Player:
|
||||
"""The base player class for Pomice.
|
||||
In order to initiate a player, you must pass it in as a cls when you connect to a channel.
|
||||
i.e: ```py
|
||||
await ctx.author.voice.channel.connect(cls=pomice.Player)
|
||||
In order to initialize a player, you must pass in a guild id and register it to a node.
|
||||
You can also pass in a node if you would like to specify which node to use.
|
||||
|
||||
Example:
|
||||
```py
|
||||
player = pomice.Player(guild_id=1234567890)
|
||||
node.register_player(player)
|
||||
```
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"client",
|
||||
"channel",
|
||||
"_bot",
|
||||
"_guild",
|
||||
"_node",
|
||||
"_current",
|
||||
"_guild_id" "_current",
|
||||
"_filters",
|
||||
"_volume",
|
||||
"_paused",
|
||||
|
|
@ -149,9 +149,11 @@ class Player:
|
|||
def __init__(
|
||||
self,
|
||||
*,
|
||||
guild_id: int,
|
||||
node: Optional[Node] = None,
|
||||
) -> None:
|
||||
self._node: Node = node if node else NodePool.get_node()
|
||||
self._guild_id: int = guild_id
|
||||
self._current: Optional[Track] = None
|
||||
self._filters: Filters = Filters()
|
||||
self._volume: int = 100
|
||||
|
|
@ -163,13 +165,11 @@ class Player:
|
|||
self._ending_track: Optional[Track] = None
|
||||
self._log = self._node._log
|
||||
|
||||
self._voice_state: dict = {}
|
||||
|
||||
self._player_endpoint_uri: str = f"sessions/{self._node._session_id}/players"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Pomice.player bot={self.bot} guildId={self.guild.id} "
|
||||
f"<Pomice.player bot_id={self.bot_id} guild_id={self.guild_id} "
|
||||
f"is_connected={self.is_connected} is_playing={self.is_playing}>"
|
||||
)
|
||||
|
||||
|
|
@ -236,6 +236,11 @@ class Player:
|
|||
"""Property which returns the node the player is connected to"""
|
||||
return self._node
|
||||
|
||||
@property
|
||||
def guild_id(self) -> int:
|
||||
"""Property which returns the guild id associated with this player instance"""
|
||||
return self._guild_id
|
||||
|
||||
@property
|
||||
def volume(self) -> int:
|
||||
"""Property which returns the players current volume"""
|
||||
|
|
@ -271,26 +276,37 @@ class Player:
|
|||
self._last_position = int(state.get("position", 0))
|
||||
self._log.debug(f"Got player update state with data {state}")
|
||||
|
||||
async def _dispatch_voice_update(self, voice_data: Optional[Dict[str, Any]] = None) -> None:
|
||||
if {"sessionId", "event"} != self._voice_state.keys():
|
||||
return
|
||||
async def dispatch_voice_update(self, voice_data: dict) -> None:
|
||||
"""
|
||||
Dispatches a voice update to the node.
|
||||
This method is required for the player to work properly.
|
||||
|
||||
state = voice_data or self._voice_state
|
||||
|
||||
data = {
|
||||
"token": state["event"]["token"],
|
||||
"endpoint": state["event"]["endpoint"],
|
||||
"sessionId": state["sessionId"],
|
||||
All data must be formatted as follows:
|
||||
```json
|
||||
{
|
||||
"token": "voice token",
|
||||
"endpoint": "voice endpoint url",
|
||||
"sessionId": "voice session id"
|
||||
}
|
||||
```
|
||||
|
||||
"""
|
||||
if not voice_data:
|
||||
raise ValueError("Voice data must be passed in.")
|
||||
|
||||
if not all(key in voice_data for key in ("token", "endpoint", "sessionId")):
|
||||
raise ValueError("Voice data must contain 'token', 'endpoint' and 'sessionId' keys.")
|
||||
|
||||
await self._node.send(
|
||||
method="PATCH",
|
||||
path=self._player_endpoint_uri,
|
||||
guild_id=self._guild.id,
|
||||
data={"voice": data},
|
||||
guild_id=self._guild_id,
|
||||
data={"voice": voice_data},
|
||||
)
|
||||
|
||||
self._log.debug(f"Dispatched voice update to {state['event']['endpoint']} with data {data}")
|
||||
self._log.debug(
|
||||
f"Dispatched voice update to {voice_data['endpoint']} with data {voice_data}",
|
||||
)
|
||||
|
||||
async def _dispatch_event(self, data: dict) -> None:
|
||||
event_type: str = data["type"]
|
||||
|
|
@ -313,16 +329,16 @@ class Player:
|
|||
if self.current:
|
||||
data: dict = {"position": self.position, "encodedTrack": self.current.track_id}
|
||||
|
||||
del self._node._players[self._guild.id]
|
||||
del self._node._players[self._guild_id]
|
||||
self._node = new_node
|
||||
self._node._players[self._guild.id] = self
|
||||
self._node._players[self._guild_id] = self
|
||||
# reassign uri to update session id
|
||||
await self._refresh_endpoint_uri(new_node._session_id)
|
||||
await self._dispatch_voice_update()
|
||||
await self._node.send(
|
||||
method="PATCH",
|
||||
path=self._player_endpoint_uri,
|
||||
guild_id=self._guild.id,
|
||||
guild_id=self._guild_id,
|
||||
data=data or None,
|
||||
)
|
||||
|
||||
|
|
@ -341,9 +357,6 @@ class Player:
|
|||
you can also pass in a Spotify URL of a playlist, album or track and it will be parsed
|
||||
accordingly.
|
||||
|
||||
You can pass in a discord.py Context object to get a
|
||||
Context object on any track you search.
|
||||
|
||||
You may also pass in a List of filters
|
||||
to be applied to your track once it plays.
|
||||
"""
|
||||
|
|
@ -359,8 +372,6 @@ class Player:
|
|||
async def get_recommendations(self, *, track: Track) -> Optional[Union[List[Track], Playlist]]:
|
||||
"""
|
||||
Gets recommendations from either YouTube or Spotify.
|
||||
You can pass in a discord.py Context object to get a
|
||||
Context object on all tracks that get recommended.
|
||||
"""
|
||||
return await self._node.get_recommendations(track=track)
|
||||
|
||||
|
|
@ -370,27 +381,20 @@ class Player:
|
|||
await self._node.send(
|
||||
method="PATCH",
|
||||
path=self._player_endpoint_uri,
|
||||
guild_id=self._guild.id,
|
||||
guild_id=self._guild_id,
|
||||
data={"encodedTrack": None},
|
||||
)
|
||||
|
||||
self._log.debug(f"Player has been stopped.")
|
||||
|
||||
async def destroy(self) -> None:
|
||||
"""Disconnects and destroys the player, and runs internal cleanup."""
|
||||
try:
|
||||
await self.disconnect()
|
||||
except AttributeError:
|
||||
# 'NoneType' has no attribute '_get_voice_client_key' raised by self.cleanup() ->
|
||||
# assume we're already disconnected and cleaned up
|
||||
assert self.channel is None and not self.is_connected
|
||||
|
||||
self._node._players.pop(self.guild.id)
|
||||
"""Destroys the player and removes it from the node's stored players."""
|
||||
self._node._players.pop(self.guild_id)
|
||||
if self.node.is_connected:
|
||||
await self._node.send(
|
||||
method="DELETE",
|
||||
path=self._player_endpoint_uri,
|
||||
guild_id=self._guild.id,
|
||||
guild_id=self._guild_id,
|
||||
)
|
||||
|
||||
self._log.debug("Player has been destroyed.")
|
||||
|
|
@ -473,7 +477,7 @@ class Player:
|
|||
await self._node.send(
|
||||
method="PATCH",
|
||||
path=self._player_endpoint_uri,
|
||||
guild_id=self._guild.id,
|
||||
guild_id=self._guild_id,
|
||||
data=data,
|
||||
query=f"noReplace={ignore_if_playing}",
|
||||
)
|
||||
|
|
@ -497,7 +501,7 @@ class Player:
|
|||
await self._node.send(
|
||||
method="PATCH",
|
||||
path=self._player_endpoint_uri,
|
||||
guild_id=self._guild.id,
|
||||
guild_id=self._guild_id,
|
||||
data={"position": position},
|
||||
)
|
||||
|
||||
|
|
@ -509,7 +513,7 @@ class Player:
|
|||
await self._node.send(
|
||||
method="PATCH",
|
||||
path=self._player_endpoint_uri,
|
||||
guild_id=self._guild.id,
|
||||
guild_id=self._guild_id,
|
||||
data={"paused": pause},
|
||||
)
|
||||
self._paused = pause
|
||||
|
|
@ -522,7 +526,7 @@ class Player:
|
|||
await self._node.send(
|
||||
method="PATCH",
|
||||
path=self._player_endpoint_uri,
|
||||
guild_id=self._guild.id,
|
||||
guild_id=self._guild_id,
|
||||
data={"volume": volume},
|
||||
)
|
||||
self._volume = volume
|
||||
|
|
@ -543,7 +547,7 @@ class Player:
|
|||
await self._node.send(
|
||||
method="PATCH",
|
||||
path=self._player_endpoint_uri,
|
||||
guild_id=self._guild.id,
|
||||
guild_id=self._guild_id,
|
||||
data={"filters": payload},
|
||||
)
|
||||
|
||||
|
|
@ -567,7 +571,7 @@ class Player:
|
|||
await self._node.send(
|
||||
method="PATCH",
|
||||
path=self._player_endpoint_uri,
|
||||
guild_id=self._guild.id,
|
||||
guild_id=self._guild_id,
|
||||
data={"filters": payload},
|
||||
)
|
||||
self._log.debug(f"Filter has been removed from player with tag {filter_tag}")
|
||||
|
|
@ -594,7 +598,7 @@ class Player:
|
|||
await self._node.send(
|
||||
method="PATCH",
|
||||
path=self._player_endpoint_uri,
|
||||
guild_id=self._guild.id,
|
||||
guild_id=self._guild_id,
|
||||
data={"filters": payload},
|
||||
)
|
||||
self._log.debug(f"Filter with tag {filter_tag} has been edited to {edited_filter!r}")
|
||||
|
|
@ -620,7 +624,7 @@ class Player:
|
|||
await self._node.send(
|
||||
method="PATCH",
|
||||
path=self._player_endpoint_uri,
|
||||
guild_id=self._guild.id,
|
||||
guild_id=self._guild_id,
|
||||
data={"filters": {}},
|
||||
)
|
||||
self._log.debug(f"All filters have been removed from player.")
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ from .exceptions import NodeCreationError
|
|||
from .exceptions import NodeNotAvailable
|
||||
from .exceptions import NodeRestException
|
||||
from .exceptions import NoNodesAvailable
|
||||
from .exceptions import PlayerCreationError
|
||||
from .exceptions import TrackLoadError
|
||||
from .filters import Filter
|
||||
from .objects import Playlist
|
||||
|
|
@ -64,8 +65,7 @@ class Node:
|
|||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_bot",
|
||||
"_bot_user",
|
||||
"_bot_id",
|
||||
"_host",
|
||||
"_port",
|
||||
"_pool",
|
||||
|
|
@ -153,14 +153,9 @@ class Node:
|
|||
self._route_planner = RoutePlanner(self)
|
||||
self._log = self._setup_logging(self._log_level)
|
||||
|
||||
if not self._bot.user:
|
||||
raise NodeCreationError("Bot user is not ready yet.")
|
||||
|
||||
self._bot_user = self._bot.user
|
||||
|
||||
self._headers = {
|
||||
"Authorization": self._password,
|
||||
"User-Id": str(self._bot_user.id),
|
||||
"User-Id": str(self._bot_id),
|
||||
"Client-Name": f"Pomice/{__version__}",
|
||||
}
|
||||
|
||||
|
|
@ -181,8 +176,6 @@ class Node:
|
|||
if apple_music:
|
||||
self._apple_music_client = applemusic.Client()
|
||||
|
||||
self._bot.add_listener(self._update_handler, "on_socket_response")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<Pomice.node ws_uri={self._websocket_uri} rest_uri={self._rest_uri} "
|
||||
|
|
@ -291,31 +284,6 @@ class Node:
|
|||
if self._apple_music_client:
|
||||
await self._apple_music_client._set_session(session=session)
|
||||
|
||||
async def _update_handler(self, data: dict) -> None:
|
||||
await self._bot.wait_until_ready()
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
if data["t"] == "VOICE_SERVER_UPDATE":
|
||||
guild_id = int(data["d"]["guild_id"])
|
||||
try:
|
||||
player = self._players[guild_id]
|
||||
await player.on_voice_server_update(data["d"])
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
elif data["t"] == "VOICE_STATE_UPDATE":
|
||||
if int(data["d"]["user_id"]) != self._bot_user.id:
|
||||
return
|
||||
|
||||
guild_id = int(data["d"]["guild_id"])
|
||||
try:
|
||||
player = self._players[guild_id]
|
||||
await player.on_voice_state_update(data["d"])
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
async def _handle_node_switch(self) -> None:
|
||||
nodes = [node for node in self.pool._nodes.copy().values() if node.is_connected]
|
||||
new_node = random.choice(nodes)
|
||||
|
|
@ -444,9 +412,25 @@ class Node:
|
|||
"""Takes a guild ID as a parameter. Returns a pomice Player object or None."""
|
||||
return self._players.get(guild_id, None)
|
||||
|
||||
def register_player(self, player: Player) -> None:
|
||||
"""Registers a player to the node."""
|
||||
if not player.guild_id:
|
||||
raise PlayerCreationError(
|
||||
"You must pass in a guild ID to create a player.",
|
||||
)
|
||||
|
||||
if player.guild_id in self._players:
|
||||
raise PlayerCreationError(
|
||||
f"Player with guild ID {player.guild_id} already exists.",
|
||||
)
|
||||
|
||||
self._players[player.guild_id] = player
|
||||
self._log.debug(
|
||||
f"Registered player with guild ID {player.guild_id} to Node {self._identifier}",
|
||||
)
|
||||
|
||||
async def connect(self, *, reconnect: bool = False) -> "Node":
|
||||
"""Initiates a connection with a Lavalink node and adds it to the node pool."""
|
||||
await self._bot.wait_until_ready()
|
||||
|
||||
start = time.perf_counter()
|
||||
|
||||
|
|
@ -563,9 +547,6 @@ class Node:
|
|||
If you passed in Spotify API credentials, you can also pass in a
|
||||
Spotify URL of a playlist, album or track and it will be parsed accordingly.
|
||||
|
||||
You can pass in a discord.py Context object to get a
|
||||
Context object on any track you search.
|
||||
|
||||
You may also pass in a List of filters
|
||||
to be applied to your track once it plays.
|
||||
"""
|
||||
|
|
@ -827,8 +808,6 @@ class Node:
|
|||
Gets recommendations from either YouTube or Spotify.
|
||||
The track that is passed in must be either from
|
||||
YouTube or Spotify or else this will not work.
|
||||
You can pass in a discord.py Context object to get a
|
||||
Context object on all tracks that get recommended.
|
||||
"""
|
||||
if track.track_type == TrackType.SPOTIFY:
|
||||
results = await self._spotify_client.get_recommendations(query=track.uri) # type: ignore
|
||||
|
|
|
|||
Loading…
Reference in New Issue