mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-23 19:44:20 +00:00
Managed to get Play working
This commit is contained in:
parent
8ff0731aa5
commit
8d08d7e010
13 changed files with 106 additions and 53 deletions
|
@ -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?"""
|
||||
|
|
|
@ -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,9 +86,13 @@ class YtdlFile:
|
|||
filename = ytdl.prepare_filename(self.info.__dict__)
|
||||
with YoutubeDL({**self.ytdl_args, "outtmpl": filename}) as ytdl:
|
||||
ytdl.download([self.info.webpage_url])
|
||||
# "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()
|
||||
if not self.is_downloaded:
|
||||
async with self.lock.exclusive():
|
||||
log.debug(f"Downloading with youtube-dl: {self}")
|
||||
await asyncify(download, loop=self._loop)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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?"""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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))
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue