From c6f0f1ee1cd6a58bd15097ceee41d695cfd0365d Mon Sep 17 00:00:00 2001 From: Will Date: Sun, 5 Jun 2016 08:25:31 -0400 Subject: [PATCH] Audio patch (#292) * PEP8 * deal with duration problem * Limit output stream to 64k * stop duration checks over and over for _download_next * reset state correctly on stop and skip --- cogs/audio.py | 116 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 77 insertions(+), 39 deletions(-) diff --git a/cogs/audio.py b/cogs/audio.py index 8f6226659..5fa900330 100644 --- a/cogs/audio.py +++ b/cogs/audio.py @@ -70,15 +70,19 @@ class AuthorNotConnected(NotConnected): class VoiceNotConnected(NotConnected): pass + class UnauthorizedConnect(Exception): pass + class UnauthorizedSpeak(Exception): pass + class UnauthorizedSave(Exception): pass + class ConnectTimeout(NotConnected): pass @@ -127,7 +131,7 @@ class Playlist: self.name = name self.author = author self.url = url - self.main_class = main_class # reference to Audio + self.main_class = main_class # reference to Audio self.path = path if url is None and "link" in kwargs: @@ -173,10 +177,11 @@ class Downloader(threading.Thread): super().__init__(*args, **kwargs) self.url = url self.max_duration = max_duration - self.done = False + self.done = threading.Event() self.song = None self.failed = False self._download = download + self.hit_max_length = threading.Event() self._yt = None def run(self): @@ -184,22 +189,27 @@ class Downloader(threading.Thread): self.get_info() if self._download: self.download() + except MaximumLength: + self.hit_max_length.set() except: - self.done = True self.failed = True - else: - self.done = True + self.done.set() def download(self): - if self.max_duration and self.song.duration > self.max_duration: - log.debug("not downloading {} because of duration".format( - self.song.id)) - return + self.duration_check() if not os.path.isfile('data/audio/cache' + self.song.id): video = self._yt.extract_info(self.url) self.song = Song(**video) + def duration_check(self): + log.debug("duration {} for songid {}".format(self.song.duration, + self.song.id)) + if self.max_duration and self.song.duration > self.max_duration: + log.debug("songid {} too long".format(self.song.id)) + raise MaximumLength("songid {} has duration {} > {}".format( + self.song.id, self.song.duration, self.max_duration)) + def get_info(self): if self._yt is None: self._yt = youtube_dl.YoutubeDL(youtube_dl_options) @@ -208,8 +218,9 @@ class Downloader(threading.Thread): process=False) else: self.url = self.url[9:] - yt_id = self._yt.extract_info(self.url, - download=False)["entries"][0]["id"] # Should handle errors here. + yt_id = self._yt.extract_info( + self.url, download=False)["entries"][0]["id"] + # Should handle errors here ^ self.url = "https://youtube.com/watch?v={}".format(yt_id) video = self._yt.extract_info(self.url, download=False, process=False) @@ -325,7 +336,8 @@ class Audio: use_avconv = self.settings["AVCONV"] volume = self.get_server_settings(server)["VOLUME"] / 100 - options = '-filter "volume=volume={}"'.format(volume) + options = \ + '-filter "volume=volume={}" -b:a 64k -bufsize 64k'.format(volume) try: voice_client.audio_player.process.kill() @@ -384,6 +396,10 @@ class Audio: log.debug("downloader ID's mismatch on sid {}".format(server.id) + " gonna start dl-ing the next thing on the queue" " id {}".format(next_dl.song.id)) + try: + next_dl.duration_check() + except MaximumLength: + return self.downloaders[server.id] = Downloader(next_dl.url, max_length, download=True) self.downloaders[server.id].start() @@ -428,6 +444,18 @@ class Audio: return self.queue[server.id]["NOW_PLAYING"] + def _get_queue_playlist(self, server): + if server.id not in self.queue: + return None + + return self.queue[server.id]["PLAYLIST"] + + def _get_queue_repeat(self, server): + if server.id not in self.queue: + return None + + return self.queue[server.id]["REPEAT"] + async def _guarantee_downloaded(self, server, url): max_length = self.settings["MAX_LENGTH"] if server.id not in self.downloaders: # We don't have a downloader @@ -449,10 +477,13 @@ class Audio: # Queue manager already started it for us, isn't that nice? pass - while self.downloaders[server.id].is_alive(): # Getting info w/o DL - await asyncio.sleep(0.5) + # Getting info w/o download + self.downloaders[server.id].done.wait() + # This will throw a maxlength exception if required + self.downloaders[server.id].duration_check() song = self.downloaders[server.id].song + log.debug("sid {} wants to play songid {}".format(server.id, song.id)) # Now we check to see if we have a cache hit @@ -645,7 +676,13 @@ class Audio: log.debug('starting to play on "{}"'.format(server.name)) if self._valid_playable_url(url) or "[SEARCH:]" in url: - song = await self._guarantee_downloaded(server, url) + try: + song = await self._guarantee_downloaded(server, url) + except MaximumLength: + log.warning("I can't play URL below because it is too long." + " Use {}audioset maxlength to change this.\n\n" + "{}".format(self.bot.command_prefix[0], url)) + raise local = False else: # Assume local try: @@ -803,7 +840,7 @@ class Audio: "NOW_PLAYING": None} def _stop(self, server): - self._clear_queue(server) + self._setup_queue(server) self._stop_player(server) self._stop_downloader(server) @@ -1000,7 +1037,7 @@ class Audio: if not self.voice_connected(server): try: - can_connect = self.has_connect_perm(author, server) + self.has_connect_perm(author, server) except AuthorNotConnected: await self.bot.say("You must join a voice channel before I can" " play anything.") @@ -1075,11 +1112,9 @@ class Audio: # Checking already connected, will join if not - caller = inspect.currentframe().f_back.f_code.co_name - if not self.voice_connected(server): try: - can_connect = self.has_connect_perm(author, server) + self.has_connect_perm(author, server) except AuthorNotConnected: await self.bot.say("You must join a voice channel before I can" " play anything.") @@ -1179,8 +1214,8 @@ class Audio: playlist.server = server self._save_playlist(server, name, playlist) - await self.bot.say("Playlist '{}' saved. Tracks: {}".format(name, - len(songlist))) + await self.bot.say("Playlist '{}' saved. Tracks: {}".format( + name, len(songlist))) else: await self.bot.say("That URL is not a valid Soundcloud or YouTube" " playlist link. If you think this is in error" @@ -1195,9 +1230,8 @@ class Audio: if name not in self._list_playlists(server): await self.bot.say("There is no playlist with that name.") return - playlist = self._load_playlist(server, name, - local=self._playlist_exists_local( - server, name)) + playlist = self._load_playlist( + server, name, local=self._playlist_exists_local(server, name)) try: playlist.append_song(author, url) except UnauthorizedSave: @@ -1213,7 +1247,6 @@ class Audio: # Need better wording ^ await self.bot.say("Not implemented yet.") - @playlist.command(pass_context=True, no_pm=True, name="list") async def playlist_list(self, ctx): """Lists all available playlists""" @@ -1279,18 +1312,18 @@ class Audio: if self._playlist_exists(server, name): if not self.voice_connected(server): try: - can_connect = self.has_connect_perm(author, server) + self.has_connect_perm(author, server) except AuthorNotConnected: - await self.bot.say("You must join a voice channel before I can" - " play anything.") + await self.bot.say("You must join a voice channel before" + " I can play anything.") return except UnauthorizedConnect: await self.bot.say("I don't have permissions to join your" " voice channel.") return except UnauthorizedSpeak: - await self.bot.say("I don't have permissions to speak in your" - " voice channel.") + await self.bot.say("I don't have permissions to speak in" + " your voice channel.") return else: await self._join_voice_channel(voice_channel) @@ -1421,6 +1454,8 @@ class Audio: if self.is_playing(server): vc = self.voice_client(server) vc.audio_player.stop() + if self._get_queue_repeat(server) is False: + self._set_queue_nowplaying(server, None) await self.bot.say("Skipping...") else: await self.bot.say("Can't skip if I'm not playing.") @@ -1441,8 +1476,9 @@ class Audio: song = self.queue[server.id]["NOW_PLAYING"] if song: msg = ("\n**Title:** {}\n**Author:** {}\n**Uploader:** {}\n" - "**Views:** {}\n\n<{}>".format(song.title, song.creator, - song.uploader, song.view_count, song.webpage_url)) + "**Views:** {}\n\n<{}>".format( + song.title, song.creator, song.uploader, + song.view_count, song.webpage_url)) await self.bot.say(msg.replace("**Author:** None\n", "")) else: await self.bot.say("I don't know what this song is either.") @@ -1582,13 +1618,18 @@ class Audio: if len(temp_queue) > 0: # Fake queue for irdumb's temp playlist songs log.debug("calling _play because temp_queue is non-empty") - song = await self._play(sid, temp_queue.popleft()) + try: + song = await self._play(sid, temp_queue.popleft()) + except MaximumLength: + return elif len(queue) > 0: # We're in the normal queue url = queue.popleft() log.debug("calling _play on the normal queue") - song = await self._play(sid, url) + try: + song = await self._play(sid, url) + except MaximumLength: + return if repeat and last_song: - # TODO: stick NOW_PLAYING.url back into queue AFTER ending queue.append(last_song.webpage_url) else: song = None @@ -1716,17 +1757,14 @@ def setup(bot): check_files() if youtube_dl is None: raise RuntimeError("You need to run `pip3 install youtube_dl`") - return if opus is False: raise RuntimeError( "Your opus library's bitness must match your python installation's" " bitness. They both must be either 32bit or 64bit.") - return elif opus is None: raise RuntimeError( "You need to install ffmpeg and opus. See \"https://github.com/" "Twentysix26/Red-DiscordBot/wiki/Requirements\"") - return try: bot.voice_clients except AttributeError: