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.pcm_filename: typing.Optional[str] = None
|
||||||
self.lock: MultiLock = MultiLock()
|
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
|
@property
|
||||||
def is_converted(self):
|
def is_converted(self):
|
||||||
"""Has the file been converted?"""
|
"""Has the file been converted?"""
|
||||||
|
@ -44,9 +54,9 @@ class YtdlDiscord:
|
||||||
log.debug(f"Converting to PCM: {self.ytdl_file.filename}")
|
log.debug(f"Converting to PCM: {self.ytdl_file.filename}")
|
||||||
await asyncify(
|
await asyncify(
|
||||||
ffmpeg.input(self.ytdl_file.filename)
|
ffmpeg.input(self.ytdl_file.filename)
|
||||||
.output(destination_filename, format="s16le", ac=2, ar="48000")
|
.output(destination_filename, format="s16le", ac=2, ar="48000")
|
||||||
.overwrite_output()
|
.overwrite_output()
|
||||||
.run
|
.run
|
||||||
)
|
)
|
||||||
self.pcm_filename = destination_filename
|
self.pcm_filename = destination_filename
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import Optional, List, Dict, Any
|
from typing import *
|
||||||
from royalnet.utils import asyncify, MultiLock
|
from royalnet.utils import *
|
||||||
from asyncio import AbstractEventLoop, get_event_loop
|
from asyncio import AbstractEventLoop, get_event_loop
|
||||||
from .ytdlinfo import YtdlInfo
|
from .ytdlinfo import YtdlInfo
|
||||||
from .errors import NotFoundError, MultipleFilesError
|
from .errors import NotFoundError, MultipleFilesError
|
||||||
|
@ -23,7 +24,7 @@ class YtdlFile:
|
||||||
"quiet": not __debug__, # Do not print messages to stdout.
|
"quiet": not __debug__, # Do not print messages to stdout.
|
||||||
"noplaylist": True, # Download single video instead of a playlist if in doubt.
|
"noplaylist": True, # Download single video instead of a playlist if in doubt.
|
||||||
"no_warnings": not __debug__, # Do not print out anything for warnings.
|
"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
|
"ignoreerrors": True # Ignore unavailable videos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +47,14 @@ class YtdlFile:
|
||||||
loop = get_event_loop()
|
loop = get_event_loop()
|
||||||
self._loop = 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
|
@property
|
||||||
def has_info(self) -> bool:
|
def has_info(self) -> bool:
|
||||||
"""Does the :class:`YtdlFile` have info available?"""
|
"""Does the :class:`YtdlFile` have info available?"""
|
||||||
|
@ -77,12 +86,16 @@ class YtdlFile:
|
||||||
filename = ytdl.prepare_filename(self.info.__dict__)
|
filename = ytdl.prepare_filename(self.info.__dict__)
|
||||||
with YoutubeDL({**self.ytdl_args, "outtmpl": filename}) as ytdl:
|
with YoutubeDL({**self.ytdl_args, "outtmpl": filename}) as ytdl:
|
||||||
ytdl.download([self.info.webpage_url])
|
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()
|
await self.retrieve_info()
|
||||||
async with self.lock.exclusive():
|
if not self.is_downloaded:
|
||||||
log.debug(f"Downloading with youtube-dl: {self}")
|
async with self.lock.exclusive():
|
||||||
await asyncify(download, loop=self._loop)
|
log.debug(f"Downloading with youtube-dl: {self}")
|
||||||
|
await asyncify(download, loop=self._loop)
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def aopen(self):
|
async def aopen(self):
|
||||||
|
|
|
@ -21,7 +21,7 @@ class YtdlInfo:
|
||||||
"quiet": True, # Do not print messages to stdout.
|
"quiet": True, # Do not print messages to stdout.
|
||||||
"noplaylist": True, # Download single video instead of a playlist if in doubt.
|
"noplaylist": True, # Download single video instead of a playlist if in doubt.
|
||||||
"no_warnings": True, # Do not print out anything for warnings.
|
"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
|
"ignoreerrors": True # Ignore unavailable videos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,16 @@ class YtdlMp3:
|
||||||
self.mp3_filename: typing.Optional[str] = None
|
self.mp3_filename: typing.Optional[str] = None
|
||||||
self.lock: MultiLock = MultiLock()
|
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
|
@property
|
||||||
def is_converted(self):
|
def is_converted(self):
|
||||||
"""Has the file been converted?"""
|
"""Has the file been converted?"""
|
||||||
|
|
|
@ -129,14 +129,20 @@ class Link:
|
||||||
@requires_identification
|
@requires_identification
|
||||||
async def send(self, package: Package):
|
async def send(self, package: Package):
|
||||||
"""Send a package to the :class:`Server`."""
|
"""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}")
|
log.debug(f"Sent package: {package}")
|
||||||
|
|
||||||
@requires_identification
|
@requires_identification
|
||||||
async def broadcast(self, destination: str, broadcast: Broadcast) -> None:
|
async def broadcast(self, destination: str, broadcast: Broadcast) -> None:
|
||||||
package = Package(broadcast.to_dict(), source=self.nid, destination=destination)
|
package = Package(broadcast.to_dict(), source=self.nid, destination=destination)
|
||||||
await self.send(package)
|
await self.send(package)
|
||||||
log.debug(f"Sent broadcast: {broadcast}")
|
log.debug(f"Sent broadcast to {destination}: {broadcast}")
|
||||||
|
|
||||||
@requires_identification
|
@requires_identification
|
||||||
async def request(self, destination: str, request: Request) -> Response:
|
async def request(self, destination: str, request: Request) -> Response:
|
||||||
|
|
|
@ -229,9 +229,16 @@ class DiscordSerf(Serf):
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
# Give priority to channels with the most people
|
# Give priority to channels with the most people
|
||||||
def people_count(c: discord.VoiceChannel):
|
def people_count(c: "discord.VoiceChannel"):
|
||||||
return len(c.members)
|
return len(c.members)
|
||||||
|
|
||||||
channels.sort(key=people_count, reverse=True)
|
channels.sort(key=people_count, reverse=True)
|
||||||
|
|
||||||
return channels[0]
|
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 asyncio
|
||||||
|
import threading
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from .errors import *
|
from .errors import *
|
||||||
|
@ -19,6 +20,8 @@ class VoicePlayer:
|
||||||
self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
||||||
else:
|
else:
|
||||||
self.loop = loop
|
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":
|
async def connect(self, channel: "discord.VoiceChannel") -> "discord.VoiceClient":
|
||||||
"""Connect the :class:`VoicePlayer` to a :class:`discord.VoiceChannel`, creating a :class:`discord.VoiceClient`
|
"""Connect the :class:`VoicePlayer` to a :class:`discord.VoiceChannel`, creating a :class:`discord.VoiceClient`
|
||||||
|
@ -92,12 +95,20 @@ class VoicePlayer:
|
||||||
return
|
return
|
||||||
log.debug(f"Next: {next_source}")
|
log.debug(f"Next: {next_source}")
|
||||||
self.voice_client.play(next_source, after=self._playback_ended)
|
self.voice_client.play(next_source, after=self._playback_ended)
|
||||||
|
self.loop.create_task(self._playback_check())
|
||||||
|
|
||||||
def _playback_ended(self, error: Exception = None):
|
async def _playback_check(self):
|
||||||
"""An helper method that is called when the :attr:`.voice_client._player` has finished playing."""
|
# 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:
|
if error is not None:
|
||||||
# TODO: capture exception with Sentry
|
# TODO: catch with Sentry
|
||||||
log.error(f"Error during playback: {error}")
|
log.error(error)
|
||||||
return
|
return
|
||||||
# Create a new task to create
|
self._playback_ended_event.set()
|
||||||
self.loop.create_task(self.start())
|
|
||||||
|
|
|
@ -331,7 +331,9 @@ class Serf:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
log.info("Sentry: not installed")
|
log.info("Sentry: not installed")
|
||||||
|
|
||||||
serf = cls(loop=aio.get_event_loop(), **kwargs)
|
loop = aio.get_event_loop()
|
||||||
|
|
||||||
|
serf = cls(loop=loop, **kwargs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
serf.loop.run_until_complete(serf.run())
|
serf.loop.run_until_complete(serf.run())
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from .asyncify import asyncify
|
from .asyncify import asyncify
|
||||||
from .safeformat import safeformat
|
|
||||||
from .sleep_until import sleep_until
|
from .sleep_until import sleep_until
|
||||||
from .formatters import andformat, underscorize, ytdldateformat, numberemojiformat, ordinalformat
|
from .formatters import andformat, underscorize, ytdldateformat, numberemojiformat, ordinalformat
|
||||||
from .urluuid import to_urluuid, from_urluuid
|
from .urluuid import to_urluuid, from_urluuid
|
||||||
|
@ -10,7 +9,6 @@ from .log import init_logging
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"asyncify",
|
"asyncify",
|
||||||
"safeformat",
|
|
||||||
"sleep_until",
|
"sleep_until",
|
||||||
"andformat",
|
"andformat",
|
||||||
"underscorize",
|
"underscorize",
|
||||||
|
|
|
@ -6,18 +6,24 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
coloredlogs = None
|
coloredlogs = None
|
||||||
|
|
||||||
|
l: logging.Logger = logging.getLogger(__name__)
|
||||||
log_format = "{asctime}\t| {processName}\t| {name}\t| {message}"
|
|
||||||
|
|
||||||
|
|
||||||
def init_logging(logging_cfg: Dict[str, Any]):
|
def init_logging(logging_cfg: Dict[str, Any]):
|
||||||
royalnet_log: logging.Logger = logging.getLogger("royalnet")
|
loggers_cfg = logging_cfg["Loggers"]
|
||||||
royalnet_log.setLevel(logging_cfg["log_level"])
|
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()
|
stream_handler = logging.StreamHandler()
|
||||||
if coloredlogs is not None:
|
if coloredlogs is not None:
|
||||||
stream_handler.formatter = coloredlogs.ColoredFormatter(log_format, style="{")
|
stream_handler.formatter = coloredlogs.ColoredFormatter(logging_cfg["log_format"], style="{")
|
||||||
else:
|
else:
|
||||||
stream_handler.formatter = logging.Formatter(log_format, style="{")
|
stream_handler.formatter = logging.Formatter(logging_cfg["log_format"], style="{")
|
||||||
if len(royalnet_log.handlers) < 1:
|
if len(logging.root.handlers) < 1:
|
||||||
royalnet_log.addHandler(stream_handler)
|
logging.root.addHandler(stream_handler)
|
||||||
royalnet_log.debug("Logging: ready")
|
|
||||||
|
l.debug("Logging: ready")
|
||||||
|
|
|
@ -46,11 +46,11 @@ class MultiLock:
|
||||||
log.debug(f"Waiting for normal lock end: {self}")
|
log.debug(f"Waiting for normal lock end: {self}")
|
||||||
await self._normal_event.wait()
|
await self._normal_event.wait()
|
||||||
try:
|
try:
|
||||||
log.debug("Acquiring exclusive lock: {self}")
|
log.debug(f"Acquiring exclusive lock: {self}")
|
||||||
self._exclusive_event.clear()
|
self._exclusive_event.clear()
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
log.debug("Releasing exclusive lock: {self}")
|
log.debug(f"Releasing exclusive lock: {self}")
|
||||||
self._exclusive_event.set()
|
self._exclusive_event.set()
|
||||||
|
|
||||||
def __repr__(self):
|
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"
|
token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
|
||||||
|
|
||||||
[Logging]
|
[Logging]
|
||||||
# Print to stderr all logging events of an equal or greater level than this
|
# TODO: document this
|
||||||
# Possible values are "DEBUG", "INFO", "WARNING", "ERROR", "FATAL"
|
log_format = "{asctime}\t| {processName}\t| {name}\t| {message}"
|
||||||
log_level = "INFO"
|
|
||||||
# Optional: install the `coloredlogs` extra for colored output!
|
[Logging.Loggers]
|
||||||
|
root = "ERROR"
|
||||||
|
"royalnet" = "INFO"
|
||||||
|
# "royalnet.commands" = "DEBUG"
|
||||||
|
# "websockets.protocol" = "ERROR"
|
||||||
|
# ...
|
||||||
|
|
||||||
[Sentry]
|
[Sentry]
|
||||||
# Connect Royalnet to a https://sentry.io/ project for error logging
|
# Connect Royalnet to a https://sentry.io/ project for error logging
|
||||||
|
|
Loading…
Reference in a new issue