From 8d08d7e0103dd779f6c590eab99cee589533c74b Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 1 Dec 2019 23:53:50 +0100 Subject: [PATCH] Managed to get Play working --- royalnet/bard/ytdldiscord.py | 16 +++++++++++++--- royalnet/bard/ytdlfile.py | 27 ++++++++++++++++++++------- royalnet/bard/ytdlinfo.py | 2 +- royalnet/bard/ytdlmp3.py | 10 ++++++++++ royalnet/herald/link.py | 10 ++++++++-- royalnet/serf/discord/discordserf.py | 9 ++++++++- royalnet/serf/discord/voiceplayer.py | 23 +++++++++++++++++------ royalnet/serf/serf.py | 4 +++- royalnet/utils/__init__.py | 2 -- royalnet/utils/log.py | 24 +++++++++++++++--------- royalnet/utils/multilock.py | 4 ++-- royalnet/utils/safeformat.py | 15 --------------- sample_config.toml | 13 +++++++++---- 13 files changed, 106 insertions(+), 53 deletions(-) delete mode 100644 royalnet/utils/safeformat.py diff --git a/royalnet/bard/ytdldiscord.py b/royalnet/bard/ytdldiscord.py index 3fc80d9b..cda4356a 100644 --- a/royalnet/bard/ytdldiscord.py +++ b/royalnet/bard/ytdldiscord.py @@ -27,6 +27,16 @@ class YtdlDiscord: self.pcm_filename: typing.Optional[str] = None self.lock: MultiLock = MultiLock() + def __repr__(self): + if not self.ytdl_file.has_info: + return f"<{self.__class__.__qualname__} without info>" + elif not self.ytdl_file.is_downloaded: + return f"<{self.__class__.__qualname__} not downloaded>" + elif not self.is_converted: + return f"<{self.__class__.__qualname__} at '{self.ytdl_file.filename}' not converted>" + else: + return f"<{self.__class__.__qualname__} at '{self.pcm_filename}'>" + @property def is_converted(self): """Has the file been converted?""" @@ -44,9 +54,9 @@ class YtdlDiscord: log.debug(f"Converting to PCM: {self.ytdl_file.filename}") await asyncify( ffmpeg.input(self.ytdl_file.filename) - .output(destination_filename, format="s16le", ac=2, ar="48000") - .overwrite_output() - .run + .output(destination_filename, format="s16le", ac=2, ar="48000") + .overwrite_output() + .run ) self.pcm_filename = destination_filename diff --git a/royalnet/bard/ytdlfile.py b/royalnet/bard/ytdlfile.py index 432c10cc..b60d3533 100644 --- a/royalnet/bard/ytdlfile.py +++ b/royalnet/bard/ytdlfile.py @@ -1,8 +1,9 @@ import os import logging +import re from contextlib import asynccontextmanager -from typing import Optional, List, Dict, Any -from royalnet.utils import asyncify, MultiLock +from typing import * +from royalnet.utils import * from asyncio import AbstractEventLoop, get_event_loop from .ytdlinfo import YtdlInfo from .errors import NotFoundError, MultipleFilesError @@ -23,7 +24,7 @@ class YtdlFile: "quiet": not __debug__, # Do not print messages to stdout. "noplaylist": True, # Download single video instead of a playlist if in doubt. "no_warnings": not __debug__, # Do not print out anything for warnings. - "outtmpl": "%(epoch)s-%(title)s-%(id)s.%(ext)s", # Use the default outtmpl. + "outtmpl": "./downloads/%(epoch)s-%(title)s-%(id)s.%(ext)s", # Use the default outtmpl. "ignoreerrors": True # Ignore unavailable videos } @@ -46,6 +47,14 @@ class YtdlFile: loop = get_event_loop() self._loop = loop + def __repr__(self): + if not self.has_info: + return f"<{self.__class__.__qualname__} without info>" + elif not self.is_downloaded: + return f"<{self.__class__.__qualname__} not downloaded>" + else: + return f"<{self.__class__.__qualname__} at '{self.filename}'>" + @property def has_info(self) -> bool: """Does the :class:`YtdlFile` have info available?""" @@ -77,12 +86,16 @@ class YtdlFile: filename = ytdl.prepare_filename(self.info.__dict__) with YoutubeDL({**self.ytdl_args, "outtmpl": filename}) as ytdl: ytdl.download([self.info.webpage_url]) - self.filename = filename + # "WARNING: Requested formats are incompatible for merge and will be merged into mkv." + if not os.path.exists(filename): + filename = re.sub(r"\.[^.]+$", ".mkv", filename) + self.filename = filename await self.retrieve_info() - async with self.lock.exclusive(): - log.debug(f"Downloading with youtube-dl: {self}") - await asyncify(download, loop=self._loop) + if not self.is_downloaded: + async with self.lock.exclusive(): + log.debug(f"Downloading with youtube-dl: {self}") + await asyncify(download, loop=self._loop) @asynccontextmanager async def aopen(self): diff --git a/royalnet/bard/ytdlinfo.py b/royalnet/bard/ytdlinfo.py index 1ebcafda..de7d9add 100644 --- a/royalnet/bard/ytdlinfo.py +++ b/royalnet/bard/ytdlinfo.py @@ -21,7 +21,7 @@ class YtdlInfo: "quiet": True, # Do not print messages to stdout. "noplaylist": True, # Download single video instead of a playlist if in doubt. "no_warnings": True, # Do not print out anything for warnings. - "outtmpl": "%(title)s-%(id)s.%(ext)s", # Use the default outtmpl. + "outtmpl": "./downloads/%(epoch)s-%(title)s-%(id)s.%(ext)s", # Use the default outtmpl. "ignoreerrors": True # Ignore unavailable videos } diff --git a/royalnet/bard/ytdlmp3.py b/royalnet/bard/ytdlmp3.py index 1a76560a..b355ced8 100644 --- a/royalnet/bard/ytdlmp3.py +++ b/royalnet/bard/ytdlmp3.py @@ -18,6 +18,16 @@ class YtdlMp3: self.mp3_filename: typing.Optional[str] = None self.lock: MultiLock = MultiLock() + def __repr__(self): + if not self.ytdl_file.has_info: + return f"<{self.__class__.__qualname__} without info>" + elif not self.ytdl_file.is_downloaded: + return f"<{self.__class__.__qualname__} not downloaded>" + elif not self.is_converted: + return f"<{self.__class__.__qualname__} at '{self.ytdl_file.filename}' not converted>" + else: + return f"<{self.__class__.__qualname__} at '{self.mp3_filename}'>" + @property def is_converted(self): """Has the file been converted?""" diff --git a/royalnet/herald/link.py b/royalnet/herald/link.py index 39b741ff..5a36cf34 100644 --- a/royalnet/herald/link.py +++ b/royalnet/herald/link.py @@ -129,14 +129,20 @@ class Link: @requires_identification async def send(self, package: Package): """Send a package to the :class:`Server`.""" - await self.websocket.send(package.to_json_bytes()) + log.debug(f"Trying to send package: {package}") + try: + jbytes = package.to_json_bytes() + except TypeError as e: + log.fatal(f"Could not send package: {' '.join(e.args)}") + raise + await self.websocket.send(jbytes) log.debug(f"Sent package: {package}") @requires_identification async def broadcast(self, destination: str, broadcast: Broadcast) -> None: package = Package(broadcast.to_dict(), source=self.nid, destination=destination) await self.send(package) - log.debug(f"Sent broadcast: {broadcast}") + log.debug(f"Sent broadcast to {destination}: {broadcast}") @requires_identification async def request(self, destination: str, request: Request) -> Response: diff --git a/royalnet/serf/discord/discordserf.py b/royalnet/serf/discord/discordserf.py index c3b06ec2..71e25466 100644 --- a/royalnet/serf/discord/discordserf.py +++ b/royalnet/serf/discord/discordserf.py @@ -229,9 +229,16 @@ class DiscordSerf(Serf): return None else: # Give priority to channels with the most people - def people_count(c: discord.VoiceChannel): + def people_count(c: "discord.VoiceChannel"): return len(c.members) channels.sort(key=people_count, reverse=True) return channels[0] + + def find_voice_player(self, guild: "discord.Guild") -> Optional[VoicePlayer]: + for voice_player in self.voice_players: + if voice_player.voice_client.guild == guild: + return voice_player + else: + return None diff --git a/royalnet/serf/discord/voiceplayer.py b/royalnet/serf/discord/voiceplayer.py index 8d6d2466..abecc5b4 100644 --- a/royalnet/serf/discord/voiceplayer.py +++ b/royalnet/serf/discord/voiceplayer.py @@ -1,4 +1,5 @@ import asyncio +import threading import logging from typing import Optional from .errors import * @@ -19,6 +20,8 @@ class VoicePlayer: self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() else: self.loop = loop + # FIXME: this looks like spaghetti + self._playback_ended_event: threading.Event = threading.Event() async def connect(self, channel: "discord.VoiceChannel") -> "discord.VoiceClient": """Connect the :class:`VoicePlayer` to a :class:`discord.VoiceChannel`, creating a :class:`discord.VoiceClient` @@ -92,12 +95,20 @@ class VoicePlayer: return log.debug(f"Next: {next_source}") self.voice_client.play(next_source, after=self._playback_ended) + self.loop.create_task(self._playback_check()) - def _playback_ended(self, error: Exception = None): - """An helper method that is called when the :attr:`.voice_client._player` has finished playing.""" + async def _playback_check(self): + # FIXME: quite spaghetti + while True: + if self._playback_ended_event.is_set(): + self._playback_ended_event.clear() + await self.start() + break + await asyncio.sleep(1) + + def _playback_ended(self, error=None): if error is not None: - # TODO: capture exception with Sentry - log.error(f"Error during playback: {error}") + # TODO: catch with Sentry + log.error(error) return - # Create a new task to create - self.loop.create_task(self.start()) + self._playback_ended_event.set() diff --git a/royalnet/serf/serf.py b/royalnet/serf/serf.py index 9265ce64..f8caca25 100644 --- a/royalnet/serf/serf.py +++ b/royalnet/serf/serf.py @@ -331,7 +331,9 @@ class Serf: except ImportError: log.info("Sentry: not installed") - serf = cls(loop=aio.get_event_loop(), **kwargs) + loop = aio.get_event_loop() + + serf = cls(loop=loop, **kwargs) try: serf.loop.run_until_complete(serf.run()) diff --git a/royalnet/utils/__init__.py b/royalnet/utils/__init__.py index d8a107e6..4db07821 100644 --- a/royalnet/utils/__init__.py +++ b/royalnet/utils/__init__.py @@ -1,5 +1,4 @@ from .asyncify import asyncify -from .safeformat import safeformat from .sleep_until import sleep_until from .formatters import andformat, underscorize, ytdldateformat, numberemojiformat, ordinalformat from .urluuid import to_urluuid, from_urluuid @@ -10,7 +9,6 @@ from .log import init_logging __all__ = [ "asyncify", - "safeformat", "sleep_until", "andformat", "underscorize", diff --git a/royalnet/utils/log.py b/royalnet/utils/log.py index edc7e21c..ba1ab9c5 100644 --- a/royalnet/utils/log.py +++ b/royalnet/utils/log.py @@ -6,18 +6,24 @@ try: except ImportError: coloredlogs = None - -log_format = "{asctime}\t| {processName}\t| {name}\t| {message}" +l: logging.Logger = logging.getLogger(__name__) def init_logging(logging_cfg: Dict[str, Any]): - royalnet_log: logging.Logger = logging.getLogger("royalnet") - royalnet_log.setLevel(logging_cfg["log_level"]) + loggers_cfg = logging_cfg["Loggers"] + for logger_name in loggers_cfg: + if logger_name == "root": + log: logging.Logger = logging.root + else: + log: logging.Logger = logging.getLogger(logger_name) + log.setLevel(loggers_cfg[logger_name]) + stream_handler = logging.StreamHandler() if coloredlogs is not None: - stream_handler.formatter = coloredlogs.ColoredFormatter(log_format, style="{") + stream_handler.formatter = coloredlogs.ColoredFormatter(logging_cfg["log_format"], style="{") else: - stream_handler.formatter = logging.Formatter(log_format, style="{") - if len(royalnet_log.handlers) < 1: - royalnet_log.addHandler(stream_handler) - royalnet_log.debug("Logging: ready") + stream_handler.formatter = logging.Formatter(logging_cfg["log_format"], style="{") + if len(logging.root.handlers) < 1: + logging.root.addHandler(stream_handler) + + l.debug("Logging: ready") diff --git a/royalnet/utils/multilock.py b/royalnet/utils/multilock.py index 0861a2fb..459ebdca 100644 --- a/royalnet/utils/multilock.py +++ b/royalnet/utils/multilock.py @@ -46,11 +46,11 @@ class MultiLock: log.debug(f"Waiting for normal lock end: {self}") await self._normal_event.wait() try: - log.debug("Acquiring exclusive lock: {self}") + log.debug(f"Acquiring exclusive lock: {self}") self._exclusive_event.clear() yield finally: - log.debug("Releasing exclusive lock: {self}") + log.debug(f"Releasing exclusive lock: {self}") self._exclusive_event.set() def __repr__(self): diff --git a/royalnet/utils/safeformat.py b/royalnet/utils/safeformat.py deleted file mode 100644 index f2c6913a..00000000 --- a/royalnet/utils/safeformat.py +++ /dev/null @@ -1,15 +0,0 @@ -class SafeDict(dict): - def __missing__(self, key): - return "{" + key + "}" - - -def safeformat(string: str, **words: str) -> str: - """:py:func:`str.format` something, but ignore missing keys instead of raising an error. - - Parameters: - string: The base string to be formatted. - words: The words to format the string with. - - Returns: - The formatted string.""" - return string.format_map(SafeDict(**words)) diff --git a/sample_config.toml b/sample_config.toml index fc2feb99..b6845f71 100644 --- a/sample_config.toml +++ b/sample_config.toml @@ -82,10 +82,15 @@ enabled = true token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" [Logging] -# Print to stderr all logging events of an equal or greater level than this -# Possible values are "DEBUG", "INFO", "WARNING", "ERROR", "FATAL" -log_level = "INFO" -# Optional: install the `coloredlogs` extra for colored output! +# TODO: document this +log_format = "{asctime}\t| {processName}\t| {name}\t| {message}" + +[Logging.Loggers] +root = "ERROR" +"royalnet" = "INFO" +# "royalnet.commands" = "DEBUG" +# "websockets.protocol" = "ERROR" +# ... [Sentry] # Connect Royalnet to a https://sentry.io/ project for error logging