Initial commit
This commit is contained in:
commit
07b158988a
|
|
@ -0,0 +1,5 @@
|
|||
*.pyc
|
||||
.git/
|
||||
__pycache/
|
||||
dist/
|
||||
pomice.egg-info/
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
|
||||
The modern [Lavalink](https://github.com/freyacodes/Lavalink) wrapper designed for [discord.py](https://github.com/Rapptz/discord.py)
|
||||
|
||||
 
|
||||
|
||||
|
||||
# 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"<Pomice.TrackStartEvent track_id={self.track_id}>"
|
||||
|
||||
|
||||
|
||||
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"<Pomice.TrackEndEvent track_id={self.track_id} reason={self.reason}>"
|
||||
|
||||
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"<Pomice.TrackStuckEvent track_id={self.track_id} threshold={self.threshold}>"
|
||||
|
||||
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"<Pomice.TrackExceptionEvent> 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"<Pomice.WebsocketClosedEvent reason={self.reason} code={self.code}>"
|
||||
|
||||
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"<Pomice.WebsocketOpenEvent target={self.target} ssrc={self.ssrc}>"
|
||||
|
|
@ -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
|
||||
|
|
@ -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"<Pomice.TimescaleFilter speed={self.speed} pitch={self.pitch} rate={self.rate}>"
|
||||
|
||||
|
||||
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"<Pomice.KaraokeFilter level={self.level} mono_level={self.mono_level} filter_band={self.filter_band} filter_width={self.filter_width}>"
|
||||
|
||||
|
||||
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"<Pomice.TremoloFilter frequency={self.frequency} depth={self.depth}>"
|
||||
|
||||
|
||||
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"<Pomice.VibratoFilter frequency={self.frequency} depth={self.depth}>"
|
||||
|
|
@ -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"<Pomice.node ws_uri={self._websocket_uri} rest_uri={self._rest_uri} player_count={len(self._players)}>"
|
||||
|
||||
@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"]]
|
||||
|
|
@ -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"<Pomice.track title={self.title!r} uri=<{self.uri!r}> 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"<Pomice.playlist name={self.name!r} track_count={len(self.tracks)}>"
|
||||
|
|
@ -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"<Pomice.player bot={self._bot} guildId={self._guild.id} is_connected={self.is_connected} is_playing={self.is_playing}>"
|
||||
|
||||
@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
|
||||
|
||||
|
||||
|
|
@ -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"<Pomice.NodePool node_count={len(self._nodes.values())}>"
|
||||
|
||||
@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
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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'<Pomice.NodeStats total_players={self.players_total} playing_active={self.players_active}>'
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=42",
|
||||
"wheel"
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
|
@ -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"],
|
||||
)
|
||||
Loading…
Reference in New Issue