finsh player and started events
This commit is contained in:
parent
a1b3d5bb1b
commit
5da841cba8
|
|
@ -0,0 +1,10 @@
|
||||||
|
# This configuration file was automatically generated by Gitpod.
|
||||||
|
# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml)
|
||||||
|
# and commit this file to your remote git repository to share the goodness with others.
|
||||||
|
|
||||||
|
# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- init: pip install .
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
"""
|
||||||
|
Pomice
|
||||||
|
~~~~~~
|
||||||
|
The modern Lavalink wrapper designed for discord.py.
|
||||||
|
|
||||||
|
:copyright: 2023, cloudwithax
|
||||||
|
:license: GPL-3.0
|
||||||
|
"""
|
||||||
|
import discord
|
||||||
|
|
||||||
|
if not discord.version_info.major >= 2:
|
||||||
|
class DiscordPyOutdated(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise DiscordPyOutdated(
|
||||||
|
"You must have discord.py (v2.0 or greater) to use this library. "
|
||||||
|
"Uninstall your current version and install discord.py 2.0 "
|
||||||
|
"using 'pip install discord.py'"
|
||||||
|
)
|
||||||
|
|
||||||
|
__version__ = "2.1.1"
|
||||||
|
__title__ = "pomice"
|
||||||
|
__author__ = "cloudwithax"
|
||||||
|
|
||||||
|
from .enums import *
|
||||||
|
from .events import *
|
||||||
|
from .exceptions import *
|
||||||
|
from .filters import *
|
||||||
|
from .objects import *
|
||||||
|
from .queue import *
|
||||||
|
from .player import *
|
||||||
|
from .pool import *
|
||||||
|
from .routeplanner import *
|
||||||
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
"""Apple Music module for Pomice, made possible by cloudwithax 2023"""
|
||||||
|
|
||||||
|
from .exceptions import *
|
||||||
|
from .objects import *
|
||||||
|
from .client import Client
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
import re
|
||||||
|
import aiohttp
|
||||||
|
import orjson as json
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from .objects import *
|
||||||
|
from .exceptions import *
|
||||||
|
|
||||||
|
AM_URL_REGEX = re.compile(r"https?://music.apple.com/(?P<country>[a-zA-Z]{2})/(?P<type>album|playlist|song|artist)/(?P<name>.+)/(?P<id>[^?]+)")
|
||||||
|
AM_SINGLE_IN_ALBUM_REGEX = re.compile(r"https?://music.apple.com/(?P<country>[a-zA-Z]{2})/(?P<type>album|playlist|song|artist)/(?P<name>.+)/(?P<id>.+)(\?i=)(?P<id2>.+)")
|
||||||
|
AM_REQ_URL = "https://api.music.apple.com/v1/catalog/{country}/{type}s/{id}"
|
||||||
|
AM_BASE_URL = "https://api.music.apple.com"
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
"""The base Apple Music client for Pomice.
|
||||||
|
This will do all the heavy lifting of getting tracks from Apple Music
|
||||||
|
and translating it to a valid Lavalink track. No client auth is required here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.token: str = None
|
||||||
|
self.expiry: datetime = None
|
||||||
|
self.session: aiohttp.ClientSession = aiohttp.ClientSession()
|
||||||
|
self.headers = None
|
||||||
|
|
||||||
|
|
||||||
|
async def request_token(self):
|
||||||
|
async with self.session.get("https://music.apple.com/assets/index.919fe17f.js") as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
raise AppleMusicRequestException(
|
||||||
|
f"Error while fetching results: {resp.status} {resp.reason}"
|
||||||
|
)
|
||||||
|
text = await resp.text()
|
||||||
|
result = re.search("\"(eyJ.+?)\"", text).group(1)
|
||||||
|
self.token = result
|
||||||
|
self.headers = {
|
||||||
|
'Authorization': f"Bearer {result}",
|
||||||
|
'Origin': 'https://apple.com',
|
||||||
|
}
|
||||||
|
token_split = self.token.split(".")[1]
|
||||||
|
token_json = base64.b64decode(token_split + '=' * (-len(token_split) % 4)).decode()
|
||||||
|
token_data = json.loads(token_json)
|
||||||
|
self.expiry = datetime.fromtimestamp(token_data["exp"])
|
||||||
|
|
||||||
|
|
||||||
|
async def search(self, query: str):
|
||||||
|
if not self.token or datetime.utcnow() > self.expiry:
|
||||||
|
await self.request_token()
|
||||||
|
|
||||||
|
result = AM_URL_REGEX.match(query)
|
||||||
|
|
||||||
|
country = result.group("country")
|
||||||
|
type = result.group("type")
|
||||||
|
id = result.group("id")
|
||||||
|
|
||||||
|
if type == "album" and (sia_result := AM_SINGLE_IN_ALBUM_REGEX.match(query)):
|
||||||
|
# apple music likes to generate links for singles off an album
|
||||||
|
# by adding a param at the end of the url
|
||||||
|
# so we're gonna scan for that and correct it
|
||||||
|
id = sia_result.group("id2")
|
||||||
|
type = "song"
|
||||||
|
request_url = AM_REQ_URL.format(country=country, type=type, id=id)
|
||||||
|
else:
|
||||||
|
request_url = AM_REQ_URL.format(country=country, type=type, id=id)
|
||||||
|
|
||||||
|
|
||||||
|
async with self.session.get(request_url, headers=self.headers) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
raise AppleMusicRequestException(
|
||||||
|
f"Error while fetching results: {resp.status} {resp.reason}"
|
||||||
|
)
|
||||||
|
data: dict = await resp.json(loads=json.loads)
|
||||||
|
|
||||||
|
data = data["data"][0]
|
||||||
|
|
||||||
|
|
||||||
|
if type == "song":
|
||||||
|
return Song(data)
|
||||||
|
|
||||||
|
elif type == "album":
|
||||||
|
return Album(data)
|
||||||
|
|
||||||
|
elif type == "artist":
|
||||||
|
async with self.session.get(f"{request_url}/view/top-songs", headers=self.headers) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
raise AppleMusicRequestException(
|
||||||
|
f"Error while fetching results: {resp.status} {resp.reason}"
|
||||||
|
)
|
||||||
|
top_tracks: dict = await resp.json(loads=json.loads)
|
||||||
|
tracks: dict = top_tracks["data"]
|
||||||
|
|
||||||
|
return Artist(data, tracks=tracks)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
track_data: dict = data["relationships"]["tracks"]
|
||||||
|
|
||||||
|
tracks = [Song(track) for track in track_data.get("data")]
|
||||||
|
|
||||||
|
if not len(tracks):
|
||||||
|
raise AppleMusicRequestException("This playlist is empty and therefore cannot be queued.")
|
||||||
|
|
||||||
|
if track_data.get("next"):
|
||||||
|
next_page_url = AM_BASE_URL + track_data.get("next")
|
||||||
|
|
||||||
|
while next_page_url is not None:
|
||||||
|
async with self.session.get(next_page_url, headers=self.headers) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
raise AppleMusicRequestException(
|
||||||
|
f"Error while fetching results: {resp.status} {resp.reason}"
|
||||||
|
)
|
||||||
|
|
||||||
|
next_data: dict = await resp.json(loads=json.loads)
|
||||||
|
|
||||||
|
tracks += [Song(track) for track in next_data["data"]]
|
||||||
|
if next_data.get("next"):
|
||||||
|
next_page_url = AM_BASE_URL + next_data.get("next")
|
||||||
|
else:
|
||||||
|
next_page_url = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return Playlist(data, tracks)
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
class AppleMusicRequestException(Exception):
|
||||||
|
"""An error occurred when making a request to the Apple Music API"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAppleMusicURL(Exception):
|
||||||
|
"""An invalid Apple Music URL was passed"""
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
"""Module for managing Apple Music objects"""
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
class Song:
|
||||||
|
"""The base class for an Apple Music song"""
|
||||||
|
def __init__(self, data: dict) -> None:
|
||||||
|
|
||||||
|
self.name: str = data["attributes"]["name"]
|
||||||
|
self.url: str = data["attributes"]["url"]
|
||||||
|
self.isrc: str = data["attributes"]["isrc"]
|
||||||
|
self.length: float = data["attributes"]["durationInMillis"]
|
||||||
|
self.id: str = data["id"]
|
||||||
|
self.artists: str = data["attributes"]["artistName"]
|
||||||
|
self.image: str = data["attributes"]["artwork"]["url"].replace(
|
||||||
|
"{w}x{h}",
|
||||||
|
f'{data["attributes"]["artwork"]["width"]}x{data["attributes"]["artwork"]["height"]}'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Pomice.applemusic.Song name={self.name} artists={self.artists} "
|
||||||
|
f"length={self.length} id={self.id} isrc={self.isrc}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Playlist:
|
||||||
|
"""The base class for an Apple Music playlist"""
|
||||||
|
def __init__(self, data: dict, tracks: List[Song]) -> None:
|
||||||
|
self.name: str = data["attributes"]["name"]
|
||||||
|
self.owner: str = data["attributes"]["curatorName"]
|
||||||
|
self.id: str = data["id"]
|
||||||
|
self.tracks: List[Song] = tracks
|
||||||
|
self.total_tracks: int = len(tracks)
|
||||||
|
self.url: str = data["attributes"]["url"]
|
||||||
|
# we'll use the first song's image as the image for the playlist
|
||||||
|
# because apple dynamically generates playlist covers client-side
|
||||||
|
self.image = self.tracks[0].image
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Pomice.applemusic.Playlist name={self.name} owner={self.owner} id={self.id} "
|
||||||
|
f"total_tracks={self.total_tracks} tracks={self.tracks}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Album:
|
||||||
|
"""The base class for an Apple Music album"""
|
||||||
|
def __init__(self, data: dict) -> None:
|
||||||
|
self.name: str = data["attributes"]["name"]
|
||||||
|
self.url: str = data["attributes"]["url"]
|
||||||
|
self.id: str = data["id"]
|
||||||
|
self.artists: str = data["attributes"]["artistName"]
|
||||||
|
self.total_tracks: int = data["attributes"]["trackCount"]
|
||||||
|
self.tracks: List[Song] = [Song(track) for track in data["relationships"]["tracks"]["data"]]
|
||||||
|
self.image: str = data["attributes"]["artwork"]["url"].replace(
|
||||||
|
"{w}x{h}",
|
||||||
|
f'{data["attributes"]["artwork"]["width"]}x{data["attributes"]["artwork"]["height"]}'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Pomice.applemusic.Album name={self.name} artists={self.artists} id={self.id} "
|
||||||
|
f"total_tracks={self.total_tracks} tracks={self.tracks}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Artist:
|
||||||
|
"""The base class for an Apple Music artist"""
|
||||||
|
def __init__(self, data: dict, tracks: dict) -> None:
|
||||||
|
self.name: str = f'Top tracks for {data["attributes"]["name"]}'
|
||||||
|
self.url: str = data["attributes"]["url"]
|
||||||
|
self.id: str = data["id"]
|
||||||
|
self.genres: str = ", ".join(genre for genre in data["attributes"]["genreNames"])
|
||||||
|
self.tracks: List[Song] = [Song(track) for track in tracks]
|
||||||
|
self.image: str = data["attributes"]["artwork"]["url"].replace(
|
||||||
|
"{w}x{h}",
|
||||||
|
f'{data["attributes"]["artwork"]["width"]}x{data["attributes"]["artwork"]["height"]}'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Pomice.applemusic.Artist name={self.name} id={self.id} "
|
||||||
|
f"tracks={self.tracks}>"
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,260 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class SearchType(Enum):
|
||||||
|
"""
|
||||||
|
The enum for the different search types for Pomice.
|
||||||
|
This feature is exclusively for the Spotify search feature of Pomice.
|
||||||
|
If you are not using this feature, this class is not necessary.
|
||||||
|
|
||||||
|
SearchType.ytsearch searches using regular Youtube,
|
||||||
|
which is best for all scenarios.
|
||||||
|
|
||||||
|
SearchType.ytmsearch searches using YouTube Music,
|
||||||
|
which is best for getting audio-only results.
|
||||||
|
|
||||||
|
SearchType.scsearch searches using SoundCloud,
|
||||||
|
which is an alternative to YouTube or YouTube Music.
|
||||||
|
"""
|
||||||
|
ytsearch = "ytsearch"
|
||||||
|
ytmsearch = "ytmsearch"
|
||||||
|
scsearch = "scsearch"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
class TrackType(Enum):
|
||||||
|
"""
|
||||||
|
The enum for the different track types for Pomice.
|
||||||
|
|
||||||
|
TrackType.YOUTUBE defines that the track is from YouTube
|
||||||
|
|
||||||
|
TrackType.SOUNDCLOUD defines that the track is from SoundCloud.
|
||||||
|
|
||||||
|
TrackType.SPOTIFY defines that the track is from Spotify
|
||||||
|
|
||||||
|
TrackType.APPLE_MUSIC defines that the track is from Apple Music.
|
||||||
|
|
||||||
|
TrackType.HTTP defines that the track is from an HTTP source.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We don't have to define anything special for these, since these just serve as flags
|
||||||
|
YOUTUBE = "youtube_track"
|
||||||
|
SOUNDCLOUD = "soundcloud_track"
|
||||||
|
SPOTIFY = "spotify_track"
|
||||||
|
APPLE_MUSIC = "apple_music_track"
|
||||||
|
HTTP = "http_source"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
class PlaylistType(Enum):
|
||||||
|
"""
|
||||||
|
The enum for the different playlist types for Pomice.
|
||||||
|
|
||||||
|
PlaylistType.YOUTUBE defines that the playlist is from YouTube
|
||||||
|
|
||||||
|
PlaylistType.SOUNDCLOUD defines that the playlist is from SoundCloud.
|
||||||
|
|
||||||
|
PlaylistType.SPOTIFY defines that the playlist is from Spotify
|
||||||
|
|
||||||
|
PlaylistType.APPLE_MUSIC defines that the playlist is from Apple Music.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We don't have to define anything special for these, since these just serve as flags
|
||||||
|
YOUTUBE = "youtube_playlist"
|
||||||
|
SOUNDCLOUD = "soundcloud_playlist"
|
||||||
|
SPOTIFY = "spotify_playlist"
|
||||||
|
APPLE_MUSIC = "apple_music_list"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class NodeAlgorithm(Enum):
|
||||||
|
"""
|
||||||
|
The enum for the different node algorithms in Pomice.
|
||||||
|
|
||||||
|
The enums in this class are to only differentiate different
|
||||||
|
methods, since the actual method is handled in the
|
||||||
|
get_best_node() method.
|
||||||
|
|
||||||
|
NodeAlgorithm.by_ping returns a node based on it's latency,
|
||||||
|
preferring a node with the lowest response time
|
||||||
|
|
||||||
|
|
||||||
|
NodeAlgorithm.by_players return a nodes based on how many players it has.
|
||||||
|
This algorithm prefers nodes with the least amount of players.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We don't have to define anything special for these, since these just serve as flags
|
||||||
|
by_ping = "BY_PING"
|
||||||
|
by_players = "BY_PLAYERS"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
class LoopMode(Enum):
|
||||||
|
"""
|
||||||
|
The enum for the different loop modes.
|
||||||
|
This feature is exclusively for the queue utility of pomice.
|
||||||
|
If you are not using this feature, this class is not necessary.
|
||||||
|
|
||||||
|
LoopMode.TRACK sets the queue loop to the current track.
|
||||||
|
|
||||||
|
LoopMode.QUEUE sets the queue loop to the whole queue.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# We don't have to define anything special for these, since these just serve as flags
|
||||||
|
TRACK = "track"
|
||||||
|
QUEUE = "queue"
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
class PlatformRecommendation(Enum):
|
||||||
|
|
||||||
|
"""
|
||||||
|
The enum for choosing what platform you want for recommendations.
|
||||||
|
This feature is exclusively for the recommendations function.
|
||||||
|
If you are not using this feature, this class is not necessary.
|
||||||
|
|
||||||
|
PlatformRecommendation.SPOTIFY sets the recommendations to come from Spotify
|
||||||
|
|
||||||
|
PlatformRecommendation.YOUTUBE sets the recommendations to come from YouTube
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We don't have to define anything special for these, since these just serve as flags
|
||||||
|
SPOTIFY = "spotify"
|
||||||
|
YOUTUBE = "youtube"
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
class RouteStrategy(Enum):
|
||||||
|
"""
|
||||||
|
The enum for specifying the route planner strategy for Lavalink.
|
||||||
|
This feature is exclusively for the RoutePlanner class.
|
||||||
|
If you are not using this feature, this class is not necessary.
|
||||||
|
|
||||||
|
RouteStrategy.ROTATE_ON_BAN specifies that the node is rotating IPs
|
||||||
|
whenever they get banned by Youtube.
|
||||||
|
|
||||||
|
RouteStrategy.LOAD_BALANCE specifies that the node is selecting
|
||||||
|
random IPs to balance out requests between them.
|
||||||
|
|
||||||
|
RouteStrategy.NANO_SWITCH specifies that the node is switching
|
||||||
|
between IPs every CPU clock cycle.
|
||||||
|
|
||||||
|
RouteStrategy.ROTATING_NANO_SWITCH specifies that the node is switching
|
||||||
|
between IPs every CPU clock cycle and is rotating between IP blocks on
|
||||||
|
ban.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
ROTATE_ON_BAN = "RotatingIpRoutePlanner"
|
||||||
|
LOAD_BALANCE = "BalancingIpRoutePlanner"
|
||||||
|
NANO_SWITCH = "NanoIpRoutePlanner"
|
||||||
|
ROTATING_NANO_SWITCH = "RotatingNanoIpRoutePlanner"
|
||||||
|
|
||||||
|
|
||||||
|
class RouteIPType(Enum):
|
||||||
|
"""
|
||||||
|
The enum for specifying the route planner IP block type for Lavalink.
|
||||||
|
This feature is exclusively for the RoutePlanner class.
|
||||||
|
If you are not using this feature, this class is not necessary.
|
||||||
|
|
||||||
|
RouteIPType.IPV4 specifies that the IP block type is IPV4
|
||||||
|
|
||||||
|
RouteIPType.IPV6 specifies that the IP block type is IPV6
|
||||||
|
"""
|
||||||
|
|
||||||
|
IPV4 = "Inet4Address"
|
||||||
|
IPV6 = "Inet6Address"
|
||||||
|
|
||||||
|
|
||||||
|
class URLRegex():
|
||||||
|
"""
|
||||||
|
The enums for all the URL Regexes in use by Pomice.
|
||||||
|
|
||||||
|
URLRegex.SPOTIFY_URL returns the Spotify URL Regex.
|
||||||
|
|
||||||
|
URLRegex.DISCORD_MP3_URL returns the Discord MP3 URL Regex.
|
||||||
|
|
||||||
|
URLRegex.YOUTUBE_URL returns the Youtube URL Regex.
|
||||||
|
|
||||||
|
URLRegex.YOUTUBE_PLAYLIST returns the Youtube Playlist Regex.
|
||||||
|
|
||||||
|
URLRegex.YOUTUBE_TIMESTAMP returns the Youtube Timestamp Regex.
|
||||||
|
|
||||||
|
URLRegex.AM_URL returns the Apple Music URL Regex.
|
||||||
|
|
||||||
|
URLRegex.SOUNDCLOUD_URL returns the SoundCloud URL Regex.
|
||||||
|
|
||||||
|
URLRegex.BASE_URL returns the standard URL Regex.
|
||||||
|
|
||||||
|
"""
|
||||||
|
SPOTIFY_URL = re.compile(
|
||||||
|
r"https?://open.spotify.com/(?P<type>album|playlist|track|artist)/(?P<id>[a-zA-Z0-9]+)"
|
||||||
|
)
|
||||||
|
|
||||||
|
DISCORD_MP3_URL = re.compile(
|
||||||
|
r"https?://cdn.discordapp.com/attachments/(?P<channel_id>[0-9]+)/"
|
||||||
|
r"(?P<message_id>[0-9]+)/(?P<file>[a-zA-Z0-9_.]+)+"
|
||||||
|
)
|
||||||
|
|
||||||
|
YOUTUBE_URL = re.compile(
|
||||||
|
r"^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))"
|
||||||
|
r"(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$"
|
||||||
|
)
|
||||||
|
|
||||||
|
YOUTUBE_PLAYLIST_URL = re.compile(
|
||||||
|
r"^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))/playlist\?list=.*"
|
||||||
|
)
|
||||||
|
|
||||||
|
YOUTUBE_VID_IN_PLAYLIST = re.compile(
|
||||||
|
r"(?P<video>^.*?v.*?)(?P<list>&list.*)"
|
||||||
|
)
|
||||||
|
|
||||||
|
YOUTUBE_TIMESTAMP = re.compile(
|
||||||
|
r"(?P<video>^.*?)(\?t|&start)=(?P<time>\d+)?.*"
|
||||||
|
)
|
||||||
|
|
||||||
|
AM_URL = re.compile(
|
||||||
|
r"https?://music.apple.com/(?P<country>[a-zA-Z]{2})/"
|
||||||
|
r"(?P<type>album|playlist|song|artist)/(?P<name>.+)/(?P<id>[^?]+)"
|
||||||
|
)
|
||||||
|
|
||||||
|
AM_SINGLE_IN_ALBUM_REGEX = re.compile(
|
||||||
|
r"https?://music.apple.com/(?P<country>[a-zA-Z]{2})/(?P<type>album|playlist|song|artist)/"
|
||||||
|
r"(?P<name>.+)/(?P<id>.+)(\?i=)(?P<id2>.+)"
|
||||||
|
)
|
||||||
|
|
||||||
|
SOUNDCLOUD_URL = re.compile(
|
||||||
|
r"((?:https?:)?\/\/)?((?:www|m)\.)?soundcloud.com\/.*/.*"
|
||||||
|
)
|
||||||
|
|
||||||
|
SOUNDCLOUD_PLAYLIST_URL = re.compile(
|
||||||
|
r"^(https?:\/\/)?(www.)?(m\.)?soundcloud\.com\/.*/sets/.*"
|
||||||
|
)
|
||||||
|
|
||||||
|
SOUNDCLOUD_TRACK_IN_SET_URL = re.compile(
|
||||||
|
r"^(https?:\/\/)?(www.)?(m\.)?soundcloud\.com/[a-zA-Z0-9-._]+/[a-zA-Z0-9-._]+(\?in)"
|
||||||
|
)
|
||||||
|
|
||||||
|
LAVALINK_SEARCH = re.compile(
|
||||||
|
r"(?P<type>ytm?|sc)search:"
|
||||||
|
)
|
||||||
|
|
||||||
|
BASE_URL = re.compile(
|
||||||
|
r"https?://(?:www\.)?.+"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from discord import Client
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from .pool import NodePool
|
||||||
|
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Union
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .player import Player
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class PomiceEvent:
|
||||||
|
"""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):
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
name = "event"
|
||||||
|
handler_args = ()
|
||||||
|
|
||||||
|
def dispatch(self, bot: Union[Client, commands.Bot]):
|
||||||
|
bot.dispatch(f"pomice_{self.name}", *self.handler_args)
|
||||||
|
|
||||||
|
|
||||||
|
class TrackStartEvent(PomiceEvent):
|
||||||
|
"""Fired when a track has successfully started.
|
||||||
|
Returns the player associated with the event and the pomice.Track object.
|
||||||
|
"""
|
||||||
|
name = "track_start"
|
||||||
|
|
||||||
|
def __init__(self, data: dict, player: Player):
|
||||||
|
self.player = player
|
||||||
|
self.track = self.player._current
|
||||||
|
|
||||||
|
# on_pomice_track_start(player, track)
|
||||||
|
self.handler_args = self.player, self.track
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Pomice.TrackStartEvent player={self.player} track_id={self.track.track_id}>"
|
||||||
|
|
||||||
|
|
||||||
|
class TrackEndEvent(PomiceEvent):
|
||||||
|
"""Fired when a track has successfully ended.
|
||||||
|
Returns the player associated with the event along with the pomice.Track object and reason.
|
||||||
|
"""
|
||||||
|
name = "track_end"
|
||||||
|
|
||||||
|
def __init__(self, data: dict, player: Player):
|
||||||
|
self.player = player
|
||||||
|
self.track = self.player._ending_track
|
||||||
|
self.reason: str = data["reason"]
|
||||||
|
|
||||||
|
# on_pomice_track_end(player, track, reason)
|
||||||
|
self.handler_args = self.player, self.track, self.reason
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Pomice.TrackEndEvent player={self.player} track_id={self.track.track_id} "
|
||||||
|
f"reason={self.reason}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TrackStuckEvent(PomiceEvent):
|
||||||
|
"""Fired when a track is stuck and cannot be played. Returns the player
|
||||||
|
associated with the event along with the pomice.Track object
|
||||||
|
to be further parsed by the end user.
|
||||||
|
"""
|
||||||
|
name = "track_stuck"
|
||||||
|
|
||||||
|
def __init__(self, data: dict, player: Player):
|
||||||
|
self.player = player
|
||||||
|
self.track = self.player._ending_track
|
||||||
|
self.threshold: float = data["thresholdMs"]
|
||||||
|
|
||||||
|
# on_pomice_track_stuck(player, track, threshold)
|
||||||
|
self.handler_args = self.player, self.track, self.threshold
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Pomice.TrackStuckEvent player={self.player!r} track={self.track!r} " \
|
||||||
|
f"threshold={self.threshold!r}>"
|
||||||
|
|
||||||
|
|
||||||
|
class TrackExceptionEvent(PomiceEvent):
|
||||||
|
"""Fired when a track error has occured.
|
||||||
|
Returns the player associated with the event along with the error code and exception.
|
||||||
|
"""
|
||||||
|
name = "track_exception"
|
||||||
|
|
||||||
|
def __init__(self, data: dict, player: Player):
|
||||||
|
self.player = player
|
||||||
|
self.track = self.player._ending_track
|
||||||
|
if data.get('error'):
|
||||||
|
# User is running Lavalink <= 3.3
|
||||||
|
self.exception: str = data["error"]
|
||||||
|
else:
|
||||||
|
# User is running Lavalink >=3.4
|
||||||
|
self.exception: str = data["exception"]
|
||||||
|
|
||||||
|
# on_pomice_track_exception(player, track, error)
|
||||||
|
self.handler_args = self.player, self.track, self.exception
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Pomice.TrackExceptionEvent player={self.player!r} exception={self.exception!r}>"
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketClosedPayload:
|
||||||
|
def __init__(self, data: dict):
|
||||||
|
self.guild = NodePool.get_node().bot.get_guild(int(data["guildId"]))
|
||||||
|
self.code: int = data["code"]
|
||||||
|
self.reason: str = data["code"]
|
||||||
|
self.by_remote: bool = data["byRemote"]
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Pomice.WebSocketClosedPayload guild={self.guild!r} code={self.code!r} " \
|
||||||
|
f"reason={self.reason!r} by_remote={self.by_remote!r}>"
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketClosedEvent(PomiceEvent):
|
||||||
|
"""Fired when a websocket connection to a node has been closed.
|
||||||
|
Returns the reason and the error code.
|
||||||
|
"""
|
||||||
|
name = "websocket_closed"
|
||||||
|
|
||||||
|
def __init__(self, data: dict, _):
|
||||||
|
self.payload = WebSocketClosedPayload(data)
|
||||||
|
|
||||||
|
# on_pomice_websocket_closed(payload)
|
||||||
|
self.handler_args = self.payload,
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Pomice.WebsocketClosedEvent payload={self.payload!r}>"
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketOpenEvent(PomiceEvent):
|
||||||
|
"""Fired when a websocket connection to a node has been initiated.
|
||||||
|
Returns the target and the session SSRC.
|
||||||
|
"""
|
||||||
|
name = "websocket_open"
|
||||||
|
|
||||||
|
def __init__(self, data: dict, _):
|
||||||
|
self.target: str = data["target"]
|
||||||
|
self.ssrc: int = data["ssrc"]
|
||||||
|
|
||||||
|
# on_pomice_websocket_open(target, ssrc)
|
||||||
|
self.handler_args = self.target, self.ssrc
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Pomice.WebsocketOpenEvent target={self.target!r} ssrc={self.ssrc!r}>"
|
||||||
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
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 node's connection is closed."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NodeRestException(NodeException):
|
||||||
|
"""A request made using the node's REST uri failed"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NodeNotAvailable(PomiceException):
|
||||||
|
"""The node is currently unavailable."""
|
||||||
|
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
|
||||||
|
|
||||||
|
class FilterTagInvalid(PomiceException):
|
||||||
|
"""An invalid tag was passed or Pomice was unable to find a filter tag"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class FilterTagAlreadyInUse(PomiceException):
|
||||||
|
"""A filter with a tag is already in use by another filter"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SpotifyAlbumLoadFailed(PomiceException):
|
||||||
|
"""The pomice Spotify client was unable to load an album."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SpotifyTrackLoadFailed(PomiceException):
|
||||||
|
"""The pomice Spotify client was unable to load a track."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SpotifyPlaylistLoadFailed(PomiceException):
|
||||||
|
"""The pomice Spotify client was unable to load a playlist."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidSpotifyClientAuthorization(PomiceException):
|
||||||
|
"""No Spotify client authorization was provided for track searching."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AppleMusicNotEnabled(PomiceException):
|
||||||
|
"""An Apple Music Link was passed in when Apple Music functionality was not enabled."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class QueueException(Exception):
|
||||||
|
"""Base Pomice queue exception."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class QueueFull(QueueException):
|
||||||
|
"""Exception raised when attempting to add to a full Queue."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class QueueEmpty(QueueException):
|
||||||
|
"""Exception raised when attempting to retrieve from an empty Queue."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class LavalinkVersionIncompatible(PomiceException):
|
||||||
|
"""Lavalink version is incompatible. Must be using Lavalink > 3.7.0 to avoid this error."""
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,393 @@
|
||||||
|
import collections
|
||||||
|
from .exceptions import FilterInvalidArgument
|
||||||
|
|
||||||
|
|
||||||
|
class Filter:
|
||||||
|
"""
|
||||||
|
The base class for all filters.
|
||||||
|
You can use these filters if you have the latest Lavalink version
|
||||||
|
installed. If you do not have the latest Lavalink version,
|
||||||
|
these filters will not work.
|
||||||
|
|
||||||
|
You must specify a tag for each filter you put on.
|
||||||
|
This is necessary for the removal of filters.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.payload = None
|
||||||
|
self.tag: str = None
|
||||||
|
self.preload: bool = False
|
||||||
|
|
||||||
|
def set_preload(self) -> bool:
|
||||||
|
"""Internal method to set whether or not the filter was preloaded."""
|
||||||
|
self.preload = True
|
||||||
|
return self.preload
|
||||||
|
|
||||||
|
|
||||||
|
class Equalizer(Filter):
|
||||||
|
"""
|
||||||
|
Filter which represents a 15 band equalizer.
|
||||||
|
You can adjust the dynamic of the sound using this filter.
|
||||||
|
i.e: Applying a bass boost filter to emphasize the bass in a song.
|
||||||
|
The format for the levels is: List[Tuple[int, float]]
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *, tag: str, levels: list):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.eq = self._factory(levels)
|
||||||
|
self.raw = levels
|
||||||
|
|
||||||
|
self.payload = {"equalizer": self.eq}
|
||||||
|
self.tag = tag
|
||||||
|
|
||||||
|
def _factory(self, levels: list):
|
||||||
|
_dict = collections.defaultdict(int)
|
||||||
|
|
||||||
|
_dict.update(levels)
|
||||||
|
_dict = [{"band": i, "gain": _dict[i]} for i in range(15)]
|
||||||
|
|
||||||
|
return _dict
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Pomice.EqualizerFilter tag={self.tag} eq={self.eq} raw={self.raw}>"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def flat(cls):
|
||||||
|
"""Equalizer preset which represents a flat EQ board,
|
||||||
|
with all levels set to their default values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
levels = [
|
||||||
|
(0, 0.0), (1, 0.0), (2, 0.0), (3, 0.0), (4, 0.0),
|
||||||
|
(5, 0.0), (6, 0.0), (7, 0.0), (8, 0.0), (9, 0.0),
|
||||||
|
(10, 0.0), (11, 0.0), (12, 0.0), (13, 0.0), (14, 0.0)
|
||||||
|
]
|
||||||
|
return cls(tag="flat", levels=levels)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def boost(cls):
|
||||||
|
"""Equalizer preset which boosts the sound of a track,
|
||||||
|
making it sound fun and energetic by increasing the bass
|
||||||
|
and the highs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
levels = [
|
||||||
|
(0, -0.075), (1, 0.125), (2, 0.125), (3, 0.1), (4, 0.1),
|
||||||
|
(5, .05), (6, 0.075), (7, 0.0), (8, 0.0), (9, 0.0),
|
||||||
|
(10, 0.0), (11, 0.0), (12, 0.125), (13, 0.15), (14, 0.05)
|
||||||
|
]
|
||||||
|
return cls(tag="boost", levels=levels)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def metal(cls):
|
||||||
|
"""Equalizer preset which increases the mids of a track,
|
||||||
|
preferably one of the metal genre, to make it sound
|
||||||
|
more full and concert-like.
|
||||||
|
"""
|
||||||
|
|
||||||
|
levels = [
|
||||||
|
(0, 0.0), (1, 0.1), (2, 0.1), (3, 0.15), (4, 0.13),
|
||||||
|
(5, 0.1), (6, 0.0), (7, 0.125), (8, 0.175), (9, 0.175),
|
||||||
|
(10, 0.125), (11, 0.125), (12, 0.1), (13, 0.075), (14, 0.0)
|
||||||
|
]
|
||||||
|
|
||||||
|
return cls(tag="metal", levels=levels)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def piano(cls):
|
||||||
|
"""Equalizer preset which increases the mids and highs
|
||||||
|
of a track, preferably a piano based one, to make it
|
||||||
|
stand out.
|
||||||
|
"""
|
||||||
|
|
||||||
|
levels = [
|
||||||
|
(0, -0.25), (1, -0.25), (2, -0.125), (3, 0.0),
|
||||||
|
(4, 0.25), (5, 0.25), (6, 0.0), (7, -0.25), (8, -0.25),
|
||||||
|
(9, 0.0), (10, 0.0), (11, 0.5), (12, 0.25), (13, -0.025)
|
||||||
|
]
|
||||||
|
return cls(tag="piano", levels=levels)
|
||||||
|
|
||||||
|
|
||||||
|
class Timescale(Filter):
|
||||||
|
"""Filter which changes the speed and pitch of a track.
|
||||||
|
You can make some very nice effects with this filter,
|
||||||
|
i.e: a vaporwave-esque filter which slows the track down
|
||||||
|
a certain amount to produce said effect.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
tag: str,
|
||||||
|
speed: float = 1.0,
|
||||||
|
pitch: float = 1.0,
|
||||||
|
rate: float = 1.0
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
if speed < 0:
|
||||||
|
raise FilterInvalidArgument("Timescale speed must be more than 0.")
|
||||||
|
if pitch < 0:
|
||||||
|
raise FilterInvalidArgument("Timescale pitch must be more than 0.")
|
||||||
|
if rate < 0:
|
||||||
|
raise FilterInvalidArgument("Timescale rate must be more than 0.")
|
||||||
|
|
||||||
|
self.speed = speed
|
||||||
|
self.pitch = pitch
|
||||||
|
self.rate = rate
|
||||||
|
self.tag = tag
|
||||||
|
|
||||||
|
self.payload = {"timescale": {"speed": self.speed,
|
||||||
|
"pitch": self.pitch,
|
||||||
|
"rate": self.rate}}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def vaporwave(cls):
|
||||||
|
"""Timescale preset which slows down the currently playing track,
|
||||||
|
giving it the effect of a half-speed record/casette playing.
|
||||||
|
|
||||||
|
This preset will assign the tag 'vaporwave'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return cls(tag="vaporwave", speed=0.8, pitch=0.8)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def nightcore(cls):
|
||||||
|
"""Timescale preset which speeds up the currently playing track,
|
||||||
|
which matches up to nightcore, a genre of sped-up music
|
||||||
|
|
||||||
|
This preset will assign the tag 'nightcore'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return cls(tag="nightcore", speed=1.25, pitch=1.3)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Pomice.TimescaleFilter tag={self.tag} speed={self.speed} pitch={self.pitch} rate={self.rate}>"
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
*,
|
||||||
|
tag: str,
|
||||||
|
level: float = 1.0,
|
||||||
|
mono_level: float = 1.0,
|
||||||
|
filter_band: float = 220.0,
|
||||||
|
filter_width: float = 100.0
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.level = level
|
||||||
|
self.mono_level = mono_level
|
||||||
|
self.filter_band = filter_band
|
||||||
|
self.filter_width = filter_width
|
||||||
|
self.tag = tag
|
||||||
|
|
||||||
|
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 tag={self.tag} level={self.level} mono_level={self.mono_level} "
|
||||||
|
f"filter_band={self.filter_band} filter_width={self.filter_width}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
*,
|
||||||
|
tag: str,
|
||||||
|
frequency: float = 2.0,
|
||||||
|
depth: float = 0.5
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
if frequency < 0:
|
||||||
|
raise FilterInvalidArgument(
|
||||||
|
"Tremolo frequency must be more than 0.")
|
||||||
|
if depth < 0 or depth > 1:
|
||||||
|
raise FilterInvalidArgument(
|
||||||
|
"Tremolo depth must be between 0 and 1.")
|
||||||
|
|
||||||
|
self.frequency = frequency
|
||||||
|
self.depth = depth
|
||||||
|
self.tag = tag
|
||||||
|
|
||||||
|
self.payload = {"tremolo": {"frequency": self.frequency,
|
||||||
|
"depth": self.depth}}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Pomice.TremoloFilter tag={self.tag} frequency={self.frequency} depth={self.depth}>"
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
*,
|
||||||
|
tag: str,
|
||||||
|
frequency: float = 2.0,
|
||||||
|
depth: float = 0.5
|
||||||
|
):
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
if frequency < 0 or frequency > 14:
|
||||||
|
raise FilterInvalidArgument(
|
||||||
|
"Vibrato frequency must be between 0 and 14.")
|
||||||
|
if depth < 0 or depth > 1:
|
||||||
|
raise FilterInvalidArgument(
|
||||||
|
"Vibrato depth must be between 0 and 1.")
|
||||||
|
|
||||||
|
self.frequency = frequency
|
||||||
|
self.depth = depth
|
||||||
|
self.tag = tag
|
||||||
|
|
||||||
|
self.payload = {"vibrato": {"frequency": self.frequency,
|
||||||
|
"depth": self.depth}}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Pomice.VibratoFilter tag={self.tag} frequency={self.frequency} depth={self.depth}>"
|
||||||
|
|
||||||
|
|
||||||
|
class Rotation(Filter):
|
||||||
|
"""Filter which produces a stereo-like panning effect, which sounds like
|
||||||
|
the audio is being rotated around the listener's head
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *, tag: str, rotation_hertz: float = 5):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.rotation_hertz = rotation_hertz
|
||||||
|
self.tag = tag
|
||||||
|
self.payload = {"rotation": {"rotationHz": self.rotation_hertz}}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Pomice.RotationFilter tag={self.tag} rotation_hertz={self.rotation_hertz}>"
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelMix(Filter):
|
||||||
|
"""Filter which manually adjusts the panning of the audio, which can make
|
||||||
|
for some cool effects when done correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
tag: str,
|
||||||
|
left_to_left: float = 1,
|
||||||
|
right_to_right: float = 1,
|
||||||
|
left_to_right: float = 0,
|
||||||
|
right_to_left: float = 0
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
if 0 > left_to_left > 1:
|
||||||
|
raise ValueError(
|
||||||
|
"'left_to_left' value must be more than or equal to 0 or less than or equal to 1.")
|
||||||
|
if 0 > right_to_right > 1:
|
||||||
|
raise ValueError(
|
||||||
|
"'right_to_right' value must be more than or equal to 0 or less than or equal to 1.")
|
||||||
|
if 0 > left_to_right > 1:
|
||||||
|
raise ValueError(
|
||||||
|
"'left_to_right' value must be more than or equal to 0 or less than or equal to 1.")
|
||||||
|
if 0 > right_to_left > 1:
|
||||||
|
raise ValueError(
|
||||||
|
"'right_to_left' value must be more than or equal to 0 or less than or equal to 1.")
|
||||||
|
|
||||||
|
self.left_to_left = left_to_left
|
||||||
|
self.left_to_right = left_to_right
|
||||||
|
self.right_to_left = right_to_left
|
||||||
|
self.right_to_right = right_to_right
|
||||||
|
self.tag = tag
|
||||||
|
|
||||||
|
self.payload = {"channelMix": {"leftToLeft": self.left_to_left,
|
||||||
|
"leftToRight": self.left_to_right,
|
||||||
|
"rightToLeft": self.right_to_left,
|
||||||
|
"rightToRight": self.right_to_right}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Pomice.ChannelMix tag={self.tag} left_to_left={self.left_to_left} left_to_right={self.left_to_right} "
|
||||||
|
f"right_to_left={self.right_to_left} right_to_right={self.right_to_right}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Distortion(Filter):
|
||||||
|
"""Filter which generates a distortion effect. Useful for certain filter implementations where
|
||||||
|
distortion is needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
tag: str,
|
||||||
|
sin_offset: float = 0,
|
||||||
|
sin_scale: float = 1,
|
||||||
|
cos_offset: float = 0,
|
||||||
|
cos_scale: float = 1,
|
||||||
|
tan_offset: float = 0,
|
||||||
|
tan_scale: float = 1,
|
||||||
|
offset: float = 0,
|
||||||
|
scale: float = 1
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.sin_offset = sin_offset
|
||||||
|
self.sin_scale = sin_scale
|
||||||
|
self.cos_offset = cos_offset
|
||||||
|
self.cos_scale = cos_scale
|
||||||
|
self.tan_offset = tan_offset
|
||||||
|
self.tan_scale = tan_scale
|
||||||
|
self.offset = offset
|
||||||
|
self.scale = scale
|
||||||
|
self.tag = tag
|
||||||
|
|
||||||
|
self.payload = {"distortion": {
|
||||||
|
"sinOffset": self.sin_offset,
|
||||||
|
"sinScale": self.sin_scale,
|
||||||
|
"cosOffset": self.cos_offset,
|
||||||
|
"cosScale": self.cos_scale,
|
||||||
|
"tanOffset": self.tan_offset,
|
||||||
|
"tanScale": self.tan_scale,
|
||||||
|
"offset": self.offset,
|
||||||
|
"scale": self.scale
|
||||||
|
}}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Pomice.Distortion tag={self.tag} sin_offset={self.sin_offset} sin_scale={self.sin_scale}> "
|
||||||
|
f"cos_offset={self.cos_offset} cos_scale={self.cos_scale} tan_offset={self.tan_offset} "
|
||||||
|
f"tan_scale={self.tan_scale} offset={self.offset} scale={self.scale}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LowPass(Filter):
|
||||||
|
"""Filter which supresses higher frequencies and allows lower frequencies to pass.
|
||||||
|
You can also do this with the Equalizer filter, but this is an easier way to do it.
|
||||||
|
"""
|
||||||
|
def __init__(self, *, tag: str, smoothing: float = 20):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.smoothing = smoothing
|
||||||
|
self.tag = tag
|
||||||
|
self.payload = {"lowPass": {"smoothing": self.smoothing}}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Pomice.LowPass tag={self.tag} smoothing={self.smoothing}>"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
from discord import Member, User
|
||||||
|
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from .enums import SearchType, TrackType, PlaylistType
|
||||||
|
from .filters import Filter
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
spotify,
|
||||||
|
applemusic
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Track:
|
||||||
|
"""The base track object. Returns critical track information needed for parsing by Lavalink.
|
||||||
|
You can also pass in commands.Context to get a discord.py Context object in your track.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
track_id: str,
|
||||||
|
info: dict,
|
||||||
|
ctx: Optional[commands.Context] = None,
|
||||||
|
track_type: TrackType,
|
||||||
|
search_type: SearchType = SearchType.ytsearch,
|
||||||
|
filters: Optional[List[Filter]] = None,
|
||||||
|
timestamp: Optional[float] = None,
|
||||||
|
requester: Optional[Union[Member, User]] = None,
|
||||||
|
):
|
||||||
|
self.track_id = track_id
|
||||||
|
self.info = info
|
||||||
|
self.track_type: TrackType = track_type
|
||||||
|
self.filters: Optional[List[Filter]] = filters
|
||||||
|
self.timestamp: Optional[float] = timestamp
|
||||||
|
|
||||||
|
if self.track_type == TrackType.SPOTIFY or self.track_type == TrackType.APPLE_MUSIC:
|
||||||
|
self.original: Optional[Track] = None
|
||||||
|
else:
|
||||||
|
self.original = self
|
||||||
|
self._search_type = search_type
|
||||||
|
|
||||||
|
self.playlist: Playlist = None
|
||||||
|
|
||||||
|
self.title = info.get("title")
|
||||||
|
self.author = info.get("author")
|
||||||
|
self.uri = info.get("uri")
|
||||||
|
self.identifier = info.get("identifier")
|
||||||
|
self.isrc = info.get("isrc")
|
||||||
|
|
||||||
|
if self.uri:
|
||||||
|
if info.get("thumbnail"):
|
||||||
|
self.thumbnail = info.get("thumbnail")
|
||||||
|
elif self.track_type == TrackType.SOUNDCLOUD:
|
||||||
|
# ok so theres no feasible way of getting a Soundcloud image URL
|
||||||
|
# so we're just gonna leave it blank for brevity
|
||||||
|
self.thumbnail = None
|
||||||
|
else:
|
||||||
|
self.thumbnail = f"https://img.youtube.com/vi/{self.identifier}/mqdefault.jpg"
|
||||||
|
|
||||||
|
self.length = info.get("length")
|
||||||
|
self.ctx = ctx
|
||||||
|
if requester:
|
||||||
|
self.requester = requester
|
||||||
|
else:
|
||||||
|
self.requester = self.ctx.author if ctx else None
|
||||||
|
self.is_stream = info.get("isStream")
|
||||||
|
self.is_seekable = info.get("isSeekable")
|
||||||
|
self.position = info.get("position")
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, Track):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.ctx and other.ctx:
|
||||||
|
return other.track_id == self.track_id and other.ctx.message.id == self.ctx.message.id
|
||||||
|
|
||||||
|
return other.track_id == self.track_id
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""The base playlist object.
|
||||||
|
Returns critical playlist information needed for parsing by Lavalink.
|
||||||
|
You can also pass in commands.Context to get a discord.py Context object in your tracks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
playlist_info: dict,
|
||||||
|
tracks: list,
|
||||||
|
playlist_type: PlaylistType,
|
||||||
|
thumbnail: Optional[str] = None,
|
||||||
|
uri: Optional[str] = None
|
||||||
|
):
|
||||||
|
self.playlist_info = playlist_info
|
||||||
|
self.tracks: List[Track] = tracks
|
||||||
|
self.name = playlist_info.get("name")
|
||||||
|
self.playlist_type = playlist_type
|
||||||
|
|
||||||
|
self._thumbnail = thumbnail
|
||||||
|
self._uri = uri
|
||||||
|
|
||||||
|
for track in self.tracks:
|
||||||
|
track.playlist = self
|
||||||
|
|
||||||
|
if (index := playlist_info.get("selectedTrack")) == -1:
|
||||||
|
self.selected_track = None
|
||||||
|
else:
|
||||||
|
self.selected_track = self.tracks[index]
|
||||||
|
|
||||||
|
self.track_count = len(self.tracks)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Pomice.playlist name={self.name!r} track_count={len(self.tracks)}>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uri(self) -> Optional[str]:
|
||||||
|
"""Returns either an Apple Music/Spotify URL/URI, or None if its neither of those."""
|
||||||
|
return self._uri
|
||||||
|
|
||||||
|
@property
|
||||||
|
def thumbnail(self) -> Optional[str]:
|
||||||
|
"""Returns either an Apple Music/Spotify album/playlist thumbnail, or None if its neither of those."""
|
||||||
|
return self._thumbnail
|
||||||
|
|
@ -0,0 +1,520 @@
|
||||||
|
import time
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Union
|
||||||
|
)
|
||||||
|
|
||||||
|
from discord import (
|
||||||
|
Client,
|
||||||
|
Guild,
|
||||||
|
VoiceChannel,
|
||||||
|
VoiceProtocol
|
||||||
|
)
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from . import events
|
||||||
|
from .enums import SearchType, PlatformRecommendation
|
||||||
|
from .events import PomiceEvent, TrackEndEvent, TrackStartEvent
|
||||||
|
from .exceptions import FilterInvalidArgument, FilterTagAlreadyInUse, FilterTagInvalid, TrackInvalidPosition, TrackLoadError
|
||||||
|
from .filters import Filter
|
||||||
|
from .objects import Track, Playlist
|
||||||
|
from .pool import Node, NodePool
|
||||||
|
|
||||||
|
class Filters:
|
||||||
|
"""Helper class for filters"""
|
||||||
|
def __init__(self):
|
||||||
|
self._filters: List[Filter] = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_preload(self):
|
||||||
|
"""Property which checks if any applied filters were preloaded"""
|
||||||
|
return any(f for f in self._filters if f.preload == True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_global(self):
|
||||||
|
"""Property which checks if any applied filters are global"""
|
||||||
|
return any(f for f in self._filters if f.preload == False)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def empty(self):
|
||||||
|
"""Property which checks if the filter list is empty"""
|
||||||
|
return len(self._filters) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def add_filter(self, *, filter: Filter):
|
||||||
|
"""Adds a filter to the list of filters applied"""
|
||||||
|
if any(f for f in self._filters if f.tag == filter.tag):
|
||||||
|
raise FilterTagAlreadyInUse(
|
||||||
|
"A filter with that tag is already in use."
|
||||||
|
)
|
||||||
|
self._filters.append(filter)
|
||||||
|
|
||||||
|
def remove_filter(self, *, filter_tag: str):
|
||||||
|
"""Removes a filter from the list of filters applied using its filter tag"""
|
||||||
|
if not any(f for f in self._filters if f.tag == filter_tag):
|
||||||
|
raise FilterTagInvalid(
|
||||||
|
"A filter with that tag was not found."
|
||||||
|
)
|
||||||
|
|
||||||
|
for index, filter in enumerate(self._filters):
|
||||||
|
if filter.tag == filter_tag:
|
||||||
|
del self._filters[index]
|
||||||
|
|
||||||
|
def has_filter(self, *, filter_tag: str):
|
||||||
|
"""Checks if a filter exists in the list of filters using its filter tag"""
|
||||||
|
return any(f for f in self._filters if f.tag == filter_tag)
|
||||||
|
|
||||||
|
def reset_filters(self):
|
||||||
|
"""Removes all filters from the list"""
|
||||||
|
self._filters = []
|
||||||
|
|
||||||
|
def get_preload_filters(self):
|
||||||
|
"""Get all preloaded filters"""
|
||||||
|
return [f for f in self._filters if f.preload == True]
|
||||||
|
|
||||||
|
def get_all_payloads(self):
|
||||||
|
"""Returns a formatted dict of all the filter payloads"""
|
||||||
|
payload = {}
|
||||||
|
for filter in self._filters:
|
||||||
|
payload.update(filter.payload)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def get_filters(self):
|
||||||
|
"""Returns the current list of applied filters"""
|
||||||
|
return self._filters
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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 __call__(self, client: Client, channel: VoiceChannel):
|
||||||
|
self.client: Client = client
|
||||||
|
self.channel: VoiceChannel = channel
|
||||||
|
self._guild: Guild = channel.guild
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
client: Optional[Client] = None,
|
||||||
|
channel: Optional[VoiceChannel] = None,
|
||||||
|
*,
|
||||||
|
node: Node = None
|
||||||
|
):
|
||||||
|
self.client = client
|
||||||
|
self._bot: Union[Client, commands.Bot] = client
|
||||||
|
self.channel = channel
|
||||||
|
self._guild = channel.guild if channel else None
|
||||||
|
|
||||||
|
self._node = node if node else NodePool.get_node()
|
||||||
|
self._current: Track = None
|
||||||
|
self._filters: Filters = Filters()
|
||||||
|
self._volume = 100
|
||||||
|
self._paused = False
|
||||||
|
self._is_connected = False
|
||||||
|
|
||||||
|
self._position = 0
|
||||||
|
self._last_position = 0
|
||||||
|
self._last_update = 0
|
||||||
|
self._ending_track: Optional[Track] = None
|
||||||
|
|
||||||
|
self._voice_state = {}
|
||||||
|
|
||||||
|
self._player_endpoint_uri = f'sessions/{self._node._session_id}/players'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f"<Pomice.player bot={self.bot} guildId={self.guild.id} "
|
||||||
|
f"is_connected={self.is_connected} is_playing={self.is_playing}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def position(self) -> float:
|
||||||
|
"""Property which returns the player's position in a track in milliseconds"""
|
||||||
|
current = self._current.original
|
||||||
|
|
||||||
|
if not self.is_playing or not self._current:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if self.is_paused:
|
||||||
|
return min(self._last_position, current.length)
|
||||||
|
|
||||||
|
difference = (time.time() * 1000) - self._last_update
|
||||||
|
position = self._last_position + difference
|
||||||
|
|
||||||
|
if position > current.length:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return min(position, 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 is_connected(self) -> bool:
|
||||||
|
"""Property which returns whether or not the player is connected"""
|
||||||
|
return self._is_connected
|
||||||
|
|
||||||
|
@property
|
||||||
|
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"""
|
||||||
|
return self._node
|
||||||
|
|
||||||
|
@property
|
||||||
|
def guild(self) -> Guild:
|
||||||
|
"""Property which returns the guild associated with the player"""
|
||||||
|
return self._guild
|
||||||
|
|
||||||
|
@property
|
||||||
|
def volume(self) -> int:
|
||||||
|
"""Property which returns the players current volume"""
|
||||||
|
return self._volume
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filters(self) -> Filters:
|
||||||
|
"""Property which returns the helper class for interacting with filters"""
|
||||||
|
return self._filters
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bot(self) -> Union[Client, commands.Bot]:
|
||||||
|
"""Property which returns the bot associated with this player instance"""
|
||||||
|
return self._bot
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_dead(self) -> bool:
|
||||||
|
"""Returns a bool representing whether the player is dead or not.
|
||||||
|
A player is considered dead if it has been destroyed and removed from stored players.
|
||||||
|
"""
|
||||||
|
return self.guild.id not in self._node._players
|
||||||
|
|
||||||
|
async def _update_state(self, data: dict):
|
||||||
|
state: dict = data.get("state")
|
||||||
|
self._last_update = time.time() * 1000
|
||||||
|
self._is_connected = state.get("connected")
|
||||||
|
self._last_position = state.get("position")
|
||||||
|
|
||||||
|
async def _dispatch_voice_update(self, voice_data: Dict[str, Any]):
|
||||||
|
if {"sessionId", "event"} != self._voice_state.keys():
|
||||||
|
return
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"token": voice_data['event']['token'],
|
||||||
|
"endpoint": voice_data['event']['endpoint'],
|
||||||
|
"sessionId": voice_data['sessionId'],
|
||||||
|
}
|
||||||
|
|
||||||
|
await self._node.send(
|
||||||
|
method="PATCH",
|
||||||
|
path=self._player_endpoint_uri,
|
||||||
|
guild_id=self._guild.id,
|
||||||
|
data={"voice": data}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_voice_server_update(self, data: dict):
|
||||||
|
self._voice_state.update({"event": data})
|
||||||
|
await self._dispatch_voice_update(self._voice_state)
|
||||||
|
|
||||||
|
async def on_voice_state_update(self, data: dict):
|
||||||
|
self._voice_state.update({"sessionId": data.get("session_id")})
|
||||||
|
|
||||||
|
if not (channel_id := data.get("channel_id")):
|
||||||
|
await self.disconnect()
|
||||||
|
self._voice_state.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.channel = self.guild.get_channel(int(channel_id))
|
||||||
|
|
||||||
|
if not data.get("token"):
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._dispatch_voice_update({**self._voice_state, "event": data})
|
||||||
|
|
||||||
|
async def _dispatch_event(self, data: dict):
|
||||||
|
event_type = data.get("type")
|
||||||
|
event: PomiceEvent = getattr(events, event_type)(data, self)
|
||||||
|
|
||||||
|
if isinstance(event, TrackEndEvent) and event.reason != "REPLACED":
|
||||||
|
self._current = None
|
||||||
|
|
||||||
|
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,
|
||||||
|
filters: Optional[List[Filter]] = None
|
||||||
|
):
|
||||||
|
"""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 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.
|
||||||
|
"""
|
||||||
|
return await self._node.get_tracks(query, ctx=ctx, search_type=search_type, filters=filters)
|
||||||
|
|
||||||
|
async def get_recommendations(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
track: Track,
|
||||||
|
ctx: Optional[commands.Context] = None
|
||||||
|
) -> Union[List[Track], None]:
|
||||||
|
"""
|
||||||
|
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, ctx=ctx)
|
||||||
|
|
||||||
|
async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = False, self_mute: bool = False):
|
||||||
|
await self.guild.change_voice_state(channel=self.channel, self_deaf=self_deaf, self_mute=self_mute)
|
||||||
|
self._node._players[self.guild.id] = self
|
||||||
|
self._is_connected = True
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
"""Stops the currently playing track."""
|
||||||
|
self._current = None
|
||||||
|
await self._node.send(
|
||||||
|
method="PATCH",
|
||||||
|
path=self._player_endpoint_uri,
|
||||||
|
guild_id=self._guild.id,
|
||||||
|
data={'encodedTrack': None}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def disconnect(self, *, force: bool = False):
|
||||||
|
"""Disconnects the player from voice."""
|
||||||
|
try:
|
||||||
|
await self.guild.change_voice_state(channel=None)
|
||||||
|
finally:
|
||||||
|
self.cleanup()
|
||||||
|
self._is_connected = False
|
||||||
|
self.channel = None
|
||||||
|
|
||||||
|
async def destroy(self):
|
||||||
|
"""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)
|
||||||
|
await self._node.send(method="DELETE", path=self._player_endpoint_uri, guild_id=self._guild.id)
|
||||||
|
|
||||||
|
async def play(
|
||||||
|
self,
|
||||||
|
track: Track,
|
||||||
|
*,
|
||||||
|
start: int = 0,
|
||||||
|
end: int = 0,
|
||||||
|
ignore_if_playing: bool = False
|
||||||
|
) -> Track:
|
||||||
|
"""Plays a track. If a Spotify track is passed in, it will be handled accordingly."""
|
||||||
|
|
||||||
|
# Make sure we've never searched the track before
|
||||||
|
if track.original is None:
|
||||||
|
# First lets try using the tracks ISRC, every track has one (hopefully)
|
||||||
|
try:
|
||||||
|
if not track.isrc:
|
||||||
|
# We have to bare raise here because theres no other way to skip this block feasibly
|
||||||
|
raise
|
||||||
|
search: Track = (await self._node.get_tracks(
|
||||||
|
f"{track._search_type}:{track.isrc}", ctx=track.ctx))[0]
|
||||||
|
except Exception:
|
||||||
|
# First method didn't work, lets try just searching it up
|
||||||
|
try:
|
||||||
|
search: Track = (await self._node.get_tracks(
|
||||||
|
f"{track._search_type}:{track.title} - {track.author}", ctx=track.ctx))[0]
|
||||||
|
except:
|
||||||
|
# The song wasn't able to be found, raise error
|
||||||
|
raise TrackLoadError (
|
||||||
|
"No equivalent track was able to be found."
|
||||||
|
)
|
||||||
|
data = {
|
||||||
|
"encodedTrack": search.track_id,
|
||||||
|
"position": str(start),
|
||||||
|
"endTime": str(end)
|
||||||
|
}
|
||||||
|
track.original = search
|
||||||
|
track.track_id = search.track_id
|
||||||
|
# Set track_id for later lavalink searches
|
||||||
|
else:
|
||||||
|
data = {
|
||||||
|
"encodedTrack": track.track_id,
|
||||||
|
"position": str(start),
|
||||||
|
"endTime": str(end)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Lets set the current track before we play it so any
|
||||||
|
# corresponding events can capture it correctly
|
||||||
|
|
||||||
|
self._current = track
|
||||||
|
|
||||||
|
# Remove preloaded filters if last track had any
|
||||||
|
if self.filters.has_preload:
|
||||||
|
for filter in self.filters.get_preload_filters():
|
||||||
|
await self.remove_filter(filter_tag=filter.tag)
|
||||||
|
|
||||||
|
# Global filters take precedence over track filters
|
||||||
|
# So if no global filters are detected, lets apply any
|
||||||
|
# necessary track filters
|
||||||
|
|
||||||
|
# Check if theres no global filters and if the track has any filters
|
||||||
|
# that need to be applied
|
||||||
|
|
||||||
|
if track.filters and not self.filters.has_global:
|
||||||
|
# Now apply all filters
|
||||||
|
for filter in track.filters:
|
||||||
|
await self.add_filter(filter=filter)
|
||||||
|
|
||||||
|
if end > 0:
|
||||||
|
data["endTime"] = str(end)
|
||||||
|
|
||||||
|
await self._node.send(
|
||||||
|
method="PATCH",
|
||||||
|
path=self._player_endpoint_uri,
|
||||||
|
guild_id=self._guild.id,
|
||||||
|
data=data,
|
||||||
|
query=f"noReplace={ignore_if_playing}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._current
|
||||||
|
|
||||||
|
async def seek(self, position: float) -> float:
|
||||||
|
"""Seeks to a position in the currently playing track milliseconds"""
|
||||||
|
if position < 0 or position > self._current.original.length:
|
||||||
|
raise TrackInvalidPosition(
|
||||||
|
"Seek position must be between 0 and the track length"
|
||||||
|
)
|
||||||
|
|
||||||
|
await self._node.send(
|
||||||
|
method="PATCH",
|
||||||
|
path=self._player_endpoint_uri,
|
||||||
|
guild_id=self._guild.id,
|
||||||
|
data={"position": position}
|
||||||
|
)
|
||||||
|
return self._position
|
||||||
|
|
||||||
|
async def set_pause(self, pause: bool) -> bool:
|
||||||
|
"""Sets the pause state of the currently playing track."""
|
||||||
|
await self._node.send(
|
||||||
|
method="PATCH",
|
||||||
|
path=self._player_endpoint_uri,
|
||||||
|
guild_id=self._guild.id,
|
||||||
|
data={"paused": pause}
|
||||||
|
)
|
||||||
|
self._paused = pause
|
||||||
|
return self._paused
|
||||||
|
|
||||||
|
async def set_volume(self, volume: int) -> int:
|
||||||
|
"""Sets the volume of the player as an integer. Lavalink accepts values from 0 to 500."""
|
||||||
|
await self._node.send(
|
||||||
|
method="PATCH",
|
||||||
|
path=self._player_endpoint_uri,
|
||||||
|
guild_id=self._guild.id,
|
||||||
|
data={"volume": volume}
|
||||||
|
)
|
||||||
|
self._volume = volume
|
||||||
|
return self._volume
|
||||||
|
|
||||||
|
async def add_filter(self, filter: Filter, fast_apply: bool = False) -> Filter:
|
||||||
|
"""Adds a filter to the player. Takes a pomice.Filter object.
|
||||||
|
This will only work if you are using a version of Lavalink that supports filters.
|
||||||
|
If you would like for the filter to apply instantly, set the `fast_apply` arg to `True`.
|
||||||
|
|
||||||
|
(You must have a song playing in order for `fast_apply` to work.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._filters.add_filter(filter=filter)
|
||||||
|
payload = self._filters.get_all_payloads()
|
||||||
|
await self._node.send(
|
||||||
|
method="PATCH",
|
||||||
|
path=self._player_endpoint_uri,
|
||||||
|
guild_id=self._guild.id,
|
||||||
|
data={"filters": payload}
|
||||||
|
)
|
||||||
|
if fast_apply:
|
||||||
|
await self.seek(self.position)
|
||||||
|
|
||||||
|
return self._filters
|
||||||
|
|
||||||
|
async def remove_filter(self, filter_tag: str, fast_apply: bool = False) -> Filter:
|
||||||
|
"""Removes a filter from the player. Takes a filter tag.
|
||||||
|
This will only work if you are using a version of Lavalink that supports filters.
|
||||||
|
If you would like for the filter to apply instantly, set the `fast_apply` arg to `True`.
|
||||||
|
|
||||||
|
(You must have a song playing in order for `fast_apply` to work.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._filters.remove_filter(filter_tag=filter_tag)
|
||||||
|
payload = self._filters.get_all_payloads()
|
||||||
|
await self._node.send(
|
||||||
|
method="PATCH",
|
||||||
|
path=self._player_endpoint_uri,
|
||||||
|
guild_id=self._guild.id,
|
||||||
|
data={"filters": payload}
|
||||||
|
)
|
||||||
|
if fast_apply:
|
||||||
|
await self.seek(self.position)
|
||||||
|
|
||||||
|
return self._filters
|
||||||
|
|
||||||
|
async def reset_filters(self, *, fast_apply: bool = False):
|
||||||
|
"""Resets all currently applied filters to their default parameters.
|
||||||
|
You must have filters applied in order for this to work.
|
||||||
|
If you would like the filters to be removed instantly, set the `fast_apply` arg to `True`.
|
||||||
|
|
||||||
|
(You must have a song playing in order for `fast_apply` to work.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self._filters:
|
||||||
|
raise FilterInvalidArgument(
|
||||||
|
"You must have filters applied first in order to use this method."
|
||||||
|
)
|
||||||
|
self._filters.reset_filters()
|
||||||
|
await self._node.send(
|
||||||
|
method="PATCH",
|
||||||
|
path=self._player_endpoint_uri,
|
||||||
|
guild_id=self._guild.id,
|
||||||
|
data={"filters": {}}
|
||||||
|
)
|
||||||
|
|
||||||
|
if fast_apply:
|
||||||
|
await self.seek(self.position)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,726 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
from typing import Dict, List, Optional, TYPE_CHECKING, Union
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from discord import Client
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
__version__,
|
||||||
|
spotify,
|
||||||
|
applemusic
|
||||||
|
)
|
||||||
|
|
||||||
|
from .enums import *
|
||||||
|
from .exceptions import (
|
||||||
|
AppleMusicNotEnabled,
|
||||||
|
InvalidSpotifyClientAuthorization,
|
||||||
|
LavalinkVersionIncompatible,
|
||||||
|
NodeConnectionFailure,
|
||||||
|
NodeCreationError,
|
||||||
|
NodeNotAvailable,
|
||||||
|
NoNodesAvailable,
|
||||||
|
NodeRestException,
|
||||||
|
TrackLoadError
|
||||||
|
)
|
||||||
|
from .filters import Filter
|
||||||
|
from .objects import Playlist, Track
|
||||||
|
from .utils import ExponentialBackoff, NodeStats, Ping
|
||||||
|
from .routeplanner import RoutePlanner
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .player import Player
|
||||||
|
|
||||||
|
|
||||||
|
class Node:
|
||||||
|
"""The base class for a node.
|
||||||
|
This node object represents a Lavalink node.
|
||||||
|
To enable Spotify searching, pass in a proper Spotify Client ID and Spotify Client Secret
|
||||||
|
To enable Apple music, set the "apple_music" parameter to "True"
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
pool,
|
||||||
|
bot: Union[Client, commands.Bot],
|
||||||
|
host: str,
|
||||||
|
port: int,
|
||||||
|
password: str,
|
||||||
|
identifier: str,
|
||||||
|
secure: bool = False,
|
||||||
|
heartbeat: int = 30,
|
||||||
|
session: Optional[aiohttp.ClientSession] = None,
|
||||||
|
spotify_client_id: Optional[str] = None,
|
||||||
|
spotify_client_secret: Optional[str] = None,
|
||||||
|
apple_music: bool = False
|
||||||
|
|
||||||
|
):
|
||||||
|
self._bot = bot
|
||||||
|
self._host = host
|
||||||
|
self._port = port
|
||||||
|
self._pool = pool
|
||||||
|
self._password = password
|
||||||
|
self._identifier = identifier
|
||||||
|
self._heartbeat = heartbeat
|
||||||
|
self._secure = secure
|
||||||
|
|
||||||
|
|
||||||
|
self._websocket_uri = f"{'wss' if self._secure else 'ws'}://{self._host}:{self._port}/v3/websocket"
|
||||||
|
self._rest_uri = f"{'https' if self._secure else 'http'}://{self._host}:{self._port}"
|
||||||
|
|
||||||
|
self._session = session or aiohttp.ClientSession()
|
||||||
|
self._websocket: aiohttp.ClientWebSocketResponse = None
|
||||||
|
self._task: asyncio.Task = None
|
||||||
|
|
||||||
|
self._session_id: str = None
|
||||||
|
self._metadata = None
|
||||||
|
self._available = None
|
||||||
|
self._version: str = None
|
||||||
|
self._route_planner = RoutePlanner(self)
|
||||||
|
|
||||||
|
self._headers = {
|
||||||
|
"Authorization": self._password,
|
||||||
|
"User-Id": str(self._bot.user.id),
|
||||||
|
"Client-Name": f"Pomice/{__version__}"
|
||||||
|
}
|
||||||
|
|
||||||
|
self._players: Dict[int, Player] = {}
|
||||||
|
|
||||||
|
self._spotify_client_id = spotify_client_id
|
||||||
|
self._spotify_client_secret = spotify_client_secret
|
||||||
|
|
||||||
|
self._apple_music_client = None
|
||||||
|
|
||||||
|
if self._spotify_client_id and self._spotify_client_secret:
|
||||||
|
self._spotify_client = spotify.Client(
|
||||||
|
self._spotify_client_id, self._spotify_client_secret
|
||||||
|
)
|
||||||
|
|
||||||
|
if apple_music:
|
||||||
|
self._apple_music_client = applemusic.Client()
|
||||||
|
|
||||||
|
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} "
|
||||||
|
f"player_count={len(self._players)}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
@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
|
||||||
|
def stats(self) -> NodeStats:
|
||||||
|
"""Property which returns the node stats."""
|
||||||
|
return self._stats
|
||||||
|
|
||||||
|
@property
|
||||||
|
def players(self) -> Dict[int, Player]:
|
||||||
|
"""Property which returns a dict containing the guild ID and the player object."""
|
||||||
|
return self._players
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bot(self) -> Union[Client, commands.Bot]:
|
||||||
|
"""Property which returns the discord.py client linked to this node"""
|
||||||
|
return self._bot
|
||||||
|
|
||||||
|
@property
|
||||||
|
def player_count(self) -> int:
|
||||||
|
"""Property which returns how many players are connected to this node"""
|
||||||
|
return len(self.players)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pool(self):
|
||||||
|
"""Property which returns the pool this node is apart of"""
|
||||||
|
return self._pool
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latency(self):
|
||||||
|
"""Property which returns the latency of the node"""
|
||||||
|
return Ping(self._host, port=self._port).get_ping()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ping(self):
|
||||||
|
"""Alias for `Node.latency`, returns the latency of the node"""
|
||||||
|
return self.latency
|
||||||
|
|
||||||
|
|
||||||
|
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.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 _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):
|
||||||
|
op = data.get("op", None)
|
||||||
|
if not op:
|
||||||
|
return
|
||||||
|
|
||||||
|
if op == "stats":
|
||||||
|
self._stats = NodeStats(data)
|
||||||
|
return
|
||||||
|
|
||||||
|
if op == "ready":
|
||||||
|
self._session_id = data.get("sessionId")
|
||||||
|
|
||||||
|
if "guildId" in data:
|
||||||
|
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)
|
||||||
|
|
||||||
|
def _get_type(self, query: str):
|
||||||
|
if match := URLRegex.LAVALINK_SEARCH.match(query):
|
||||||
|
type = match.group("type")
|
||||||
|
if type == "sc":
|
||||||
|
return TrackType.SOUNDCLOUD
|
||||||
|
|
||||||
|
return TrackType.YOUTUBE
|
||||||
|
|
||||||
|
|
||||||
|
elif URLRegex.YOUTUBE_URL.match(query):
|
||||||
|
if URLRegex.YOUTUBE_PLAYLIST_URL.match(query):
|
||||||
|
return PlaylistType.YOUTUBE
|
||||||
|
|
||||||
|
return TrackType.YOUTUBE
|
||||||
|
|
||||||
|
elif URLRegex.SOUNDCLOUD_URL.match(query):
|
||||||
|
if URLRegex.SOUNDCLOUD_TRACK_IN_SET_URL.match(query):
|
||||||
|
return TrackType.SOUNDCLOUD
|
||||||
|
if URLRegex.SOUNDCLOUD_PLAYLIST_URL.match(query):
|
||||||
|
return PlaylistType.SOUNDCLOUD
|
||||||
|
|
||||||
|
return TrackType.SOUNDCLOUD
|
||||||
|
|
||||||
|
else:
|
||||||
|
return TrackType.HTTP
|
||||||
|
|
||||||
|
async def send(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
include_version: bool = True,
|
||||||
|
guild_id: Optional[Union[int, str]] = None,
|
||||||
|
query: Optional[str] = None,
|
||||||
|
data: Optional[Union[dict, str]] = None
|
||||||
|
):
|
||||||
|
if not self._available:
|
||||||
|
raise NodeNotAvailable(
|
||||||
|
f"The node '{self._identifier}' is unavailable."
|
||||||
|
)
|
||||||
|
|
||||||
|
uri: str = f'{self._rest_uri}/' \
|
||||||
|
f'{f"v{self._version}/" if include_version else ""}' \
|
||||||
|
f'{path}' \
|
||||||
|
f'{f"/{guild_id}" if guild_id else ""}' \
|
||||||
|
f'{f"?{query}" if query else ""}'
|
||||||
|
|
||||||
|
async with self._session.request(method=method, url=uri, headers=self._headers, json=data or {}) as resp:
|
||||||
|
if resp.status >= 300:
|
||||||
|
raise NodeRestException(f'Error fetching from Lavalink REST api: {resp.status} {resp.reason}')
|
||||||
|
|
||||||
|
if method == "DELETE" or resp.status == 204:
|
||||||
|
return await resp.json(content_type=None)
|
||||||
|
|
||||||
|
if resp.content_type == "text/plain":
|
||||||
|
return await resp.text()
|
||||||
|
|
||||||
|
return await resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
self._websocket = await self._session.ws_connect(
|
||||||
|
self._websocket_uri, headers=self._headers, heartbeat=self._heartbeat
|
||||||
|
)
|
||||||
|
self._task = self._bot.loop.create_task(self._listen())
|
||||||
|
self._available = True
|
||||||
|
version = await self.send(method="GET", path="version", include_version=False)
|
||||||
|
version = version.replace(".", "")
|
||||||
|
if int(version) < 370:
|
||||||
|
raise LavalinkVersionIncompatible(
|
||||||
|
"The Lavalink version you're using is incompatible."
|
||||||
|
"Lavalink version 3.7.0 or above is required to use this library."
|
||||||
|
)
|
||||||
|
|
||||||
|
self._version = version[:1]
|
||||||
|
return self
|
||||||
|
|
||||||
|
except aiohttp.ClientConnectorError:
|
||||||
|
raise NodeConnectionFailure(
|
||||||
|
f"The connection to node '{self._identifier}' failed."
|
||||||
|
)
|
||||||
|
except aiohttp.WSServerHandshakeError:
|
||||||
|
raise NodeConnectionFailure(
|
||||||
|
f"The password for node '{self._identifier}' is invalid."
|
||||||
|
)
|
||||||
|
except aiohttp.InvalidURL:
|
||||||
|
raise NodeConnectionFailure(
|
||||||
|
f"The URI for node '{self._identifier}' is invalid."
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
await self._websocket.close()
|
||||||
|
del self._pool.nodes[self._identifier]
|
||||||
|
self.available = False
|
||||||
|
self._task.cancel()
|
||||||
|
|
||||||
|
async def build_track(
|
||||||
|
self,
|
||||||
|
identifier: str,
|
||||||
|
ctx: Optional[commands.Context] = None
|
||||||
|
) -> Track:
|
||||||
|
"""
|
||||||
|
Builds a track using a valid track identifier
|
||||||
|
|
||||||
|
You can also pass in a discord.py Context object to get a
|
||||||
|
Context object on the track it builds.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
data: dict = await self.send(method="GET", path="decodetrack", query=f"encodedTrack={identifier}")
|
||||||
|
return Track(track_id=identifier, ctx=ctx, info=data)
|
||||||
|
|
||||||
|
async def get_tracks(
|
||||||
|
self,
|
||||||
|
query: str,
|
||||||
|
*,
|
||||||
|
ctx: Optional[commands.Context] = None,
|
||||||
|
search_type: SearchType = SearchType.ytsearch,
|
||||||
|
filters: Optional[List[Filter]] = 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 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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
timestamp = None
|
||||||
|
|
||||||
|
if not URLRegex.BASE_URL.match(query) and not re.match(r"(?:ytm?|sc)search:.", query):
|
||||||
|
query = f"{search_type}:{query}"
|
||||||
|
|
||||||
|
if filters:
|
||||||
|
for filter in filters:
|
||||||
|
filter.set_preload()
|
||||||
|
|
||||||
|
if URLRegex.AM_URL.match(query):
|
||||||
|
if not self._apple_music_client:
|
||||||
|
raise AppleMusicNotEnabled(
|
||||||
|
"You must have Apple Music functionality enabled in order to play Apple Music tracks."
|
||||||
|
"Please set apple_music to True in your Node class."
|
||||||
|
)
|
||||||
|
|
||||||
|
apple_music_results = await self._apple_music_client.search(query=query)
|
||||||
|
if isinstance(apple_music_results, applemusic.Song):
|
||||||
|
return [
|
||||||
|
Track(
|
||||||
|
track_id=apple_music_results.id,
|
||||||
|
ctx=ctx,
|
||||||
|
track_type=TrackType.APPLE_MUSIC,
|
||||||
|
search_type=search_type,
|
||||||
|
filters=filters,
|
||||||
|
info={
|
||||||
|
"title": apple_music_results.name,
|
||||||
|
"author": apple_music_results.artists,
|
||||||
|
"length": apple_music_results.length,
|
||||||
|
"identifier": apple_music_results.id,
|
||||||
|
"uri": apple_music_results.url,
|
||||||
|
"isStream": False,
|
||||||
|
"isSeekable": True,
|
||||||
|
"position": 0,
|
||||||
|
"thumbnail": apple_music_results.image,
|
||||||
|
"isrc": apple_music_results.isrc
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
tracks = [
|
||||||
|
Track(
|
||||||
|
track_id=track.id,
|
||||||
|
ctx=ctx,
|
||||||
|
track_type=TrackType.APPLE_MUSIC,
|
||||||
|
search_type=search_type,
|
||||||
|
filters=filters,
|
||||||
|
info={
|
||||||
|
"title": track.name,
|
||||||
|
"author": track.artists,
|
||||||
|
"length": track.length,
|
||||||
|
"identifier": track.id,
|
||||||
|
"uri": track.url,
|
||||||
|
"isStream": False,
|
||||||
|
"isSeekable": True,
|
||||||
|
"position": 0,
|
||||||
|
"thumbnail": track.image,
|
||||||
|
"isrc": track.isrc
|
||||||
|
}
|
||||||
|
) for track in apple_music_results.tracks
|
||||||
|
]
|
||||||
|
|
||||||
|
return Playlist(
|
||||||
|
playlist_info={"name": apple_music_results.name, "selectedTrack": 0},
|
||||||
|
tracks=tracks,
|
||||||
|
playlist_type=PlaylistType.APPLE_MUSIC,
|
||||||
|
thumbnail=apple_music_results.image,
|
||||||
|
uri=apple_music_results.url
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
elif URLRegex.SPOTIFY_URL.match(query):
|
||||||
|
if not self._spotify_client_id and not self._spotify_client_secret:
|
||||||
|
raise 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/"
|
||||||
|
)
|
||||||
|
|
||||||
|
spotify_results = await self._spotify_client.search(query=query)
|
||||||
|
|
||||||
|
if isinstance(spotify_results, spotify.Track):
|
||||||
|
return [
|
||||||
|
Track(
|
||||||
|
track_id=spotify_results.id,
|
||||||
|
ctx=ctx,
|
||||||
|
track_type=TrackType.SPOTIFY,
|
||||||
|
search_type=search_type,
|
||||||
|
filters=filters,
|
||||||
|
info={
|
||||||
|
"title": spotify_results.name,
|
||||||
|
"author": spotify_results.artists,
|
||||||
|
"length": spotify_results.length,
|
||||||
|
"identifier": spotify_results.id,
|
||||||
|
"uri": spotify_results.uri,
|
||||||
|
"isStream": False,
|
||||||
|
"isSeekable": True,
|
||||||
|
"position": 0,
|
||||||
|
"thumbnail": spotify_results.image,
|
||||||
|
"isrc": spotify_results.isrc
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
tracks = [
|
||||||
|
Track(
|
||||||
|
track_id=track.id,
|
||||||
|
ctx=ctx,
|
||||||
|
track_type=TrackType.SPOTIFY,
|
||||||
|
search_type=search_type,
|
||||||
|
filters=filters,
|
||||||
|
info={
|
||||||
|
"title": track.name,
|
||||||
|
"author": track.artists,
|
||||||
|
"length": track.length,
|
||||||
|
"identifier": track.id,
|
||||||
|
"uri": track.uri,
|
||||||
|
"isStream": False,
|
||||||
|
"isSeekable": True,
|
||||||
|
"position": 0,
|
||||||
|
"thumbnail": track.image,
|
||||||
|
"isrc": track.isrc
|
||||||
|
}
|
||||||
|
) for track in spotify_results.tracks
|
||||||
|
]
|
||||||
|
|
||||||
|
return Playlist(
|
||||||
|
playlist_info={"name": spotify_results.name, "selectedTrack": 0},
|
||||||
|
tracks=tracks,
|
||||||
|
playlist_type=PlaylistType.SPOTIFY,
|
||||||
|
thumbnail=spotify_results.image,
|
||||||
|
uri=spotify_results.uri
|
||||||
|
)
|
||||||
|
|
||||||
|
elif discord_url := URLRegex.DISCORD_MP3_URL.match(query):
|
||||||
|
|
||||||
|
data: dict = await self.send(method="GET", path="loadtracks", query=f"identifier={quote(query)}")
|
||||||
|
|
||||||
|
track: dict = data["tracks"][0]
|
||||||
|
info: dict = track.get("info")
|
||||||
|
|
||||||
|
return [
|
||||||
|
Track(
|
||||||
|
track_id=track["track"],
|
||||||
|
info={
|
||||||
|
"title": discord_url.group("file"),
|
||||||
|
"author": "Unknown",
|
||||||
|
"length": info.get("length"),
|
||||||
|
"uri": info.get("uri"),
|
||||||
|
"position": info.get("position"),
|
||||||
|
"identifier": info.get("identifier")
|
||||||
|
},
|
||||||
|
ctx=ctx,
|
||||||
|
track_type=TrackType.HTTP,
|
||||||
|
filters=filters
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
else:
|
||||||
|
# If YouTube url contains a timestamp, capture it for use later.
|
||||||
|
|
||||||
|
if (match := URLRegex.YOUTUBE_TIMESTAMP.match(query)):
|
||||||
|
timestamp = float(match.group("time"))
|
||||||
|
|
||||||
|
# If query is a video thats part of a playlist, get the video and queue that instead
|
||||||
|
# (I can't tell you how much i've wanted to implement this in here)
|
||||||
|
|
||||||
|
if (match := URLRegex.YOUTUBE_VID_IN_PLAYLIST.match(query)):
|
||||||
|
query = match.group("video")
|
||||||
|
|
||||||
|
data: dict = await self.send(method="GET", path="loadtracks", query=f"identifier={quote(query)}")
|
||||||
|
|
||||||
|
load_type = data.get("loadType")
|
||||||
|
|
||||||
|
query_type = self._get_type(query)
|
||||||
|
|
||||||
|
if not load_type:
|
||||||
|
raise TrackLoadError("There was an error while trying to load this track.")
|
||||||
|
|
||||||
|
elif load_type == "LOAD_FAILED":
|
||||||
|
exception = data["exception"]
|
||||||
|
raise TrackLoadError(f"{exception['message']} [{exception['severity']}]")
|
||||||
|
|
||||||
|
elif load_type == "NO_MATCHES":
|
||||||
|
return None
|
||||||
|
|
||||||
|
elif load_type == "PLAYLIST_LOADED":
|
||||||
|
if query_type == PlaylistType.SOUNDCLOUD:
|
||||||
|
track_type = TrackType.SOUNDCLOUD
|
||||||
|
else:
|
||||||
|
track_type = TrackType.YOUTUBE
|
||||||
|
|
||||||
|
tracks = [
|
||||||
|
Track(track_id=track["track"], info=track["info"], ctx=ctx, track_type=track_type)
|
||||||
|
for track in data["tracks"]
|
||||||
|
]
|
||||||
|
return Playlist(
|
||||||
|
playlist_info=data["playlistInfo"],
|
||||||
|
tracks=tracks,
|
||||||
|
playlist_type=query_type,
|
||||||
|
thumbnail=tracks[0].thumbnail,
|
||||||
|
uri=query
|
||||||
|
)
|
||||||
|
|
||||||
|
elif load_type == "SEARCH_RESULT" or load_type == "TRACK_LOADED":
|
||||||
|
return [
|
||||||
|
Track(
|
||||||
|
track_id=track["track"],
|
||||||
|
info=track["info"],
|
||||||
|
ctx=ctx,
|
||||||
|
track_type=query_type,
|
||||||
|
filters=filters,
|
||||||
|
timestamp=timestamp
|
||||||
|
)
|
||||||
|
for track in data["tracks"]
|
||||||
|
]
|
||||||
|
|
||||||
|
async def get_recommendations(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
track: Track,
|
||||||
|
ctx: Optional[commands.Context] = None
|
||||||
|
) -> Union[List[Track], None]:
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
tracks = [
|
||||||
|
Track(
|
||||||
|
track_id=track.id,
|
||||||
|
ctx=ctx,
|
||||||
|
track_type=TrackType.SPOTIFY,
|
||||||
|
info={
|
||||||
|
"title": track.name,
|
||||||
|
"author": track.artists,
|
||||||
|
"length": track.length,
|
||||||
|
"identifier": track.id,
|
||||||
|
"uri": track.uri,
|
||||||
|
"isStream": False,
|
||||||
|
"isSeekable": True,
|
||||||
|
"position": 0,
|
||||||
|
"thumbnail": track.image,
|
||||||
|
"isrc": track.isrc
|
||||||
|
},
|
||||||
|
requester=self.bot.user
|
||||||
|
) for track in results
|
||||||
|
]
|
||||||
|
|
||||||
|
return tracks
|
||||||
|
elif track.track_type == TrackType.YOUTUBE:
|
||||||
|
tracks = await self.get_tracks(query=f"ytsearch:https://www.youtube.com/watch?v={track.identifier}&list=RD{track.identifier}", ctx=ctx)
|
||||||
|
return tracks
|
||||||
|
else:
|
||||||
|
raise TrackLoadError("The specfied track must be either a YouTube or Spotify track to recieve recommendations.")
|
||||||
|
|
||||||
|
|
||||||
|
class NodePool:
|
||||||
|
"""The base class for the node pool.
|
||||||
|
This holds all the nodes that are to be used by the bot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_nodes: dict = {}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Pomice.NodePool node_count={self.node_count}>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nodes(self) -> Dict[str, Node]:
|
||||||
|
"""Property which returns a dict with the node identifier and the Node object."""
|
||||||
|
return self._nodes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def node_count(self):
|
||||||
|
return len(self._nodes.values())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_best_node(cls, *, algorithm: NodeAlgorithm) -> Node:
|
||||||
|
"""Fetches the best node based on an NodeAlgorithm.
|
||||||
|
This option is preferred if you want to choose the best node
|
||||||
|
from a multi-node setup using either the node's latency
|
||||||
|
or the node's voice region.
|
||||||
|
|
||||||
|
Use NodeAlgorithm.by_ping if you want to get the best node
|
||||||
|
based on the node's latency.
|
||||||
|
|
||||||
|
|
||||||
|
Use NodeAlgorithm.by_players if you want to get the best node
|
||||||
|
based on how players it has. This method will return a node with
|
||||||
|
the least amount of players
|
||||||
|
"""
|
||||||
|
available_nodes = [node for node in cls._nodes.values() if node._available]
|
||||||
|
|
||||||
|
if not available_nodes:
|
||||||
|
raise NoNodesAvailable("There are no nodes available.")
|
||||||
|
|
||||||
|
if algorithm == NodeAlgorithm.by_ping:
|
||||||
|
tested_nodes = {node: node.latency for node in available_nodes}
|
||||||
|
return min(tested_nodes, key=tested_nodes.get)
|
||||||
|
|
||||||
|
elif algorithm == NodeAlgorithm.by_players:
|
||||||
|
tested_nodes = {node: len(node.players.keys()) for node in available_nodes}
|
||||||
|
return min(tested_nodes, key=tested_nodes.get)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_node(cls, *, 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 cls._nodes.items() if node._available
|
||||||
|
}
|
||||||
|
|
||||||
|
if not available_nodes:
|
||||||
|
raise 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(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
bot: Client,
|
||||||
|
host: str,
|
||||||
|
port: str,
|
||||||
|
password: str,
|
||||||
|
identifier: str,
|
||||||
|
secure: bool = False,
|
||||||
|
heartbeat: int = 30,
|
||||||
|
spotify_client_id: Optional[str] = None,
|
||||||
|
spotify_client_secret: Optional[str] = None,
|
||||||
|
session: Optional[aiohttp.ClientSession] = None,
|
||||||
|
apple_music: bool = False
|
||||||
|
|
||||||
|
) -> Node:
|
||||||
|
"""Creates a Node object to be then added into the node pool.
|
||||||
|
For Spotify searching capabilites, pass in valid Spotify API credentials.
|
||||||
|
"""
|
||||||
|
if identifier in cls._nodes.keys():
|
||||||
|
raise NodeCreationError(f"A node with identifier '{identifier}' already exists.")
|
||||||
|
|
||||||
|
node = Node(
|
||||||
|
pool=cls, bot=bot, host=host, port=port, password=password,
|
||||||
|
identifier=identifier, secure=secure, heartbeat=heartbeat,
|
||||||
|
spotify_client_id=spotify_client_id,
|
||||||
|
session=session, spotify_client_secret=spotify_client_secret,
|
||||||
|
apple_music=apple_music
|
||||||
|
)
|
||||||
|
|
||||||
|
await node.connect()
|
||||||
|
cls._nodes[node._identifier] = node
|
||||||
|
return node
|
||||||
|
|
@ -0,0 +1,344 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import random
|
||||||
|
from copy import copy
|
||||||
|
from typing import (
|
||||||
|
Iterable,
|
||||||
|
Iterator,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .objects import Track
|
||||||
|
from .enums import LoopMode
|
||||||
|
from .exceptions import QueueEmpty, QueueException, QueueFull
|
||||||
|
|
||||||
|
|
||||||
|
class Queue(Iterable[Track]):
|
||||||
|
"""Queue for Pomice. This queue takes pomice.Track as an input and includes looping and shuffling."""
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
max_size: Optional[int] = None,
|
||||||
|
*,
|
||||||
|
overflow: bool = True,
|
||||||
|
):
|
||||||
|
self.max_size: Optional[int] = max_size
|
||||||
|
self._queue: List[Track] = [] # type: ignore
|
||||||
|
self._overflow: bool = overflow
|
||||||
|
self._loop_mode: Optional[LoopMode] = None
|
||||||
|
self._current_item: Optional[Track] = None
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""String showing all Track objects appearing as a list."""
|
||||||
|
return str(list(f"'{t}'" for t in self))
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""Official representation with max_size and member count."""
|
||||||
|
return (
|
||||||
|
f"<{self.__class__.__name__} max_size={self.max_size} members={self.count}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
"""Treats the queue as a bool, with it evaluating True when it contains members."""
|
||||||
|
return bool(self.count)
|
||||||
|
|
||||||
|
def __call__(self, item: Track) -> None:
|
||||||
|
"""Allows the queue instance to be called directly in order to add a member."""
|
||||||
|
self.put(item)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
"""Return the number of members in the queue."""
|
||||||
|
return self.count
|
||||||
|
|
||||||
|
def __getitem__(self, index: int) -> Track:
|
||||||
|
"""Returns a member at the given position.
|
||||||
|
Does not remove item from queue.
|
||||||
|
"""
|
||||||
|
if not isinstance(index, int):
|
||||||
|
raise ValueError("'int' type required.'")
|
||||||
|
|
||||||
|
return self._queue[index]
|
||||||
|
|
||||||
|
def __setitem__(self, index: int, item: Track):
|
||||||
|
"""Inserts an item at given position."""
|
||||||
|
if not isinstance(index, int):
|
||||||
|
raise ValueError("'int' type required.'")
|
||||||
|
|
||||||
|
self.put_at_index(index, item)
|
||||||
|
|
||||||
|
def __delitem__(self, index: int) -> None:
|
||||||
|
"""Delete item at given position."""
|
||||||
|
self._queue.__delitem__(index)
|
||||||
|
|
||||||
|
def __iter__(self) -> Iterator[Track]:
|
||||||
|
"""Iterate over members in the queue.
|
||||||
|
Does not remove items when iterating.
|
||||||
|
"""
|
||||||
|
return self._queue.__iter__()
|
||||||
|
|
||||||
|
def __reversed__(self) -> Iterator[Track]:
|
||||||
|
"""Iterate over members in reverse order."""
|
||||||
|
return self._queue.__reversed__()
|
||||||
|
|
||||||
|
def __contains__(self, item: Track) -> bool:
|
||||||
|
"""Check if an item is a member of the queue."""
|
||||||
|
return item in self._queue
|
||||||
|
|
||||||
|
def __add__(self, other: Iterable[Track]) -> Queue:
|
||||||
|
"""Return a new queue containing all members.
|
||||||
|
The new queue will have the same max_size as the original.
|
||||||
|
"""
|
||||||
|
if not isinstance(other, Iterable):
|
||||||
|
raise TypeError(f"Adding with the '{type(other)}' type is not supported.")
|
||||||
|
|
||||||
|
new_queue = self.copy()
|
||||||
|
new_queue.extend(other)
|
||||||
|
return new_queue
|
||||||
|
|
||||||
|
def __iadd__(self, other: Union[Iterable[Track], Track]) -> Queue:
|
||||||
|
"""Add items to queue."""
|
||||||
|
if isinstance(other, Track):
|
||||||
|
self.put(other)
|
||||||
|
return self
|
||||||
|
|
||||||
|
if isinstance(other, Iterable):
|
||||||
|
self.extend(other)
|
||||||
|
return self
|
||||||
|
|
||||||
|
raise TypeError(f"Adding '{type(other)}' type to the queue is not supported.")
|
||||||
|
|
||||||
|
def _get(self) -> Track:
|
||||||
|
return self._queue.pop(0)
|
||||||
|
|
||||||
|
def _drop(self) -> Track:
|
||||||
|
return self._queue.pop()
|
||||||
|
|
||||||
|
def _index(self, item: Track) -> int:
|
||||||
|
return self._queue.index(item)
|
||||||
|
|
||||||
|
|
||||||
|
def _put(self, item: Track) -> None:
|
||||||
|
self._queue.append(item)
|
||||||
|
|
||||||
|
def _insert(self, index: int, item: Track) -> None:
|
||||||
|
self._queue.insert(index, item)
|
||||||
|
|
||||||
|
def _remove(self, item: Track) -> None:
|
||||||
|
self._queue.remove(item)
|
||||||
|
|
||||||
|
def _get_random_float(self) -> float:
|
||||||
|
return random.random()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _check_track(item: Track) -> Track:
|
||||||
|
if not isinstance(item, Track):
|
||||||
|
raise TypeError("Only pomice.Track objects are supported.")
|
||||||
|
|
||||||
|
return item
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _check_track_container(cls, iterable: Iterable) -> List[Track]:
|
||||||
|
iterable = list(iterable)
|
||||||
|
for item in iterable:
|
||||||
|
cls._check_track(item)
|
||||||
|
|
||||||
|
return iterable
|
||||||
|
|
||||||
|
@property
|
||||||
|
def count(self) -> int:
|
||||||
|
"""Returns queue member count."""
|
||||||
|
return len(self._queue)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_empty(self) -> bool:
|
||||||
|
"""Returns True if queue has no members."""
|
||||||
|
return not bool(self.count)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_full(self) -> bool:
|
||||||
|
"""Returns True if queue item count has reached max_size."""
|
||||||
|
return False if self.max_size is None else self.count >= self.max_size
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_looping(self) -> bool:
|
||||||
|
"""Returns True if the queue is looping either a track or the queue"""
|
||||||
|
return bool(self._loop_mode)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def loop_mode(self) -> LoopMode:
|
||||||
|
"""Returns the LoopMode enum set in the queue object"""
|
||||||
|
return self._loop_mode
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size(self) -> int:
|
||||||
|
"""Returns the amount of items in the queue"""
|
||||||
|
return len(self._queue)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_queue(self) -> List:
|
||||||
|
"""Returns the queue as a List"""
|
||||||
|
return self._queue
|
||||||
|
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
"""Return next immediately available item in queue if any.
|
||||||
|
Raises QueueEmpty if no items in queue.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._loop_mode == LoopMode.TRACK:
|
||||||
|
return self._current_item
|
||||||
|
|
||||||
|
if self.is_empty:
|
||||||
|
raise QueueEmpty("No items in the queue.")
|
||||||
|
|
||||||
|
if self._loop_mode == LoopMode.QUEUE:
|
||||||
|
|
||||||
|
# recurse if the item isnt in the queue
|
||||||
|
if self._current_item not in self._queue:
|
||||||
|
self.get()
|
||||||
|
|
||||||
|
# set current item to first track in queue if not set already
|
||||||
|
if not self._current_item:
|
||||||
|
self._current_item = self._queue[0]
|
||||||
|
item = self._current_item
|
||||||
|
|
||||||
|
# we reached the end of the queue, go back to first track
|
||||||
|
if self._index(self._current_item) == len(self._queue) - 1:
|
||||||
|
item = self._queue[0]
|
||||||
|
|
||||||
|
# we are in the middle of the queue, go the next item
|
||||||
|
else:
|
||||||
|
index = self._index(self._current_item) + 1
|
||||||
|
item = self._queue[index]
|
||||||
|
else:
|
||||||
|
item = self._get()
|
||||||
|
|
||||||
|
self._current_item = item
|
||||||
|
return item
|
||||||
|
|
||||||
|
def pop(self) -> Track:
|
||||||
|
"""Return item from the right end side of the queue.
|
||||||
|
Raises QueueEmpty if no items in queue.
|
||||||
|
"""
|
||||||
|
if self.is_empty:
|
||||||
|
raise QueueEmpty("No items in the queue.")
|
||||||
|
|
||||||
|
return self._queue.pop()
|
||||||
|
|
||||||
|
def remove(self, item: Track) -> None:
|
||||||
|
"""
|
||||||
|
Removes a item within the queue.
|
||||||
|
Raises ValueError if item is not in queue.
|
||||||
|
"""
|
||||||
|
return self._remove(self._check_track(item))
|
||||||
|
|
||||||
|
|
||||||
|
def find_position(self, item: Track) -> int:
|
||||||
|
"""Find the position a given item within the queue.
|
||||||
|
Raises ValueError if item is not in queue.
|
||||||
|
"""
|
||||||
|
return self._index(self._check_track(item))
|
||||||
|
|
||||||
|
def put(self, item: Track) -> None:
|
||||||
|
"""Put the given item into the back of the queue."""
|
||||||
|
if self.is_full:
|
||||||
|
if not self._overflow:
|
||||||
|
raise QueueFull(f"Queue max_size of {self.max_size} has been reached.")
|
||||||
|
|
||||||
|
self._drop()
|
||||||
|
|
||||||
|
return self._put(self._check_track(item))
|
||||||
|
|
||||||
|
def put_at_index(self, index: int, item: Track) -> None:
|
||||||
|
"""Put the given item into the queue at the specified index."""
|
||||||
|
if self.is_full:
|
||||||
|
if not self._overflow:
|
||||||
|
raise QueueFull(f"Queue max_size of {self.max_size} has been reached.")
|
||||||
|
|
||||||
|
self._drop()
|
||||||
|
|
||||||
|
return self._insert(index, self._check_track(item))
|
||||||
|
|
||||||
|
def put_at_front(self, item: Track) -> None:
|
||||||
|
"""Put the given item into the front of the queue."""
|
||||||
|
return self.put_at_index(0, item)
|
||||||
|
|
||||||
|
def extend(self, iterable: Iterable[Track], *, atomic: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
Add the members of the given iterable to the end of the queue.
|
||||||
|
If atomic is set to True, no tracks will be added upon any exceptions.
|
||||||
|
If atomic is set to False, as many tracks will be added as possible.
|
||||||
|
When overflow is enabled for the queue, `atomic=True` won't prevent dropped items.
|
||||||
|
"""
|
||||||
|
if atomic:
|
||||||
|
iterable = self._check_track_container(iterable)
|
||||||
|
|
||||||
|
if not self._overflow and self.max_size is not None:
|
||||||
|
new_len = len(iterable)
|
||||||
|
|
||||||
|
if (new_len + self.count) > self.max_size:
|
||||||
|
raise QueueFull(
|
||||||
|
f"Queue has {self.count}/{self.max_size} items, "
|
||||||
|
f"cannot add {new_len} more."
|
||||||
|
)
|
||||||
|
|
||||||
|
for item in iterable:
|
||||||
|
self.put(item)
|
||||||
|
|
||||||
|
def copy(self) -> Queue:
|
||||||
|
"""Create a copy of the current queue including it's members."""
|
||||||
|
new_queue = self.__class__(max_size=self.max_size)
|
||||||
|
new_queue._queue = copy(self._queue)
|
||||||
|
|
||||||
|
return new_queue
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Remove all items from the queue."""
|
||||||
|
self._queue.clear()
|
||||||
|
|
||||||
|
def set_loop_mode(self, mode: LoopMode):
|
||||||
|
"""
|
||||||
|
Sets the loop mode of the queue.
|
||||||
|
Takes the LoopMode enum as an argument.
|
||||||
|
"""
|
||||||
|
self._loop_mode = mode
|
||||||
|
if self._loop_mode == LoopMode.QUEUE:
|
||||||
|
try:
|
||||||
|
index = self._index(self._current_item)
|
||||||
|
except ValueError:
|
||||||
|
index = 0
|
||||||
|
if self._current_item not in self._queue:
|
||||||
|
self._queue.insert(index, self._current_item)
|
||||||
|
self._current_item = self._queue[index]
|
||||||
|
|
||||||
|
|
||||||
|
def disable_loop(self):
|
||||||
|
"""
|
||||||
|
Disables loop mode if set.
|
||||||
|
Raises QueueException if loop mode is already None.
|
||||||
|
"""
|
||||||
|
if not self._loop_mode:
|
||||||
|
raise QueueException("Queue loop is already disabled.")
|
||||||
|
|
||||||
|
if self._loop_mode == LoopMode.QUEUE:
|
||||||
|
index = self.find_position(self._current_item) + 1
|
||||||
|
self._queue = self._queue[index:]
|
||||||
|
|
||||||
|
self._loop_mode = None
|
||||||
|
|
||||||
|
|
||||||
|
def shuffle(self):
|
||||||
|
"""Shuffles the queue."""
|
||||||
|
return random.shuffle(self._queue)
|
||||||
|
|
||||||
|
def clear_track_filters(self):
|
||||||
|
"""Clears all filters applied to tracks"""
|
||||||
|
for track in self._queue:
|
||||||
|
track.filters = None
|
||||||
|
|
||||||
|
def jump(self, item: Track):
|
||||||
|
"""Returns a new queue with the specified track at the beginning."""
|
||||||
|
index = self.find_position(item)
|
||||||
|
new_queue = self._queue[index:self.size]
|
||||||
|
self._queue = new_queue
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .pool import Node
|
||||||
|
|
||||||
|
from .utils import RouteStats
|
||||||
|
|
||||||
|
class RoutePlanner:
|
||||||
|
"""
|
||||||
|
The base route planner class for Pomice.
|
||||||
|
Handles all requests made to the route planner API for Lavalink.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, node: Node) -> None:
|
||||||
|
self.node = node
|
||||||
|
self.session = node._session
|
||||||
|
|
||||||
|
async def get_status(self):
|
||||||
|
"""Gets the status of the route planner API."""
|
||||||
|
data: dict = await self.node.send(method="GET", path="routeplanner/status")
|
||||||
|
return RouteStats(data)
|
||||||
|
|
||||||
|
|
||||||
|
async def free_address(self, ip: str):
|
||||||
|
"""Frees an address using the route planner API"""
|
||||||
|
await self.node.send(method="POST", path="routeplanner/free/address", data={"address": ip})
|
||||||
|
|
||||||
|
async def free_all_addresses(self):
|
||||||
|
"""Frees all available addresses using the route planner api"""
|
||||||
|
await self.node.send(method="POST", path="routeplanner/free/address/all")
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
"""Spotify module for Pomice, made possible by cloudwithax 2023"""
|
||||||
|
|
||||||
|
from .exceptions import *
|
||||||
|
from .objects import *
|
||||||
|
from .client import Client
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from base64 import b64encode
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import orjson as json
|
||||||
|
|
||||||
|
|
||||||
|
from .exceptions import InvalidSpotifyURL, SpotifyRequestException
|
||||||
|
from .objects import *
|
||||||
|
|
||||||
|
GRANT_URL = "https://accounts.spotify.com/api/token"
|
||||||
|
REQUEST_URL = "https://api.spotify.com/v1/{type}s/{id}"
|
||||||
|
SPOTIFY_URL_REGEX = re.compile(
|
||||||
|
r"https?://open.spotify.com/(?P<type>album|playlist|track|artist)/(?P<id>[a-zA-Z0-9]+)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
"""The base client for the Spotify module of Pomice.
|
||||||
|
This class will do all the heavy lifting of getting all the metadata
|
||||||
|
for any Spotify URL you throw at it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, client_id: str, client_secret: str) -> None:
|
||||||
|
self._client_id = client_id
|
||||||
|
self._client_secret = client_secret
|
||||||
|
|
||||||
|
self.session = aiohttp.ClientSession()
|
||||||
|
|
||||||
|
self._bearer_token: str = None
|
||||||
|
self._expiry = 0
|
||||||
|
self._auth_token = b64encode(f"{self._client_id}:{self._client_secret}".encode())
|
||||||
|
self._grant_headers = {"Authorization": f"Basic {self._auth_token.decode()}"}
|
||||||
|
self._bearer_headers = None
|
||||||
|
|
||||||
|
async def _fetch_bearer_token(self) -> None:
|
||||||
|
_data = {"grant_type": "client_credentials"}
|
||||||
|
|
||||||
|
async with self.session.post(GRANT_URL, data=_data, headers=self._grant_headers) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
raise SpotifyRequestException(
|
||||||
|
f"Error fetching bearer token: {resp.status} {resp.reason}"
|
||||||
|
)
|
||||||
|
|
||||||
|
data: dict = await resp.json(loads=json.loads)
|
||||||
|
|
||||||
|
self._bearer_token = data["access_token"]
|
||||||
|
self._expiry = time.time() + (int(data["expires_in"]) - 10)
|
||||||
|
self._bearer_headers = {"Authorization": f"Bearer {self._bearer_token}"}
|
||||||
|
|
||||||
|
async def search(self, *, query: str):
|
||||||
|
if not self._bearer_token or time.time() >= self._expiry:
|
||||||
|
await self._fetch_bearer_token()
|
||||||
|
|
||||||
|
result = SPOTIFY_URL_REGEX.match(query)
|
||||||
|
spotify_type = result.group("type")
|
||||||
|
spotify_id = result.group("id")
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
raise InvalidSpotifyURL("The Spotify link provided is not valid.")
|
||||||
|
|
||||||
|
request_url = REQUEST_URL.format(type=spotify_type, id=spotify_id)
|
||||||
|
|
||||||
|
async with self.session.get(request_url, headers=self._bearer_headers) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
raise SpotifyRequestException(
|
||||||
|
f"Error while fetching results: {resp.status} {resp.reason}"
|
||||||
|
)
|
||||||
|
|
||||||
|
data: dict = await resp.json(loads=json.loads)
|
||||||
|
|
||||||
|
if spotify_type == "track":
|
||||||
|
return Track(data)
|
||||||
|
elif spotify_type == "album":
|
||||||
|
return Album(data)
|
||||||
|
elif spotify_type == "artist":
|
||||||
|
async with self.session.get(f"{request_url}/top-tracks?market=US", headers=self._bearer_headers) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
raise SpotifyRequestException(
|
||||||
|
f"Error while fetching results: {resp.status} {resp.reason}"
|
||||||
|
)
|
||||||
|
|
||||||
|
track_data: dict = await resp.json(loads=json.loads)
|
||||||
|
tracks = track_data['tracks']
|
||||||
|
return Artist(data, tracks)
|
||||||
|
else:
|
||||||
|
tracks = [
|
||||||
|
Track(track["track"])
|
||||||
|
for track in data["tracks"]["items"] if track["track"] is not None
|
||||||
|
]
|
||||||
|
|
||||||
|
if not len(tracks):
|
||||||
|
raise SpotifyRequestException("This playlist is empty and therefore cannot be queued.")
|
||||||
|
|
||||||
|
next_page_url = data["tracks"]["next"]
|
||||||
|
|
||||||
|
while next_page_url is not None:
|
||||||
|
async with self.session.get(next_page_url, headers=self._bearer_headers) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
raise SpotifyRequestException(
|
||||||
|
f"Error while fetching results: {resp.status} {resp.reason}"
|
||||||
|
)
|
||||||
|
|
||||||
|
next_data: dict = await resp.json(loads=json.loads)
|
||||||
|
|
||||||
|
tracks += [
|
||||||
|
Track(track["track"])
|
||||||
|
for track in next_data["items"] if track["track"] is not None
|
||||||
|
]
|
||||||
|
next_page_url = next_data["next"]
|
||||||
|
|
||||||
|
return Playlist(data, tracks)
|
||||||
|
|
||||||
|
async def get_recommendations(self, *, query: str):
|
||||||
|
if not self._bearer_token or time.time() >= self._expiry:
|
||||||
|
await self._fetch_bearer_token()
|
||||||
|
|
||||||
|
result = SPOTIFY_URL_REGEX.match(query)
|
||||||
|
spotify_type = result.group("type")
|
||||||
|
spotify_id = result.group("id")
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
raise InvalidSpotifyURL("The Spotify link provided is not valid.")
|
||||||
|
|
||||||
|
if not spotify_type == "track":
|
||||||
|
raise InvalidSpotifyURL("The provided query is not a Spotify track.")
|
||||||
|
|
||||||
|
request_url = REQUEST_URL.format(type="recommendation", id=f"?seed_tracks={spotify_id}")
|
||||||
|
|
||||||
|
async with self.session.get(request_url, headers=self._bearer_headers) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
raise SpotifyRequestException(
|
||||||
|
f"Error while fetching results: {resp.status} {resp.reason}"
|
||||||
|
)
|
||||||
|
|
||||||
|
data: dict = await resp.json(loads=json.loads)
|
||||||
|
|
||||||
|
tracks = [Track(track) for track in data["tracks"]]
|
||||||
|
|
||||||
|
return tracks
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
class SpotifyRequestException(Exception):
|
||||||
|
"""An error occurred when making a request to the Spotify API"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidSpotifyURL(Exception):
|
||||||
|
"""An invalid Spotify URL was passed"""
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
class Track:
|
||||||
|
"""The base class for a Spotify Track"""
|
||||||
|
|
||||||
|
def __init__(self, data: dict, image = None) -> None:
|
||||||
|
self.name: str = data["name"]
|
||||||
|
self.artists: str = ", ".join(artist["name"] for artist in data["artists"])
|
||||||
|
self.length: float = data["duration_ms"]
|
||||||
|
self.id: str = data["id"]
|
||||||
|
|
||||||
|
if data.get("external_ids"):
|
||||||
|
self.isrc: str = data["external_ids"]["isrc"]
|
||||||
|
else:
|
||||||
|
self.isrc = None
|
||||||
|
|
||||||
|
if data.get("album") and data["album"].get("images"):
|
||||||
|
self.image: str = data["album"]["images"][0]["url"]
|
||||||
|
else:
|
||||||
|
self.image: str = image
|
||||||
|
|
||||||
|
if data["is_local"]:
|
||||||
|
self.uri = None
|
||||||
|
else:
|
||||||
|
self.uri: str = data["external_urls"]["spotify"]
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Pomice.spotify.Track name={self.name} artists={self.artists} "
|
||||||
|
f"length={self.length} id={self.id} isrc={self.isrc}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Playlist:
|
||||||
|
"""The base class for a Spotify playlist"""
|
||||||
|
|
||||||
|
def __init__(self, data: dict, tracks: List[Track]) -> None:
|
||||||
|
self.name: str = data["name"]
|
||||||
|
self.tracks = tracks
|
||||||
|
self.owner: str = data["owner"]["display_name"]
|
||||||
|
self.total_tracks: int = data["tracks"]["total"]
|
||||||
|
self.id: str = data["id"]
|
||||||
|
if data.get("images") and len(data["images"]):
|
||||||
|
self.image: str = data["images"][0]["url"]
|
||||||
|
else:
|
||||||
|
self.image = self.tracks[0].image
|
||||||
|
self.uri = data["external_urls"]["spotify"]
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Pomice.spotify.Playlist name={self.name} owner={self.owner} id={self.id} "
|
||||||
|
f"total_tracks={self.total_tracks} tracks={self.tracks}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Album:
|
||||||
|
"""The base class for a Spotify album"""
|
||||||
|
|
||||||
|
def __init__(self, data: dict) -> None:
|
||||||
|
self.name: str = data["name"]
|
||||||
|
self.artists: str = ", ".join(artist["name"] for artist in data["artists"])
|
||||||
|
self.image: str = data["images"][0]["url"]
|
||||||
|
self.tracks = [Track(track, image=self.image) for track in data["tracks"]["items"]]
|
||||||
|
self.total_tracks: int = data["total_tracks"]
|
||||||
|
self.id: str = data["id"]
|
||||||
|
self.uri: str = data["external_urls"]["spotify"]
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Pomice.spotify.Album name={self.name} artists={self.artists} id={self.id} "
|
||||||
|
f"total_tracks={self.total_tracks} tracks={self.tracks}>"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Artist:
|
||||||
|
"""The base class for a Spotify artist"""
|
||||||
|
|
||||||
|
def __init__(self, data: dict, tracks: dict) -> None:
|
||||||
|
self.name: str = f"Top tracks for {data['name']}" # Setting that because its only playing top tracks
|
||||||
|
self.genres: str = ", ".join(genre for genre in data["genres"])
|
||||||
|
self.followers: int = data["followers"]["total"]
|
||||||
|
self.image: str = data["images"][0]["url"]
|
||||||
|
self.tracks = [Track(track, image=self.image) for track in tracks]
|
||||||
|
self.id: str = data["id"]
|
||||||
|
self.uri: str = data["external_urls"]["spotify"]
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<Pomice.spotify.Artist name={self.name} id={self.id} "
|
||||||
|
f"tracks={self.tracks}>"
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from .enums import RouteStrategy, RouteIPType
|
||||||
|
from timeit import default_timer as timer
|
||||||
|
from itertools import zip_longest
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ExponentialBackoff",
|
||||||
|
"NodeStats"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ExponentialBackoff:
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""The base class for the node stats object.
|
||||||
|
Gives critical information on the node, which is updated every minute.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, data: dict) -> None:
|
||||||
|
|
||||||
|
memory: dict = data.get("memory")
|
||||||
|
self.used = memory.get("used")
|
||||||
|
self.free = memory.get("free")
|
||||||
|
self.reservable = memory.get("reservable")
|
||||||
|
self.allocated = memory.get("allocated")
|
||||||
|
|
||||||
|
cpu: dict = 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!r} playing_active={self.players_active!r}>"
|
||||||
|
|
||||||
|
class FailingIPBlock:
|
||||||
|
"""
|
||||||
|
The base class for the failing IP block object from the route planner stats.
|
||||||
|
Gives critical information about any failing addresses on the block
|
||||||
|
and the time they failed.
|
||||||
|
"""
|
||||||
|
def __init__(self, data: dict) -> None:
|
||||||
|
self.address = data.get("address")
|
||||||
|
self.failing_time = datetime.fromtimestamp(float(data.get("failingTimestamp")))
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Pomice.FailingIPBlock address={self.address} failing_time={self.failing_time}>"
|
||||||
|
|
||||||
|
|
||||||
|
class RouteStats:
|
||||||
|
"""
|
||||||
|
The base class for the route planner stats object.
|
||||||
|
Gives critical information about the route planner strategy on the node.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, data: dict) -> None:
|
||||||
|
self.strategy = RouteStrategy(data.get("class"))
|
||||||
|
|
||||||
|
details: dict = data.get("details")
|
||||||
|
|
||||||
|
ip_block: dict = details.get("ipBlock")
|
||||||
|
self.ip_block_type = RouteIPType(ip_block.get("type"))
|
||||||
|
self.ip_block_size = ip_block.get("size")
|
||||||
|
self.failing_addresses = [FailingIPBlock(data) for data in details.get("failingAddresses")]
|
||||||
|
|
||||||
|
self.block_index = details.get("blockIndex")
|
||||||
|
self.address_index = details.get("currentAddressIndex")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Pomice.RouteStats route_strategy={self.strategy!r} failing_addresses={len(self.failing_addresses)}>"
|
||||||
|
|
||||||
|
|
||||||
|
class Ping:
|
||||||
|
# Thanks to https://github.com/zhengxiaowai/tcping for the nice ping impl
|
||||||
|
def __init__(self, host, port, timeout=5):
|
||||||
|
self.timer = self.Timer()
|
||||||
|
|
||||||
|
self._successed = 0
|
||||||
|
self._failed = 0
|
||||||
|
self._conn_time = None
|
||||||
|
self._host = host
|
||||||
|
self._port = port
|
||||||
|
self._timeout = timeout
|
||||||
|
|
||||||
|
class Socket(object):
|
||||||
|
def __init__(self, family, type_, timeout):
|
||||||
|
s = socket.socket(family, type_)
|
||||||
|
s.settimeout(timeout)
|
||||||
|
self._s = s
|
||||||
|
|
||||||
|
def connect(self, host, port):
|
||||||
|
self._s.connect((host, int(port)))
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
self._s.shutdown(socket.SHUT_RD)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self._s.close()
|
||||||
|
|
||||||
|
|
||||||
|
class Timer(object):
|
||||||
|
def __init__(self):
|
||||||
|
self._start = 0
|
||||||
|
self._stop = 0
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self._start = timer()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._stop = timer()
|
||||||
|
|
||||||
|
def cost(self, funcs, args):
|
||||||
|
self.start()
|
||||||
|
for func, arg in zip_longest(funcs, args):
|
||||||
|
if arg:
|
||||||
|
func(*arg)
|
||||||
|
else:
|
||||||
|
func()
|
||||||
|
|
||||||
|
self.stop()
|
||||||
|
return self._stop - self._start
|
||||||
|
|
||||||
|
def _create_socket(self, family, type_):
|
||||||
|
return self.Socket(family, type_, self._timeout)
|
||||||
|
|
||||||
|
def get_ping(self):
|
||||||
|
s = self._create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
|
||||||
|
cost_time = self.timer.cost(
|
||||||
|
(s.connect, s.shutdown),
|
||||||
|
((self._host, self._port), None))
|
||||||
|
s_runtime = 1000 * (cost_time)
|
||||||
|
|
||||||
|
return s_runtime
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,4 +0,0 @@
|
||||||
# Sphinx build info version 1
|
|
||||||
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
|
||||||
config: 0b739fcaef66703cf1454de38e053a4a
|
|
||||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
|
||||||
|
|
@ -1,900 +0,0 @@
|
||||||
/*
|
|
||||||
* basic.css
|
|
||||||
* ~~~~~~~~~
|
|
||||||
*
|
|
||||||
* Sphinx stylesheet -- basic theme.
|
|
||||||
*
|
|
||||||
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
|
|
||||||
* :license: BSD, see LICENSE for details.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* -- main layout ----------------------------------------------------------- */
|
|
||||||
|
|
||||||
div.clearer {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.section::after {
|
|
||||||
display: block;
|
|
||||||
content: '';
|
|
||||||
clear: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- relbar ---------------------------------------------------------------- */
|
|
||||||
|
|
||||||
div.related {
|
|
||||||
width: 100%;
|
|
||||||
font-size: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.related h3 {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.related ul {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 0 0 10px;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.related li {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.related li.right {
|
|
||||||
float: right;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- sidebar --------------------------------------------------------------- */
|
|
||||||
|
|
||||||
div.sphinxsidebarwrapper {
|
|
||||||
padding: 10px 5px 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar {
|
|
||||||
float: left;
|
|
||||||
width: 230px;
|
|
||||||
margin-left: -100%;
|
|
||||||
font-size: 90%;
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow-wrap : break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar ul {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar ul ul,
|
|
||||||
div.sphinxsidebar ul.want-points {
|
|
||||||
margin-left: 20px;
|
|
||||||
list-style: square;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar ul ul {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar form {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar input {
|
|
||||||
border: 1px solid #98dbcc;
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar #searchbox form.search {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar #searchbox input[type="text"] {
|
|
||||||
float: left;
|
|
||||||
width: 80%;
|
|
||||||
padding: 0.25em;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar #searchbox input[type="submit"] {
|
|
||||||
float: left;
|
|
||||||
width: 20%;
|
|
||||||
border-left: none;
|
|
||||||
padding: 0.25em;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
img {
|
|
||||||
border: 0;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- search page ----------------------------------------------------------- */
|
|
||||||
|
|
||||||
ul.search {
|
|
||||||
margin: 10px 0 0 20px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.search li {
|
|
||||||
padding: 5px 0 5px 20px;
|
|
||||||
background-image: url(file.png);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: 0 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.search li a {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.search li p.context {
|
|
||||||
color: #888;
|
|
||||||
margin: 2px 0 0 30px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.keywordmatches li.goodmatch a {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- index page ------------------------------------------------------------ */
|
|
||||||
|
|
||||||
table.contentstable {
|
|
||||||
width: 90%;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.contentstable p.biglink {
|
|
||||||
line-height: 150%;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.biglink {
|
|
||||||
font-size: 1.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.linkdescr {
|
|
||||||
font-style: italic;
|
|
||||||
padding-top: 5px;
|
|
||||||
font-size: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- general index --------------------------------------------------------- */
|
|
||||||
|
|
||||||
table.indextable {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.indextable td {
|
|
||||||
text-align: left;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.indextable ul {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.indextable > tbody > tr > td > ul {
|
|
||||||
padding-left: 0em;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.indextable tr.pcap {
|
|
||||||
height: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.indextable tr.cap {
|
|
||||||
margin-top: 10px;
|
|
||||||
background-color: #f2f2f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.toggler {
|
|
||||||
margin-right: 3px;
|
|
||||||
margin-top: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.modindex-jumpbox {
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
margin: 1em 0 1em 0;
|
|
||||||
padding: 0.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.genindex-jumpbox {
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
margin: 1em 0 1em 0;
|
|
||||||
padding: 0.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- domain module index --------------------------------------------------- */
|
|
||||||
|
|
||||||
table.modindextable td {
|
|
||||||
padding: 2px;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- general body styles --------------------------------------------------- */
|
|
||||||
|
|
||||||
div.body {
|
|
||||||
min-width: 360px;
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body p, div.body dd, div.body li, div.body blockquote {
|
|
||||||
-moz-hyphens: auto;
|
|
||||||
-ms-hyphens: auto;
|
|
||||||
-webkit-hyphens: auto;
|
|
||||||
hyphens: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
a.headerlink {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1:hover > a.headerlink,
|
|
||||||
h2:hover > a.headerlink,
|
|
||||||
h3:hover > a.headerlink,
|
|
||||||
h4:hover > a.headerlink,
|
|
||||||
h5:hover > a.headerlink,
|
|
||||||
h6:hover > a.headerlink,
|
|
||||||
dt:hover > a.headerlink,
|
|
||||||
caption:hover > a.headerlink,
|
|
||||||
p.caption:hover > a.headerlink,
|
|
||||||
div.code-block-caption:hover > a.headerlink {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body p.caption {
|
|
||||||
text-align: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body td {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.first {
|
|
||||||
margin-top: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.rubric {
|
|
||||||
margin-top: 30px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.align-left, figure.align-left, .figure.align-left, object.align-left {
|
|
||||||
clear: left;
|
|
||||||
float: left;
|
|
||||||
margin-right: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.align-right, figure.align-right, .figure.align-right, object.align-right {
|
|
||||||
clear: right;
|
|
||||||
float: right;
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.align-center, figure.align-center, .figure.align-center, object.align-center {
|
|
||||||
display: block;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.align-default, figure.align-default, .figure.align-default {
|
|
||||||
display: block;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-left {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-default {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-right {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- sidebars -------------------------------------------------------------- */
|
|
||||||
|
|
||||||
div.sidebar,
|
|
||||||
aside.sidebar {
|
|
||||||
margin: 0 0 0.5em 1em;
|
|
||||||
border: 1px solid #ddb;
|
|
||||||
padding: 7px;
|
|
||||||
background-color: #ffe;
|
|
||||||
width: 40%;
|
|
||||||
float: right;
|
|
||||||
clear: right;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.sidebar-title {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
nav.contents,
|
|
||||||
aside.topic,
|
|
||||||
div.admonition, div.topic, blockquote {
|
|
||||||
clear: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- topics ---------------------------------------------------------------- */
|
|
||||||
nav.contents,
|
|
||||||
aside.topic,
|
|
||||||
div.topic {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
padding: 7px;
|
|
||||||
margin: 10px 0 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.topic-title {
|
|
||||||
font-size: 1.1em;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- admonitions ----------------------------------------------------------- */
|
|
||||||
|
|
||||||
div.admonition {
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.admonition dt {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.admonition-title {
|
|
||||||
margin: 0px 10px 5px 0px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body p.centered {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- content of sidebars/topics/admonitions -------------------------------- */
|
|
||||||
|
|
||||||
div.sidebar > :last-child,
|
|
||||||
aside.sidebar > :last-child,
|
|
||||||
nav.contents > :last-child,
|
|
||||||
aside.topic > :last-child,
|
|
||||||
div.topic > :last-child,
|
|
||||||
div.admonition > :last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sidebar::after,
|
|
||||||
aside.sidebar::after,
|
|
||||||
nav.contents::after,
|
|
||||||
aside.topic::after,
|
|
||||||
div.topic::after,
|
|
||||||
div.admonition::after,
|
|
||||||
blockquote::after {
|
|
||||||
display: block;
|
|
||||||
content: '';
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- tables ---------------------------------------------------------------- */
|
|
||||||
|
|
||||||
table.docutils {
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border: 0;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.align-center {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.align-default {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
table caption span.caption-number {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
table caption span.caption-text {
|
|
||||||
}
|
|
||||||
|
|
||||||
table.docutils td, table.docutils th {
|
|
||||||
padding: 1px 8px 1px 5px;
|
|
||||||
border-top: 0;
|
|
||||||
border-left: 0;
|
|
||||||
border-right: 0;
|
|
||||||
border-bottom: 1px solid #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
text-align: left;
|
|
||||||
padding-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.citation {
|
|
||||||
border-left: solid 1px gray;
|
|
||||||
margin-left: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.citation td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
th > :first-child,
|
|
||||||
td > :first-child {
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th > :last-child,
|
|
||||||
td > :last-child {
|
|
||||||
margin-bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- figures --------------------------------------------------------------- */
|
|
||||||
|
|
||||||
div.figure, figure {
|
|
||||||
margin: 0.5em;
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.figure p.caption, figcaption {
|
|
||||||
padding: 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.figure p.caption span.caption-number,
|
|
||||||
figcaption span.caption-number {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.figure p.caption span.caption-text,
|
|
||||||
figcaption span.caption-text {
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- field list styles ----------------------------------------------------- */
|
|
||||||
|
|
||||||
table.field-list td, table.field-list th {
|
|
||||||
border: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-list ul {
|
|
||||||
margin: 0;
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-list p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-name {
|
|
||||||
-moz-hyphens: manual;
|
|
||||||
-ms-hyphens: manual;
|
|
||||||
-webkit-hyphens: manual;
|
|
||||||
hyphens: manual;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- hlist styles ---------------------------------------------------------- */
|
|
||||||
|
|
||||||
table.hlist {
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.hlist td {
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- object description styles --------------------------------------------- */
|
|
||||||
|
|
||||||
.sig {
|
|
||||||
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig-name, code.descname {
|
|
||||||
background-color: transparent;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig-name {
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
code.descname {
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig-prename, code.descclassname {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.optional {
|
|
||||||
font-size: 1.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig-paren {
|
|
||||||
font-size: larger;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig-param.n {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* C++ specific styling */
|
|
||||||
|
|
||||||
.sig-inline.c-texpr,
|
|
||||||
.sig-inline.cpp-texpr {
|
|
||||||
font-family: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig.c .k, .sig.c .kt,
|
|
||||||
.sig.cpp .k, .sig.cpp .kt {
|
|
||||||
color: #0033B3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig.c .m,
|
|
||||||
.sig.cpp .m {
|
|
||||||
color: #1750EB;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig.c .s, .sig.c .sc,
|
|
||||||
.sig.cpp .s, .sig.cpp .sc {
|
|
||||||
color: #067D17;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* -- other body styles ----------------------------------------------------- */
|
|
||||||
|
|
||||||
ol.arabic {
|
|
||||||
list-style: decimal;
|
|
||||||
}
|
|
||||||
|
|
||||||
ol.loweralpha {
|
|
||||||
list-style: lower-alpha;
|
|
||||||
}
|
|
||||||
|
|
||||||
ol.upperalpha {
|
|
||||||
list-style: upper-alpha;
|
|
||||||
}
|
|
||||||
|
|
||||||
ol.lowerroman {
|
|
||||||
list-style: lower-roman;
|
|
||||||
}
|
|
||||||
|
|
||||||
ol.upperroman {
|
|
||||||
list-style: upper-roman;
|
|
||||||
}
|
|
||||||
|
|
||||||
:not(li) > ol > li:first-child > :first-child,
|
|
||||||
:not(li) > ul > li:first-child > :first-child {
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:not(li) > ol > li:last-child > :last-child,
|
|
||||||
:not(li) > ul > li:last-child > :last-child {
|
|
||||||
margin-bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ol.simple ol p,
|
|
||||||
ol.simple ul p,
|
|
||||||
ul.simple ol p,
|
|
||||||
ul.simple ul p {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ol.simple > li:not(:first-child) > p,
|
|
||||||
ul.simple > li:not(:first-child) > p {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ol.simple p,
|
|
||||||
ul.simple p {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
aside.footnote > span,
|
|
||||||
div.citation > span {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
aside.footnote > span:last-of-type,
|
|
||||||
div.citation > span:last-of-type {
|
|
||||||
padding-right: 0.5em;
|
|
||||||
}
|
|
||||||
aside.footnote > p {
|
|
||||||
margin-left: 2em;
|
|
||||||
}
|
|
||||||
div.citation > p {
|
|
||||||
margin-left: 4em;
|
|
||||||
}
|
|
||||||
aside.footnote > p:last-of-type,
|
|
||||||
div.citation > p:last-of-type {
|
|
||||||
margin-bottom: 0em;
|
|
||||||
}
|
|
||||||
aside.footnote > p:last-of-type:after,
|
|
||||||
div.citation > p:last-of-type:after {
|
|
||||||
content: "";
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl.field-list {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: fit-content(30%) auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl.field-list > dt {
|
|
||||||
font-weight: bold;
|
|
||||||
word-break: break-word;
|
|
||||||
padding-left: 0.5em;
|
|
||||||
padding-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl.field-list > dd {
|
|
||||||
padding-left: 0.5em;
|
|
||||||
margin-top: 0em;
|
|
||||||
margin-left: 0em;
|
|
||||||
margin-bottom: 0em;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
dd > :first-child {
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
dd ul, dd table {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
dd {
|
|
||||||
margin-top: 3px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
margin-left: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl > dd:last-child,
|
|
||||||
dl > dd:last-child > :last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
dt:target, span.highlighted {
|
|
||||||
background-color: #fbe54e;
|
|
||||||
}
|
|
||||||
|
|
||||||
rect.highlighted {
|
|
||||||
fill: #fbe54e;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl.glossary dt {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.versionmodified {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.system-message {
|
|
||||||
background-color: #fda;
|
|
||||||
padding: 5px;
|
|
||||||
border: 3px solid red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footnote:target {
|
|
||||||
background-color: #ffa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-block {
|
|
||||||
display: block;
|
|
||||||
margin-top: 1em;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-block .line-block {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
margin-left: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.guilabel, .menuselection {
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.accelerator {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.classifier {
|
|
||||||
font-style: oblique;
|
|
||||||
}
|
|
||||||
|
|
||||||
.classifier:before {
|
|
||||||
font-style: normal;
|
|
||||||
margin: 0 0.5em;
|
|
||||||
content: ":";
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
abbr, acronym {
|
|
||||||
border-bottom: dotted 1px;
|
|
||||||
cursor: help;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- code displays --------------------------------------------------------- */
|
|
||||||
|
|
||||||
pre {
|
|
||||||
overflow: auto;
|
|
||||||
overflow-y: hidden; /* fixes display issues on Chrome browsers */
|
|
||||||
}
|
|
||||||
|
|
||||||
pre, div[class*="highlight-"] {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.pre {
|
|
||||||
-moz-hyphens: none;
|
|
||||||
-ms-hyphens: none;
|
|
||||||
-webkit-hyphens: none;
|
|
||||||
hyphens: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
div[class*="highlight-"] {
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.linenos pre {
|
|
||||||
border: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.highlighttable {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.highlighttable tbody {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.highlighttable tr {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.highlighttable td {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.highlighttable td.linenos {
|
|
||||||
padding-right: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.highlighttable td.code {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlight .hll {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.highlight pre,
|
|
||||||
table.highlighttable pre {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-block-caption + div {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-block-caption {
|
|
||||||
margin-top: 1em;
|
|
||||||
padding: 2px 5px;
|
|
||||||
font-size: small;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-block-caption code {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.highlighttable td.linenos,
|
|
||||||
span.linenos,
|
|
||||||
div.highlight span.gp { /* gp: Generic.Prompt */
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: text; /* Safari fallback only */
|
|
||||||
-webkit-user-select: none; /* Chrome/Safari */
|
|
||||||
-moz-user-select: none; /* Firefox */
|
|
||||||
-ms-user-select: none; /* IE10+ */
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-block-caption span.caption-number {
|
|
||||||
padding: 0.1em 0.3em;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-block-caption span.caption-text {
|
|
||||||
}
|
|
||||||
|
|
||||||
div.literal-block-wrapper {
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
code.xref, a code {
|
|
||||||
background-color: transparent;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewcode-link {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewcode-back {
|
|
||||||
float: right;
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.viewcode-block:target {
|
|
||||||
margin: -1px -10px;
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- math display ---------------------------------------------------------- */
|
|
||||||
|
|
||||||
img.math {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body div.math p {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.eqno {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.eqno a.headerlink {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.math:hover a.headerlink {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- printout stylesheet --------------------------------------------------- */
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
div.document,
|
|
||||||
div.documentwrapper,
|
|
||||||
div.bodywrapper {
|
|
||||||
margin: 0 !important;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar,
|
|
||||||
div.related,
|
|
||||||
div.footer,
|
|
||||||
#top-link {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
/*
|
|
||||||
* doctools.js
|
|
||||||
* ~~~~~~~~~~~
|
|
||||||
*
|
|
||||||
* Base JavaScript utilities for all Sphinx HTML documentation.
|
|
||||||
*
|
|
||||||
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
|
|
||||||
* :license: BSD, see LICENSE for details.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([
|
|
||||||
"TEXTAREA",
|
|
||||||
"INPUT",
|
|
||||||
"SELECT",
|
|
||||||
"BUTTON",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const _ready = (callback) => {
|
|
||||||
if (document.readyState !== "loading") {
|
|
||||||
callback();
|
|
||||||
} else {
|
|
||||||
document.addEventListener("DOMContentLoaded", callback);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Small JavaScript module for the documentation.
|
|
||||||
*/
|
|
||||||
const Documentation = {
|
|
||||||
init: () => {
|
|
||||||
Documentation.initDomainIndexTable();
|
|
||||||
Documentation.initOnKeyListeners();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* i18n support
|
|
||||||
*/
|
|
||||||
TRANSLATIONS: {},
|
|
||||||
PLURAL_EXPR: (n) => (n === 1 ? 0 : 1),
|
|
||||||
LOCALE: "unknown",
|
|
||||||
|
|
||||||
// gettext and ngettext don't access this so that the functions
|
|
||||||
// can safely bound to a different name (_ = Documentation.gettext)
|
|
||||||
gettext: (string) => {
|
|
||||||
const translated = Documentation.TRANSLATIONS[string];
|
|
||||||
switch (typeof translated) {
|
|
||||||
case "undefined":
|
|
||||||
return string; // no translation
|
|
||||||
case "string":
|
|
||||||
return translated; // translation exists
|
|
||||||
default:
|
|
||||||
return translated[0]; // (singular, plural) translation tuple exists
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
ngettext: (singular, plural, n) => {
|
|
||||||
const translated = Documentation.TRANSLATIONS[singular];
|
|
||||||
if (typeof translated !== "undefined")
|
|
||||||
return translated[Documentation.PLURAL_EXPR(n)];
|
|
||||||
return n === 1 ? singular : plural;
|
|
||||||
},
|
|
||||||
|
|
||||||
addTranslations: (catalog) => {
|
|
||||||
Object.assign(Documentation.TRANSLATIONS, catalog.messages);
|
|
||||||
Documentation.PLURAL_EXPR = new Function(
|
|
||||||
"n",
|
|
||||||
`return (${catalog.plural_expr})`
|
|
||||||
);
|
|
||||||
Documentation.LOCALE = catalog.locale;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* helper function to focus on search bar
|
|
||||||
*/
|
|
||||||
focusSearchBar: () => {
|
|
||||||
document.querySelectorAll("input[name=q]")[0]?.focus();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialise the domain index toggle buttons
|
|
||||||
*/
|
|
||||||
initDomainIndexTable: () => {
|
|
||||||
const toggler = (el) => {
|
|
||||||
const idNumber = el.id.substr(7);
|
|
||||||
const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`);
|
|
||||||
if (el.src.substr(-9) === "minus.png") {
|
|
||||||
el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`;
|
|
||||||
toggledRows.forEach((el) => (el.style.display = "none"));
|
|
||||||
} else {
|
|
||||||
el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`;
|
|
||||||
toggledRows.forEach((el) => (el.style.display = ""));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const togglerElements = document.querySelectorAll("img.toggler");
|
|
||||||
togglerElements.forEach((el) =>
|
|
||||||
el.addEventListener("click", (event) => toggler(event.currentTarget))
|
|
||||||
);
|
|
||||||
togglerElements.forEach((el) => (el.style.display = ""));
|
|
||||||
if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler);
|
|
||||||
},
|
|
||||||
|
|
||||||
initOnKeyListeners: () => {
|
|
||||||
// only install a listener if it is really needed
|
|
||||||
if (
|
|
||||||
!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS &&
|
|
||||||
!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
document.addEventListener("keydown", (event) => {
|
|
||||||
// bail for input elements
|
|
||||||
if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return;
|
|
||||||
// bail with special keys
|
|
||||||
if (event.altKey || event.ctrlKey || event.metaKey) return;
|
|
||||||
|
|
||||||
if (!event.shiftKey) {
|
|
||||||
switch (event.key) {
|
|
||||||
case "ArrowLeft":
|
|
||||||
if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break;
|
|
||||||
|
|
||||||
const prevLink = document.querySelector('link[rel="prev"]');
|
|
||||||
if (prevLink && prevLink.href) {
|
|
||||||
window.location.href = prevLink.href;
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "ArrowRight":
|
|
||||||
if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break;
|
|
||||||
|
|
||||||
const nextLink = document.querySelector('link[rel="next"]');
|
|
||||||
if (nextLink && nextLink.href) {
|
|
||||||
window.location.href = nextLink.href;
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// some keyboard layouts may need Shift to get /
|
|
||||||
switch (event.key) {
|
|
||||||
case "/":
|
|
||||||
if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break;
|
|
||||||
Documentation.focusSearchBar();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// quick alias for translations
|
|
||||||
const _ = Documentation.gettext;
|
|
||||||
|
|
||||||
_ready(Documentation.init);
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
var DOCUMENTATION_OPTIONS = {
|
|
||||||
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
|
||||||
VERSION: '2.1.1',
|
|
||||||
LANGUAGE: 'en',
|
|
||||||
COLLAPSE_INDEX: false,
|
|
||||||
BUILDER: 'html',
|
|
||||||
FILE_SUFFIX: '.html',
|
|
||||||
LINK_SUFFIX: '.html',
|
|
||||||
HAS_SOURCE: true,
|
|
||||||
SOURCELINK_SUFFIX: '.txt',
|
|
||||||
NAVIGATION_WITH_KEYS: false,
|
|
||||||
SHOW_SEARCH_SUMMARY: true,
|
|
||||||
ENABLE_SEARCH_SHORTCUTS: true,
|
|
||||||
};
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 286 B |
File diff suppressed because one or more lines are too long
|
|
@ -1,199 +0,0 @@
|
||||||
/*
|
|
||||||
* language_data.js
|
|
||||||
* ~~~~~~~~~~~~~~~~
|
|
||||||
*
|
|
||||||
* This script contains the language-specific data used by searchtools.js,
|
|
||||||
* namely the list of stopwords, stemmer, scorer and splitter.
|
|
||||||
*
|
|
||||||
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
|
|
||||||
* :license: BSD, see LICENSE for details.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"];
|
|
||||||
|
|
||||||
|
|
||||||
/* Non-minified version is copied as a separate JS file, is available */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Porter Stemmer
|
|
||||||
*/
|
|
||||||
var Stemmer = function() {
|
|
||||||
|
|
||||||
var step2list = {
|
|
||||||
ational: 'ate',
|
|
||||||
tional: 'tion',
|
|
||||||
enci: 'ence',
|
|
||||||
anci: 'ance',
|
|
||||||
izer: 'ize',
|
|
||||||
bli: 'ble',
|
|
||||||
alli: 'al',
|
|
||||||
entli: 'ent',
|
|
||||||
eli: 'e',
|
|
||||||
ousli: 'ous',
|
|
||||||
ization: 'ize',
|
|
||||||
ation: 'ate',
|
|
||||||
ator: 'ate',
|
|
||||||
alism: 'al',
|
|
||||||
iveness: 'ive',
|
|
||||||
fulness: 'ful',
|
|
||||||
ousness: 'ous',
|
|
||||||
aliti: 'al',
|
|
||||||
iviti: 'ive',
|
|
||||||
biliti: 'ble',
|
|
||||||
logi: 'log'
|
|
||||||
};
|
|
||||||
|
|
||||||
var step3list = {
|
|
||||||
icate: 'ic',
|
|
||||||
ative: '',
|
|
||||||
alize: 'al',
|
|
||||||
iciti: 'ic',
|
|
||||||
ical: 'ic',
|
|
||||||
ful: '',
|
|
||||||
ness: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
var c = "[^aeiou]"; // consonant
|
|
||||||
var v = "[aeiouy]"; // vowel
|
|
||||||
var C = c + "[^aeiouy]*"; // consonant sequence
|
|
||||||
var V = v + "[aeiou]*"; // vowel sequence
|
|
||||||
|
|
||||||
var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0
|
|
||||||
var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1
|
|
||||||
var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1
|
|
||||||
var s_v = "^(" + C + ")?" + v; // vowel in stem
|
|
||||||
|
|
||||||
this.stemWord = function (w) {
|
|
||||||
var stem;
|
|
||||||
var suffix;
|
|
||||||
var firstch;
|
|
||||||
var origword = w;
|
|
||||||
|
|
||||||
if (w.length < 3)
|
|
||||||
return w;
|
|
||||||
|
|
||||||
var re;
|
|
||||||
var re2;
|
|
||||||
var re3;
|
|
||||||
var re4;
|
|
||||||
|
|
||||||
firstch = w.substr(0,1);
|
|
||||||
if (firstch == "y")
|
|
||||||
w = firstch.toUpperCase() + w.substr(1);
|
|
||||||
|
|
||||||
// Step 1a
|
|
||||||
re = /^(.+?)(ss|i)es$/;
|
|
||||||
re2 = /^(.+?)([^s])s$/;
|
|
||||||
|
|
||||||
if (re.test(w))
|
|
||||||
w = w.replace(re,"$1$2");
|
|
||||||
else if (re2.test(w))
|
|
||||||
w = w.replace(re2,"$1$2");
|
|
||||||
|
|
||||||
// Step 1b
|
|
||||||
re = /^(.+?)eed$/;
|
|
||||||
re2 = /^(.+?)(ed|ing)$/;
|
|
||||||
if (re.test(w)) {
|
|
||||||
var fp = re.exec(w);
|
|
||||||
re = new RegExp(mgr0);
|
|
||||||
if (re.test(fp[1])) {
|
|
||||||
re = /.$/;
|
|
||||||
w = w.replace(re,"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (re2.test(w)) {
|
|
||||||
var fp = re2.exec(w);
|
|
||||||
stem = fp[1];
|
|
||||||
re2 = new RegExp(s_v);
|
|
||||||
if (re2.test(stem)) {
|
|
||||||
w = stem;
|
|
||||||
re2 = /(at|bl|iz)$/;
|
|
||||||
re3 = new RegExp("([^aeiouylsz])\\1$");
|
|
||||||
re4 = new RegExp("^" + C + v + "[^aeiouwxy]$");
|
|
||||||
if (re2.test(w))
|
|
||||||
w = w + "e";
|
|
||||||
else if (re3.test(w)) {
|
|
||||||
re = /.$/;
|
|
||||||
w = w.replace(re,"");
|
|
||||||
}
|
|
||||||
else if (re4.test(w))
|
|
||||||
w = w + "e";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 1c
|
|
||||||
re = /^(.+?)y$/;
|
|
||||||
if (re.test(w)) {
|
|
||||||
var fp = re.exec(w);
|
|
||||||
stem = fp[1];
|
|
||||||
re = new RegExp(s_v);
|
|
||||||
if (re.test(stem))
|
|
||||||
w = stem + "i";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2
|
|
||||||
re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;
|
|
||||||
if (re.test(w)) {
|
|
||||||
var fp = re.exec(w);
|
|
||||||
stem = fp[1];
|
|
||||||
suffix = fp[2];
|
|
||||||
re = new RegExp(mgr0);
|
|
||||||
if (re.test(stem))
|
|
||||||
w = stem + step2list[suffix];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3
|
|
||||||
re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;
|
|
||||||
if (re.test(w)) {
|
|
||||||
var fp = re.exec(w);
|
|
||||||
stem = fp[1];
|
|
||||||
suffix = fp[2];
|
|
||||||
re = new RegExp(mgr0);
|
|
||||||
if (re.test(stem))
|
|
||||||
w = stem + step3list[suffix];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4
|
|
||||||
re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;
|
|
||||||
re2 = /^(.+?)(s|t)(ion)$/;
|
|
||||||
if (re.test(w)) {
|
|
||||||
var fp = re.exec(w);
|
|
||||||
stem = fp[1];
|
|
||||||
re = new RegExp(mgr1);
|
|
||||||
if (re.test(stem))
|
|
||||||
w = stem;
|
|
||||||
}
|
|
||||||
else if (re2.test(w)) {
|
|
||||||
var fp = re2.exec(w);
|
|
||||||
stem = fp[1] + fp[2];
|
|
||||||
re2 = new RegExp(mgr1);
|
|
||||||
if (re2.test(stem))
|
|
||||||
w = stem;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5
|
|
||||||
re = /^(.+?)e$/;
|
|
||||||
if (re.test(w)) {
|
|
||||||
var fp = re.exec(w);
|
|
||||||
stem = fp[1];
|
|
||||||
re = new RegExp(mgr1);
|
|
||||||
re2 = new RegExp(meq1);
|
|
||||||
re3 = new RegExp("^" + C + v + "[^aeiouwxy]$");
|
|
||||||
if (re.test(stem) || (re2.test(stem) && !(re3.test(stem))))
|
|
||||||
w = stem;
|
|
||||||
}
|
|
||||||
re = /ll$/;
|
|
||||||
re2 = new RegExp(mgr1);
|
|
||||||
if (re.test(w) && re2.test(w)) {
|
|
||||||
re = /.$/;
|
|
||||||
w = w.replace(re,"");
|
|
||||||
}
|
|
||||||
|
|
||||||
// and turn initial Y back to y
|
|
||||||
if (firstch == "y")
|
|
||||||
w = firstch.toLowerCase() + w.substr(1);
|
|
||||||
return w;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 90 B |
Binary file not shown.
|
Before Width: | Height: | Size: 90 B |
|
|
@ -1,255 +0,0 @@
|
||||||
.highlight pre { line-height: 125%; }
|
|
||||||
.highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
|
||||||
.highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
|
||||||
.highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
|
||||||
.highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
|
||||||
.highlight .hll { background-color: #ffffcc }
|
|
||||||
.highlight { background: #f8f8f8; }
|
|
||||||
.highlight .c { color: #8f5902; font-style: italic } /* Comment */
|
|
||||||
.highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */
|
|
||||||
.highlight .g { color: #000000 } /* Generic */
|
|
||||||
.highlight .k { color: #204a87; font-weight: bold } /* Keyword */
|
|
||||||
.highlight .l { color: #000000 } /* Literal */
|
|
||||||
.highlight .n { color: #000000 } /* Name */
|
|
||||||
.highlight .o { color: #ce5c00; font-weight: bold } /* Operator */
|
|
||||||
.highlight .x { color: #000000 } /* Other */
|
|
||||||
.highlight .p { color: #000000; font-weight: bold } /* Punctuation */
|
|
||||||
.highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */
|
|
||||||
.highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */
|
|
||||||
.highlight .cp { color: #8f5902; font-style: italic } /* Comment.Preproc */
|
|
||||||
.highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */
|
|
||||||
.highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */
|
|
||||||
.highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */
|
|
||||||
.highlight .gd { color: #a40000 } /* Generic.Deleted */
|
|
||||||
.highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */
|
|
||||||
.highlight .gr { color: #ef2929 } /* Generic.Error */
|
|
||||||
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
|
||||||
.highlight .gi { color: #00A000 } /* Generic.Inserted */
|
|
||||||
.highlight .go { color: #000000; font-style: italic } /* Generic.Output */
|
|
||||||
.highlight .gp { color: #8f5902 } /* Generic.Prompt */
|
|
||||||
.highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */
|
|
||||||
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
|
||||||
.highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */
|
|
||||||
.highlight .kc { color: #204a87; font-weight: bold } /* Keyword.Constant */
|
|
||||||
.highlight .kd { color: #204a87; font-weight: bold } /* Keyword.Declaration */
|
|
||||||
.highlight .kn { color: #204a87; font-weight: bold } /* Keyword.Namespace */
|
|
||||||
.highlight .kp { color: #204a87; font-weight: bold } /* Keyword.Pseudo */
|
|
||||||
.highlight .kr { color: #204a87; font-weight: bold } /* Keyword.Reserved */
|
|
||||||
.highlight .kt { color: #204a87; font-weight: bold } /* Keyword.Type */
|
|
||||||
.highlight .ld { color: #000000 } /* Literal.Date */
|
|
||||||
.highlight .m { color: #0000cf; font-weight: bold } /* Literal.Number */
|
|
||||||
.highlight .s { color: #4e9a06 } /* Literal.String */
|
|
||||||
.highlight .na { color: #c4a000 } /* Name.Attribute */
|
|
||||||
.highlight .nb { color: #204a87 } /* Name.Builtin */
|
|
||||||
.highlight .nc { color: #000000 } /* Name.Class */
|
|
||||||
.highlight .no { color: #000000 } /* Name.Constant */
|
|
||||||
.highlight .nd { color: #5c35cc; font-weight: bold } /* Name.Decorator */
|
|
||||||
.highlight .ni { color: #ce5c00 } /* Name.Entity */
|
|
||||||
.highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */
|
|
||||||
.highlight .nf { color: #000000 } /* Name.Function */
|
|
||||||
.highlight .nl { color: #f57900 } /* Name.Label */
|
|
||||||
.highlight .nn { color: #000000 } /* Name.Namespace */
|
|
||||||
.highlight .nx { color: #000000 } /* Name.Other */
|
|
||||||
.highlight .py { color: #000000 } /* Name.Property */
|
|
||||||
.highlight .nt { color: #204a87; font-weight: bold } /* Name.Tag */
|
|
||||||
.highlight .nv { color: #000000 } /* Name.Variable */
|
|
||||||
.highlight .ow { color: #204a87; font-weight: bold } /* Operator.Word */
|
|
||||||
.highlight .pm { color: #000000; font-weight: bold } /* Punctuation.Marker */
|
|
||||||
.highlight .w { color: #f8f8f8 } /* Text.Whitespace */
|
|
||||||
.highlight .mb { color: #0000cf; font-weight: bold } /* Literal.Number.Bin */
|
|
||||||
.highlight .mf { color: #0000cf; font-weight: bold } /* Literal.Number.Float */
|
|
||||||
.highlight .mh { color: #0000cf; font-weight: bold } /* Literal.Number.Hex */
|
|
||||||
.highlight .mi { color: #0000cf; font-weight: bold } /* Literal.Number.Integer */
|
|
||||||
.highlight .mo { color: #0000cf; font-weight: bold } /* Literal.Number.Oct */
|
|
||||||
.highlight .sa { color: #4e9a06 } /* Literal.String.Affix */
|
|
||||||
.highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */
|
|
||||||
.highlight .sc { color: #4e9a06 } /* Literal.String.Char */
|
|
||||||
.highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */
|
|
||||||
.highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */
|
|
||||||
.highlight .s2 { color: #4e9a06 } /* Literal.String.Double */
|
|
||||||
.highlight .se { color: #4e9a06 } /* Literal.String.Escape */
|
|
||||||
.highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */
|
|
||||||
.highlight .si { color: #4e9a06 } /* Literal.String.Interpol */
|
|
||||||
.highlight .sx { color: #4e9a06 } /* Literal.String.Other */
|
|
||||||
.highlight .sr { color: #4e9a06 } /* Literal.String.Regex */
|
|
||||||
.highlight .s1 { color: #4e9a06 } /* Literal.String.Single */
|
|
||||||
.highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */
|
|
||||||
.highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */
|
|
||||||
.highlight .fm { color: #000000 } /* Name.Function.Magic */
|
|
||||||
.highlight .vc { color: #000000 } /* Name.Variable.Class */
|
|
||||||
.highlight .vg { color: #000000 } /* Name.Variable.Global */
|
|
||||||
.highlight .vi { color: #000000 } /* Name.Variable.Instance */
|
|
||||||
.highlight .vm { color: #000000 } /* Name.Variable.Magic */
|
|
||||||
.highlight .il { color: #0000cf; font-weight: bold } /* Literal.Number.Integer.Long */
|
|
||||||
@media not print {
|
|
||||||
body[data-theme="dark"] .highlight pre { line-height: 125%; }
|
|
||||||
body[data-theme="dark"] .highlight td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
|
||||||
body[data-theme="dark"] .highlight span.linenos { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
|
||||||
body[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
|
||||||
body[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
|
||||||
body[data-theme="dark"] .highlight .hll { background-color: #404040 }
|
|
||||||
body[data-theme="dark"] .highlight { background: #202020; color: #d0d0d0 }
|
|
||||||
body[data-theme="dark"] .highlight .c { color: #ababab; font-style: italic } /* Comment */
|
|
||||||
body[data-theme="dark"] .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
|
|
||||||
body[data-theme="dark"] .highlight .esc { color: #d0d0d0 } /* Escape */
|
|
||||||
body[data-theme="dark"] .highlight .g { color: #d0d0d0 } /* Generic */
|
|
||||||
body[data-theme="dark"] .highlight .k { color: #6ebf26; font-weight: bold } /* Keyword */
|
|
||||||
body[data-theme="dark"] .highlight .l { color: #d0d0d0 } /* Literal */
|
|
||||||
body[data-theme="dark"] .highlight .n { color: #d0d0d0 } /* Name */
|
|
||||||
body[data-theme="dark"] .highlight .o { color: #d0d0d0 } /* Operator */
|
|
||||||
body[data-theme="dark"] .highlight .x { color: #d0d0d0 } /* Other */
|
|
||||||
body[data-theme="dark"] .highlight .p { color: #d0d0d0 } /* Punctuation */
|
|
||||||
body[data-theme="dark"] .highlight .ch { color: #ababab; font-style: italic } /* Comment.Hashbang */
|
|
||||||
body[data-theme="dark"] .highlight .cm { color: #ababab; font-style: italic } /* Comment.Multiline */
|
|
||||||
body[data-theme="dark"] .highlight .cp { color: #cd2828; font-weight: bold } /* Comment.Preproc */
|
|
||||||
body[data-theme="dark"] .highlight .cpf { color: #ababab; font-style: italic } /* Comment.PreprocFile */
|
|
||||||
body[data-theme="dark"] .highlight .c1 { color: #ababab; font-style: italic } /* Comment.Single */
|
|
||||||
body[data-theme="dark"] .highlight .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
|
|
||||||
body[data-theme="dark"] .highlight .gd { color: #d22323 } /* Generic.Deleted */
|
|
||||||
body[data-theme="dark"] .highlight .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */
|
|
||||||
body[data-theme="dark"] .highlight .gr { color: #d22323 } /* Generic.Error */
|
|
||||||
body[data-theme="dark"] .highlight .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */
|
|
||||||
body[data-theme="dark"] .highlight .gi { color: #589819 } /* Generic.Inserted */
|
|
||||||
body[data-theme="dark"] .highlight .go { color: #cccccc } /* Generic.Output */
|
|
||||||
body[data-theme="dark"] .highlight .gp { color: #aaaaaa } /* Generic.Prompt */
|
|
||||||
body[data-theme="dark"] .highlight .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */
|
|
||||||
body[data-theme="dark"] .highlight .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */
|
|
||||||
body[data-theme="dark"] .highlight .gt { color: #d22323 } /* Generic.Traceback */
|
|
||||||
body[data-theme="dark"] .highlight .kc { color: #6ebf26; font-weight: bold } /* Keyword.Constant */
|
|
||||||
body[data-theme="dark"] .highlight .kd { color: #6ebf26; font-weight: bold } /* Keyword.Declaration */
|
|
||||||
body[data-theme="dark"] .highlight .kn { color: #6ebf26; font-weight: bold } /* Keyword.Namespace */
|
|
||||||
body[data-theme="dark"] .highlight .kp { color: #6ebf26 } /* Keyword.Pseudo */
|
|
||||||
body[data-theme="dark"] .highlight .kr { color: #6ebf26; font-weight: bold } /* Keyword.Reserved */
|
|
||||||
body[data-theme="dark"] .highlight .kt { color: #6ebf26; font-weight: bold } /* Keyword.Type */
|
|
||||||
body[data-theme="dark"] .highlight .ld { color: #d0d0d0 } /* Literal.Date */
|
|
||||||
body[data-theme="dark"] .highlight .m { color: #51b2fd } /* Literal.Number */
|
|
||||||
body[data-theme="dark"] .highlight .s { color: #ed9d13 } /* Literal.String */
|
|
||||||
body[data-theme="dark"] .highlight .na { color: #bbbbbb } /* Name.Attribute */
|
|
||||||
body[data-theme="dark"] .highlight .nb { color: #2fbccd } /* Name.Builtin */
|
|
||||||
body[data-theme="dark"] .highlight .nc { color: #71adff; text-decoration: underline } /* Name.Class */
|
|
||||||
body[data-theme="dark"] .highlight .no { color: #40ffff } /* Name.Constant */
|
|
||||||
body[data-theme="dark"] .highlight .nd { color: #ffa500 } /* Name.Decorator */
|
|
||||||
body[data-theme="dark"] .highlight .ni { color: #d0d0d0 } /* Name.Entity */
|
|
||||||
body[data-theme="dark"] .highlight .ne { color: #bbbbbb } /* Name.Exception */
|
|
||||||
body[data-theme="dark"] .highlight .nf { color: #71adff } /* Name.Function */
|
|
||||||
body[data-theme="dark"] .highlight .nl { color: #d0d0d0 } /* Name.Label */
|
|
||||||
body[data-theme="dark"] .highlight .nn { color: #71adff; text-decoration: underline } /* Name.Namespace */
|
|
||||||
body[data-theme="dark"] .highlight .nx { color: #d0d0d0 } /* Name.Other */
|
|
||||||
body[data-theme="dark"] .highlight .py { color: #d0d0d0 } /* Name.Property */
|
|
||||||
body[data-theme="dark"] .highlight .nt { color: #6ebf26; font-weight: bold } /* Name.Tag */
|
|
||||||
body[data-theme="dark"] .highlight .nv { color: #40ffff } /* Name.Variable */
|
|
||||||
body[data-theme="dark"] .highlight .ow { color: #6ebf26; font-weight: bold } /* Operator.Word */
|
|
||||||
body[data-theme="dark"] .highlight .pm { color: #d0d0d0 } /* Punctuation.Marker */
|
|
||||||
body[data-theme="dark"] .highlight .w { color: #666666 } /* Text.Whitespace */
|
|
||||||
body[data-theme="dark"] .highlight .mb { color: #51b2fd } /* Literal.Number.Bin */
|
|
||||||
body[data-theme="dark"] .highlight .mf { color: #51b2fd } /* Literal.Number.Float */
|
|
||||||
body[data-theme="dark"] .highlight .mh { color: #51b2fd } /* Literal.Number.Hex */
|
|
||||||
body[data-theme="dark"] .highlight .mi { color: #51b2fd } /* Literal.Number.Integer */
|
|
||||||
body[data-theme="dark"] .highlight .mo { color: #51b2fd } /* Literal.Number.Oct */
|
|
||||||
body[data-theme="dark"] .highlight .sa { color: #ed9d13 } /* Literal.String.Affix */
|
|
||||||
body[data-theme="dark"] .highlight .sb { color: #ed9d13 } /* Literal.String.Backtick */
|
|
||||||
body[data-theme="dark"] .highlight .sc { color: #ed9d13 } /* Literal.String.Char */
|
|
||||||
body[data-theme="dark"] .highlight .dl { color: #ed9d13 } /* Literal.String.Delimiter */
|
|
||||||
body[data-theme="dark"] .highlight .sd { color: #ed9d13 } /* Literal.String.Doc */
|
|
||||||
body[data-theme="dark"] .highlight .s2 { color: #ed9d13 } /* Literal.String.Double */
|
|
||||||
body[data-theme="dark"] .highlight .se { color: #ed9d13 } /* Literal.String.Escape */
|
|
||||||
body[data-theme="dark"] .highlight .sh { color: #ed9d13 } /* Literal.String.Heredoc */
|
|
||||||
body[data-theme="dark"] .highlight .si { color: #ed9d13 } /* Literal.String.Interpol */
|
|
||||||
body[data-theme="dark"] .highlight .sx { color: #ffa500 } /* Literal.String.Other */
|
|
||||||
body[data-theme="dark"] .highlight .sr { color: #ed9d13 } /* Literal.String.Regex */
|
|
||||||
body[data-theme="dark"] .highlight .s1 { color: #ed9d13 } /* Literal.String.Single */
|
|
||||||
body[data-theme="dark"] .highlight .ss { color: #ed9d13 } /* Literal.String.Symbol */
|
|
||||||
body[data-theme="dark"] .highlight .bp { color: #2fbccd } /* Name.Builtin.Pseudo */
|
|
||||||
body[data-theme="dark"] .highlight .fm { color: #71adff } /* Name.Function.Magic */
|
|
||||||
body[data-theme="dark"] .highlight .vc { color: #40ffff } /* Name.Variable.Class */
|
|
||||||
body[data-theme="dark"] .highlight .vg { color: #40ffff } /* Name.Variable.Global */
|
|
||||||
body[data-theme="dark"] .highlight .vi { color: #40ffff } /* Name.Variable.Instance */
|
|
||||||
body[data-theme="dark"] .highlight .vm { color: #40ffff } /* Name.Variable.Magic */
|
|
||||||
body[data-theme="dark"] .highlight .il { color: #51b2fd } /* Literal.Number.Integer.Long */
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body:not([data-theme="light"]) .highlight pre { line-height: 125%; }
|
|
||||||
body:not([data-theme="light"]) .highlight td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
|
||||||
body:not([data-theme="light"]) .highlight span.linenos { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
|
||||||
body:not([data-theme="light"]) .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
|
||||||
body:not([data-theme="light"]) .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
|
||||||
body:not([data-theme="light"]) .highlight .hll { background-color: #404040 }
|
|
||||||
body:not([data-theme="light"]) .highlight { background: #202020; color: #d0d0d0 }
|
|
||||||
body:not([data-theme="light"]) .highlight .c { color: #ababab; font-style: italic } /* Comment */
|
|
||||||
body:not([data-theme="light"]) .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
|
|
||||||
body:not([data-theme="light"]) .highlight .esc { color: #d0d0d0 } /* Escape */
|
|
||||||
body:not([data-theme="light"]) .highlight .g { color: #d0d0d0 } /* Generic */
|
|
||||||
body:not([data-theme="light"]) .highlight .k { color: #6ebf26; font-weight: bold } /* Keyword */
|
|
||||||
body:not([data-theme="light"]) .highlight .l { color: #d0d0d0 } /* Literal */
|
|
||||||
body:not([data-theme="light"]) .highlight .n { color: #d0d0d0 } /* Name */
|
|
||||||
body:not([data-theme="light"]) .highlight .o { color: #d0d0d0 } /* Operator */
|
|
||||||
body:not([data-theme="light"]) .highlight .x { color: #d0d0d0 } /* Other */
|
|
||||||
body:not([data-theme="light"]) .highlight .p { color: #d0d0d0 } /* Punctuation */
|
|
||||||
body:not([data-theme="light"]) .highlight .ch { color: #ababab; font-style: italic } /* Comment.Hashbang */
|
|
||||||
body:not([data-theme="light"]) .highlight .cm { color: #ababab; font-style: italic } /* Comment.Multiline */
|
|
||||||
body:not([data-theme="light"]) .highlight .cp { color: #cd2828; font-weight: bold } /* Comment.Preproc */
|
|
||||||
body:not([data-theme="light"]) .highlight .cpf { color: #ababab; font-style: italic } /* Comment.PreprocFile */
|
|
||||||
body:not([data-theme="light"]) .highlight .c1 { color: #ababab; font-style: italic } /* Comment.Single */
|
|
||||||
body:not([data-theme="light"]) .highlight .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
|
|
||||||
body:not([data-theme="light"]) .highlight .gd { color: #d22323 } /* Generic.Deleted */
|
|
||||||
body:not([data-theme="light"]) .highlight .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */
|
|
||||||
body:not([data-theme="light"]) .highlight .gr { color: #d22323 } /* Generic.Error */
|
|
||||||
body:not([data-theme="light"]) .highlight .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */
|
|
||||||
body:not([data-theme="light"]) .highlight .gi { color: #589819 } /* Generic.Inserted */
|
|
||||||
body:not([data-theme="light"]) .highlight .go { color: #cccccc } /* Generic.Output */
|
|
||||||
body:not([data-theme="light"]) .highlight .gp { color: #aaaaaa } /* Generic.Prompt */
|
|
||||||
body:not([data-theme="light"]) .highlight .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */
|
|
||||||
body:not([data-theme="light"]) .highlight .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */
|
|
||||||
body:not([data-theme="light"]) .highlight .gt { color: #d22323 } /* Generic.Traceback */
|
|
||||||
body:not([data-theme="light"]) .highlight .kc { color: #6ebf26; font-weight: bold } /* Keyword.Constant */
|
|
||||||
body:not([data-theme="light"]) .highlight .kd { color: #6ebf26; font-weight: bold } /* Keyword.Declaration */
|
|
||||||
body:not([data-theme="light"]) .highlight .kn { color: #6ebf26; font-weight: bold } /* Keyword.Namespace */
|
|
||||||
body:not([data-theme="light"]) .highlight .kp { color: #6ebf26 } /* Keyword.Pseudo */
|
|
||||||
body:not([data-theme="light"]) .highlight .kr { color: #6ebf26; font-weight: bold } /* Keyword.Reserved */
|
|
||||||
body:not([data-theme="light"]) .highlight .kt { color: #6ebf26; font-weight: bold } /* Keyword.Type */
|
|
||||||
body:not([data-theme="light"]) .highlight .ld { color: #d0d0d0 } /* Literal.Date */
|
|
||||||
body:not([data-theme="light"]) .highlight .m { color: #51b2fd } /* Literal.Number */
|
|
||||||
body:not([data-theme="light"]) .highlight .s { color: #ed9d13 } /* Literal.String */
|
|
||||||
body:not([data-theme="light"]) .highlight .na { color: #bbbbbb } /* Name.Attribute */
|
|
||||||
body:not([data-theme="light"]) .highlight .nb { color: #2fbccd } /* Name.Builtin */
|
|
||||||
body:not([data-theme="light"]) .highlight .nc { color: #71adff; text-decoration: underline } /* Name.Class */
|
|
||||||
body:not([data-theme="light"]) .highlight .no { color: #40ffff } /* Name.Constant */
|
|
||||||
body:not([data-theme="light"]) .highlight .nd { color: #ffa500 } /* Name.Decorator */
|
|
||||||
body:not([data-theme="light"]) .highlight .ni { color: #d0d0d0 } /* Name.Entity */
|
|
||||||
body:not([data-theme="light"]) .highlight .ne { color: #bbbbbb } /* Name.Exception */
|
|
||||||
body:not([data-theme="light"]) .highlight .nf { color: #71adff } /* Name.Function */
|
|
||||||
body:not([data-theme="light"]) .highlight .nl { color: #d0d0d0 } /* Name.Label */
|
|
||||||
body:not([data-theme="light"]) .highlight .nn { color: #71adff; text-decoration: underline } /* Name.Namespace */
|
|
||||||
body:not([data-theme="light"]) .highlight .nx { color: #d0d0d0 } /* Name.Other */
|
|
||||||
body:not([data-theme="light"]) .highlight .py { color: #d0d0d0 } /* Name.Property */
|
|
||||||
body:not([data-theme="light"]) .highlight .nt { color: #6ebf26; font-weight: bold } /* Name.Tag */
|
|
||||||
body:not([data-theme="light"]) .highlight .nv { color: #40ffff } /* Name.Variable */
|
|
||||||
body:not([data-theme="light"]) .highlight .ow { color: #6ebf26; font-weight: bold } /* Operator.Word */
|
|
||||||
body:not([data-theme="light"]) .highlight .pm { color: #d0d0d0 } /* Punctuation.Marker */
|
|
||||||
body:not([data-theme="light"]) .highlight .w { color: #666666 } /* Text.Whitespace */
|
|
||||||
body:not([data-theme="light"]) .highlight .mb { color: #51b2fd } /* Literal.Number.Bin */
|
|
||||||
body:not([data-theme="light"]) .highlight .mf { color: #51b2fd } /* Literal.Number.Float */
|
|
||||||
body:not([data-theme="light"]) .highlight .mh { color: #51b2fd } /* Literal.Number.Hex */
|
|
||||||
body:not([data-theme="light"]) .highlight .mi { color: #51b2fd } /* Literal.Number.Integer */
|
|
||||||
body:not([data-theme="light"]) .highlight .mo { color: #51b2fd } /* Literal.Number.Oct */
|
|
||||||
body:not([data-theme="light"]) .highlight .sa { color: #ed9d13 } /* Literal.String.Affix */
|
|
||||||
body:not([data-theme="light"]) .highlight .sb { color: #ed9d13 } /* Literal.String.Backtick */
|
|
||||||
body:not([data-theme="light"]) .highlight .sc { color: #ed9d13 } /* Literal.String.Char */
|
|
||||||
body:not([data-theme="light"]) .highlight .dl { color: #ed9d13 } /* Literal.String.Delimiter */
|
|
||||||
body:not([data-theme="light"]) .highlight .sd { color: #ed9d13 } /* Literal.String.Doc */
|
|
||||||
body:not([data-theme="light"]) .highlight .s2 { color: #ed9d13 } /* Literal.String.Double */
|
|
||||||
body:not([data-theme="light"]) .highlight .se { color: #ed9d13 } /* Literal.String.Escape */
|
|
||||||
body:not([data-theme="light"]) .highlight .sh { color: #ed9d13 } /* Literal.String.Heredoc */
|
|
||||||
body:not([data-theme="light"]) .highlight .si { color: #ed9d13 } /* Literal.String.Interpol */
|
|
||||||
body:not([data-theme="light"]) .highlight .sx { color: #ffa500 } /* Literal.String.Other */
|
|
||||||
body:not([data-theme="light"]) .highlight .sr { color: #ed9d13 } /* Literal.String.Regex */
|
|
||||||
body:not([data-theme="light"]) .highlight .s1 { color: #ed9d13 } /* Literal.String.Single */
|
|
||||||
body:not([data-theme="light"]) .highlight .ss { color: #ed9d13 } /* Literal.String.Symbol */
|
|
||||||
body:not([data-theme="light"]) .highlight .bp { color: #2fbccd } /* Name.Builtin.Pseudo */
|
|
||||||
body:not([data-theme="light"]) .highlight .fm { color: #71adff } /* Name.Function.Magic */
|
|
||||||
body:not([data-theme="light"]) .highlight .vc { color: #40ffff } /* Name.Variable.Class */
|
|
||||||
body:not([data-theme="light"]) .highlight .vg { color: #40ffff } /* Name.Variable.Global */
|
|
||||||
body:not([data-theme="light"]) .highlight .vi { color: #40ffff } /* Name.Variable.Instance */
|
|
||||||
body:not([data-theme="light"]) .highlight .vm { color: #40ffff } /* Name.Variable.Magic */
|
|
||||||
body:not([data-theme="light"]) .highlight .il { color: #51b2fd } /* Literal.Number.Integer.Long */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,566 +0,0 @@
|
||||||
/*
|
|
||||||
* searchtools.js
|
|
||||||
* ~~~~~~~~~~~~~~~~
|
|
||||||
*
|
|
||||||
* Sphinx JavaScript utilities for the full-text search.
|
|
||||||
*
|
|
||||||
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
|
|
||||||
* :license: BSD, see LICENSE for details.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple result scoring code.
|
|
||||||
*/
|
|
||||||
if (typeof Scorer === "undefined") {
|
|
||||||
var Scorer = {
|
|
||||||
// Implement the following function to further tweak the score for each result
|
|
||||||
// The function takes a result array [docname, title, anchor, descr, score, filename]
|
|
||||||
// and returns the new score.
|
|
||||||
/*
|
|
||||||
score: result => {
|
|
||||||
const [docname, title, anchor, descr, score, filename] = result
|
|
||||||
return score
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
|
|
||||||
// query matches the full name of an object
|
|
||||||
objNameMatch: 11,
|
|
||||||
// or matches in the last dotted part of the object name
|
|
||||||
objPartialMatch: 6,
|
|
||||||
// Additive scores depending on the priority of the object
|
|
||||||
objPrio: {
|
|
||||||
0: 15, // used to be importantResults
|
|
||||||
1: 5, // used to be objectResults
|
|
||||||
2: -5, // used to be unimportantResults
|
|
||||||
},
|
|
||||||
// Used when the priority is not in the mapping.
|
|
||||||
objPrioDefault: 0,
|
|
||||||
|
|
||||||
// query found in title
|
|
||||||
title: 15,
|
|
||||||
partialTitle: 7,
|
|
||||||
// query found in terms
|
|
||||||
term: 5,
|
|
||||||
partialTerm: 2,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const _removeChildren = (element) => {
|
|
||||||
while (element && element.lastChild) element.removeChild(element.lastChild);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
|
||||||
*/
|
|
||||||
const _escapeRegExp = (string) =>
|
|
||||||
string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
|
|
||||||
|
|
||||||
const _displayItem = (item, searchTerms) => {
|
|
||||||
const docBuilder = DOCUMENTATION_OPTIONS.BUILDER;
|
|
||||||
const docUrlRoot = DOCUMENTATION_OPTIONS.URL_ROOT;
|
|
||||||
const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX;
|
|
||||||
const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX;
|
|
||||||
const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY;
|
|
||||||
|
|
||||||
const [docName, title, anchor, descr, score, _filename] = item;
|
|
||||||
|
|
||||||
let listItem = document.createElement("li");
|
|
||||||
let requestUrl;
|
|
||||||
let linkUrl;
|
|
||||||
if (docBuilder === "dirhtml") {
|
|
||||||
// dirhtml builder
|
|
||||||
let dirname = docName + "/";
|
|
||||||
if (dirname.match(/\/index\/$/))
|
|
||||||
dirname = dirname.substring(0, dirname.length - 6);
|
|
||||||
else if (dirname === "index/") dirname = "";
|
|
||||||
requestUrl = docUrlRoot + dirname;
|
|
||||||
linkUrl = requestUrl;
|
|
||||||
} else {
|
|
||||||
// normal html builders
|
|
||||||
requestUrl = docUrlRoot + docName + docFileSuffix;
|
|
||||||
linkUrl = docName + docLinkSuffix;
|
|
||||||
}
|
|
||||||
let linkEl = listItem.appendChild(document.createElement("a"));
|
|
||||||
linkEl.href = linkUrl + anchor;
|
|
||||||
linkEl.dataset.score = score;
|
|
||||||
linkEl.innerHTML = title;
|
|
||||||
if (descr)
|
|
||||||
listItem.appendChild(document.createElement("span")).innerHTML =
|
|
||||||
" (" + descr + ")";
|
|
||||||
else if (showSearchSummary)
|
|
||||||
fetch(requestUrl)
|
|
||||||
.then((responseData) => responseData.text())
|
|
||||||
.then((data) => {
|
|
||||||
if (data)
|
|
||||||
listItem.appendChild(
|
|
||||||
Search.makeSearchSummary(data, searchTerms)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
Search.output.appendChild(listItem);
|
|
||||||
};
|
|
||||||
const _finishSearch = (resultCount) => {
|
|
||||||
Search.stopPulse();
|
|
||||||
Search.title.innerText = _("Search Results");
|
|
||||||
if (!resultCount)
|
|
||||||
Search.status.innerText = Documentation.gettext(
|
|
||||||
"Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories."
|
|
||||||
);
|
|
||||||
else
|
|
||||||
Search.status.innerText = _(
|
|
||||||
`Search finished, found ${resultCount} page(s) matching the search query.`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const _displayNextItem = (
|
|
||||||
results,
|
|
||||||
resultCount,
|
|
||||||
searchTerms
|
|
||||||
) => {
|
|
||||||
// results left, load the summary and display it
|
|
||||||
// this is intended to be dynamic (don't sub resultsCount)
|
|
||||||
if (results.length) {
|
|
||||||
_displayItem(results.pop(), searchTerms);
|
|
||||||
setTimeout(
|
|
||||||
() => _displayNextItem(results, resultCount, searchTerms),
|
|
||||||
5
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// search finished, update title and status message
|
|
||||||
else _finishSearch(resultCount);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default splitQuery function. Can be overridden in ``sphinx.search`` with a
|
|
||||||
* custom function per language.
|
|
||||||
*
|
|
||||||
* The regular expression works by splitting the string on consecutive characters
|
|
||||||
* that are not Unicode letters, numbers, underscores, or emoji characters.
|
|
||||||
* This is the same as ``\W+`` in Python, preserving the surrogate pair area.
|
|
||||||
*/
|
|
||||||
if (typeof splitQuery === "undefined") {
|
|
||||||
var splitQuery = (query) => query
|
|
||||||
.split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu)
|
|
||||||
.filter(term => term) // remove remaining empty strings
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search Module
|
|
||||||
*/
|
|
||||||
const Search = {
|
|
||||||
_index: null,
|
|
||||||
_queued_query: null,
|
|
||||||
_pulse_status: -1,
|
|
||||||
|
|
||||||
htmlToText: (htmlString) => {
|
|
||||||
const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html');
|
|
||||||
htmlElement.querySelectorAll(".headerlink").forEach((el) => { el.remove() });
|
|
||||||
const docContent = htmlElement.querySelector('[role="main"]');
|
|
||||||
if (docContent !== undefined) return docContent.textContent;
|
|
||||||
console.warn(
|
|
||||||
"Content block not found. Sphinx search tries to obtain it via '[role=main]'. Could you check your theme or template."
|
|
||||||
);
|
|
||||||
return "";
|
|
||||||
},
|
|
||||||
|
|
||||||
init: () => {
|
|
||||||
const query = new URLSearchParams(window.location.search).get("q");
|
|
||||||
document
|
|
||||||
.querySelectorAll('input[name="q"]')
|
|
||||||
.forEach((el) => (el.value = query));
|
|
||||||
if (query) Search.performSearch(query);
|
|
||||||
},
|
|
||||||
|
|
||||||
loadIndex: (url) =>
|
|
||||||
(document.body.appendChild(document.createElement("script")).src = url),
|
|
||||||
|
|
||||||
setIndex: (index) => {
|
|
||||||
Search._index = index;
|
|
||||||
if (Search._queued_query !== null) {
|
|
||||||
const query = Search._queued_query;
|
|
||||||
Search._queued_query = null;
|
|
||||||
Search.query(query);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
hasIndex: () => Search._index !== null,
|
|
||||||
|
|
||||||
deferQuery: (query) => (Search._queued_query = query),
|
|
||||||
|
|
||||||
stopPulse: () => (Search._pulse_status = -1),
|
|
||||||
|
|
||||||
startPulse: () => {
|
|
||||||
if (Search._pulse_status >= 0) return;
|
|
||||||
|
|
||||||
const pulse = () => {
|
|
||||||
Search._pulse_status = (Search._pulse_status + 1) % 4;
|
|
||||||
Search.dots.innerText = ".".repeat(Search._pulse_status);
|
|
||||||
if (Search._pulse_status >= 0) window.setTimeout(pulse, 500);
|
|
||||||
};
|
|
||||||
pulse();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* perform a search for something (or wait until index is loaded)
|
|
||||||
*/
|
|
||||||
performSearch: (query) => {
|
|
||||||
// create the required interface elements
|
|
||||||
const searchText = document.createElement("h2");
|
|
||||||
searchText.textContent = _("Searching");
|
|
||||||
const searchSummary = document.createElement("p");
|
|
||||||
searchSummary.classList.add("search-summary");
|
|
||||||
searchSummary.innerText = "";
|
|
||||||
const searchList = document.createElement("ul");
|
|
||||||
searchList.classList.add("search");
|
|
||||||
|
|
||||||
const out = document.getElementById("search-results");
|
|
||||||
Search.title = out.appendChild(searchText);
|
|
||||||
Search.dots = Search.title.appendChild(document.createElement("span"));
|
|
||||||
Search.status = out.appendChild(searchSummary);
|
|
||||||
Search.output = out.appendChild(searchList);
|
|
||||||
|
|
||||||
const searchProgress = document.getElementById("search-progress");
|
|
||||||
// Some themes don't use the search progress node
|
|
||||||
if (searchProgress) {
|
|
||||||
searchProgress.innerText = _("Preparing search...");
|
|
||||||
}
|
|
||||||
Search.startPulse();
|
|
||||||
|
|
||||||
// index already loaded, the browser was quick!
|
|
||||||
if (Search.hasIndex()) Search.query(query);
|
|
||||||
else Search.deferQuery(query);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* execute search (requires search index to be loaded)
|
|
||||||
*/
|
|
||||||
query: (query) => {
|
|
||||||
const filenames = Search._index.filenames;
|
|
||||||
const docNames = Search._index.docnames;
|
|
||||||
const titles = Search._index.titles;
|
|
||||||
const allTitles = Search._index.alltitles;
|
|
||||||
const indexEntries = Search._index.indexentries;
|
|
||||||
|
|
||||||
// stem the search terms and add them to the correct list
|
|
||||||
const stemmer = new Stemmer();
|
|
||||||
const searchTerms = new Set();
|
|
||||||
const excludedTerms = new Set();
|
|
||||||
const highlightTerms = new Set();
|
|
||||||
const objectTerms = new Set(splitQuery(query.toLowerCase().trim()));
|
|
||||||
splitQuery(query.trim()).forEach((queryTerm) => {
|
|
||||||
const queryTermLower = queryTerm.toLowerCase();
|
|
||||||
|
|
||||||
// maybe skip this "word"
|
|
||||||
// stopwords array is from language_data.js
|
|
||||||
if (
|
|
||||||
stopwords.indexOf(queryTermLower) !== -1 ||
|
|
||||||
queryTerm.match(/^\d+$/)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// stem the word
|
|
||||||
let word = stemmer.stemWord(queryTermLower);
|
|
||||||
// select the correct list
|
|
||||||
if (word[0] === "-") excludedTerms.add(word.substr(1));
|
|
||||||
else {
|
|
||||||
searchTerms.add(word);
|
|
||||||
highlightTerms.add(queryTermLower);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js
|
|
||||||
localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" "))
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.debug("SEARCH: searching for:");
|
|
||||||
// console.info("required: ", [...searchTerms]);
|
|
||||||
// console.info("excluded: ", [...excludedTerms]);
|
|
||||||
|
|
||||||
// array of [docname, title, anchor, descr, score, filename]
|
|
||||||
let results = [];
|
|
||||||
_removeChildren(document.getElementById("search-progress"));
|
|
||||||
|
|
||||||
const queryLower = query.toLowerCase();
|
|
||||||
for (const [title, foundTitles] of Object.entries(allTitles)) {
|
|
||||||
if (title.toLowerCase().includes(queryLower) && (queryLower.length >= title.length/2)) {
|
|
||||||
for (const [file, id] of foundTitles) {
|
|
||||||
let score = Math.round(100 * queryLower.length / title.length)
|
|
||||||
results.push([
|
|
||||||
docNames[file],
|
|
||||||
titles[file] !== title ? `${titles[file]} > ${title}` : title,
|
|
||||||
id !== null ? "#" + id : "",
|
|
||||||
null,
|
|
||||||
score,
|
|
||||||
filenames[file],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// search for explicit entries in index directives
|
|
||||||
for (const [entry, foundEntries] of Object.entries(indexEntries)) {
|
|
||||||
if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) {
|
|
||||||
for (const [file, id] of foundEntries) {
|
|
||||||
let score = Math.round(100 * queryLower.length / entry.length)
|
|
||||||
results.push([
|
|
||||||
docNames[file],
|
|
||||||
titles[file],
|
|
||||||
id ? "#" + id : "",
|
|
||||||
null,
|
|
||||||
score,
|
|
||||||
filenames[file],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// lookup as object
|
|
||||||
objectTerms.forEach((term) =>
|
|
||||||
results.push(...Search.performObjectSearch(term, objectTerms))
|
|
||||||
);
|
|
||||||
|
|
||||||
// lookup as search terms in fulltext
|
|
||||||
results.push(...Search.performTermsSearch(searchTerms, excludedTerms));
|
|
||||||
|
|
||||||
// let the scorer override scores with a custom scoring function
|
|
||||||
if (Scorer.score) results.forEach((item) => (item[4] = Scorer.score(item)));
|
|
||||||
|
|
||||||
// now sort the results by score (in opposite order of appearance, since the
|
|
||||||
// display function below uses pop() to retrieve items) and then
|
|
||||||
// alphabetically
|
|
||||||
results.sort((a, b) => {
|
|
||||||
const leftScore = a[4];
|
|
||||||
const rightScore = b[4];
|
|
||||||
if (leftScore === rightScore) {
|
|
||||||
// same score: sort alphabetically
|
|
||||||
const leftTitle = a[1].toLowerCase();
|
|
||||||
const rightTitle = b[1].toLowerCase();
|
|
||||||
if (leftTitle === rightTitle) return 0;
|
|
||||||
return leftTitle > rightTitle ? -1 : 1; // inverted is intentional
|
|
||||||
}
|
|
||||||
return leftScore > rightScore ? 1 : -1;
|
|
||||||
});
|
|
||||||
|
|
||||||
// remove duplicate search results
|
|
||||||
// note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept
|
|
||||||
let seen = new Set();
|
|
||||||
results = results.reverse().reduce((acc, result) => {
|
|
||||||
let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(',');
|
|
||||||
if (!seen.has(resultStr)) {
|
|
||||||
acc.push(result);
|
|
||||||
seen.add(resultStr);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
results = results.reverse();
|
|
||||||
|
|
||||||
// for debugging
|
|
||||||
//Search.lastresults = results.slice(); // a copy
|
|
||||||
// console.info("search results:", Search.lastresults);
|
|
||||||
|
|
||||||
// print the results
|
|
||||||
_displayNextItem(results, results.length, searchTerms);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* search for object names
|
|
||||||
*/
|
|
||||||
performObjectSearch: (object, objectTerms) => {
|
|
||||||
const filenames = Search._index.filenames;
|
|
||||||
const docNames = Search._index.docnames;
|
|
||||||
const objects = Search._index.objects;
|
|
||||||
const objNames = Search._index.objnames;
|
|
||||||
const titles = Search._index.titles;
|
|
||||||
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
const objectSearchCallback = (prefix, match) => {
|
|
||||||
const name = match[4]
|
|
||||||
const fullname = (prefix ? prefix + "." : "") + name;
|
|
||||||
const fullnameLower = fullname.toLowerCase();
|
|
||||||
if (fullnameLower.indexOf(object) < 0) return;
|
|
||||||
|
|
||||||
let score = 0;
|
|
||||||
const parts = fullnameLower.split(".");
|
|
||||||
|
|
||||||
// check for different match types: exact matches of full name or
|
|
||||||
// "last name" (i.e. last dotted part)
|
|
||||||
if (fullnameLower === object || parts.slice(-1)[0] === object)
|
|
||||||
score += Scorer.objNameMatch;
|
|
||||||
else if (parts.slice(-1)[0].indexOf(object) > -1)
|
|
||||||
score += Scorer.objPartialMatch; // matches in last name
|
|
||||||
|
|
||||||
const objName = objNames[match[1]][2];
|
|
||||||
const title = titles[match[0]];
|
|
||||||
|
|
||||||
// If more than one term searched for, we require other words to be
|
|
||||||
// found in the name/title/description
|
|
||||||
const otherTerms = new Set(objectTerms);
|
|
||||||
otherTerms.delete(object);
|
|
||||||
if (otherTerms.size > 0) {
|
|
||||||
const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase();
|
|
||||||
if (
|
|
||||||
[...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let anchor = match[3];
|
|
||||||
if (anchor === "") anchor = fullname;
|
|
||||||
else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname;
|
|
||||||
|
|
||||||
const descr = objName + _(", in ") + title;
|
|
||||||
|
|
||||||
// add custom score for some objects according to scorer
|
|
||||||
if (Scorer.objPrio.hasOwnProperty(match[2]))
|
|
||||||
score += Scorer.objPrio[match[2]];
|
|
||||||
else score += Scorer.objPrioDefault;
|
|
||||||
|
|
||||||
results.push([
|
|
||||||
docNames[match[0]],
|
|
||||||
fullname,
|
|
||||||
"#" + anchor,
|
|
||||||
descr,
|
|
||||||
score,
|
|
||||||
filenames[match[0]],
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
Object.keys(objects).forEach((prefix) =>
|
|
||||||
objects[prefix].forEach((array) =>
|
|
||||||
objectSearchCallback(prefix, array)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* search for full-text terms in the index
|
|
||||||
*/
|
|
||||||
performTermsSearch: (searchTerms, excludedTerms) => {
|
|
||||||
// prepare search
|
|
||||||
const terms = Search._index.terms;
|
|
||||||
const titleTerms = Search._index.titleterms;
|
|
||||||
const filenames = Search._index.filenames;
|
|
||||||
const docNames = Search._index.docnames;
|
|
||||||
const titles = Search._index.titles;
|
|
||||||
|
|
||||||
const scoreMap = new Map();
|
|
||||||
const fileMap = new Map();
|
|
||||||
|
|
||||||
// perform the search on the required terms
|
|
||||||
searchTerms.forEach((word) => {
|
|
||||||
const files = [];
|
|
||||||
const arr = [
|
|
||||||
{ files: terms[word], score: Scorer.term },
|
|
||||||
{ files: titleTerms[word], score: Scorer.title },
|
|
||||||
];
|
|
||||||
// add support for partial matches
|
|
||||||
if (word.length > 2) {
|
|
||||||
const escapedWord = _escapeRegExp(word);
|
|
||||||
Object.keys(terms).forEach((term) => {
|
|
||||||
if (term.match(escapedWord) && !terms[word])
|
|
||||||
arr.push({ files: terms[term], score: Scorer.partialTerm });
|
|
||||||
});
|
|
||||||
Object.keys(titleTerms).forEach((term) => {
|
|
||||||
if (term.match(escapedWord) && !titleTerms[word])
|
|
||||||
arr.push({ files: titleTerms[word], score: Scorer.partialTitle });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// no match but word was a required one
|
|
||||||
if (arr.every((record) => record.files === undefined)) return;
|
|
||||||
|
|
||||||
// found search word in contents
|
|
||||||
arr.forEach((record) => {
|
|
||||||
if (record.files === undefined) return;
|
|
||||||
|
|
||||||
let recordFiles = record.files;
|
|
||||||
if (recordFiles.length === undefined) recordFiles = [recordFiles];
|
|
||||||
files.push(...recordFiles);
|
|
||||||
|
|
||||||
// set score for the word in each file
|
|
||||||
recordFiles.forEach((file) => {
|
|
||||||
if (!scoreMap.has(file)) scoreMap.set(file, {});
|
|
||||||
scoreMap.get(file)[word] = record.score;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// create the mapping
|
|
||||||
files.forEach((file) => {
|
|
||||||
if (fileMap.has(file) && fileMap.get(file).indexOf(word) === -1)
|
|
||||||
fileMap.get(file).push(word);
|
|
||||||
else fileMap.set(file, [word]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// now check if the files don't contain excluded terms
|
|
||||||
const results = [];
|
|
||||||
for (const [file, wordList] of fileMap) {
|
|
||||||
// check if all requirements are matched
|
|
||||||
|
|
||||||
// as search terms with length < 3 are discarded
|
|
||||||
const filteredTermCount = [...searchTerms].filter(
|
|
||||||
(term) => term.length > 2
|
|
||||||
).length;
|
|
||||||
if (
|
|
||||||
wordList.length !== searchTerms.size &&
|
|
||||||
wordList.length !== filteredTermCount
|
|
||||||
)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// ensure that none of the excluded terms is in the search result
|
|
||||||
if (
|
|
||||||
[...excludedTerms].some(
|
|
||||||
(term) =>
|
|
||||||
terms[term] === file ||
|
|
||||||
titleTerms[term] === file ||
|
|
||||||
(terms[term] || []).includes(file) ||
|
|
||||||
(titleTerms[term] || []).includes(file)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
break;
|
|
||||||
|
|
||||||
// select one (max) score for the file.
|
|
||||||
const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w]));
|
|
||||||
// add result to the result list
|
|
||||||
results.push([
|
|
||||||
docNames[file],
|
|
||||||
titles[file],
|
|
||||||
"",
|
|
||||||
null,
|
|
||||||
score,
|
|
||||||
filenames[file],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* helper function to return a node containing the
|
|
||||||
* search summary for a given text. keywords is a list
|
|
||||||
* of stemmed words.
|
|
||||||
*/
|
|
||||||
makeSearchSummary: (htmlText, keywords) => {
|
|
||||||
const text = Search.htmlToText(htmlText);
|
|
||||||
if (text === "") return null;
|
|
||||||
|
|
||||||
const textLower = text.toLowerCase();
|
|
||||||
const actualStartPosition = [...keywords]
|
|
||||||
.map((k) => textLower.indexOf(k.toLowerCase()))
|
|
||||||
.filter((i) => i > -1)
|
|
||||||
.slice(-1)[0];
|
|
||||||
const startWithContext = Math.max(actualStartPosition - 120, 0);
|
|
||||||
|
|
||||||
const top = startWithContext === 0 ? "" : "...";
|
|
||||||
const tail = startWithContext + 240 < text.length ? "..." : "";
|
|
||||||
|
|
||||||
let summary = document.createElement("p");
|
|
||||||
summary.classList.add("context");
|
|
||||||
summary.textContent = top + text.substr(startWithContext, 240).trim() + tail;
|
|
||||||
|
|
||||||
return summary;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
_ready(Search.init);
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
|
@ -1,307 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html class="no-js" lang="en">
|
|
||||||
<head><meta charset="utf-8"/>
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
||||||
<meta name="color-scheme" content="light dark"><meta name="generator" content="Docutils 0.19: https://docutils.sourceforge.io/" />
|
|
||||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="Installation" href="installation.html" />
|
|
||||||
|
|
||||||
<!-- Generated with Sphinx 5.3.0 and Furo 2022.12.07 -->
|
|
||||||
<title>Pomice</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=91d0f0d1c444bdcb17a68e833c7a53903343c195" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
--color-code-background: #f8f8f8;
|
|
||||||
--color-code-foreground: black;
|
|
||||||
|
|
||||||
}
|
|
||||||
@media not print {
|
|
||||||
body[data-theme="dark"] {
|
|
||||||
--color-code-background: #202020;
|
|
||||||
--color-code-foreground: #d0d0d0;
|
|
||||||
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body:not([data-theme="light"]) {
|
|
||||||
--color-code-background: #202020;
|
|
||||||
--color-code-foreground: #d0d0d0;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style></head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
|
||||||
<symbol id="svg-toc" viewBox="0 0 24 24">
|
|
||||||
<title>Contents</title>
|
|
||||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
|
|
||||||
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="svg-menu" viewBox="0 0 24 24">
|
|
||||||
<title>Menu</title>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
|
|
||||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
|
||||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
|
||||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
|
|
||||||
<title>Expand</title>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
|
|
||||||
<polyline points="9 18 15 12 9 6"></polyline>
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="svg-sun" viewBox="0 0 24 24">
|
|
||||||
<title>Light mode</title>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
|
|
||||||
<circle cx="12" cy="12" r="5"></circle>
|
|
||||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
|
||||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
|
||||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
|
||||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
|
||||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
|
||||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
|
||||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
|
||||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="svg-moon" viewBox="0 0 24 24">
|
|
||||||
<title>Dark mode</title>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="svg-sun-half" viewBox="0 0 24 24">
|
|
||||||
<title>Auto light/dark mode</title>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
||||||
<circle cx="12" cy="12" r="9" />
|
|
||||||
<path d="M13 12h5" />
|
|
||||||
<path d="M13 15h4" />
|
|
||||||
<path d="M13 18h1" />
|
|
||||||
<path d="M13 9h4" />
|
|
||||||
<path d="M13 6h1" />
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
|
|
||||||
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
|
|
||||||
<label class="overlay sidebar-overlay" for="__navigation">
|
|
||||||
<div class="visually-hidden">Hide navigation sidebar</div>
|
|
||||||
</label>
|
|
||||||
<label class="overlay toc-overlay" for="__toc">
|
|
||||||
<div class="visually-hidden">Hide table of contents sidebar</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="page">
|
|
||||||
<header class="mobile-header">
|
|
||||||
<div class="header-left">
|
|
||||||
<label class="nav-overlay-icon" for="__navigation">
|
|
||||||
<div class="visually-hidden">Toggle site navigation sidebar</div>
|
|
||||||
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="header-center">
|
|
||||||
<a href="#"><div class="brand">Pomice</div></a>
|
|
||||||
</div>
|
|
||||||
<div class="header-right">
|
|
||||||
<div class="theme-toggle-container theme-toggle-header">
|
|
||||||
<button class="theme-toggle">
|
|
||||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
|
||||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
|
||||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
|
||||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<label class="toc-overlay-icon toc-header-icon no-toc" for="__toc">
|
|
||||||
<div class="visually-hidden">Toggle table of contents sidebar</div>
|
|
||||||
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<aside class="sidebar-drawer">
|
|
||||||
<div class="sidebar-container">
|
|
||||||
|
|
||||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="#">
|
|
||||||
|
|
||||||
|
|
||||||
<span class="sidebar-brand-text">Pomice</span>
|
|
||||||
|
|
||||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
|
||||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
|
||||||
<input type="hidden" name="check_keywords" value="yes">
|
|
||||||
<input type="hidden" name="area" value="default">
|
|
||||||
</form>
|
|
||||||
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
|
|
||||||
<p class="caption" role="heading"><span class="caption-text">Before You Start</span></p>
|
|
||||||
<ul>
|
|
||||||
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
|
|
||||||
<li class="toctree-l1"><a class="reference internal" href="quickstart.html">Quick Jumpstart</a></li>
|
|
||||||
<li class="toctree-l1"><a class="reference internal" href="faq.html">Frequently Asked Questions</a></li>
|
|
||||||
</ul>
|
|
||||||
<p class="caption" role="heading"><span class="caption-text">How Do I?</span></p>
|
|
||||||
<ul>
|
|
||||||
<li class="toctree-l1 has-children"><a class="reference internal" href="hdi/index.html">How Do I?</a><input class="toctree-checkbox" id="toctree-checkbox-1" name="toctree-checkbox-1" role="switch" type="checkbox"/><label for="toctree-checkbox-1"><div class="visually-hidden">Toggle child pages in navigation</div><i class="icon"><svg><use href="#svg-arrow-right"></use></svg></i></label><ul>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/pool.html">Use the NodePool class</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/node.html">Use the Node class</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/player.html">Use the Player class</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/filters.html">Use the Filter class</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/queue.html">Use the Queue class</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/events.html">Use the Events class</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p class="caption" role="heading"><span class="caption-text">API Reference</span></p>
|
|
||||||
<ul>
|
|
||||||
<li class="toctree-l1 has-children"><a class="reference internal" href="api/index.html">API Reference</a><input class="toctree-checkbox" id="toctree-checkbox-2" name="toctree-checkbox-2" role="switch" type="checkbox"/><label for="toctree-checkbox-2"><div class="visually-hidden">Toggle child pages in navigation</div><i class="icon"><svg><use href="#svg-arrow-right"></use></svg></i></label><ul>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/enums.html">Enums</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/events.html">Events</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/exceptions.html">Exceptions</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/filters.html">Filters</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/objects.html">Objects</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/player.html">Player</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/pool.html">Pool</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/queue.html">Queue</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/utils.html">Utils</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
<div class="main">
|
|
||||||
<div class="content">
|
|
||||||
<div class="article-container">
|
|
||||||
<a href="#" class="back-to-top muted-link">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
|
|
||||||
</svg>
|
|
||||||
<span>Back to top</span>
|
|
||||||
</a>
|
|
||||||
<div class="content-icon-container">
|
|
||||||
<div class="edit-this-page">
|
|
||||||
<a class="muted-link" href="https://github.com/cloudwithax/pomice/edit/main/docs/index.md" title="Edit this page">
|
|
||||||
<svg aria-hidden="true" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
||||||
<path d="M4 20h4l10.5 -10.5a1.5 1.5 0 0 0 -4 -4l-10.5 10.5v4" />
|
|
||||||
<line x1="13.5" y1="6.5" x2="17.5" y2="10.5" />
|
|
||||||
</svg>
|
|
||||||
<span class="visually-hidden">Edit this page</span>
|
|
||||||
</a>
|
|
||||||
</div><div class="theme-toggle-container theme-toggle-content">
|
|
||||||
<button class="theme-toggle">
|
|
||||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
|
||||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
|
||||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
|
||||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<label class="toc-overlay-icon toc-content-icon no-toc" for="__toc">
|
|
||||||
<div class="visually-hidden">Toggle table of contents sidebar</div>
|
|
||||||
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<article role="main">
|
|
||||||
<section class="tex2jax_ignore mathjax_ignore" id="pomice">
|
|
||||||
<h1>Pomice<a class="headerlink" href="#pomice" title="Permalink to this heading">#</a></h1>
|
|
||||||
<p><img alt="" src="https://raw.githubusercontent.com/cloudwithax/pomice/main/banner.jpg" /></p>
|
|
||||||
<p>Pomice is a fully asynchronous Python library designed for communicating with <a class="reference external" href="https://github.com/freyacodes/Lavalink">Lavalink</a> seamlessly within the <a class="reference external" href="https://github.com/Rapptz/discord.py">discord.py</a> library. It features 100% API coverage of the entire <a class="reference external" href="https://github.com/freyacodes/Lavalink">Lavalink</a> spec that can be accessed with easy-to-understand functions. We also include Spotify and Apple Music querying capabilites using built-in custom clients, making it easier to develop your next big music bot.</p>
|
|
||||||
<section id="quick-links">
|
|
||||||
<h2>Quick Links:<a class="headerlink" href="#quick-links" title="Permalink to this heading">#</a></h2>
|
|
||||||
<ul class="simple">
|
|
||||||
<li><p><a class="reference internal" href="installation.html"><span class="doc std std-doc">Installation</span></a></p></li>
|
|
||||||
<li><p><a class="reference internal" href="quickstart.html"><span class="doc std std-doc">Quickstart</span></a></p></li>
|
|
||||||
<li><p><a class="reference internal" href="faq.html"><span class="doc std std-doc">Frequently Asked Questions</span></a></p></li>
|
|
||||||
<li><p><a class="reference internal" href="hdi/index.html"><span class="doc std std-doc">How Do I?</span></a></p></li>
|
|
||||||
<li><p><a class="reference internal" href="api/index.html"><span class="doc std std-doc">API Reference</span></a></p></li>
|
|
||||||
</ul>
|
|
||||||
<div class="toctree-wrapper compound">
|
|
||||||
</div>
|
|
||||||
<div class="toctree-wrapper compound">
|
|
||||||
</div>
|
|
||||||
<div class="toctree-wrapper compound">
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
<footer>
|
|
||||||
|
|
||||||
<div class="related-pages">
|
|
||||||
<a class="next-page" href="installation.html">
|
|
||||||
<div class="page-info">
|
|
||||||
<div class="context">
|
|
||||||
<span>Next</span>
|
|
||||||
</div>
|
|
||||||
<div class="title">Installation</div>
|
|
||||||
</div>
|
|
||||||
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="bottom-of-page">
|
|
||||||
<div class="left-details">
|
|
||||||
<div class="copyright">
|
|
||||||
Copyright © 2023, cloudwithax
|
|
||||||
</div>
|
|
||||||
Made with <a href="https://www.sphinx-doc.org/">Sphinx</a> and <a class="muted-link" href="https://pradyunsg.me">@pradyunsg</a>'s
|
|
||||||
|
|
||||||
<a href="https://github.com/pradyunsg/furo">Furo</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="right-details">
|
|
||||||
<div class="icons">
|
|
||||||
<a class="muted-link " href="https://github.com/cloudwithax/pomice" aria-label="GitHub">
|
|
||||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
|
|
||||||
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
<aside class="toc-drawer no-toc">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</div><script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
|
||||||
<script src="_static/jquery.js"></script>
|
|
||||||
<script src="_static/underscore.js"></script>
|
|
||||||
<script src="_static/_sphinx_javascript_frameworks_compat.js"></script>
|
|
||||||
<script src="_static/doctools.js"></script>
|
|
||||||
<script src="_static/sphinx_highlight.js"></script>
|
|
||||||
<script src="_static/scripts/furo.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Binary file not shown.
|
|
@ -1,341 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html class="no-js" lang="en">
|
|
||||||
<head><meta charset="utf-8"/>
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
||||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" />
|
|
||||||
|
|
||||||
<!-- Generated with Sphinx 5.3.0 and Furo 2022.12.07 --><title>Python Module Index - Pomice</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=91d0f0d1c444bdcb17a68e833c7a53903343c195" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
--color-code-background: #f8f8f8;
|
|
||||||
--color-code-foreground: black;
|
|
||||||
|
|
||||||
}
|
|
||||||
@media not print {
|
|
||||||
body[data-theme="dark"] {
|
|
||||||
--color-code-background: #202020;
|
|
||||||
--color-code-foreground: #d0d0d0;
|
|
||||||
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body:not([data-theme="light"]) {
|
|
||||||
--color-code-background: #202020;
|
|
||||||
--color-code-foreground: #d0d0d0;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style></head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
|
||||||
<symbol id="svg-toc" viewBox="0 0 24 24">
|
|
||||||
<title>Contents</title>
|
|
||||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
|
|
||||||
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="svg-menu" viewBox="0 0 24 24">
|
|
||||||
<title>Menu</title>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
|
|
||||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
|
||||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
|
||||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
|
|
||||||
<title>Expand</title>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
|
|
||||||
<polyline points="9 18 15 12 9 6"></polyline>
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="svg-sun" viewBox="0 0 24 24">
|
|
||||||
<title>Light mode</title>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
|
|
||||||
<circle cx="12" cy="12" r="5"></circle>
|
|
||||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
|
||||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
|
||||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
|
||||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
|
||||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
|
||||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
|
||||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
|
||||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="svg-moon" viewBox="0 0 24 24">
|
|
||||||
<title>Dark mode</title>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="svg-sun-half" viewBox="0 0 24 24">
|
|
||||||
<title>Auto light/dark mode</title>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
||||||
<circle cx="12" cy="12" r="9" />
|
|
||||||
<path d="M13 12h5" />
|
|
||||||
<path d="M13 15h4" />
|
|
||||||
<path d="M13 18h1" />
|
|
||||||
<path d="M13 9h4" />
|
|
||||||
<path d="M13 6h1" />
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
|
|
||||||
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
|
|
||||||
<label class="overlay sidebar-overlay" for="__navigation">
|
|
||||||
<div class="visually-hidden">Hide navigation sidebar</div>
|
|
||||||
</label>
|
|
||||||
<label class="overlay toc-overlay" for="__toc">
|
|
||||||
<div class="visually-hidden">Hide table of contents sidebar</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="page">
|
|
||||||
<header class="mobile-header">
|
|
||||||
<div class="header-left">
|
|
||||||
<label class="nav-overlay-icon" for="__navigation">
|
|
||||||
<div class="visually-hidden">Toggle site navigation sidebar</div>
|
|
||||||
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="header-center">
|
|
||||||
<a href="index.html"><div class="brand">Pomice</div></a>
|
|
||||||
</div>
|
|
||||||
<div class="header-right">
|
|
||||||
<div class="theme-toggle-container theme-toggle-header">
|
|
||||||
<button class="theme-toggle">
|
|
||||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
|
||||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
|
||||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
|
||||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<label class="toc-overlay-icon toc-header-icon no-toc" for="__toc">
|
|
||||||
<div class="visually-hidden">Toggle table of contents sidebar</div>
|
|
||||||
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<aside class="sidebar-drawer">
|
|
||||||
<div class="sidebar-container">
|
|
||||||
|
|
||||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
|
|
||||||
|
|
||||||
|
|
||||||
<span class="sidebar-brand-text">Pomice</span>
|
|
||||||
|
|
||||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
|
||||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
|
||||||
<input type="hidden" name="check_keywords" value="yes">
|
|
||||||
<input type="hidden" name="area" value="default">
|
|
||||||
</form>
|
|
||||||
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
|
|
||||||
<p class="caption" role="heading"><span class="caption-text">Before You Start</span></p>
|
|
||||||
<ul>
|
|
||||||
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
|
|
||||||
<li class="toctree-l1"><a class="reference internal" href="quickstart.html">Quick Jumpstart</a></li>
|
|
||||||
<li class="toctree-l1"><a class="reference internal" href="faq.html">Frequently Asked Questions</a></li>
|
|
||||||
</ul>
|
|
||||||
<p class="caption" role="heading"><span class="caption-text">How Do I?</span></p>
|
|
||||||
<ul>
|
|
||||||
<li class="toctree-l1 has-children"><a class="reference internal" href="hdi/index.html">How Do I?</a><input class="toctree-checkbox" id="toctree-checkbox-1" name="toctree-checkbox-1" role="switch" type="checkbox"/><label for="toctree-checkbox-1"><div class="visually-hidden">Toggle child pages in navigation</div><i class="icon"><svg><use href="#svg-arrow-right"></use></svg></i></label><ul>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/pool.html">Use the NodePool class</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/node.html">Use the Node class</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/player.html">Use the Player class</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/filters.html">Use the Filter class</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/queue.html">Use the Queue class</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/events.html">Use the Events class</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p class="caption" role="heading"><span class="caption-text">API Reference</span></p>
|
|
||||||
<ul>
|
|
||||||
<li class="toctree-l1 has-children"><a class="reference internal" href="api/index.html">API Reference</a><input class="toctree-checkbox" id="toctree-checkbox-2" name="toctree-checkbox-2" role="switch" type="checkbox"/><label for="toctree-checkbox-2"><div class="visually-hidden">Toggle child pages in navigation</div><i class="icon"><svg><use href="#svg-arrow-right"></use></svg></i></label><ul>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/enums.html">Enums</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/events.html">Events</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/exceptions.html">Exceptions</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/filters.html">Filters</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/objects.html">Objects</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/player.html">Player</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/pool.html">Pool</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/queue.html">Queue</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/utils.html">Utils</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
<div class="main">
|
|
||||||
<div class="content">
|
|
||||||
<div class="article-container">
|
|
||||||
<a href="#" class="back-to-top muted-link">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
|
|
||||||
</svg>
|
|
||||||
<span>Back to top</span>
|
|
||||||
</a>
|
|
||||||
<div class="content-icon-container">
|
|
||||||
<div class="theme-toggle-container theme-toggle-content">
|
|
||||||
<button class="theme-toggle">
|
|
||||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
|
||||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
|
||||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
|
||||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<label class="toc-overlay-icon toc-content-icon no-toc" for="__toc">
|
|
||||||
<div class="visually-hidden">Toggle table of contents sidebar</div>
|
|
||||||
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<article role="main">
|
|
||||||
|
|
||||||
<section class="domainindex-section">
|
|
||||||
<h1>Python Module Index</h1>
|
|
||||||
<div class="domainindex-jumpbox"><a href="#cap-p"><strong>p</strong></a></div>
|
|
||||||
</section>
|
|
||||||
<table class="domainindex-table">
|
|
||||||
<tr class="pcap">
|
|
||||||
<td></td><td> </td><td></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="cap" id="cap-p">
|
|
||||||
<td></td><td><strong>p</strong></td><td></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><img src="_static/minus.png" class="toggler"
|
|
||||||
id="toggle-1" style="display: none" alt="-" /></td>
|
|
||||||
<td>
|
|
||||||
<code class="xref">pomice</code></td><td>
|
|
||||||
<em></em></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="cg-1">
|
|
||||||
<td></td>
|
|
||||||
<td>   
|
|
||||||
<a href="api/enums.html#module-pomice.enums"><code class="xref">pomice.enums</code></a></td><td>
|
|
||||||
<em></em></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="cg-1">
|
|
||||||
<td></td>
|
|
||||||
<td>   
|
|
||||||
<a href="api/events.html#module-pomice.events"><code class="xref">pomice.events</code></a></td><td>
|
|
||||||
<em></em></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="cg-1">
|
|
||||||
<td></td>
|
|
||||||
<td>   
|
|
||||||
<a href="api/exceptions.html#module-pomice.exceptions"><code class="xref">pomice.exceptions</code></a></td><td>
|
|
||||||
<em></em></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="cg-1">
|
|
||||||
<td></td>
|
|
||||||
<td>   
|
|
||||||
<a href="api/filters.html#module-pomice.filters"><code class="xref">pomice.filters</code></a></td><td>
|
|
||||||
<em></em></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="cg-1">
|
|
||||||
<td></td>
|
|
||||||
<td>   
|
|
||||||
<a href="api/objects.html#module-pomice.objects"><code class="xref">pomice.objects</code></a></td><td>
|
|
||||||
<em></em></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="cg-1">
|
|
||||||
<td></td>
|
|
||||||
<td>   
|
|
||||||
<a href="api/player.html#module-pomice.player"><code class="xref">pomice.player</code></a></td><td>
|
|
||||||
<em></em></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="cg-1">
|
|
||||||
<td></td>
|
|
||||||
<td>   
|
|
||||||
<a href="api/pool.html#module-pomice.pool"><code class="xref">pomice.pool</code></a></td><td>
|
|
||||||
<em></em></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="cg-1">
|
|
||||||
<td></td>
|
|
||||||
<td>   
|
|
||||||
<a href="api/queue.html#module-pomice.queue"><code class="xref">pomice.queue</code></a></td><td>
|
|
||||||
<em></em></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="cg-1">
|
|
||||||
<td></td>
|
|
||||||
<td>   
|
|
||||||
<a href="api/utils.html#module-pomice.utils"><code class="xref">pomice.utils</code></a></td><td>
|
|
||||||
<em></em></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
<footer>
|
|
||||||
|
|
||||||
<div class="related-pages">
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="bottom-of-page">
|
|
||||||
<div class="left-details">
|
|
||||||
<div class="copyright">
|
|
||||||
Copyright © 2023, cloudwithax
|
|
||||||
</div>
|
|
||||||
Made with <a href="https://www.sphinx-doc.org/">Sphinx</a> and <a class="muted-link" href="https://pradyunsg.me">@pradyunsg</a>'s
|
|
||||||
|
|
||||||
<a href="https://github.com/pradyunsg/furo">Furo</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="right-details">
|
|
||||||
<div class="icons">
|
|
||||||
<a class="muted-link " href="https://github.com/cloudwithax/pomice" aria-label="GitHub">
|
|
||||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
|
|
||||||
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
<aside class="toc-drawer no-toc">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</div><script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
|
||||||
<script src="_static/jquery.js"></script>
|
|
||||||
<script src="_static/underscore.js"></script>
|
|
||||||
<script src="_static/_sphinx_javascript_frameworks_compat.js"></script>
|
|
||||||
<script src="_static/doctools.js"></script>
|
|
||||||
<script src="_static/sphinx_highlight.js"></script>
|
|
||||||
<script src="_static/scripts/furo.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,280 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html class="no-js" lang="en">
|
|
||||||
<head><meta charset="utf-8"/>
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
||||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="#" />
|
|
||||||
|
|
||||||
<!-- Generated with Sphinx 5.3.0 and Furo 2022.12.07 --><title>Search - Pomice</title><link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=91d0f0d1c444bdcb17a68e833c7a53903343c195" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
--color-code-background: #f8f8f8;
|
|
||||||
--color-code-foreground: black;
|
|
||||||
|
|
||||||
}
|
|
||||||
@media not print {
|
|
||||||
body[data-theme="dark"] {
|
|
||||||
--color-code-background: #202020;
|
|
||||||
--color-code-foreground: #d0d0d0;
|
|
||||||
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body:not([data-theme="light"]) {
|
|
||||||
--color-code-background: #202020;
|
|
||||||
--color-code-foreground: #d0d0d0;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style></head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
|
||||||
<symbol id="svg-toc" viewBox="0 0 24 24">
|
|
||||||
<title>Contents</title>
|
|
||||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
|
|
||||||
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="svg-menu" viewBox="0 0 24 24">
|
|
||||||
<title>Menu</title>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
|
|
||||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
|
||||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
|
||||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
|
|
||||||
<title>Expand</title>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
|
|
||||||
<polyline points="9 18 15 12 9 6"></polyline>
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="svg-sun" viewBox="0 0 24 24">
|
|
||||||
<title>Light mode</title>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
|
|
||||||
<circle cx="12" cy="12" r="5"></circle>
|
|
||||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
|
||||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
|
||||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
|
||||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
|
||||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
|
||||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
|
||||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
|
||||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="svg-moon" viewBox="0 0 24 24">
|
|
||||||
<title>Dark mode</title>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
|
||||||
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="svg-sun-half" viewBox="0 0 24 24">
|
|
||||||
<title>Auto light/dark mode</title>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
||||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
||||||
<circle cx="12" cy="12" r="9" />
|
|
||||||
<path d="M13 12h5" />
|
|
||||||
<path d="M13 15h4" />
|
|
||||||
<path d="M13 18h1" />
|
|
||||||
<path d="M13 9h4" />
|
|
||||||
<path d="M13 6h1" />
|
|
||||||
</svg>
|
|
||||||
</symbol>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
|
|
||||||
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
|
|
||||||
<label class="overlay sidebar-overlay" for="__navigation">
|
|
||||||
<div class="visually-hidden">Hide navigation sidebar</div>
|
|
||||||
</label>
|
|
||||||
<label class="overlay toc-overlay" for="__toc">
|
|
||||||
<div class="visually-hidden">Hide table of contents sidebar</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="page">
|
|
||||||
<header class="mobile-header">
|
|
||||||
<div class="header-left">
|
|
||||||
<label class="nav-overlay-icon" for="__navigation">
|
|
||||||
<div class="visually-hidden">Toggle site navigation sidebar</div>
|
|
||||||
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="header-center">
|
|
||||||
<a href="index.html"><div class="brand">Pomice</div></a>
|
|
||||||
</div>
|
|
||||||
<div class="header-right">
|
|
||||||
<div class="theme-toggle-container theme-toggle-header">
|
|
||||||
<button class="theme-toggle">
|
|
||||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
|
||||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
|
||||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
|
||||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<label class="toc-overlay-icon toc-header-icon no-toc" for="__toc">
|
|
||||||
<div class="visually-hidden">Toggle table of contents sidebar</div>
|
|
||||||
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<aside class="sidebar-drawer">
|
|
||||||
<div class="sidebar-container">
|
|
||||||
|
|
||||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
|
|
||||||
|
|
||||||
|
|
||||||
<span class="sidebar-brand-text">Pomice</span>
|
|
||||||
|
|
||||||
</a><form class="sidebar-search-container" method="get" action="#" role="search">
|
|
||||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
|
||||||
<input type="hidden" name="check_keywords" value="yes">
|
|
||||||
<input type="hidden" name="area" value="default">
|
|
||||||
</form>
|
|
||||||
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
|
|
||||||
<p class="caption" role="heading"><span class="caption-text">Before You Start</span></p>
|
|
||||||
<ul>
|
|
||||||
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
|
|
||||||
<li class="toctree-l1"><a class="reference internal" href="quickstart.html">Quick Jumpstart</a></li>
|
|
||||||
<li class="toctree-l1"><a class="reference internal" href="faq.html">Frequently Asked Questions</a></li>
|
|
||||||
</ul>
|
|
||||||
<p class="caption" role="heading"><span class="caption-text">How Do I?</span></p>
|
|
||||||
<ul>
|
|
||||||
<li class="toctree-l1 has-children"><a class="reference internal" href="hdi/index.html">How Do I?</a><input class="toctree-checkbox" id="toctree-checkbox-1" name="toctree-checkbox-1" role="switch" type="checkbox"/><label for="toctree-checkbox-1"><div class="visually-hidden">Toggle child pages in navigation</div><i class="icon"><svg><use href="#svg-arrow-right"></use></svg></i></label><ul>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/pool.html">Use the NodePool class</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/node.html">Use the Node class</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/player.html">Use the Player class</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/filters.html">Use the Filter class</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/queue.html">Use the Queue class</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="hdi/events.html">Use the Events class</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p class="caption" role="heading"><span class="caption-text">API Reference</span></p>
|
|
||||||
<ul>
|
|
||||||
<li class="toctree-l1 has-children"><a class="reference internal" href="api/index.html">API Reference</a><input class="toctree-checkbox" id="toctree-checkbox-2" name="toctree-checkbox-2" role="switch" type="checkbox"/><label for="toctree-checkbox-2"><div class="visually-hidden">Toggle child pages in navigation</div><i class="icon"><svg><use href="#svg-arrow-right"></use></svg></i></label><ul>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/enums.html">Enums</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/events.html">Events</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/exceptions.html">Exceptions</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/filters.html">Filters</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/objects.html">Objects</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/player.html">Player</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/pool.html">Pool</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/queue.html">Queue</a></li>
|
|
||||||
<li class="toctree-l2"><a class="reference internal" href="api/utils.html">Utils</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
<div class="main">
|
|
||||||
<div class="content">
|
|
||||||
<div class="article-container">
|
|
||||||
<a href="#" class="back-to-top muted-link">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
|
|
||||||
</svg>
|
|
||||||
<span>Back to top</span>
|
|
||||||
</a>
|
|
||||||
<div class="content-icon-container">
|
|
||||||
<div class="theme-toggle-container theme-toggle-content">
|
|
||||||
<button class="theme-toggle">
|
|
||||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
|
||||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
|
||||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
|
||||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<label class="toc-overlay-icon toc-content-icon no-toc" for="__toc">
|
|
||||||
<div class="visually-hidden">Toggle table of contents sidebar</div>
|
|
||||||
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<article role="main">
|
|
||||||
|
|
||||||
<noscript>
|
|
||||||
<div class="admonition error">
|
|
||||||
<p class="admonition-title">Error</p>
|
|
||||||
<p>
|
|
||||||
Please activate JavaScript to enable the search functionality.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</noscript>
|
|
||||||
|
|
||||||
<div id="search-results"></div>
|
|
||||||
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
<footer>
|
|
||||||
|
|
||||||
<div class="related-pages">
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="bottom-of-page">
|
|
||||||
<div class="left-details">
|
|
||||||
<div class="copyright">
|
|
||||||
Copyright © 2023, cloudwithax
|
|
||||||
</div>
|
|
||||||
Made with <a href="https://www.sphinx-doc.org/">Sphinx</a> and <a class="muted-link" href="https://pradyunsg.me">@pradyunsg</a>'s
|
|
||||||
|
|
||||||
<a href="https://github.com/pradyunsg/furo">Furo</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="right-details">
|
|
||||||
<div class="icons">
|
|
||||||
<a class="muted-link " href="https://github.com/cloudwithax/pomice" aria-label="GitHub">
|
|
||||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
|
|
||||||
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
<aside class="toc-drawer no-toc">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</div><script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
|
||||||
<script src="_static/jquery.js"></script>
|
|
||||||
<script src="_static/underscore.js"></script>
|
|
||||||
<script src="_static/_sphinx_javascript_frameworks_compat.js"></script>
|
|
||||||
<script src="_static/doctools.js"></script>
|
|
||||||
<script src="_static/sphinx_highlight.js"></script>
|
|
||||||
<script src="_static/scripts/furo.js"></script>
|
|
||||||
|
|
||||||
<script src="_static/searchtools.js"></script>
|
|
||||||
<script src="_static/language_data.js"></script>
|
|
||||||
<script src="searchindex.js"></script></body>
|
|
||||||
</html>
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1,50 @@
|
||||||
# Use the Events class
|
# Use the Events class
|
||||||
|
|
||||||
|
Pomice has different events that are triggered depending on events that Lavalink emits:
|
||||||
|
- `Event.TrackEndEvent()`
|
||||||
|
- `Event.TrackExceptionEvent()`
|
||||||
|
- `Event.TrackStartEvent()`
|
||||||
|
- `Event.TrackStuckEvent()`
|
||||||
|
- `Event.WebsoocketClosedEvent()`
|
||||||
|
- `Event.WebsocketOpenEvent()`
|
||||||
|
|
||||||
|
|
||||||
|
The classes listed here are as they appear in Pomice. When you use them within your application,
|
||||||
|
the way you use them will be different. Here's an example on how you would use the `TrackStartEvent` within an event listener in a cog:
|
||||||
|
|
||||||
|
```py
|
||||||
|
|
||||||
|
@commands.Cog.listener
|
||||||
|
async def on_pomice_track_start(self, player: Player, track: Track):
|
||||||
|
...
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event definitions
|
||||||
|
|
||||||
|
Each event within Pomice has an event definition you can use to listen for said event within
|
||||||
|
your application. Here are all the definitions:
|
||||||
|
|
||||||
|
- `Event.TrackEndEvent()` -> `on_pomice_track_end`
|
||||||
|
- `Event.TrackExceptionEvent()` -> `on_pomice_track_exception`
|
||||||
|
- `Event.TrackStartEvent()` -> `on_pomice_track_start`
|
||||||
|
- `Event.TrackStuckEvent()` -> `on_pomice_track_stuck`
|
||||||
|
- `Event.WebsoocketClosedEvent()` -> `on_pomice_websocket_closed`
|
||||||
|
- `Event.WebsocketOpenEvent()` -> `on_pomice_websocket_open`
|
||||||
|
|
||||||
|
|
||||||
|
All events related to tracks carry a `Player` object so you can access player-specific functions
|
||||||
|
and properties for further evaluation. They also carry a `Track` object so you can access track-specific functions and properites for further evaluation as well.
|
||||||
|
|
||||||
|
`Event.TrackEndEvent()` carries the reason for the track ending. If the track ends suddenly, you can use the reason provided to determine a solution.
|
||||||
|
|
||||||
|
`Event.TrackExceptionEvent()` carries the exception, or reason why the track failed to play. The format for the exception is `REASON [SEVERITY]`.
|
||||||
|
|
||||||
|
`Event.TrackStuckEvent()` carries the threshold, or amount of time Lavalink will wait before it discards the stuck track and stops it from playing.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -459,7 +459,13 @@ await Player.reset_filters()
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
After you have initialized your function, you can optionally include the `fast_apply` parameter, which is a boolean. If this is set to `True`, it'll remove all filters (almost) instantly if theres a track playing.
|
||||||
|
|
||||||
|
```py
|
||||||
|
|
||||||
|
await Player.reset_filters(fast_apply=<True/False>)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue