mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-27 21:44:21 +00:00
WIP: moar stuff
This commit is contained in:
parent
8556a2f0dc
commit
d3f9bedf0b
1 changed files with 116 additions and 179 deletions
291
discordbot.py
291
discordbot.py
|
@ -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("<", "<").replace(">", ">")
|
return message.replace("<", "<").replace(">", ">")
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue