diff --git a/pomice/player.py b/pomice/player.py index b5c40fc..8349a32 100644 --- a/pomice/player.py +++ b/pomice/player.py @@ -1,46 +1,36 @@ import time -from typing import ( - Any, - Dict, - Optional -) +from typing import Any, Dict, Optional, Type, Union -from discord import ( - Guild, - VoiceChannel, - VoiceProtocol -) +import discord +from discord import Client, Guild, VoiceChannel, VoiceProtocol from discord.ext import commands + from . import events from .enums import SearchType -from .events import PomiceEvent, TrackStartEvent from .exceptions import TrackInvalidPosition from .filters import Filter -from .objects import Track from .pool import Node, NodePool -from .utils import ClientType +from .objects import Track +from .utils import MISSING - -class Player(VoiceProtocol): - """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) - ``` +class BasePlayer(VoiceProtocol): + """Base Player Class For Pomice This Class Has to Be Inherited if your are Building + your own Player Class, Unless you know what you are doing. """ - def __init__(self, client: ClientType, channel: VoiceChannel): - super().__init__(client=client, channel=channel) + def __call__(self, client: discord.Client, channel: VoiceChannel): + self.client: discord.Client = client + self.channel : VoiceChannel = channel + + def __init__(self, client : Type[Client] = MISSING, channel: VoiceChannel = MISSING): self.client = client self._bot = client self.channel = channel - self._guild: Guild = self.channel.guild + self._guild : Guild = channel.guild self._node = NodePool.get_node() - self._current: Track = None - self._filter: Filter = None self._volume = 100 self._paused = False self._is_connected = False @@ -48,38 +38,11 @@ class Player(VoiceProtocol): self._position = 0 self._last_position = 0 self._last_update = 0 - self._ending_track: Optional[Track] = None self._voice_state = {} - - def __repr__(self): - return ( - f"" - ) - - @property - def position(self) -> float: - """Property which returns the player's position in a track in milliseconds""" - - if not self.is_playing or not self.current: - return 0 - - if self.is_paused: - return min(self._last_position, self.current.length) - - difference = (time.time() * 1000) - self._last_update - position = self._last_position + difference - - if position > self.current.length: - return 0 - - return min(position, self.current.length) - - @property - def is_playing(self) -> bool: - """Property which returns whether or not the player is actively playing a track.""" - return self._is_connected and self.current is not None + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}(bot={self._bot}, guildId={self._guild.id})>" @property def is_connected(self) -> bool: @@ -90,12 +53,7 @@ class Player(VoiceProtocol): def is_paused(self) -> bool: """Property which returns whether or not the player has a track which is paused or not.""" return self._is_connected and self._paused - - @property - def current(self) -> Track: - """Property which returns the currently playing track""" - return self._current - + @property def node(self) -> Node: """Property which returns the node the player is connected to""" @@ -112,12 +70,7 @@ class Player(VoiceProtocol): return self._volume @property - def filter(self) -> Filter: - """Property which returns the currently applied filter, if one is applied""" - return self._filter - - @property - def bot(self) -> ClientType: + def bot(self) -> Type[Client]: """Property which returns the bot associated with this player instance""" return self._bot @@ -158,29 +111,20 @@ class Player(VoiceProtocol): async def _dispatch_event(self, data: dict): event_type = data.get("type") - event: PomiceEvent = getattr(events, event_type)(data) - event.dispatch(self._bot) - - if isinstance(event, TrackStartEvent): - self._ending_track = self._current - - async def get_tracks( - self, - query: str, - *, - ctx: Optional[commands.Context] = None, - search_type: SearchType = SearchType.ytsearch - ): - """Fetches tracks from the node's REST api to parse into Lavalink. - - If you passed in Spotify API credentials when you created the node, - you can also pass in a Spotify URL of a playlist, album or track and it will be parsed - accordingly. - - You can also pass in a discord.py Context object to get a - Context object on any track you search. - """ - return await self._node.get_tracks(query, ctx=ctx, search_type=search_type) + if _track := data.get("track", None): + track = await self._node.build_track(_track) + + _events = { + "TrackStartEvent" : (self, track), + "TrackEndEvent" : (self, track, data.get("reason", None)), + "TrackExceptionEvent" : (self, track, data.get("error", None)), + "TrackStuckEvent" : (self, track, data.get("thresholdMs", None)), + "WebSocketOpenEvent": (data.get("target", None), data.get("ssrc", None)), + "WebSocketClosedEvent" : (self._guild, data.get("reason", None), data.get("code", None)) + } + if (event := getattr(events, event_type, None)) and (args := _events.get(event_type, None)): + + self.bot.dispatch(f"pomice_{event.name}", event, *args) async def connect(self, *, timeout: float, reconnect: bool): await self.guild.change_voice_state(channel=self.channel) @@ -205,6 +149,81 @@ class Player(VoiceProtocol): await self.disconnect() await self._node.send(op="destroy", guildId=str(self.guild.id)) + + +class Player(BasePlayer): + """The Default Basic 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) + ``` + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._current: Track = None + self._filter: Filter = None + + + def __repr__(self): + return ( + f"" + ) + + @property + def position(self) -> float: + """Property which returns the player's position in a track in milliseconds""" + + if not self.is_playing or not self.current: + return 0 + + if self.is_paused: + return min(self._last_position, self.current.length) + + difference = (time.time() * 1000) - self._last_update + position = self._last_position + difference + + if position > self.current.length: + return 0 + + return min(position, self.current.length) + + @property + def is_playing(self) -> bool: + """Property which returns whether or not the player is actively playing a track.""" + return self._is_connected and self.current is not None + + @property + def current(self) -> Track: + """Property which returns the currently playing track""" + return self._current + + @property + def filter(self) -> Filter: + """Property which returns the currently applied filter, if one is applied""" + return self._filter + + + async def get_tracks( + self, + query: str, + *, + ctx: Optional[commands.Context] = None, + search_type: SearchType = SearchType.ytsearch + ): + """Fetches tracks from the node's REST api to parse into Lavalink. + + If you passed in Spotify API credentials when you created the node, + you can also pass in a Spotify URL of a playlist, album or track and it will be parsed + accordingly. + + You can also pass in a discord.py Context object to get a + Context object on any track you search. + """ + return await self._node.get_tracks(query, ctx=ctx, search_type=search_type) + async def play(self, track: Track, *, start_position: int = 0) -> Track: """Plays a track. If a Spotify track is passed in, it will be handled accordingly.""" if track.spotify: