From 6f48b4fd2b42ef6cffd15d6836adbd3cdd8834b1 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sat, 20 Apr 2019 18:44:02 +0200 Subject: [PATCH 1/3] Update playmode.py --- royalnet/commands/playmode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/royalnet/commands/playmode.py b/royalnet/commands/playmode.py index 5ff279ee..19aa1f4a 100644 --- a/royalnet/commands/playmode.py +++ b/royalnet/commands/playmode.py @@ -22,7 +22,7 @@ class PlaymodeNH(NetworkHandler): @classmethod async def discord(cls, bot: "DiscordBot", message: PlaymodeMessage): - """Handle a play Royalnet request. That is, add audio to a PlayMode.""" + """Handle a playmode Royalnet request. That is, change current PlayMode.""" # Find the matching guild if message.guild_name: guild = bot.client.find_guild(message.guild_name) From 89c1f6fe229c2c17e110049340d5e5de7abf527a Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 23 Apr 2019 15:48:05 +0200 Subject: [PATCH 2/3] Play command improvements --- royalnet/audio/royalpcmaudio.py | 7 +++- royalnet/audio/royalpcmfile.py | 9 ++++- royalnet/bots/discord.py | 8 ++--- royalnet/bots/telegram.py | 4 ++- royalnet/commands/play.py | 60 +++++++++++++++++++++++++++++---- 5 files changed, 74 insertions(+), 14 deletions(-) diff --git a/royalnet/audio/royalpcmaudio.py b/royalnet/audio/royalpcmaudio.py index c8381535..ae883095 100644 --- a/royalnet/audio/royalpcmaudio.py +++ b/royalnet/audio/royalpcmaudio.py @@ -10,10 +10,15 @@ class RoyalPCMAudio(AudioSource): self._file = open(self.rpf.audio_filename, "rb") @staticmethod - def create_from_url(url) -> typing.List["RoyalPCMAudio"]: + def create_from_url(url: str) -> typing.List["RoyalPCMAudio"]: rpf_list = RoyalPCMFile.create_from_url(url) return [RoyalPCMAudio(rpf) for rpf in rpf_list] + @staticmethod + def create_from_ytsearch(search: str, amount: int = 1) -> typing.List["RoyalPCMAudio"]: + rpf_list = RoyalPCMFile.create_from_ytsearch(search, amount) + return [RoyalPCMAudio(rpf) for rpf in rpf_list] + def is_opus(self): return False diff --git a/royalnet/audio/royalpcmfile.py b/royalnet/audio/royalpcmfile.py index 2450b846..3c87998f 100644 --- a/royalnet/audio/royalpcmfile.py +++ b/royalnet/audio/royalpcmfile.py @@ -48,7 +48,14 @@ class RoyalPCMFile(YtdlFile): @staticmethod def create_from_url(url, **ytdl_args) -> typing.List["RoyalPCMFile"]: info_list = YtdlInfo.create_from_url(url) - return [RoyalPCMFile(info) for info in info_list] + return [RoyalPCMFile(info, **ytdl_args) for info in info_list] + + @staticmethod + def create_from_ytsearch(search: str, amount: int = 1, **ytdl_args) -> typing.List["RoyalPCMFile"]: + """Search a string on YouTube and download the first amount videos found.""" + url = f"ytsearch{amount}:{search}" + info_list = YtdlInfo.create_from_url(url) + return [RoyalPCMFile(info, **ytdl_args) for info in info_list] @property def _ytdl_filename(self): diff --git a/royalnet/bots/discord.py b/royalnet/bots/discord.py index 318a5ba8..ab2c1834 100644 --- a/royalnet/bots/discord.py +++ b/royalnet/bots/discord.py @@ -53,7 +53,9 @@ class DiscordBot(GenericBot): .replace("[u]", "__") \ .replace("[/u]", "__") \ .replace("[c]", "`") \ - .replace("[/c]", "`") + .replace("[/c]", "`") \ + .replace("[p]", "```") \ + .replace("[/p]", "```") await call.channel.send(escaped_text) async def net_request(call, message: Message, destination: str): @@ -203,11 +205,9 @@ class DiscordBot(GenericBot): await self.client.connect() # TODO: how to stop? - async def add_to_music_data(self, url: str, guild: discord.Guild): + async def add_to_music_data(self, audio_sources: typing.List[discord.AudioSource], guild: discord.Guild): """Add a file to the corresponding music_data object.""" - log.debug(f"Adding {url} to music_data of {guild}") guild_music_data = self.music_data[guild] - audio_sources = await asyncify(RoyalPCMAudio.create_from_url, url) for audio_source in audio_sources: log.debug(f"Adding {audio_source} to music_data") guild_music_data.add(audio_source) diff --git a/royalnet/bots/telegram.py b/royalnet/bots/telegram.py index f7238cdd..3bea21c0 100644 --- a/royalnet/bots/telegram.py +++ b/royalnet/bots/telegram.py @@ -48,7 +48,9 @@ class TelegramBot(GenericBot): .replace("[u]", "") \ .replace("[/u]", "") \ .replace("[c]", "") \ - .replace("[/c]", "") + .replace("[/c]", "") \ + .replace("[p]", "
") \
+                                   .replace("[/p]", "
") await asyncify(call.channel.send_message, escaped_text, parse_mode="HTML") async def net_request(call, message: Message, destination: str): diff --git a/royalnet/commands/play.py b/royalnet/commands/play.py index e44c127c..8144164f 100644 --- a/royalnet/commands/play.py +++ b/royalnet/commands/play.py @@ -1,8 +1,11 @@ import typing import asyncio -from ..utils import Command, Call, NetworkHandler -from ..network import Message, RequestSuccessful +import youtube_dl +import ffmpeg +from ..utils import Command, Call, NetworkHandler, asyncify +from ..network import Message, RequestSuccessful, RequestError from ..error import TooManyFoundError, NoneFoundError +from ..audio import RoyalPCMAudio, YtdlInfo if typing.TYPE_CHECKING: from ..bots import DiscordBot @@ -16,6 +19,11 @@ class PlayMessage(Message): self.guild_name: typing.Optional[str] = guild_name +class PlaySuccessful(RequestSuccessful): + def __init__(self, info_list: typing.List[YtdlInfo]): + self.info_list: typing.List[YtdlInfo] = info_list + + class PlayNH(NetworkHandler): message_type = PlayMessage @@ -36,9 +44,12 @@ class PlayNH(NetworkHandler): # TODO: change Exception raise Exception("No music_data for this guild") # Start downloading - # noinspection PyAsyncCall - loop.create_task(bot.add_to_music_data(message.url, guild)) - return RequestSuccessful() + try: + audio_sources: typing.List[RoyalPCMAudio] = await asyncify(RoyalPCMAudio.create_from_url, message.url) + except youtube_dl.utils.DownloadError: + audio_sources = await asyncify(RoyalPCMAudio.create_from_ytsearch, message.url) + await bot.add_to_music_data(audio_sources, guild) + return PlaySuccessful(info_list=[source.rpf.info for source in audio_sources]) class PlayCommand(Command): @@ -51,5 +62,40 @@ class PlayCommand(Command): @classmethod async def common(cls, call: Call): guild, url = call.args.match(r"(?:\[(.+)])?\s*(.+)") - await call.net_request(PlayMessage(url, guild), "discord") - await call.reply(f"✅ Richiesta la riproduzione di [c]{url}[/c].") + response: typing.Union[RequestError, PlaySuccessful] = await call.net_request(PlayMessage(url, guild), "discord") + if isinstance(response, RequestError): + # RoyalPCMFile errors + if isinstance(response.exc, FileExistsError): + await call.reply(f"❌ Scaricare [c]{url}[/c] significherebbe sovrascrivere un file già esistente.\nQuesto è un bug, e non dovrebbe mai succedere. Se è appena successo, segnalate il problema a https://github.com/Steffo99/royalnet/issues.\n[p]{response.exc}[/p]") + return + # ffmpeg errors + if isinstance(response.exc, ffmpeg.Error): + await call.reply(f"⚠️ Errore durante la conversione a PCM di [c]{url}[/c]:\n[p]{response.exc}[/p]") + return + # youtube_dl errors + if isinstance(response.exc, youtube_dl.utils.ContentTooShortError): + await call.reply(f"⚠️ Mentre era in corso il download di [c]{url}[/c], la connessione è stata interrotta, quindi la riproduzione è stata annullata.\n[p]{response.exc}[/p]") + return + if isinstance(response.exc, youtube_dl.utils.UnavailableVideoError): + await call.reply(f"⚠️ Non è disponibile nessun audio su [c]{url}[/c].\n[p]{response.exc}[/p]") + return + if isinstance(response.exc, youtube_dl.utils.SameFileError): + await call.reply(f"❌ Scaricare [c]{url}[/c] significherebbe scaricare due file diversi sullo stesso nome.\nQuesto è un bug, e non dovrebbe mai succedere. Se è appena successo, segnalate il problema a https://github.com/Steffo99/royalnet/issues.\n[p]{response.exc}[/p]") + return + if isinstance(response.exc, youtube_dl.utils.GeoRestrictedError): + await call.reply(f"⚠️ [c]{url}[/c] non può essere visualizzato nel paese in cui si trova il bot e non può essere scaricato.\n[p]{response.exc}[/p]") + return + if isinstance(response.exc, youtube_dl.utils.UnsupportedError): + await call.reply(f"⚠️ [c]{url}[/c] non è supportato da YoutubeDL e non può essere scaricato.\n[p]{response.exc}[/p]") + return + if isinstance(response.exc, youtube_dl.utils.ExtractorError): + await call.reply(f"⚠️ Errore nella ricerca di info per [c]{url}[/c]:\n[p]{response.exc}[/p]") + return + if isinstance(response.exc, youtube_dl.utils.DownloadError): + await call.reply(f"⚠️ Errore nel download di [c]{url}[/c]:\n[p]{response.exc}[/p]") + return + if isinstance(response.exc, youtube_dl.utils.YoutubeDLError): + await call.reply(f"⚠️ Errore di youtube_dl per [c]{url}[/c]:\n[p]{response.exc}[/p]") + return + for info in response.info_list: + await call.reply(f"⬇️ Download di [i]{info.title}[/i] completato.") From 3621e60deae56c1e7c0debedaef6bab9d804c0c1 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 23 Apr 2019 16:45:21 +0200 Subject: [PATCH 3/3] Add stuff --- royalnet/commands/error_handler.py | 4 +- royalnet/commands/play.py | 61 +++++++++++++++++++----------- royalnet/utils/asyncify.py | 1 + 3 files changed, 41 insertions(+), 25 deletions(-) diff --git a/royalnet/commands/error_handler.py b/royalnet/commands/error_handler.py index 2b50d95d..d7f2d2a4 100644 --- a/royalnet/commands/error_handler.py +++ b/royalnet/commands/error_handler.py @@ -42,10 +42,10 @@ class ErrorHandlerCommand(Command): await call.reply("⚠️ Il bot non è stato configurato correttamente, quindi questo comando non può essere eseguito. L'errore è stato segnalato all'amministratore.") return if isinstance(exception, RoyalnetError): - await call.reply(f"⚠️ La richiesta a Royalnet ha restituito un errore: {exception.exc}") + await call.reply(f"⚠️ La richiesta a Royalnet ha restituito un errore: [p]{exception.exc}[/p]") return if isinstance(exception, ExternalError): await call.reply("⚠️ Una risorsa esterna necessaria per l'esecuzione del comando non ha funzionato correttamente, quindi il comando è stato annullato.") return - await call.reply(f"❌ Eccezione non gestita durante l'esecuzione del comando:\n[b]{exception.__class__.__name__}[/b]\n{exception}") + await call.reply(f"❌ Eccezione non gestita durante l'esecuzione del comando:\n[b]{exception.__class__.__name__}[/b]\n[p]{exception}[/p]") log.error(f"Unhandled exception - {exception.__class__.__name__}: {exception}") diff --git a/royalnet/commands/play.py b/royalnet/commands/play.py index 8144164f..ba143da9 100644 --- a/royalnet/commands/play.py +++ b/royalnet/commands/play.py @@ -3,7 +3,7 @@ import asyncio import youtube_dl import ffmpeg from ..utils import Command, Call, NetworkHandler, asyncify -from ..network import Message, RequestSuccessful, RequestError +from ..network import Message, RequestSuccessful from ..error import TooManyFoundError, NoneFoundError from ..audio import RoyalPCMAudio, YtdlInfo if typing.TYPE_CHECKING: @@ -52,6 +52,15 @@ class PlayNH(NetworkHandler): return PlaySuccessful(info_list=[source.rpf.info for source in audio_sources]) +async def notify_on_timeout(call: Call, url: str, time: float, repeat: bool = False): + """Send a message after a while to let the user know that the bot is still downloading the files and hasn't crashed.""" + while True: + await asyncio.sleep(time) + await call.reply(f"ℹ️ Il download di [c]{url}[/c] sta richiedendo più tempo del solito, ma è ancora in corso!") + if not repeat: + break + + class PlayCommand(Command): command_name = "play" command_description = "Riproduce una canzone in chat vocale." @@ -62,40 +71,46 @@ class PlayCommand(Command): @classmethod async def common(cls, call: Call): guild, url = call.args.match(r"(?:\[(.+)])?\s*(.+)") - response: typing.Union[RequestError, PlaySuccessful] = await call.net_request(PlayMessage(url, guild), "discord") - if isinstance(response, RequestError): + download_task = loop.create_task(call.net_request(PlayMessage(url, guild), "discord")) + notify_task = loop.create_task(notify_on_timeout(call, url, time=20, repeat=True)) + try: + response: PlaySuccessful = await download_task + except Exception as exc: # RoyalPCMFile errors - if isinstance(response.exc, FileExistsError): - await call.reply(f"❌ Scaricare [c]{url}[/c] significherebbe sovrascrivere un file già esistente.\nQuesto è un bug, e non dovrebbe mai succedere. Se è appena successo, segnalate il problema a https://github.com/Steffo99/royalnet/issues.\n[p]{response.exc}[/p]") + if isinstance(exc, FileExistsError): + await call.reply(f"❌ Scaricare [c]{url}[/c] significherebbe sovrascrivere un file già esistente.\nQuesto è un bug, e non dovrebbe mai succedere. Se è appena successo, segnalate il problema a https://github.com/Steffo99/royalnet/issues.\n[p]{exc}[/p]") return # ffmpeg errors - if isinstance(response.exc, ffmpeg.Error): - await call.reply(f"⚠️ Errore durante la conversione a PCM di [c]{url}[/c]:\n[p]{response.exc}[/p]") + if isinstance(exc, ffmpeg.Error): + await call.reply(f"⚠️ Errore durante la conversione a PCM di [c]{url}[/c]:\n[p]{exc}[/p]") return # youtube_dl errors - if isinstance(response.exc, youtube_dl.utils.ContentTooShortError): - await call.reply(f"⚠️ Mentre era in corso il download di [c]{url}[/c], la connessione è stata interrotta, quindi la riproduzione è stata annullata.\n[p]{response.exc}[/p]") + if isinstance(exc, youtube_dl.utils.ContentTooShortError): + await call.reply(f"⚠️ Mentre era in corso il download di [c]{url}[/c], la connessione è stata interrotta, quindi la riproduzione è stata annullata.\n[p]{exc}[/p]") return - if isinstance(response.exc, youtube_dl.utils.UnavailableVideoError): - await call.reply(f"⚠️ Non è disponibile nessun audio su [c]{url}[/c].\n[p]{response.exc}[/p]") + if isinstance(exc, youtube_dl.utils.UnavailableVideoError): + await call.reply(f"⚠️ Non è disponibile nessun audio su [c]{url}[/c].\n[p]{exc}[/p]") return - if isinstance(response.exc, youtube_dl.utils.SameFileError): - await call.reply(f"❌ Scaricare [c]{url}[/c] significherebbe scaricare due file diversi sullo stesso nome.\nQuesto è un bug, e non dovrebbe mai succedere. Se è appena successo, segnalate il problema a https://github.com/Steffo99/royalnet/issues.\n[p]{response.exc}[/p]") + if isinstance(exc, youtube_dl.utils.SameFileError): + await call.reply(f"❌ Scaricare [c]{url}[/c] significherebbe scaricare due file diversi sullo stesso nome.\nQuesto è un bug, e non dovrebbe mai succedere. Se è appena successo, segnalate il problema a https://github.com/Steffo99/royalnet/issues.\n[p]{exc}[/p]") return - if isinstance(response.exc, youtube_dl.utils.GeoRestrictedError): - await call.reply(f"⚠️ [c]{url}[/c] non può essere visualizzato nel paese in cui si trova il bot e non può essere scaricato.\n[p]{response.exc}[/p]") + if isinstance(exc, youtube_dl.utils.GeoRestrictedError): + await call.reply(f"⚠️ [c]{url}[/c] non può essere visualizzato nel paese in cui si trova il bot e non può essere scaricato.\n[p]{exc}[/p]") return - if isinstance(response.exc, youtube_dl.utils.UnsupportedError): - await call.reply(f"⚠️ [c]{url}[/c] non è supportato da YoutubeDL e non può essere scaricato.\n[p]{response.exc}[/p]") + if isinstance(exc, youtube_dl.utils.UnsupportedError): + await call.reply(f"⚠️ [c]{url}[/c] non è supportato da YoutubeDL e non può essere scaricato.\n[p]{exc}[/p]") return - if isinstance(response.exc, youtube_dl.utils.ExtractorError): - await call.reply(f"⚠️ Errore nella ricerca di info per [c]{url}[/c]:\n[p]{response.exc}[/p]") + if isinstance(exc, youtube_dl.utils.ExtractorError): + await call.reply(f"⚠️ Errore nella ricerca di info per [c]{url}[/c]:\n[p]{exc}[/p]") return - if isinstance(response.exc, youtube_dl.utils.DownloadError): - await call.reply(f"⚠️ Errore nel download di [c]{url}[/c]:\n[p]{response.exc}[/p]") + if isinstance(exc, youtube_dl.utils.DownloadError): + await call.reply(f"⚠️ Errore nel download di [c]{url}[/c]:\n[p]{exc}[/p]") return - if isinstance(response.exc, youtube_dl.utils.YoutubeDLError): - await call.reply(f"⚠️ Errore di youtube_dl per [c]{url}[/c]:\n[p]{response.exc}[/p]") + if isinstance(exc, youtube_dl.utils.YoutubeDLError): + await call.reply(f"⚠️ Errore di youtube_dl per [c]{url}[/c]:\n[p]{exc}[/p]") return + raise + finally: + notify_task.cancel() for info in response.info_list: await call.reply(f"⬇️ Download di [i]{info.title}[/i] completato.") diff --git a/royalnet/utils/asyncify.py b/royalnet/utils/asyncify.py index 4d4a364e..45c147bc 100644 --- a/royalnet/utils/asyncify.py +++ b/royalnet/utils/asyncify.py @@ -4,5 +4,6 @@ import typing async def asyncify(function: typing.Callable, *args, **kwargs): + # TODO: make cancellable somehow loop = asyncio.get_running_loop() return await loop.run_in_executor(None, functools.partial(function, *args, **kwargs))