1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-27 13:34:28 +00:00

MORE MORE MORE DOCS

This commit is contained in:
Steffo 2019-04-24 01:44:10 +02:00
parent 7583f1330f
commit b2c28f1735
14 changed files with 186 additions and 43 deletions

View file

@ -7,3 +7,6 @@ royalnet.audio
.. automodule:: royalnet.audio .. automodule:: royalnet.audio
:members: :members:
:private-members:
:undoc-members:

View file

@ -4,5 +4,8 @@ royalnet.bots
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
.. automodule:: royalnet.bots .. automodule:: royalnet.bots
:members: :members:
:private-members:
:undoc-members:

View file

@ -7,3 +7,5 @@ royalnet.commands
.. automodule:: royalnet.commands .. automodule:: royalnet.commands
:members: :members:
:private-members:
:special-members:

View file

@ -32,6 +32,17 @@ extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.intersphi
intersphinx_mapping = {'python': ('https://docs.python.org/3.7', None), intersphinx_mapping = {'python': ('https://docs.python.org/3.7', None),
'discord': ('https://discordpy.readthedocs.io/en/latest/', None)} 'discord': ('https://discordpy.readthedocs.io/en/latest/', None)}
def skip(app, what, name, obj, would_skip, options):
if name == "__init__":
return False
return would_skip
def setup(app):
app.connect("autodoc-skip-member", skip)
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']

View file

@ -7,6 +7,8 @@ royalnet.database
.. automodule:: royalnet.database .. automodule:: royalnet.database
:members: :members:
:private-members:
:undoc-members:
Tables Tables
@ -14,3 +16,5 @@ Tables
.. automodule:: royalnet.database.tables .. automodule:: royalnet.database.tables
:members: :members:
:private-members:
:undoc-members:

View file

@ -7,3 +7,6 @@ royalnet.network
.. automodule:: royalnet.network .. automodule:: royalnet.network
:members: :members:
:private-members:
:undoc-members:

View file

@ -7,3 +7,6 @@ royalnet.utils
.. automodule:: royalnet.utils .. automodule:: royalnet.utils
:members: :members:
:private-members:
:undoc-members:

View file

@ -10,36 +10,52 @@ class PlayMode:
def __init__(self): def __init__(self):
"""Create a new PlayMode and initialize the generator inside.""" """Create a new PlayMode and initialize the generator inside."""
self.now_playing: typing.Optional[RoyalPCMAudio] = None self.now_playing: typing.Optional[RoyalPCMAudio] = None
self.generator: typing.AsyncGenerator = self.generate_generator() self.generator: typing.AsyncGenerator = self._generate_generator()
async def next(self): async def next(self) -> typing.Optional[RoyalPCMAudio]:
"""Get the next :py:class:`royalnet.audio.RoyalPCMAudio` from the list and advance it.
Returns:
The next :py:class:`royalnet.audio.RoyalPCMAudio`."""
return await self.generator.__anext__() return await self.generator.__anext__()
def videos_left(self) -> typing.Union[int, float]: def videos_left(self) -> typing.Union[int, float]:
"""Return the number of videos left in the PlayMode. """Return the number of videos left in the PlayMode.
Usually returns an :py:class:`int`, but may return :py:obj:`math.inf` if the PlayMode is infinite.""" Returns:
Usually a :py:class:`int`, but may return also :py:obj:`math.inf` if the PlayMode is infinite."""
raise NotImplementedError() raise NotImplementedError()
async def generate_generator(self): async def _generate_generator(self):
"""Get the next :py:class:`royalnet.audio.RoyalPCMAudio` from the list and advance it.""" """Factory function for an async generator that changes the ``now_playing`` property either to a :py:class:`discord.audio.RoyalPCMAudio` or to ``None``, then yields the value it changed it to.
Yields:
The :py:class:`royalnet.audio.RoyalPCMAudio` to be played next."""
raise NotImplementedError() raise NotImplementedError()
# This is needed to make the coroutine an async generator # This is needed to make the coroutine an async generator
# noinspection PyUnreachableCode # noinspection PyUnreachableCode
yield NotImplemented yield NotImplemented
def add(self, item): def add(self, item: RoyalPCMAudio) -> None:
"""Add a new :py:class:`royalnet.audio.RoyalPCMAudio` to the PlayMode.""" """Add a new :py:class:`royalnet.audio.RoyalPCMAudio` to the PlayMode.
Args:
item: The item to add to the PlayMode."""
raise NotImplementedError() raise NotImplementedError()
def delete(self): def delete(self) -> None:
"""Delete all :py:class:`royalnet.audio.RoyalPCMAudio` contained inside this PlayMode.""" """Delete all :py:class:`royalnet.audio.RoyalPCMAudio` contained inside this PlayMode."""
raise NotImplementedError() raise NotImplementedError()
class Playlist(PlayMode): class Playlist(PlayMode):
"""A video list. :py:class:`royalnet.audio.RoyalPCMAudio` played are removed from the list.""" """A video list. :py:class:`royalnet.audio.RoyalPCMAudio` played are removed from the list."""
def __init__(self, starting_list: typing.List[RoyalPCMAudio] = None): def __init__(self, starting_list: typing.List[RoyalPCMAudio] = None):
"""Create a new Playlist.
Args:
starting_list: A list of items with which the Playlist will be created."""
super().__init__() super().__init__()
if starting_list is None: if starting_list is None:
starting_list = [] starting_list = []
@ -48,7 +64,7 @@ class Playlist(PlayMode):
def videos_left(self) -> typing.Union[int, float]: def videos_left(self) -> typing.Union[int, float]:
return len(self.list) return len(self.list)
async def generate_generator(self): async def _generate_generator(self):
while True: while True:
try: try:
next_video = self.list.pop(0) next_video = self.list.pop(0)
@ -60,10 +76,10 @@ class Playlist(PlayMode):
if self.now_playing is not None: if self.now_playing is not None:
self.now_playing.delete() self.now_playing.delete()
def add(self, item): def add(self, item) -> None:
self.list.append(item) self.list.append(item)
def delete(self): def delete(self) -> None:
while self.list: while self.list:
self.list.pop(0).delete() self.list.pop(0).delete()
self.now_playing.delete() self.now_playing.delete()
@ -71,7 +87,12 @@ class Playlist(PlayMode):
class Pool(PlayMode): class Pool(PlayMode):
"""A :py:class:`royalnet.audio.RoyalPCMAudio` pool. :py:class:`royalnet.audio.RoyalPCMAudio` are selected in random order and are not repeated until every song has been played at least once.""" """A :py:class:`royalnet.audio.RoyalPCMAudio` pool. :py:class:`royalnet.audio.RoyalPCMAudio` are selected in random order and are not repeated until every song has been played at least once."""
def __init__(self, starting_pool: typing.List[RoyalPCMAudio] = None): def __init__(self, starting_pool: typing.List[RoyalPCMAudio] = None):
"""Create a new Pool.
Args:
starting_pool: A list of items the Pool will be created from."""
super().__init__() super().__init__()
if starting_pool is None: if starting_pool is None:
starting_pool = [] starting_pool = []
@ -81,7 +102,7 @@ class Pool(PlayMode):
def videos_left(self) -> typing.Union[int, float]: def videos_left(self) -> typing.Union[int, float]:
return math.inf return math.inf
async def generate_generator(self): async def _generate_generator(self):
while True: while True:
if not self.pool: if not self.pool:
self.now_playing = None self.now_playing = None
@ -94,12 +115,12 @@ class Pool(PlayMode):
self.now_playing = next_video self.now_playing = next_video
yield next_video yield next_video
def add(self, item): def add(self, item) -> None:
self.pool.append(item) self.pool.append(item)
self._pool_copy.append(item) self._pool_copy.append(item)
random.shuffle(self._pool_copy) random.shuffle(self._pool_copy)
def delete(self): def delete(self) -> None:
for item in self.pool: for item in self.pool:
item.delete() item.delete()
self.pool = None self.pool = None

View file

@ -8,26 +8,35 @@ class RoyalPCMAudio(AudioSource):
"""A discord-compatible :py:class:`discord.AudioSource` that keeps data in a file instead of in memory.""" """A discord-compatible :py:class:`discord.AudioSource` that keeps data in a file instead of in memory."""
def __init__(self, rpf: "RoyalPCMFile"): def __init__(self, rpf: "RoyalPCMFile"):
"""Create a :py:class:`discord.audio.RoyalPCMAudio` from a :ref:`discord.audio.RoyalPCMFile`. Not recommended, use """ """Create a :py:class:`discord.audio.RoyalPCMAudio` from a :py:class:`royalnet.audio.RoyalPCMFile`.
Warning:
Not recommended, use :py:func:`royalnet.audio.RoyalPCMAudio.create_from_url` or :py:func:`royalnet.audio.RoyalPCMAudio.create_from_ytsearch` instead."""
self.rpf: "RoyalPCMFile" = rpf self.rpf: "RoyalPCMFile" = rpf
self._file = open(self.rpf.audio_filename, "rb") self._file = open(self.rpf.audio_filename, "rb")
@staticmethod @staticmethod
def create_from_url(url: str) -> typing.List["RoyalPCMAudio"]: def create_from_url(url: str) -> typing.List["RoyalPCMAudio"]:
"""Download a file with youtube_dl and create a list of :py:class:`discord.audio.RoyalPCMAudio`. """Download a file with youtube_dl and create a list of RoyalPCMAudios.
Parameters: Parameters:
url: The url of the file to download.""" url: The url of the file to download.
Returns:
A :py:class:`list` of RoyalPCMAudios, each corresponding to a downloaded video."""
rpf_list = RoyalPCMFile.create_from_url(url) rpf_list = RoyalPCMFile.create_from_url(url)
return [RoyalPCMAudio(rpf) for rpf in rpf_list] return [RoyalPCMAudio(rpf) for rpf in rpf_list]
@staticmethod @staticmethod
def create_from_ytsearch(search: str, amount: int = 1) -> typing.List["RoyalPCMAudio"]: def create_from_ytsearch(search: str, amount: int = 1) -> typing.List["RoyalPCMAudio"]:
"""Search a string on YouTube and download the first ``amount`` number of videos, then download those with youtube_dl and create a list of :py:class:`discord.audio.RoyalPCMAudio`. """Search a string on YouTube and download the first ``amount`` number of videos, then download those with youtube_dl and create a list of RoyalPCMAudios.
Parameters: Parameters:
search: The string to search on YouTube. search: The string to search on YouTube.
amount: The number of videos to download.""" amount: The number of videos to download.
Returns:
A :py:class:`list` of RoyalPCMAudios, each corresponding to a downloaded video."""
rpf_list = RoyalPCMFile.create_from_ytsearch(search, amount) rpf_list = RoyalPCMFile.create_from_ytsearch(search, amount)
return [RoyalPCMAudio(rpf) for rpf in rpf_list] return [RoyalPCMAudio(rpf) for rpf in rpf_list]

View file

@ -21,12 +21,12 @@ class RoyalPCMFile(YtdlFile):
# Set the time to generate the filename # Set the time to generate the filename
self._time = time.time() self._time = time.time()
# Ensure the file doesn't already exist # Ensure the file doesn't already exist
if os.path.exists(self._ytdl_filename) or os.path.exists(self.audio_filename): if os.path.exists(self.ytdl_filename) or os.path.exists(self.audio_filename):
raise FileExistsError("Can't overwrite file") raise FileExistsError("Can't overwrite file")
# Overwrite the new ytdl_args # Overwrite the new ytdl_args
self.ytdl_args = {**self.ytdl_args, **ytdl_args} self.ytdl_args = {**self.ytdl_args, **ytdl_args}
log.info(f"Now downloading {info.webpage_url}") log.info(f"Now downloading {info.webpage_url}")
super().__init__(info, outtmpl=self._ytdl_filename, **self.ytdl_args) super().__init__(info, outtmpl=self.ytdl_filename, **self.ytdl_args)
# Find the audio_filename with a regex (should be video.opus) # Find the audio_filename with a regex (should be video.opus)
log.info(f"Converting {self.video_filename}...") log.info(f"Converting {self.video_filename}...")
# Convert the video to pcm # Convert the video to pcm
@ -50,25 +50,48 @@ class RoyalPCMFile(YtdlFile):
"""Download a file with youtube_dl and create a list of :py:class:`discord.audio.RoyalPCMFile`. """Download a file with youtube_dl and create a list of :py:class:`discord.audio.RoyalPCMFile`.
Parameters: Parameters:
url: The url of the file to download.""" url: The url of the file to download.
ytdl_args: Extra arguments to be passed to YoutubeDL while downloading.
Returns:
A :py:class:`list` of RoyalPCMAudios, each corresponding to a downloaded video."""
info_list = YtdlInfo.create_from_url(url) info_list = YtdlInfo.create_from_url(url)
return [RoyalPCMFile(info, **ytdl_args) for info in info_list] return [RoyalPCMFile(info, **ytdl_args) for info in info_list]
@staticmethod @staticmethod
def create_from_ytsearch(search: str, amount: int = 1, **ytdl_args) -> typing.List["RoyalPCMFile"]: def create_from_ytsearch(search: str, amount: int = 1, **ytdl_args) -> typing.List["RoyalPCMFile"]:
"""Search a string on YouTube and download the first ``amount`` number of videos, then download those with youtube_dl and create a list of :py:class:`discord.audio.RoyalPCMFile`.""" """Search a string on YouTube and download the first ``amount`` number of videos, then download those with youtube_dl and create a list of :py:class:`discord.audio.RoyalPCMFile`.
Parameters:
search: The string to search on YouTube.
amount: The number of videos to download.
ytdl_args: Extra arguments to be passed to YoutubeDL while downloading.
Returns:
A :py:class:`list` of RoyalPCMFiles, each corresponding to a downloaded video."""
url = f"ytsearch{amount}:{search}" url = f"ytsearch{amount}:{search}"
info_list = YtdlInfo.create_from_url(url) info_list = YtdlInfo.create_from_url(url)
return [RoyalPCMFile(info, **ytdl_args) for info in info_list] return [RoyalPCMFile(info, **ytdl_args) for info in info_list]
@property @property
def _ytdl_filename(self): def ytdl_filename(self) -> str:
"""
Returns:
The name of the downloaded video file, as a :py:class:`str`.
Warning:
It's going to be deleted as soon as the :py:func:`discord.audio.RoyalPCMFile.__init__` function has completed, so it's probably not going to be very useful...
"""
return f"./downloads/{safefilename(self.info.title)}-{safefilename(str(int(self._time)))}.ytdl" return f"./downloads/{safefilename(self.info.title)}-{safefilename(str(int(self._time)))}.ytdl"
@property @property
def audio_filename(self): def audio_filename(self) -> str:
"""
Returns:
The name of the downloaded and PCM-converted audio file."""
return f"./downloads/{safefilename(self.info.title)}-{safefilename(str(int(self._time)))}.pcm" return f"./downloads/{safefilename(self.info.title)}-{safefilename(str(int(self._time)))}.pcm"
def delete_audio_file(self): def delete_audio_file(self):
"""Delete the PCM-converted audio file."""
log.info(f"Deleting {self.audio_filename}") log.info(f"Deleting {self.audio_filename}")
os.remove(self.audio_filename) os.remove(self.audio_filename)

View file

@ -15,6 +15,8 @@ class InterruptDownload(DownloaderError):
class YtdlFile: class YtdlFile:
"""A wrapper around a youtube_dl downloaded file."""
ytdl_args = { ytdl_args = {
"logger": log, # Log messages to a logging.Logger instance. "logger": log, # Log messages to a logging.Logger instance.
"quiet": True, # Do not print messages to stdout. "quiet": True, # Do not print messages to stdout.
@ -22,7 +24,6 @@ class YtdlFile:
"no_warnings": True, # Do not print out anything for warnings. "no_warnings": True, # Do not print out anything for warnings.
} }
"""A wrapper around a youtube_dl downloaded file."""
def __init__(self, info: "YtdlInfo", outtmpl="%(title)s-%(id)s.%(ext)s", **ytdl_args): def __init__(self, info: "YtdlInfo", outtmpl="%(title)s-%(id)s.%(ext)s", **ytdl_args):
self.info: "YtdlInfo" = info self.info: "YtdlInfo" = info
self.video_filename: str self.video_filename: str
@ -57,7 +58,11 @@ class YtdlFile:
class YtdlInfo: class YtdlInfo:
"""A wrapper around youtube_dl extracted info.""" """A wrapper around youtube_dl extracted info."""
def __init__(self, info): def __init__(self, info: typing.Dict[str, typing.Any]):
"""Create a YtdlInfo from the dict returned by the :py:func:`youtube_dl.YoutubeDL.extract_info` function.
Warning:
Does not download the info, for that use :py:func:`royalnet.audio.YtdlInfo.create_from_url`."""
self.id: typing.Optional[str] = info.get("id") self.id: typing.Optional[str] = info.get("id")
self.uploader: typing.Optional[str] = info.get("uploader") self.uploader: typing.Optional[str] = info.get("uploader")
self.uploader_id: typing.Optional[str] = info.get("uploader_id") self.uploader_id: typing.Optional[str] = info.get("uploader_id")

View file

@ -2,42 +2,70 @@ from ..error import RoyalnetError
class Message: class Message:
"""A message sent through the Royalnet."""
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}>" return f"<{self.__class__.__name__}>"
def raise_on_error(self):
pass
class IdentifySuccessfulMessage(Message): class IdentifySuccessfulMessage(Message):
pass """The Royalnet identification step was successful."""
class ServerErrorMessage(Message): class ServerErrorMessage(Message):
"""Something went wrong in the connection to the :py:class:`royalnet.network.RoyalnetServer`."""
def __init__(self, reason): def __init__(self, reason):
super().__init__() super().__init__()
self.reason = reason self.reason = reason
class InvalidSecretEM(ServerErrorMessage): class InvalidSecretEM(ServerErrorMessage):
pass """The sent secret was incorrect.
This message terminates connection to the :py:class:`royalnet.network.RoyalnetServer`."""
class InvalidPackageEM(ServerErrorMessage): class InvalidPackageEM(ServerErrorMessage):
pass """The sent :py:class:`royalnet.network.Package` was invalid."""
class InvalidDestinationEM(InvalidPackageEM): class InvalidDestinationEM(InvalidPackageEM):
"""The :py:class:`royalnet.network.Package` destination was invalid or not found."""
class Reply(Message):
"""A reply to a request sent through the Royalnet."""
def raise_on_error(self) -> None:
"""If the reply is an error, raise an error, otherwise, do nothing.
Raises:
A :py:exc:`RoyalnetError`, if the Reply is an error, otherwise, nothing."""
raise NotImplementedError()
class RequestSuccessful(Reply):
"""The sent request was successful."""
def raise_on_error(self) -> None:
"""If the reply is an error, raise an error, otherwise, do nothing.
Does nothing."""
pass pass
class RequestSuccessful(Message): class RequestError(Reply):
pass """The sent request wasn't successful."""
class RequestError(Message):
def __init__(self, exc: Exception): def __init__(self, exc: Exception):
"""Create a RequestError.
Parameters:
exc: The exception that caused the error in the request."""
self.exc: Exception = exc self.exc: Exception = exc
def raise_on_error(self): def raise_on_error(self) -> None:
"""If the reply is an error, raise an error, otherwise, do nothing.
Raises:
Always raises a :py:exc:`royalnet.error.RoyalnetError`, containing the exception that caused the error."""
raise RoyalnetError(exc=self.exc) raise RoyalnetError(exc=self.exc)

View file

@ -3,20 +3,42 @@ import uuid
class Package: class Package:
"""A Royalnet package, the data type with which a :py:class:`royalnet.network.RoyalnetLink` communicates with a :py:class:`royalnet.network.RoyalnetServer` or another link. """
def __init__(self, data, destination: str, source: str, *, source_conv_id: str = None, destination_conv_id: str = None): def __init__(self, data, destination: str, source: str, *, source_conv_id: str = None, destination_conv_id: str = None):
"""Create a Package.
Parameters:
data: The data that should be sent. Usually a :py:class:`royalnet.network.Message`.
destination: The ``link_type`` of the destination node, or alternatively, the ``nid`` of the node. Can also be the ``NULL`` value to send the message to nobody.
source: The ``nid`` of the node that created this Package.
source_conv_id: The conversation id of the node that created this package. Akin to the sequence number on IP packets.
destination_conv_id: The conversation id of the node that this Package is a reply to."""
# TODO: something is not right in these type hints. Check them.
self.data = data self.data = data
self.destination: str = destination self.destination: str = destination
self.source = source self.source: str = source
self.source_conv_id = source_conv_id or str(uuid.uuid4()) self.source_conv_id: str = source_conv_id or str(uuid.uuid4())
self.destination_conv_id = destination_conv_id self.destination_conv_id: str = destination_conv_id
def __repr__(self): def __repr__(self):
return f"<Package to {self.destination}: {self.data.__class__.__name__}>" return f"<Package to {self.destination}: {self.data.__class__.__name__}>"
def reply(self, data) -> "Package": def reply(self, data) -> "Package":
"""Reply to this Package with another Package.
Parameters:
data: The data that should be sent. Usually a :py:class:`royalnet.network.Message`.
Returns:
The reply Package."""
return Package(data, self.source, self.destination, return Package(data, self.source, self.destination,
source_conv_id=str(uuid.uuid4()), source_conv_id=str(uuid.uuid4()),
destination_conv_id=self.source_conv_id) destination_conv_id=self.source_conv_id)
def pickle(self): def pickle(self) -> bytes:
""":py:mod:`pickle` this Package.
Returns:
The pickled package in form of bytes."""
return pickle.dumps(self) return pickle.dumps(self)

View file

@ -85,7 +85,13 @@ class RoyalnetServer:
self._loop.create_task(self.route_package(package)) self._loop.create_task(self.route_package(package))
def find_destination(self, package: Package) -> typing.List[ConnectedClient]: def find_destination(self, package: Package) -> typing.List[ConnectedClient]:
"""Find a list of destinations for the sent packages""" """Find a list of destinations for the package.
Parameters:
package: The package to find the destination of.
Returns:
A :py:class:`list` of :py:class:`ConnectedClients` to send the package to."""
# Parse destination # Parse destination
# Is it nothing? # Is it nothing?
if package.destination == "NULL": if package.destination == "NULL":