commit 07b158988a39a252a7a61226c607c652003a4b60 Author: cloudwithax Date: Sat Sep 25 13:40:41 2021 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d782f0f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.pyc +.git/ +__pycache/ +dist/ +pomice.egg-info/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b51078 --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ + +The modern [Lavalink](https://github.com/freyacodes/Lavalink) wrapper designed for [discord.py](https://github.com/Rapptz/discord.py) + +![](https://img.shields.io/badge/license-GPL-2f2f2f) ![](https://img.shields.io/badge/python-3.8-2f2f2f) + + +# Install +To install the library, you need the lastest version of pip and minimum Python 3.8 + +> Stable version +``` +pip install pomice +``` + +> Unstable version (this one gets more frequent changes) +``` +pip install git+https://github.com/cloudwithax/pomice +``` + + +# Examples +In-depth examples are located in the examples folder + +Here's a quick example: + +```py +import pomice +import discord +import re + +from discord.ext import commands + +URL_REG = re.compile(r'https?://(?:www\.)?.+') + +class MyBot(commands.Bot): + + def __init__(self) -> None: + super().__init__(command_prefix='!', activity=discord.Activity(type=discord.ActivityType.listening, name='to music!')) + + self.add_cog(Music(self)) + + async def on_ready(self) -> None: + print("I'm online!") + await self.cogs["Music"].start_nodes() + + +class Music(commands.Cog): + + def __init__(self, bot) -> None: + self.bot = bot + + self.obsidian = pomice.NodePool() + + async def start_nodes(self): + await self.pomice.create_node(bot=self.bot, host='127.0.0.1', port='3030', + password='youshallnotpass', identifier='MAIN') + print(f"Node is ready!") + + + + @commands.command(name='join', aliases=['connect']) + async def join(self, ctx: commands.Context, *, channel: discord.TextChannel = None) -> None: + + if not channel: + channel = getattr(ctx.author.voice, 'channel', None) + if not channel: + raise commands.CheckFailure('You must be in a voice channel to use this command' + 'without specifying the channel argument.') + + + await ctx.author.voice.channel.connect(cls=pomice.Player) + await ctx.send(f'Joined the voice channel `{channel}`') + + @commands.command(name='play') + async def play(self, ctx, *, search: str) -> None: + + if not ctx.voice_client: + await ctx.invoke(self.join) + + player = ctx.voice_client + + results = await player.get_tracks(query=f'ytsearch:{search}') + + if not results: + raise commands.CommandError('No results were found for that search term.') + + if isinstance(results, pomice.Playlist): + await player.play(track=results.tracks[0]) + else: + await player.play(track=results[0]) + + +bot = MyBot() +bot.run("token here") + ``` + +# FAQ +Why is it saying "Cannot connect to host"? + +- You need to have a Lavalink node setup before you can use this library. Download it [here](https://github.com/freyacodes/Lavalink/releases/tag/3.3.2.5) + +What experience do I need? + +- This library requires that you have some experience with Python, asynchronous programming and the discord.py library. + +Why is it saying "No module named pomice found"? + +- You need to [install](#Install) the package before you can use it diff --git a/pomice/__init__.py b/pomice/__init__.py new file mode 100644 index 0000000..98665f7 --- /dev/null +++ b/pomice/__init__.py @@ -0,0 +1,14 @@ +"""Big poopoo peepee moment""" + +__version__ = "1.0.6.2" +__title__ = "pomice" +__author__ = "cloudwithax" + + +from .exceptions import * +from .events import * +from .filters import * +from .objects import * +from .pool import NodePool +from .node import Node +from .player import Player diff --git a/pomice/events.py b/pomice/events.py new file mode 100644 index 0000000..2576fc6 --- /dev/null +++ b/pomice/events.py @@ -0,0 +1,85 @@ +from .pool import NodePool + + +class PomiceEvent: + def __init__(self): + pass + + name = 'event' + +class TrackStartEvent(PomiceEvent): + def __init__(self, data): + super().__init__() + + self.name = "track_start" + self.player = NodePool.get_node().get_player(int(data["guildId"])) + self.track_id = data['track'] + + def __repr__(self) -> str: + return f"" + + + +class TrackEndEvent(PomiceEvent): + def __init__(self, data): + super().__init__() + + self.name = "track_end" + self.player = NodePool.get_node().get_player(int(data["guildId"])) + self.track_id = data['track'] + self.reason = data['reason'] + + def __repr__(self) -> str: + return f"" + +class TrackStuckEvent(PomiceEvent): + def __init__(self, data): + super().__init__() + + self.name = "track_stuck" + self.player = NodePool.get_node().get_player(int(data["guildId"])) + + self.track_id = data["track"] + self.threshold = data["thresholdMs"] + + def __repr__(self) -> str: + return f"" + +class TrackExceptionEvent(PomiceEvent): + def __init__(self, data): + super().__init__() + + self.name = "track_exception" + self.player = NodePool.get_node().get_player(int(data["guildId"])) + + self.error = data["error"] + self.exception = data["exception"] + + def __repr__(self) -> str: + return f" error={self.error} exeception={self.exception}" + +class WebsocketClosedEvent(PomiceEvent): + 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"] + + def __repr__(self) -> str: + return f"" + +class WebsocketOpenEvent(PomiceEvent): + 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'] + + def __repr__(self) -> str: + return f"" diff --git a/pomice/exceptions.py b/pomice/exceptions.py new file mode 100644 index 0000000..8898a0c --- /dev/null +++ b/pomice/exceptions.py @@ -0,0 +1,44 @@ +class PomiceException(Exception): + """Base of all Pomice exceptions.""" + + +class NodeException(Exception): + """Base exception for nodes.""" + + +class NodeCreationError(NodeException): + """There was a problem while creating the node.""" + + +class NodeConnectionFailure(NodeException): + """There was a problem while connecting to the node.""" + + +class NodeConnectionClosed(NodeException): + """The nodes connection is closed.""" + pass + + +class NodeNotAvailable(PomiceException): + """The node is not currently available.""" + pass + + +class NoNodesAvailable(PomiceException): + """There are no nodes currently available.""" + pass + + +class TrackInvalidPosition(PomiceException): + """An invalid position was chosen for a track.""" + pass + + +class TrackLoadError(PomiceException): + """There was an error while loading a track.""" + pass + + +class FilterInvalidArgument(PomiceException): + """An invalid argument was passed to a filter.""" + pass \ No newline at end of file diff --git a/pomice/filters.py b/pomice/filters.py new file mode 100644 index 0000000..bbb8c85 --- /dev/null +++ b/pomice/filters.py @@ -0,0 +1,90 @@ +from . import exceptions + + +class Filter: + + def __init__(self): + self.payload = None + + +class Timescale(Filter): + + def __init__(self, *, speed: float = 1.0, pitch: float = 1.0, rate: float = 1.0): + super().__init__() + + if speed < 0: + raise exceptions.FilterInvalidArgument("Timescale speed must be more than 0.") + if pitch < 0: + raise exceptions.FilterInvalidArgument("Timescale pitch must be more than 0.") + if rate < 0: + raise exceptions.FilterInvalidArgument("Timescale rate must be more than 0.") + + self.speed = speed + self.pitch = pitch + self.rate = rate + + self.payload = {"timescale": {"speed": self.speed, + "pitch": self.pitch, + "rate": self.rate}} + + def __repr__(self): + return f"" + + +class Karaoke(Filter): + + def __init__(self, *, level: float, mono_level: float, filter_band: float, filter_width: float): + super().__init__() + + self.level = level + self.mono_level = mono_level + self.filter_band = filter_band + self.filter_width = filter_width + + self.payload = {"karaoke": {"level": self.level, + "monoLevel": self.mono_level, + "filterBand": self.filter_band, + "filterWidth": self.filter_width}} + + def __repr__(self): + return f"" + + +class Tremolo(Filter): + + def __init__(self, *, frequency: float, depth: float): + super().__init__() + + if frequency < 0: + raise exceptions.FilterInvalidArgument("Tremolo frequency must be more than 0.") + if depth < 0 or depth > 1: + raise exceptions.FilterInvalidArgument("Tremolo depth must be between 0 and 1.") + + self.frequency = frequency + self.depth = depth + + self.payload = {"tremolo": {"frequency": self.frequency, + "depth": self.depth}} + + def __repr__(self): + return f"" + + +class Vibrato(Filter): + + def __init__(self, *, frequency: float, depth: float): + + super().__init__() + if frequency < 0 or frequency > 14: + raise exceptions.FilterInvalidArgument("Vibrato frequency must be between 0 and 14.") + if depth < 0 or depth > 1: + raise exceptions.FilterInvalidArgument("Vibrato depth must be between 0 and 1.") + + self.frequency = frequency + self.depth = depth + + self.payload = {"vibrato": {"frequency": self.frequency, + "depth": self.depth}} + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/pomice/node.py b/pomice/node.py new file mode 100644 index 0000000..282755d --- /dev/null +++ b/pomice/node.py @@ -0,0 +1,205 @@ +import aiohttp +import discord +import asyncio +import typing +import json +import socket +import time + +from discord.ext import commands +from typing import Optional, Union +from urllib.parse import quote +from . import events +from . import exceptions +from . import objects +from . import __version__ +from .utils import ExponentialBackoff, NodeStats + +class Node: + def __init__(self, pool, bot: Union[commands.Bot, discord.Client, commands.AutoShardedBot, discord.AutoShardedClient], host: str, port: int, password: str, identifier: str, **kwargs): + self._bot = bot + self._host = host + self._port = port + self._password = password + self._identifier = identifier + self._pool = pool + + self._websocket_uri = f"ws://{self._host}:{self._port}" + self._rest_uri = f"http://{self._host}:{self._port}" + + self._session = aiohttp.ClientSession() + self._websocket: aiohttp.ClientWebSocketResponse = None + self._task: asyncio.Task = None + + self._connection_id = None + self._metadata = None + self._available = None + + self._headers = { + "Authorization": self._password, + "User-Id": str(self._bot.user.id), + "Client-Name": f"Pomice/{__version__}" + } + + self._players = {} + + self._bot.add_listener(self._update_handler, "on_socket_response") + + def __repr__(self): + return f"" + + @property + def is_connected(self) -> bool: + return self._websocket is not None and not self._websocket.closed + + @property + async def latency(self): + start_time = time.time() + await self.send(op="ping") + end_time = await self._bot.wait_for(f"node_ping") + return (end_time - start_time) * 1000 + + @property + async def stats(self): + await self.send(op="get-stats") + node_stats = await self._bot.wait_for(f"node_stats") + return node_stats + + @property + def players(self): + return self._players + + @property + def bot(self): + return self._bot + + @property + def pool(self): + return self._pool + + async def _update_handler(self, data: dict): + 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._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._voice_state_update(data["d"]) + except KeyError: + return + + else: + return + + async def _listen(self): + backoff = ExponentialBackoff(base=7) + + while True: + msg = await self._websocket.receive() + if msg.type == aiohttp.WSMsgType.CLOSED: + retry = backoff.delay() + await asyncio.sleep(retry) + + if not self.is_connected: + self._bot.loop.create_task(self.connect()) + else: + self._bot.loop.create_task(self._handle_payload(msg.json())) + + async def _handle_payload(self, data: dict) -> None: + op = data.get('op', None) + if not op: + return + + if op == 'stats': + self._stats = NodeStats(data) + return + + if not (player := self._players.get(int(data['guildId']))): + return + + if op == 'event': + await player._dispatch_event(data) + elif op == 'playerUpdate': + await player._update_state(data) + + + 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): + return self._players.get(guild_id, None) + + async def connect(self): + await self._bot.wait_until_ready() + + try: + self._websocket = await self._session.ws_connect(self._websocket_uri, headers=self._headers, heartbeat=60) + self._task = self._bot.loop.create_task(self._listen()) + self._pool._nodes[self._identifier] = self + self.available = True + return self + + except aiohttp.WSServerHandshakeError: + raise exceptions.NodeConnectionFailure(f"The password for node '{self.identifier}' is invalid.") + except aiohttp.InvalidURL: + raise exceptions.NodeConnectionFailure(f"The URI for node '{self.identifier}' is invalid.") + except socket.gaierror: + raise exceptions.NodeConnectionFailure(f"The node '{self.identifier}' failed to connect.") + + async def disconnect(self): + for player in self.players.copy().values(): + await player.destroy() + + await self._websocket.close() + del self._pool.nodes[self._identifier] + self.available = False + self._task.cancel() + + async def get_tracks(self, query: str, ctx: commands.Context = None): + + async with self._session.get(url=f"{self._rest_uri}/loadtracks?identifier={quote(query)}", headers={"Authorization": self._password}) as response: + data = await response.json() + + load_type = data.get("loadType") + + if not load_type: + raise exceptions.TrackLoadError("There was an error while trying to load this track.") + + elif load_type == "LOAD_FAILED": + raise exceptions.TrackLoadError(f"There was an error of severity '{data['severity']}' while loading tracks.\n\n{data['cause']}") + + elif load_type == "NO_MATCHES": + return None + + elif load_type == "PLAYLIST_LOADED": + if ctx: + return objects.Playlist(playlist_info=data["playlistInfo"], tracks=data["tracks"], ctx=ctx) + else: + return objects.Playlist(playlist_info=data["playlistInfo"], tracks=data["tracks"]) + + elif load_type == "SEARCH_RESULT" or load_type == "TRACK_LOADED": + if ctx: + return [objects.Track(track_id=track["track"], info=track["info"], ctx=ctx) for track in data["tracks"]] + else: + return [objects.Track(track_id=track["track"], info=track["info"]) for track in data["tracks"]] diff --git a/pomice/objects.py b/pomice/objects.py new file mode 100644 index 0000000..7bcb9e7 --- /dev/null +++ b/pomice/objects.py @@ -0,0 +1,48 @@ +from discord.ext import commands + +class Track: + + def __init__(self, track_id: str, info: dict, ctx: commands.Context = None): + + self.track_id = track_id + self.info = info + + self.title = info.get("title") + self.author = info.get("author") + self.length = info.get("length") + if ctx: + self.ctx: commands.Context = ctx + self.requester = self.ctx.author + self.identifier = info.get("identifier") + self.uri = info.get("uri") + self.is_stream = info.get("isStream") + self.is_seekable = info.get("isSeekable") + self.position = info.get("position") + + def __str__(self): + return self.title + + def __repr__(self): + return f" length={self.length}>" + + +class Playlist: + + def __init__(self, playlist_info: dict, tracks: list, ctx: commands.Context = None): + + self.playlist_info = playlist_info + self.tracks_raw = tracks + + self.name = playlist_info.get("name") + self.selected_track = playlist_info.get("selectedTrack") + + if ctx: + self.tracks = [Track(track_id=track["track"], info=track["info"], ctx=ctx) for track in self.tracks_raw] + else: + self.tracks = [Track(track_id=track["track"], info=track["info"]) for track in self.tracks_raw] + + def __str__(self): + return self.name + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/pomice/player.py b/pomice/player.py new file mode 100644 index 0000000..f17b18f --- /dev/null +++ b/pomice/player.py @@ -0,0 +1,181 @@ +import time + +import discord + +from . import exceptions +from . import filters +from . import objects +from .node import Node +from .pool import NodePool +from . import events + +import discord +from discord import VoiceProtocol, VoiceChannel +from discord.ext import commands +from typing import Optional, Any, Union + + + +class Player(VoiceProtocol): + + def __init__(self, client: Union[commands.Bot, discord.Client, commands.AutoShardedBot, discord.AutoShardedClient], channel: VoiceChannel): + super().__init__(client=client, channel=channel) + + self.client: Union[commands.Bot, discord.Client, commands.AutoShardedBot, discord.AutoShardedClient] = client + self._bot: Union[commands.Bot, discord.Client, commands.AutoShardedBot, discord.AutoShardedClient] = client + self.channel: VoiceChannel = channel + self._guild: discord.Guild = channel.guild + self._dj: discord.Member = None + + self._node: Node = NodePool.get_node() + self._current: objects.Track = None + self._filter: filters.Filter = None + self._volume: int = 100 + self._paused: bool = False + self._is_connected: bool = False + + self._position: int = 0 + self._last_update: int = 0 + self._current_track_id = None + + + self._session_id: Optional[str] = None + self._voice_server_update_data: Optional[dict] = None + + + def __repr__(self): + return f"" + + @property + def position(self): + + if not self.is_playing: + return 0 + + if self._paused: + return min(self._position, self._current.length) + + position = round(self._position + ((time.time() * 1000) - self._last_update)) + + if position > self._current.length: + return 0 + + return position + + @property + def is_connected(self): + return self._is_connected + + @property + def is_playing(self): + return self._is_connected and self._current is not None + + @property + def is_paused(self): + return self._is_connected and self._paused is True + + @property + def node(self): + return self._node + + @property + def current(self): + return self._current + + @property + def volume(self): + return self._volume + + + async def _update_state(self, data: dict): + + state = data.get('state') + self._last_update = state.get('time') + self._is_connected = state.get('connected') + self._position = state.get('position') + + async def _dispatch_voice_update(self) -> None: + + if not self._session_id or not self._voice_server_update_data: + return + + await self._node.send(op='voiceUpdate', sessionId=self._session_id, guildId=str(self._guild.id), event={**self._voice_server_update_data}) + + async def _voice_server_update(self, data: dict): + + self._voice_server_update_data = data + await self._dispatch_voice_update() + + + async def _voice_state_update(self, data: dict): + + if not (channel_id := data.get('channel_id')): + self.channel, self._session_id, self._voice_server_update_data = None + return + + self.channel = self._guild.get_channel(int(channel_id)) + self._session_id = data.get('session_id') + await self._dispatch_voice_update() + + async def _dispatch_event(self, data: dict): + event_type = data.get('type') + event = getattr(events, event_type, None) + event = event(data) + self._bot.dispatch(f"pomice_{event.name}", event) + + async def get_tracks(self, query: str, ctx: commands.Context = None): + 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): + self._current = None + await self._node.send(op='stop', guildId=str(self._guild.id)) + + async def disconnect(self, *, force: bool = False): + await self.stop() + await self._guild.change_voice_state(channel=None) + self.cleanup() + self.channel = None + self._is_connected = False + del self._node._players[self._guild.id] + + + async def destroy(self): + 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): + 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): + + if position < 0 or position > self.current.length: + raise exceptions.TrackInvalidPosition(f"Seek position must be between 0 and the track length") + + await self._node.send(op='seek', guildId=str(self._guild.id), position=position) + return self.position + + async def set_pause(self, pause: bool): + 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): + 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): + await self._node.send(op='filters', guildId=str(self._guild.id), **filter.payload) + await self.seek(self.position) + self._filter = filter + return filter + + \ No newline at end of file diff --git a/pomice/pool.py b/pomice/pool.py new file mode 100644 index 0000000..663d9d3 --- /dev/null +++ b/pomice/pool.py @@ -0,0 +1,46 @@ +import discord +import typing +import random + +from . import exceptions +from .node import Node + +from discord.ext import commands + + +class NodePool: + + _nodes: dict = {} + + def __repr__(self): + return f"" + + @property + def nodes(self): + return self._nodes + + + @classmethod + def get_node(self, *, identifier: str = None) -> Node: + available_nodes = {identifier: node for identifier, node in self._nodes.items()} + if not available_nodes: + raise exceptions.NoNodesAvailable('There are no nodes available.') + + if identifier is None: + return random.choice(list(available_nodes.values())) + + return available_nodes.get(identifier, None) + + @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: + if identifier in self._nodes.keys(): + raise exceptions.NodeCreationError(f"A node with identifier '{identifier}' already exists.") + + node = Node(pool=self, bot=bot, host=host, port=port, password=password, identifier=identifier) + await node.connect() + self._nodes[node._identifier] = node + return node + + + + \ No newline at end of file diff --git a/pomice/utils.py b/pomice/utils.py new file mode 100644 index 0000000..fb731c1 --- /dev/null +++ b/pomice/utils.py @@ -0,0 +1,79 @@ +""" +The MIT License (MIT) +Copyright (c) 2015-present Rapptz +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +import random +import time + + +__all__ = [ + 'ExponentialBackoff', + 'PomiceStats' +] + + +class ExponentialBackoff: + + def __init__(self, base: int = 1, *, integral: bool = False) -> None: + + self._base = base + + self._exp = 0 + self._max = 10 + self._reset_time = base * 2 ** 11 + self._last_invocation = time.monotonic() + + rand = random.Random() + rand.seed() + + self._randfunc = rand.randrange if integral else rand.uniform + + def delay(self) -> float: + + invocation = time.monotonic() + interval = invocation - self._last_invocation + self._last_invocation = invocation + + if interval > self._reset_time: + self._exp = 0 + + self._exp = min(self._exp + 1, self._max) + return self._randfunc(0, self._base * 2 ** self._exp) + +class NodeStats: + + def __init__(self, data: dict) -> None: + + memory = data.get('memory') + self.used = memory.get('used') + self.free = memory.get('free') + self.reservable = memory.get('reservable') + self.allocated = memory.get('allocated') + + cpu = data.get('cpu') + self.cpu_cores = cpu.get('cores') + self.cpu_system_load = cpu.get('systemLoad') + self.cpu_process_load = cpu.get('lavalinkLoad') + + self.players_active = data.get('playingPlayers') + self.players_total = data.get('players') + self.uptime = data.get('uptime') + + def __repr__(self) -> str: + return f'' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b5a3c46 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e42b5ab --- /dev/null +++ b/setup.py @@ -0,0 +1,33 @@ +import setuptools + +with open("README.md") as f: + readme = f.read() + + +setuptools.setup( + name="pomice", + author="cloudwithax", + version="1.0.0", + url="https://github.com/cloudwithax/pomice", + packages=setuptools.find_packages(), + license="GPL", + description="The modern Lavalink wrapper designed for Discord.py", + long_description=readme, + long_description_content_type="text/markdown", + include_package_data=True, + install_requires=['discord.py>=1.7.1'], + extra_require=None, + classifiers=[ + "Framework :: AsyncIO", + 'Operating System :: OS Independent', + 'Natural Language :: English', + 'Intended Audience :: Developers', + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Software Development :: Libraries', + "Topic :: Internet" + ], + python_requires='>=3.8', + keywords=['pomice', 'lavalink', "discord.py"], +) \ No newline at end of file