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

WIP: moar stuff

This commit is contained in:
Steffo 2018-12-03 15:53:03 +00:00
parent 8556a2f0dc
commit d3f9bedf0b

View file

@ -40,16 +40,6 @@ queue_emojis = [":one:",
":nine:", ":nine:",
":keycap_ten:"] ":keycap_ten:"]
# Zalgo text
zalgo_up = ['̍', '̎', '̄', '̅', '̿', '̑', '̆', '̐', '͒', '͗', '͑', '̇', '̈', '̊', '͂', '̓', '̈́', '͊',
'͋', '͌', '̃', '̂', '̌', '͐', '́', '̋', '̏', '̽', '̉', 'ͣ', 'ͤ', 'ͥ', 'ͦ', 'ͧ', 'ͨ', 'ͩ',
'ͪ', 'ͫ', 'ͬ', 'ͭ', 'ͮ', 'ͯ', '̾', '͛', '͆', '̚', ]
zalgo_down = ['̖', '̗', '̘', '̙', '̜', '̝', '̞', '̟', '̠', '̤', '̥', '̦', '̩', '̪', '̫', '̬', '̭', '̮',
'̯', '̰', '̱', '̲', '̳', '̹', '̺', '̻', '̼', 'ͅ', '͇', '͈', '͉', '͍', '͎', '͓', '͔', '͕',
'͖', '͙', '͚', '', ]
zalgo_middle = ['̕', '̛', '̀', '́', '͘', '̡', '̢', '̧', '̨', '̴', '̵', '̶', '͜', '͝', '͞', '͟', '͠', '͢',
'̸', '̷', '͡', ]
# Init the event loop # Init the event loop
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
@ -138,91 +128,6 @@ else:
sentry = Succ() sentry = Succ()
class OldVideo:
"""A video to be played in the bot."""
def __init__(self, url: str = None, file: str = None, info: dict = None, enqueuer: discord.Member = None):
# Url of the video if it has to be downloaded
self.url = url
# Filename of the downloaded video
if file is None and info is None:
# Get it from the url hash
self.file = str(hash(url)) + ".opus"
elif info is not None:
# Get it from the video title
self.file = "./opusfiles/" + re.sub(r'[/\\?*"<>|!:]', "_", info["title"]) + ".opus"
else:
# The filename was explicitly passed
self.file = file
# Was the file already downloaded?
self.downloaded = (file is not None)
# Do we already have info on the video?
self.info = info
# Who added the video to the queue?
self.enqueuer = enqueuer
# How long and what title has the video?
if info is not None:
self.duration = info.get("duration")
self.title = info.get("title")
else:
self.duration = None
self.title = None
# No audio source exists yet
self.audio_source = None
def __str__(self):
"""Format the title to be used on Discord using Markdown."""
if self.info is None or "title" not in self.info:
return f"`{self.file}`"
return f"_{self.info['title']}_"
def __repr__(self):
return f"<discordbot.Video {str(self)}>"
def plain_text(self):
"""Format the video title without any Markdown."""
if self.info is None or "title" not in self.info:
return self.file
return self.info['title']
def download(self, progress_hooks: typing.List["function"] = None):
# File already downloaded
if self.downloaded:
raise errors.AlreadyDownloadedError()
# No progress hooks
if progress_hooks is None:
progress_hooks = []
# Check if under max duration
self.duration = datetime.timedelta(seconds=self.info.get("duration", 0))
# Refuse downloading if over YouTube max_duration
if self.info is not None and self.duration.total_seconds() > self.max_duration:
raise errors.DurationError()
# Download the file
logger.info(f"Downloading: {repr(self)}")
with youtube_dl.YoutubeDL({"noplaylist": True,
"format": "best",
"postprocessors": [{
"key": 'FFmpegExtractAudio',
"preferredcodec": 'opus'
}],
"outtmpl": self.file,
"progress_hooks": progress_hooks,
"quiet": True}) as ytdl:
ytdl.download(self.url)
logger.info(f"Download complete: {repr(self)}")
self.downloaded = True
def load(self) -> None:
# Check if the file has been downloaded
if not self.downloaded:
raise errors.FileNotDownloadedError()
self.audio_source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(f"{self.file}", **ffmpeg_settings))
def suggestion(self) -> typing.Optional[str]:
"""The suggested video to add to the queue after this one."""
raise NotImplementedError()
class Video: class Video:
def __init__(self, enqueuer: typing.Optional[discord.Member]=None): def __init__(self, enqueuer: typing.Optional[discord.Member]=None):
self.is_ready = False self.is_ready = False
@ -231,14 +136,20 @@ class Video:
self.audio_source = None self.audio_source = None
def __str__(self): def __str__(self):
if self.name is None:
return "_Untitled_"
return self.name return self.name
def plain_text(self): def plain_text(self):
"""Title without formatting to be printed on terminals.""" """Title without formatting to be printed on terminals."""
if self.name is None:
return "Untitled"
return self.name return self.name
def database_text(self): def database_text(self):
"""The text to be stored in the database for the stats. Usually the same as plain_text().""" """The text to be stored in the database for the stats. Usually the same as plain_text()."""
if self.name is None:
raise errors.VideoHasNoName()
return self.name return self.name
def __repr__(self): def __repr__(self):
@ -256,7 +167,6 @@ class Video:
"""Get the next suggested video, to be used when the queue is in LoopMode.FOLLOW_SUGGESTION""" """Get the next suggested video, to be used when the queue is in LoopMode.FOLLOW_SUGGESTION"""
raise NotImplementedError() raise NotImplementedError()
# TODO: split Video in YoutubeDLVideo and LocalFileVideo
class YoutubeDLVideo(Video): class YoutubeDLVideo(Video):
"""A file sourcing from YoutubeDL.""" """A file sourcing from YoutubeDL."""
@ -268,34 +178,67 @@ class YoutubeDLVideo(Video):
def get_info(self): def get_info(self):
"""Get info about the video.""" """Get info about the video."""
... if self.info:
return
with youtube_dl.YoutubeDL({"quiet": True,
"ignoreerrors": True,
"simulate": True}) as ytdl:
data = ytdl.extract_info(url=self.url, download=False)
if data is None:
raise errors.VideoInfoExtractionFailed()
if "entries" in info:
raise errors.VideoIsPlaylist()
self.info = data
self.name = data.get("title")
def ready_up(self): def __str__(self):
"""Download the video.""" if self.info is None:
... return f"`{self.url}`"
return f"_{self.name}_"
def plain_text(self):
if self.info is None:
return self.url
if not self.name.isprintable():
return self.url
return self.name
def get_filename(self): def get_filename(self):
"""Generate the filename of the video.""" """Generate the filename of the video."""
... if info is None:
raise errors.VideoInfoUnknown()
return f"./opusfiles/{re.sub(r'[/\\?*"<>|!:]', "_", info["title"])}.opus"
def ready_up(self):
"""Download the video."""
# Skip download if it is already ready
if self.is_ready:
return
# Retrieve info about the video
self.get_info()
# Check if the file to download already exists
if os.path.exists(self.get_filename()):
self.is_ready = True
return
# Download the file
logger.info(f"Starting youtube_dl download of {repr(self)}")
with youtube_dl.YoutubeDL({"noplaylist": True,
"format": "best",
"postprocessors": [{
"key": 'FFmpegExtractAudio',
"preferredcodec": 'opus'
}],
"outtmpl": self.get_filename(),
"quiet": True}) as ytdl:
ytdl.download(self.url)
logger.info(f"Completed youtube_dl download of {repr(self)}")
self.is_ready = True
def make_audio_source(self): def make_audio_source(self):
... if not self.is_ready:
raise errors.VideoIsNotReady()
def get_suggestion(self): self.audio_source = discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(self.get_filename(), **ffmpeg_settings))
... return self.audio_source
class LocalFileVideo(Video):
"""A file sourcing from the local opusfiles folder."""
def __init__(self, filename):
super().__init__()
self.filename = filename
def suggestion(self) -> typing.Optional[str]:
return None
...
class LoopMode(enum.Enum): class LoopMode(enum.Enum):
@ -332,15 +275,20 @@ class VideoQueue():
def advance_queue(self): def advance_queue(self):
if loop_mode == LoopMode.NORMAL: if loop_mode == LoopMode.NORMAL:
del self.list[0] try:
self.now_playing = self.list.pop(0)
except IndexError:
self.now_playing = None
elif loop_mode == LoopMode.LOOP_QUEUE: elif loop_mode == LoopMode.LOOP_QUEUE:
self.add(self.list[0]) self.add(self.list[0])
del self.list[0] self.now_playing = self.list.pop(0)
elif loop_mode == LoopMode.LOOP_SINGLE: elif loop_mode == LoopMode.LOOP_SINGLE:
pass pass
elif loop_mode == LoopMode.FOLLOW_SUGGESTIONS: elif loop_mode == LoopMode.FOLLOW_SUGGESTIONS:
self.add(self.list[0].suggestion(), 0) if now_playing is None:
del self.list[0] self.now_playing = None
return
self.now_playing = self.now_playing.suggestion()
def next_video(self) -> typing.Optional[Video]: def next_video(self) -> typing.Optional[Video]:
if len(self.list) == 0: if len(self.list) == 0:
@ -357,19 +305,18 @@ class VideoQueue():
def forward(self) -> None: def forward(self) -> None:
self.now_playing = self.list.pop(0) self.now_playing = self.list.pop(0)
def find_video(self, title: str) -> typing.Optional[Video]: def find_video(self, name: str) -> typing.Optional[Video]:
"""Returns the first video with a certain title (or filename).""" """Returns the first video with a certain name."""
for video in self.list: for video in self.list:
if title in video.title: if name in video.name:
return video
elif title in video.file:
return video return video
return None return None
def undownloaded_videos(self, limit: typing.Optional[int]=None): def not_ready_videos(self, limit: typing.Optional[int]=None):
"""Return the non-ready videos in the first limit positions of the queue."""
l = [] l = []
for video in self.list[:limit]: for video in self.list[:limit]:
if not video.downloaded: if not video.is_ready:
l.append(video) l.append(video)
return l return l
@ -378,28 +325,6 @@ class VideoQueue():
return self.list[index] return self.list[index]
class SecretVideo(Video):
"""A video to be played, but with a Zalgo'ed title."""
def __str__(self):
final_string = ""
for letter in self.file:
final_string += random.sample(zalgo_up, 1)[0]
final_string += random.sample(zalgo_middle, 1)[0]
final_string += random.sample(zalgo_down, 1)[0]
final_string += letter
return final_string
def plain_text(self):
final_string = ""
for letter in self.file:
final_string += random.sample(zalgo_up, 1)[0]
final_string += random.sample(zalgo_middle, 1)[0]
final_string += random.sample(zalgo_down, 1)[0]
final_string += letter
return final_string
def escape(message: str): def escape(message: str):
return message.replace("<", "&lt;").replace(">", "&gt;") return message.replace("<", "&lt;").replace(">", "&gt;")
@ -527,17 +452,19 @@ class RoyalDiscordBot(discord.Client):
except KeyError, ValueError: except KeyError, ValueError:
raise errors.InvalidConfigError("Missing main guild and channel ids.") raise errors.InvalidConfigError("Missing main guild and channel ids.")
# Max enqueable video duration # Max enqueable video duration
try: # Defined in the YoutubeDLVideo class
self.max_duration = int(config["YouTube"].get("max_duration"))
except KeyError, ValueError:
logger.warning("Max video duration is not set, setting it to infinity.")
self.max_duration = math.inf
# Max videos to predownload # Max videos to predownload
try: try:
self.max_videos_to_predownload = int(config["YouTube"]["predownload_videos"]) self.max_videos_to_predownload = int(config["Video"]["cache_size"])
except KeyError, ValueError: except KeyError, ValueError:
logger.warning("Max videos to predownload is not set, setting it to infinity.") logger.warning("Max videos to predownload is not set, setting it to infinity.")
self.max_videos_to_predownload = None self.max_videos_to_predownload = None
# Max time to ready a video
try:
self.max_video_ready_time = int(config["Video"]["max_ready_time"])
except KeyError, ValueError:
logger.warning("Max time to ready a video is not set, setting it to infinity. ")
self.max_video_ready_time = mathf.inf
# Radio messages # Radio messages
try: try:
self.radio_messages_enabled = True if config["Discord"]["radio_messages_enabled"] == "True" else False self.radio_messages_enabled = True if config["Discord"]["radio_messages_enabled"] == "True" else False
@ -715,24 +642,15 @@ class RoyalDiscordBot(discord.Client):
async def queue_predownload_videos(self): async def queue_predownload_videos(self):
while True: while True:
await asyncio.sleep(1)
# Might have some problems with del
for index, video in enumerate(self.video_queue.undownloaded_videos(self.max_videos_to_predownload)): for index, video in enumerate(self.video_queue.undownloaded_videos(self.max_videos_to_predownload)):
if video.downloaded:
continue
try: try:
with async_timeout.timeout(int(config["YouTube"]["download_timeout"])): with async_timeout.timeout(self.max_video_ready_time):
await loop.run_in_executor(executor, video.download) loop.run_in_executor(executor, video.ready_up)
except asyncio.TimeoutError: except asyncio.TimeoutError:
logger.warning(f"Video download took more than {config['YouTube']['download_timeout']}s:" logger.warning(f"Video {repr(video)} took more than {self.max_video_ready_time} to download, skipping...")
f" {video.plain_text()}") await self.main_channel.send(f"⚠️ La preparazione di {video} ha richiesto più di {self.max_video_ready_time} secondi, pertanto è stato rimosso dalla coda.")
await self.main_channel.send(f"⚠️ Il download di {str(video)} ha richiesto più di"
f" {config['YouTube']['download_timeout']} secondi, pertanto è stato"
f" rimosso dalla coda.")
del self.video_queue.list[index]
continue
except DurationError:
await self.main_channel.send(f"⚠️ {str(video)} dura più di"
f" {self.max_duration // 60}"
f" minuti, quindi è stato rimosso dalla coda.")
del self.video_queue.list[index] del self.video_queue.list[index]
continue continue
except Exception as e: except Exception as e:
@ -747,22 +665,41 @@ class RoyalDiscordBot(discord.Client):
"video": video.plain_text() "video": video.plain_text()
}) })
sentry.captureException() sentry.captureException()
logger.error(f"Video download error: {str(e)}") logger.error(f"Uncaught video download error: {e}")
await self.main_channel.send(f"⚠️ E' stato incontrato un errore durante il download di " await self.main_channel.send(f"⚠️ E' stato incontrato un errore durante il download di "
f"{str(video)}, quindi è stato rimosso dalla coda.\n\n" f"{str(video)}, quindi è stato rimosso dalla coda.\n\n"
f"**Dettagli sull'errore:**\n"
f"```python\n" f"```python\n"
f"{str(e)}" f"{str(e)}"
f"```") f"```")
del self.video_queue.list[index] del self.video_queue.list[index]
continue continue
await asyncio.sleep(1)
async def queue_play_next_video(self): async def queue_play_next_video(self):
await self.wait_until_ready() await self.wait_until_ready()
while True: while True:
# TODO await asyncio.sleep(1)
raise NotImplementedError("queue_play_next_video isn't done yet!") for voice_client in self.voice_clients:
# Do not add play videos if something else is playing!
if not voice_client.is_connected():
continue
if voice_client.is_playing():
continue
if voice_client.is_paused():
continue
# Advance the queue
self.voice_queue.advance_queue()
# Try to generate an AudioSource
if self.voice_queue.now_playing is None:
continue
try:
audio_source = self.voice_queue.now_playing.make_audio_source()
except errors.VideoIsNotReady():
continue
# Start playing the AudioSource
logger.info(f"Started playing {self.voice_queue.now_playing}")
voice_client.play(audio_source)
async def inactivity_countdown(self): async def inactivity_countdown(self):
while True: while True: