diff --git a/pomice/__init__.py b/pomice/__init__.py index 21e3da6..38da569 100644 --- a/pomice/__init__.py +++ b/pomice/__init__.py @@ -1,6 +1,6 @@ """Pomice wrapper for Lavalink, made possible by cloudwithax 2021""" -__version__ = "1.0.1" +__version__ = "1.0.3" __title__ = "pomice" __author__ = "cloudwithax" diff --git a/pomice/events.py b/pomice/events.py index 2576fc6..78dacc1 100644 --- a/pomice/events.py +++ b/pomice/events.py @@ -2,12 +2,21 @@ from .pool import NodePool class PomiceEvent: + """The base class for all events dispatched by a node. + Every event must be formatted within your bots 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): + ``` + """ def __init__(self): pass name = 'event' class TrackStartEvent(PomiceEvent): + """Fired when a track has successfully started. Returns the player associated with said track and the track ID""" def __init__(self, data): super().__init__() @@ -21,6 +30,7 @@ class TrackStartEvent(PomiceEvent): class TrackEndEvent(PomiceEvent): + """Fired when a track has successfully ended. Returns the player associated with the track along with the track ID and reason.""" def __init__(self, data): super().__init__() @@ -33,6 +43,7 @@ class TrackEndEvent(PomiceEvent): return f"" class TrackStuckEvent(PomiceEvent): + """Fired when a track is stuck and cannot be played. Returns the player associated with the track along with a track ID to be further parsed by the end user.""" def __init__(self, data): super().__init__() @@ -46,6 +57,7 @@ class TrackStuckEvent(PomiceEvent): return f"" class TrackExceptionEvent(PomiceEvent): + """Fired when a track error has occured. Returns the player associated with the track along with the error code and execption""" def __init__(self, data): super().__init__() @@ -59,11 +71,11 @@ class TrackExceptionEvent(PomiceEvent): return f" error={self.error} exeception={self.exception}" class WebsocketClosedEvent(PomiceEvent): + """Fired when a websocket connection to a node has been closed. Returns the reason and the error code.""" def __init__(self, data): super().__init__() self.name = "websocket_closed" - self.player = NodePool.get_node().get_player(int(data["guildId"])) self.reason = data["reason"] self.code = data["code"] @@ -72,12 +84,12 @@ class WebsocketClosedEvent(PomiceEvent): return f"" class WebsocketOpenEvent(PomiceEvent): + """Fired when a websocket connection to a node has been initiated. Returns the target and the session SSRC.""" def __init__(self, data): super().__init__() self.name = "websocket_open" - self.player = NodePool.get_node().get_player(int(data["guildId"])) - + self.target: str = data['target'] self.ssrc: int = data['ssrc'] diff --git a/pomice/exceptions.py b/pomice/exceptions.py index 12a00f3..b720e25 100644 --- a/pomice/exceptions.py +++ b/pomice/exceptions.py @@ -53,4 +53,8 @@ class SpotifyTrackLoadFailed(PomiceException): class SpotifyPlaylistLoadFailed(PomiceException): """The pomice Spotify client was unable to load a playlist""" + pass + +class InvalidSpotifyClientAuthorization(PomiceException): + """No Spotify client authorization was provided in order to use the Spotify track search feature""" pass \ No newline at end of file diff --git a/pomice/filters.py b/pomice/filters.py index bbb8c85..f65a781 100644 --- a/pomice/filters.py +++ b/pomice/filters.py @@ -8,6 +8,10 @@ class Filter: class Timescale(Filter): + """Filter which changes the speed and pitch of a track. Do be warned that this filter is bugged as of the lastest + Lavalink dev version due to the filter patch not corresponding with the track time. In short this means that your + track will either end prematurely or end later due to this. This is not the library's fault. + """ def __init__(self, *, speed: float = 1.0, pitch: float = 1.0, rate: float = 1.0): super().__init__() @@ -32,6 +36,9 @@ class Timescale(Filter): class Karaoke(Filter): + """ + Filter which filters the vocal track from any song and leaves the instrumental. Best for karaoke as the filter implies. + """ def __init__(self, *, level: float, mono_level: float, filter_band: float, filter_width: float): super().__init__() @@ -51,6 +58,7 @@ class Karaoke(Filter): class Tremolo(Filter): + """Filter which produces a wavering tone in the music, causing it to sound like the music is changing in volume rapidly.""" def __init__(self, *, frequency: float, depth: float): super().__init__() @@ -71,6 +79,7 @@ class Tremolo(Filter): class Vibrato(Filter): + """Filter which produces a wavering tone in the music, similar to the Tremolo filter, but changes in pitch rather than volume.""" def __init__(self, *, frequency: float, depth: float): diff --git a/pomice/node.py b/pomice/node.py index 4b188dc..cf9df77 100644 --- a/pomice/node.py +++ b/pomice/node.py @@ -22,6 +22,9 @@ from .utils import ExponentialBackoff, NodeStats SPOTIFY_URL_REGEX = re.compile(r'https?://open.spotify.com/(?Palbum|playlist|track)/(?P[a-zA-Z0-9]+)') class Node: + """The base class for a node. + This node object represents a Lavalink node. + If you want to enable Spotify searching, pass in a proper Spotify Client ID and Spotify Client Secret""" def __init__(self, pool, bot: Union[commands.Bot, discord.Client, commands.AutoShardedBot, discord.AutoShardedClient], host: str, port: int, password: str, identifier: str, spotify_client_id: Optional[str], spotify_client_secret: Optional[str]): self._bot = bot self._host = host @@ -63,10 +66,12 @@ class Node: @property def is_connected(self) -> bool: + """"Property which returns whether this node is connected or not""" return self._websocket is not None and not self._websocket.closed @property async def latency(self): + """Property which returns the latency of the node in milliseconds""" start_time = time.time() await self.send(op="ping") end_time = await self._bot.wait_for(f"node_ping") @@ -74,20 +79,25 @@ class Node: @property async def stats(self): + """Property which returns the node stats at any given time. + Typically, accessing this property is rare due to the fact that Lavalink automatically sends updated node stats every minutes.""" await self.send(op="get-stats") node_stats = await self._bot.wait_for(f"node_stats") return node_stats @property def players(self): + """Property which returns a dict containing the guild ID and the player object.""" return self._players @property def bot(self): + """Property which returns the discord.py Bot object linked to this node""" return self._bot @property def pool(self): + """Property which returns the node pool this node is apart of.""" return self._pool async def _update_handler(self, data: dict): @@ -154,16 +164,17 @@ class Node: async def send(self, **data): - if not self.available: raise exceptions.NodeNotAvailable(f"The node '{self.identifier}' is not currently available.") await self._websocket.send_str(json.dumps(data)) def get_player(self, guild_id: int): + """Takes a guild ID as a parameter. Returns a pomice Player object.""" return self._players.get(guild_id, None) async def connect(self): + """Initiates a connection with a Lavalink node and adds it to the node pool.""" await self._bot.wait_until_ready() try: @@ -181,6 +192,7 @@ class Node: raise exceptions.NodeConnectionFailure(f"The node '{self.identifier}' failed to connect.") async def disconnect(self): + """Disconnects a connected Lavalink node and removes it from the node pool. This also destroys any players connected to the node.""" for player in self.players.copy().values(): await player.destroy() @@ -190,8 +202,16 @@ class Node: self._task.cancel() async def get_tracks(self, query: str, ctx: commands.Context = None): + """Fetches tracks from the node's REST api to parse into Lavalink. + 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 also pass in a discord.py Context object to get a Context object on any track you search. + """ if spotify_url_check := SPOTIFY_URL_REGEX.match(query): + + if not self._spotify_client_id and not self._spotify_client_secret: + raise exceptions.InvalidSpotifyClientAuthorization("You did not provide proper Spotify client authorization credentials. If you would like to use the Spotify searching feature, please obtain Spotify API credentials here: https://developer.spotify.com/") search_type = spotify_url_check.group('type') spotify_id = spotify_url_check.group('id') diff --git a/pomice/objects.py b/pomice/objects.py index 71a3ed1..96e5856 100644 --- a/pomice/objects.py +++ b/pomice/objects.py @@ -1,6 +1,10 @@ from discord.ext import commands class Track: + """ + The base track object. Returns critical track information needed to be parsed by Lavalink. + You can also pass in commands.Context to get a discord.py Context object by passing in a valid Context object when you search for a track. + """ def __init__(self, track_id: str, info: dict, ctx: commands.Context = None): @@ -27,6 +31,10 @@ class Track: class Playlist: + """ + The base playlist object. Returns critcal playlist information like the name of the playlist and what tracks are included to be parsed by Lavalink. + You can also pass in commands.Context to get a discord.py Context object by passing in a valid Context object when you search for a track. + """ def __init__(self, playlist_info: dict, tracks: list, ctx: commands.Context = None): diff --git a/pomice/player.py b/pomice/player.py index a2b7e7f..233a08c 100644 --- a/pomice/player.py +++ b/pomice/player.py @@ -17,6 +17,10 @@ from typing import Dict, Optional, Any, Union 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)``` + """ def __init__(self, client: Union[commands.Bot, discord.Client, commands.AutoShardedBot, discord.AutoShardedClient], channel: VoiceChannel): super().__init__(client=client, channel=channel) @@ -48,6 +52,7 @@ class Player(VoiceProtocol): @property def position(self): + """Property which returns the player's position in a track in milliseconds""" if not self.is_playing or not self._current: return 0 @@ -65,26 +70,32 @@ class Player(VoiceProtocol): @property def is_connected(self): + """Property which returns whether or not the player is connected to a node.""" return self._is_connected @property def is_playing(self): + """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 is_paused(self): + """Property which returns whether or not the player has a track which is paused or not.""" return self._is_connected and self._paused is True @property def node(self): + """Property which returns what node is associated with this player.""" return self._node @property def current(self): + """Property which returns the current track as a Pomice Track object""" return self._current @property def volume(self): + """Property which returns the players current volume as an integer""" return self._volume @@ -125,15 +136,20 @@ class Player(VoiceProtocol): self._bot.dispatch(f"pomice_{event.name}", event) async def get_tracks(self, query: str, ctx: commands.Context = None): + """Fetches tracks from the node's REST api to parse into Lavalink. + 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 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) async def connect(self, *, timeout: float, reconnect: bool): await self._guild.change_voice_state(channel=self.channel) self._node._players[self._guild.id] = self self._is_connected = True - - + async def stop(self): + """Stops a currently playing track.""" self._current = None await self._node.send(op='stop', guildId=str(self._guild.id)) @@ -145,19 +161,21 @@ class Player(VoiceProtocol): self._is_connected = False del self._node._players[self._guild.id] - async def destroy(self): + """Disconnects a player and destroys the player instance.""" await self.disconnect() await self._node.send(op='destroy', guildId=str(self._guild.id)) async def play(self, track: objects.Track, start_position: int = 0): + """Plays a track. If a Spotify track is passed in, it will be handled accordingly.""" if track.track_id == "spotify": track: objects.Track = await self._node.get_tracks(f"{track.title} {track.author}") await self._node.send(op='play', guildId=str(self._guild.id), track=track.track_id, startTime=start_position, endTime=track.length, noReplace=False) self._current = track return self._current - async def seek(self, position: int): + async def seek(self, position: float): + """Seeks to a position in the currently playing track milliseconds""" if position < 0 or position > self.current.length: raise exceptions.TrackInvalidPosition(f"Seek position must be between 0 and the track length") @@ -166,16 +184,19 @@ class Player(VoiceProtocol): return self.position async def set_pause(self, pause: bool): + """Sets the pause state of the currently playing track.""" await self._node.send(op='pause', guildId=str(self._guild.id), pause=pause) self._paused = pause return self._paused - async def set_volume(self, volume: int): + async def set_volume(self, volume: int): + """Sets the volume of the player as an integer. Lavalink accepts an amount from 0 to 500.""" await self._node.send(op='volume', guildId=str(self._guild.id), volume=volume) self._volume = volume return self._volume async def set_filter(self, filter: filters.Filter): + """Sets a filter of the player. Takes a pomice.Filter object. This will only work if you are using the development version of Lavalink.""" await self._node.send(op='filters', guildId=str(self._guild.id), **filter.payload) await self.seek(self.position) self._filter = filter diff --git a/pomice/pool.py b/pomice/pool.py index 663d9d3..e0dbd70 100644 --- a/pomice/pool.py +++ b/pomice/pool.py @@ -9,6 +9,7 @@ from discord.ext import commands class NodePool: + """The base class for the node poll. This holds all the nodes that are to be used by the bot.""" _nodes: dict = {} @@ -17,11 +18,13 @@ class NodePool: @property def nodes(self): + """Property which returns a dict with the node identifier and the Node object.""" return self._nodes @classmethod def get_node(self, *, identifier: str = None) -> Node: + """Fetches a node from the node pool using it's identifier. If no identifier is provided, it will choose a node at random.""" available_nodes = {identifier: node for identifier, node in self._nodes.items()} if not available_nodes: raise exceptions.NoNodesAvailable('There are no nodes available.') @@ -33,6 +36,7 @@ class NodePool: @classmethod async def create_node(self, bot: typing.Union[commands.Bot, discord.Client, commands.AutoShardedBot, discord.AutoShardedClient], host: str, port: str, password: str, identifier: str) -> Node: + """Creates a Node object to be then added into the node pool. If you like to have Spotify searching capabilites, pass in valid Spotify API credentials.""" if identifier in self._nodes.keys(): raise exceptions.NodeCreationError(f"A node with identifier '{identifier}' already exists.") diff --git a/pomice/utils.py b/pomice/utils.py index 347bd2a..99397c6 100644 --- a/pomice/utils.py +++ b/pomice/utils.py @@ -57,6 +57,7 @@ class ExponentialBackoff: return self._randfunc(0, self._base * 2 ** self._exp) class NodeStats: + """The base class for the node stats object. Gives critcical information on the node, which is updated every minute.""" def __init__(self, data: dict) -> None: