예제 #1
0
    async def create_from_search(cls, search, loop=None):
        """Create a YTDLSource object using a search term.

        Args:
            search (str): The URL or search term to use to find audio.
            loop (async Event Loop, optional): The loop to run the search on.
        """
        loop = loop or asyncio.get_event_loop()
        for _ in range(YTDL_RETRIES):
            try:
                info = await loop.run_in_executor(
                    None, lambda: YTDL.extract_info(search, download=False))
                break
            except UnsupportedError:
                raise BananaCrime(
                    "I cannot stream audio from that website; see "
                    "https://rg3.github.io/youtube-dl/supportedsites.html "
                    "for a list of supported sites")
            except DownloadError:
                LOGGER.warn('Youtube had an issue extracting video info; '
                            'retrying in 3 seconds...')
                await asyncio.sleep(3)
        else:
            LOGGER.warn('Ran out of retries; giving up')
            raise BananaCrime(
                'After multiple attempts, I was unable to find a video '
                'using what you gave me, so either you gave me a wonky '
                'search term, the video I found required payment, or '
                'YoutubeDL is temporarily unhappy')
        # If it found multiple possibilities, just grab the 1st one
        if 'entries' in info:
            info = info['entries'][0]

        return cls(discord.FFmpegPCMAudio(info['url'], **FFMPEG_OPTS), info)
예제 #2
0
    async def play(self, ctx, *, desire: str = None):
        """Play a video's audio; give me a URL or a search term.

        Keyword/phrase searches are performed across YouTube.
        However, URLs can come from any of these sites:
        https://rg3.github.io/youtube-dl/supportedsites.html
        """

        if not desire:
            raise BananaCrime('Give me a search term')
        gvr = self.bot.guild_records[ctx.guild.id]
        if gvr.searching_ytdl:
            raise BananaCrime("I'm already trying to find a video")

        async with ctx.typing():
            gvr.searching_ytdl = True
            try:
                ytdl_src = await YTDLSource.create_from_search(
                    desire, self.bot.loop)
                async with gvr.lock:
                    vclient = await self.prepare_to_play(ctx)
                    vclient.play(ytdl_src,
                                 after=partial(ffmpeg_error_catcher,
                                               self.bot.loop, ctx.channel))
            finally:
                gvr.searching_ytdl = False

            await ctx.send(f"*Now playing:*\n{ytdl_src}")
예제 #3
0
    async def sbrequest(self, ctx, url, start_time=None, end_time=None):
        """Put in a request to add a sound to the soundboard.

        URL should link to the corresponding video.
        Can optionally provide start/end timestamps in the format hh:mm:ss,
        which would greatly be appreciated so I know what you're looking for,
        especially if the video is longer than a few seconds.
        If you give me anything invalid, I will know who you are and will
        publicly shame you accordingly.
        """
        if 'dQw4w9WgXcQ' in url:
            raise BananaCrime(
                'Did you really just try to rick roll me? In 2020? Come on')
        if os.stat(self.sb_request_filename).st_size \
                > self.sb_request_file_max_size_b:
            await ctx.send("My request queue is too full; try again later.")
            return
        if len(url) > 100:
            raise BananaCrime('URLs cannot be longer than 100 characters')
        if (start_time and len(start_time) > 10) \
            or (end_time and len(end_time) > 10):
            raise BananaCrime('Timestamps cannot be longer than 10 characters')

        with open(self.sb_request_filename, 'a') as f:
            f.write(f"[{datetime.now()}] {ctx.message.author} requests {url}")
            if start_time:
                f.write(f' from {start_time}')
                if end_time:
                    f.write(f' to {end_time}')
            f.write('\n')

        LOGGER.info('Recorded a soundboard request.')
        await ctx.send('Request made.')
예제 #4
0
    async def pause(self, ctx):
        """Pause song playback."""
        vclient = ctx.voice_client
        if not vclient or not vclient.is_playing():
            raise BananaCrime("I'm not playing anything")
        if type(vclient.source) != YTDLSource:
            raise BananaCrime("You can't pause the soundboard")

        vclient.pause()
예제 #5
0
    async def stop(self, ctx):
        """Stop all playback but stays in channel."""
        vclient = ctx.voice_client
        if not vclient:
            raise BananaCrime("I'm not in a voice channel")

        if vclient.is_playing() or vclient.is_paused():
            vclient.stop()
        else:
            raise BananaCrime("I'm not playing anything")
예제 #6
0
    async def resume(self, ctx):
        """Resume playing a song."""
        vclient = ctx.voice_client
        if not vclient or not vclient.is_connected():
            raise BananaCrime("I'm not in a voice channel")
        if type(vclient.source) != YTDLSource:
            raise BananaCrime("You can't pause/unpause the soundboard")
        if not vclient.is_paused():
            raise BananaCrime("I'm not paused")

        vclient.resume()
예제 #7
0
    async def playing(self, ctx):
        """Get the information of the currently playing song, if any."""
        vclient = ctx.voice_client
        if not vclient or not vclient.is_connected():
            raise BananaCrime("I'm not even in a voice channel")

        if (vclient.is_playing() or vclient.is_paused()) \
            and type(vclient.source) == YTDLSource:
            await ctx.send(f"*Currently playing:*\n{vclient.source}")
        else:
            raise BananaCrime("I'm not playing any songs at the moment")
예제 #8
0
    async def join(self, ctx, channel: VoiceChannel = None):
        """Join the given voice channel."""
        if not channel:
            raise BananaCrime('I need a channel to join')
        if not channel.permissions_for(ctx.guild.me).connect:
            raise BananaCrime(
                "I don't have permission to join that voice channel")

        vclient = ctx.voice_client
        async with self.bot.guild_records[ctx.guild.id].lock:
            if vclient and vclient.is_connected():
                if vclient.channel == channel:
                    raise BananaCrime("I'm already in this channel")
                await vclient.move_to(channel)
            else:
                await channel.connect()
예제 #9
0
async def choose(ctx, *choices):
    """Pick a random option from a given list of choices."""
    if not len(choices):
        raise BananaCrime('I need a list of choices')
    elif len(choices) == 1:
        ctx.send(f'`{choices[0]}`, you sneaky rapscallion.')
    else:
        entry_num = random.randint(0, len(choices) - 1)
        await ctx.send(f'`{choices[entry_num]}` all the way.' if entry_num %
                       2 else f"I'm feelin `{choices[entry_num]}`.")
예제 #10
0
    async def _summon(self, ctx):
        """Helper that attempts to join the VC of the caller.

        NOTE: Does not perform any locking.
        """
        if not ctx.author.voice:
            raise BananaCrime('You are not in a voice channel')
        target_channel = ctx.author.voice.channel
        if not target_channel.permissions_for(ctx.guild.me).connect:
            raise BananaCrime(
                "I don't have permission to join your voice channel")

        vclient = ctx.voice_client
        if vclient and vclient.is_connected():
            if vclient.channel == target_channel:
                raise BananaCrime("I'm already in this channel")
            await vclient.move_to(target_channel)
        else:
            await target_channel.connect()
예제 #11
0
 async def play_sound(self, ctx, category, sound):
     """Helper that plays a given sound belonging to a given category."""
     guild_lock = self.bot.guild_records[ctx.guild.id].lock
     if guild_lock.locked():
         raise BananaCrime("I'm already trying to process a VC command")
     async with guild_lock:
         vclient = await self.prepare_to_play(ctx, False)
     path = f'{SOUND_DIR}/{category}/{sound}.mp3'
     vclient.play(
         discord.PCMVolumeTransformer(discord.FFmpegPCMAudio(path), 0.5))
예제 #12
0
    async def volume(self, ctx, vol: int = None):
        """Get or set the volume of whatever is currently playing.

        Takes a percentage as an integer, if any.
        """
        vclient = ctx.voice_client
        if not vclient or not vclient.is_connected():
            raise BananaCrime("I'm not even in a voice channel")
        if not vclient.is_playing():
            raise BananaCrime("I'm not playing anything")
        if vol is None:
            await ctx.send(
                f'Volume is currently at {int(vclient.source.volume * 100)}%.')
            return
        if not vol >= 0 or not vol <= 100:
            raise BananaCrime("That's not a valid integer percentage (0-100)")

        vclient.source.volume = vol / 100
        await ctx.send(f"Volume set to {vol}%.")
예제 #13
0
async def roll(ctx, desire=None):
    """Handle a dice roll in the form (A)dB(+C-D...).

    Examples:
    * `d6` rolls a d6
    * `2d4` rolls 2 d4's and adds them
    * `4d20+4+2-1` rolls 4 d20's, adds them, then adds 5
    """
    if not desire:
        raise BananaCrime("Standard form using integers, d, +, and - "
                          "(i.e. `q.roll 2d4+2-1` rolls 2 d4's and adds 1)")
    desire = desire.lower()
    if 'd' not in desire:
        raise BananaCrime('A d is required (i.e. `2d6`)')

    count_raw, modifiers = desire.split('d', 1)

    if count_raw and not count_raw.isdigit():
        raise BananaCrime('Invalid number of times to roll')
    count = int(count_raw) if count_raw else 1

    curr_num = ''
    curr_sum = 0
    for c in reversed(modifiers):
        if c.isdigit():
            curr_num = c + curr_num
        elif c == '+' and curr_num:
            curr_sum += int(curr_num)
            curr_num = ''
        elif c == '-' and curr_num:
            curr_sum -= int(curr_num)
            curr_num = ''
        else:
            raise BananaCrime('Invalid format to the right of the `d`')
    if not curr_num:
        raise BananaCrime('Invalid format to the right of the `d`')

    rolls = [random.randint(1, int(curr_num)) for _ in range(count)]
    str_rolls = " + ".join(str(roll) for roll in rolls)
    result = sum(rolls) + curr_sum
    await ctx.send(f'`{desire}` gave {str_rolls} *+ {curr_sum}* = **{result}**'
                   )
예제 #14
0
    async def saylang(self, ctx, lang=None, *, desire=None):
        """Use gTTS to play a text-to-speech message using a given language."""
        if not lang:
            await ctx.send('Available languages:\n' + '\n'.join(
                [f'{v}: `{k}`' for k, v in self.valid_tts_langs.items()]))
            return
        if not desire:
            raise BananaCrime('Give me text to speak')

        async with self.bot.guild_records[ctx.guild.id].lock:
            vclient = await self.prepare_to_play(ctx)
        try:
            tts = gTTS(desire, lang=lang)
        except ValueError:
            raise BananaCrime('Invalid language')
        tts.save(GTTS_TEMP_FILE)

        vclient.play(
            discord.PCMVolumeTransformer(
                discord.FFmpegPCMAudio(
                    # Suppress bitrate estimation warning
                    GTTS_TEMP_FILE,
                    options='-loglevel error'),
                1))
예제 #15
0
    async def prepare_to_play(self, ctx, require_silence=True):
        """Prepare to play an AudioSource.

        Specifically, stop playback or complain if something's already playing
        depending on the require_silence parameter, and if I'm not in a VC,
        attempt to join that of the caller.
        NOTE: Does not perform any locking.
        """
        vclient = ctx.voice_client
        if not vclient or not vclient.is_connected():
            await self._summon(ctx)
            return ctx.voice_client

        if vclient.is_playing():
            if require_silence:
                raise BananaCrime("I'm already playing something")
            else:
                vclient.stop()
        elif vclient.is_paused():
            vclient.stop()

        return vclient
예제 #16
0
    async def leave(self, ctx):
        """Leave the current voice channel."""
        vclient = ctx.voice_client
        if not vclient:
            raise BananaCrime("I'm not in a voice channel")

        guild_lock = self.bot.guild_records[ctx.guild.id].lock
        # If I was called at the same time as I'm processing a command,
        #   when I eventually get my turn to execute, I need to wait a moment
        #   before leaving, otherwise the library will be confused
        # In addition, passing this condition is a sneaky/mean thing to do, so
        #   I'll also be sassy
        should_wait = guild_lock.locked()
        async with guild_lock:
            if should_wait:
                await asyncio.sleep(0.1)
                await ctx.send(
                    "You people need to make up your minds; do you want me to "
                    "play something or not? :rolling_eyes:")
            ctx.voice_client.stop()
            await ctx.voice_client.disconnect()
        self.bot.guild_records[ctx.guild.id].mark_inactive()
예제 #17
0
    async def sb(self, ctx, *desires):
        """Play one or more given sounds from the soundboard.

        Call this command with no argument to see available sounds.
        """
        if not desires:
            await ctx.send('Available categories: '
                           f'{", ".join(self.category_to_sounds)}, new, all')
            return

        tgt_paths = []
        to_send = []
        for desire in desires:
            if desire == 'all':
                # Use split send since this guy can easily grow above 2k chars
                await split_send(
                    ctx, 'All available sounds:\n' + '\n'.join([
                        '\n'.join(sounds)
                        for sounds in self.category_to_sounds.values()
                    ]))
                return

            elif desire == 'new':
                await ctx.send('Most recently added sounds:\n' +
                               '\n'.join(self.newest_sounds))
                return

            # If they searched a category
            elif desire in self.category_to_sounds:
                await ctx.send(f'Category {desire}: '
                               f'{", ".join(self.category_to_sounds[desire])}')
                return

            elif desire == 'random':
                category, sounds = random.choice(
                    list(self.category_to_sounds.items()))
                sound = random.choice(sounds)
                to_send.append(f"You just heard `{sound}`.")

            # If the sound exists
            elif desire in self.sound_to_category:
                sound = desire
                category = self.sound_to_category[sound]

            # See if we can guess what they meant
            else:
                # Always go with edit distance before substring matching
                sound = min(self.sound_to_category,
                            key=partial(editdistance.eval, desire))
                # Ensure it is within the minimum edit distance
                # If no luck with edit distance, go with substring matching
                if editdistance.eval(desire, sound) > MIN_EDIT_DIST:
                    substring_matches = [
                        sound for sound in self.sound_to_category
                        if desire in sound or sound in desire
                    ]
                    if substring_matches:
                        sound = min(
                            substring_matches,
                            key=lambda p, l=len(desire): abs(len(p) - l))
                    # If I still didn't find anything, give up
                    else:
                        continue
                # Otherwise, go for it
                category = self.sound_to_category.get(sound)
                to_send.append(f"`{desire}` isn't a valid sound, "
                               f"so I'll play `{sound}` instead.")

            tgt_paths.append(f'{SOUND_DIR}/{category}/{sound}.mp3')

        if not tgt_paths:
            raise BananaCrime(
                'Invalid category/sound name(s); '
                f'try `{self.bot.command_prefix}sb` with no arguments')

        record = self.bot.guild_records[ctx.guild.id]

        # TODO run this in other places too, like maybe explicitly on shutdown
        #   or, if you were a cool kid, on leave, stop, blah blah blah
        #   ok especially because calling leave raises an exception in the thread lol
        #   BUT HEY, it *seemingly* doesn't break anything
        #   and stop skips the sound (lol)
        await record.reap_sb_task()

        if record.lock.locked():
            raise BananaCrime("I'm already trying to process a VC command")
        async with record.lock:
            vclient = await self.prepare_to_play(ctx, False)

        if len(tgt_paths) == 1:
            vclient.play(
                discord.PCMVolumeTransformer(
                    discord.FFmpegPCMAudio(tgt_paths[0]), 0.5))
        else:
            record.sb_task = asyncio.create_task(
                schedule_sb_queue(ctx, tgt_paths))

        if to_send:
            await ctx.send('\n'.join(to_send))