예제 #1
0
async def cmd_help(self, command=None):
    """
    Usage:
        {command_prefix}help [command]

    Prints a help message.
    If a command is specified, it prints a help message for that command.
    Otherwise, it lists the available commands.
    """

    if command:
        cmd = all_commands.get(command, None)["f"]

        if not cmd:
            return Response("No such command", delete_after=10)

        return Response("```\n{}```".format(
            dedent(cmd.__doc__), command_prefix=self.config.command_prefix),
                        delete_after=60)

    else:
        commands = []

        for cmd in all_commands:
            if cmd == 'help':
                continue

            commands.append("{}{}".format(self.config.command_prefix, cmd))

        helpmsg = "**Commands**\n```{commands}```".format(
            commands=", ".join(commands))

        return Response(helpmsg, reply=True, delete_after=60)
예제 #2
0
async def cmd_surprise(self,
                       player,
                       channel,
                       author,
                       permissions,
                       redis,
                       mode="fun"):

    if redis.exists("surpriserig"):
        url = redis.spop("surpriserig")
    elif mode.lower() in ("serious", "whiteperson", "shit", "shitty", "pop",
                          "popular", "bullshit", "horrible", "nickelback"):
        url = await get_random_top(self, redis)
    else:
        urls = redis.hgetall("musicbot:played")
        url = weighted_choice(urls, 100)

    if mode == "prepend":
        url = "prepend:" + url

    if url:
        return await cmd_play(self, player, channel, author, permissions, None,
                              url)
    else:
        return Response(
            "There are no songs that can be played. Play a few songs and try this command again.",
            delete_after=25)
예제 #3
0
async def cmd_id(self, author, user_mentions):
    """
    Usage:
        {command_prefix}id [@user]

    Tells the user their id or the id of another user.
    """
    if not user_mentions:
        return Response('your id is `%s`' % author.id,
                        reply=True,
                        delete_after=35)
    else:
        usr = user_mentions[0]
        return Response("%s's id is `%s`" % (usr.name, usr.id),
                        reply=True,
                        delete_after=35)
예제 #4
0
async def cmd_np(self, player, channel, server, message):
    """
    Usage:
        {command_prefix}np

    Displays the current song in chat.
    """

    if player.current_entry:
        if self.server_specific_data[server]['last_np_msg']:
            await self.safe_delete_message(self.server_specific_data[server]['last_np_msg'])
            self.server_specific_data[server]['last_np_msg'] = None

        song_progress = str(timedelta(seconds=player.progress)).lstrip('0').lstrip(':')
        song_total = str(timedelta(seconds=player.current_entry.duration)).lstrip('0').lstrip(':')
        prog_str = '`[%s/%s]`' % (song_progress, song_total)

        if player.current_entry.meta.get('channel', False) and player.current_entry.meta.get('author', False):
            np_text = "Now Playing: **%s** added by **%s** %s\n" % (
                player.current_entry.title, player.current_entry.meta['author'].name, prog_str)
        else:
            np_text = "Now Playing: **%s** %s\n" % (player.current_entry.title, prog_str)

        self.server_specific_data[server]['last_np_msg'] = await self.safe_send_message(channel, np_text)
        await self._manual_delete_check(message)
    else:
        return Response(
            'There are no songs queued! Queue something with {}play.'.format(self.config.command_prefix),
            delete_after=30
        )
예제 #5
0
async def cmd_volume(self, message, player, new_volume=None):
    """
    Usage:
        {command_prefix}volume (+/-)[volume]

    Sets the playback volume. Accepted values are from 1 to 100.
    Putting + or - before the volume will make the volume change relative to the current volume.
    """

    if not new_volume:
        return Response('Current volume: `%s%%`' % int(player.volume * 100), reply=True, delete_after=20)

    relative = False
    if new_volume[0] in '+-':
        relative = True

    try:
        new_volume = int(new_volume)

    except ValueError:
        raise CommandError('{} is not a valid number'.format(new_volume), expire_in=20)

    if relative:
        vol_change = new_volume
        new_volume += (player.volume * 100)

    old_volume = int(player.volume * 100)

    if 0 < new_volume <= 100:
        player.volume = new_volume / 100.0

        return Response('updated volume from %d to %d' % (old_volume, new_volume), reply=True, delete_after=20)

    else:
        if relative:
            raise CommandError(
                'Unreasonable volume change provided: {}{:+} -> {}%.  Provide a change between {} and {:+}.'.format(
                    old_volume,
                    vol_change,
                    old_volume + vol_change, 1 - old_volume, 100 - old_volume
                ), expire_in=20)
        else:
            raise CommandError(
                'Unreasonable volume provided: {}%. Provide a value between 1 and 100.'.format(new_volume),
                expire_in=20
            )
예제 #6
0
async def cmd_pldump(self, channel, song_url):
    """
    Usage:
        {command_prefix}pldump url

    Dumps the individual urls of a playlist
    """

    try:
        info = await self.downloader.extract_info(self.loop,
                                                  song_url.strip('<>'),
                                                  download=False,
                                                  process=False)
    except Exception as e:
        raise CommandError("Could not extract info from input url\n%s\n" % e,
                           expire_in=25)

    if not info:
        raise CommandError("Could not extract info from input url, no data.",
                           expire_in=25)

    if not info.get('entries', None):
        # TODO: Retarded playlist checking
        # set(url, webpageurl).difference(set(url))

        if info.get('url', None) != info.get('webpage_url',
                                             info.get('url', None)):
            raise CommandError("This does not seem to be a playlist.",
                               expire_in=25)
        else:
            return await cmd_pldump(self, channel, info.get(''))

    linegens = defaultdict(
        lambda: None, **{
            "youtube":
            lambda d: 'https://www.youtube.com/watch?v=%s' % d['id'],
            "soundcloud": lambda d: d['url'],
            "bandcamp": lambda d: d['url']
        })

    exfunc = linegens[info['extractor'].split(':')[0]]

    if not exfunc:
        raise CommandError(
            "Could not extract info from input url, unsupported playlist type.",
            expire_in=25)

    with BytesIO() as fcontent:
        for item in info['entries']:
            fcontent.write(exfunc(item).encode('utf8') + b'\n')

        fcontent.seek(0)
        await self.send_file(channel,
                             fcontent,
                             filename='playlist.txt',
                             content="Here's the url dump for <%s>" % song_url)

    return Response(":mailbox_with_mail:", delete_after=20)
예제 #7
0
async def cmd_reloadconfig(bot):
    """
    Usage:
        {command_prefix}reloadconfig

    This reloads the bot configs.
    """
    load_config(bot)
    return Response(":ok_hand:", delete_after=20)
예제 #8
0
async def cmd_clear(self, player, author):
    """
    Usage:
        {command_prefix}clear

    Clears the playlist.
    """

    player.playlist.clear()
    return Response(':put_litter_in_its_place:', delete_after=20)
예제 #9
0
async def cmd_settopic(self, message, channel, leftover_args, topic):
    """
    Usage:
        {command_prefix}settopic [topic]

    Changes the current text or voice channel's topic.
    """
    topic = " ".join([topic, *leftover_args])

    await self.edit_channel(channel, topic=topic)

    return Response(":ok_hand:", delete_after=20)
예제 #10
0
async def cmd_renamevoice(self, message, player, channelname, leftover_args):
    """
    Usage:
        {command_prefix}renamevoice [name]

    Changes the current voice channel's name.
    """

    channelname = " ".join([channelname, *leftover_args])

    await self.edit_channel(player.voice_client.channel, name=channelname)

    return Response(":ok_hand:", delete_after=20)
예제 #11
0
파일: server.py 프로젝트: nepeat/MusicBot
async def cmd_summon(self, channel, server, author, voice_channel):
    """
    Usage:
        {command_prefix}summon

    Call the bot to the summoner's voice channel.
    """

    if not author.voice_channel:
        raise CommandError('You are not in a voice channel!')

    voice_client = self.voice_client_in(server)
    if voice_client and server == author.voice_channel.server:
        await voice_client.move_to(author.voice_channel)
        return

    # move to _verify_vc_perms?
    chperms = author.voice_channel.permissions_for(server.me)

    if not chperms.connect:
        log.info("Cannot join channel \"%s\", no permission." %
                 author.voice_channel.name)
        return Response("```Cannot join channel \"%s\", no permission.```" %
                        author.voice_channel.name,
                        delete_after=25)

    elif not chperms.speak:
        log.info("Will not join channel \"%s\", no permission to speak." %
                 author.voice_channel.name)
        return Response(
            "```Will not join channel \"%s\", no permission to speak.```" %
            author.voice_channel.name,
            delete_after=25)

    player = await self.get_player(author.voice_channel, create=True)

    if player.is_stopped:
        player.play()
예제 #12
0
async def cmd_shuffle(self, channel, player, leftover_args, seed=None):
    """
    Usage:
        {command_prefix}shuffle [seed]

    Shuffles the playlist.
    """

    if leftover_args:
        seed = ' '.join([seed, *leftover_args])

    player.playlist.shuffle(seed)

    return Response("Shuffled playlist!", delete_after=15)
예제 #13
0
async def cmd_setgame(self, message, leftover_args, game=None):
    """
    Usage:
        {command_prefix}setname [game]

    Changes the bot's game status.
    """
    if game:
        game = Game(name=" ".join([game, *leftover_args]))
    else:
        game = Game(name="")

    await self.change_status(game)

    return Response(":ok_hand:", delete_after=20)
예제 #14
0
async def cmd_seek(self, message, player, leftover_args, seek=None):
    """
    Usage:
        {command_prefix}seek [seconds]

    Seeks the player to a specific time in seconds.
    """

    if player.is_stopped:
        raise CommandError("Can't seek! The player is not playing!", expire_in=20)

    if not seek:
        return Response('A time is required to seek.', reply=True, delete_after=20)

    try:
        original_seek = seek

        seek = ' '.join([seek, *leftover_args])
        seek = pytimeparse.parse(seek)

        if not seek:
            seek = int(original_seek)

        if seek < 0:
            raise ValueError()
    except (TypeError, ValueError):
        return Response('The time you have given is invalid.', reply=True, delete_after=20)

    try:
        player.seek(seek)
    except ValueError as e:
        return Response(str(e), delete_after=20)

    return Response('Seeked video to %s!' % (
        str(timedelta(seconds=seek)).lstrip('0').lstrip(':')
    ), delete_after=20)
예제 #15
0
async def cmd_setname(self, leftover_args, name):
    """
    Usage:
        {command_prefix}setname name

    Changes the bot's username.
    Note: This operation is limited by discord to twice per hour.
    """

    name = ' '.join([name, *leftover_args])

    try:
        await self.edit_profile(username=name)
    except Exception as e:
        raise CommandError(e, expire_in=20)

    return Response(":ok_hand:", delete_after=20)
예제 #16
0
async def cmd_perms(self, author, channel, server, permissions):
    """
    Usage:
        {command_prefix}perms

    Sends the user a list of their permissions.
    """

    lines = ['Command permissions in %s\n' % server.name, '```', '```']

    for perm in permissions.__dict__:
        if perm in ['user_list'] or permissions.__dict__[perm] == set():
            continue

        lines.insert(
            len(lines) - 1, "%s: %s" % (perm, permissions.__dict__[perm]))

    await self.send_message(author, '\n'.join(lines))
    return Response(":mailbox_with_mail:", delete_after=20)
예제 #17
0
async def cmd_setnick(self, server, channel, leftover_args, nick):
    """
    Usage:
        {command_prefix}setnick nick

    Changes the bot's nickname.
    """

    if not channel.permissions_for(server.me).change_nickname:
        raise CommandError("Unable to change nickname: no permission.")

    nick = ' '.join([nick, *leftover_args])

    try:
        await self.change_nickname(server.me, nick)
    except Exception as e:
        raise CommandError(e, expire_in=20)

    return Response(":ok_hand:", delete_after=20)
예제 #18
0
async def cmd_setavatar(self, message, url=None):
    """
    Usage:
        {command_prefix}setavatar [url]

    Changes the bot's avatar.
    Attaching a file and leaving the url parameter blank also works.
    """

    if message.attachments:
        thing = message.attachments[0]['url']
    else:
        thing = url.strip('<>')

    try:
        with aiohttp.Timeout(10):
            async with self.aiosession.get(thing) as res:
                await self.edit_profile(avatar=await res.read())

    except Exception as e:
        raise CommandError("Unable to change avatar: %s" % e, expire_in=20)

    return Response(":ok_hand:", delete_after=20)
예제 #19
0
async def cmd_listids(self, server, author, leftover_args, cat='all'):
    """
    Usage:
        {command_prefix}listids [categories]

    Lists the ids for various things.  Categories are:
       all, users, roles, channels
    """

    cats = ['channels', 'roles', 'users']

    if cat not in cats and cat != 'all':
        return Response("Valid categories: " +
                        ' '.join(['`%s`' % c for c in cats]),
                        reply=True,
                        delete_after=25)

    if cat == 'all':
        requested_cats = cats
    else:
        requested_cats = [cat] + [c.strip(',') for c in leftover_args]

    data = ['Your ID: %s' % author.id]

    for cur_cat in requested_cats:
        rawudata = None

        if cur_cat == 'users':
            data.append("\nUser IDs:")
            rawudata = [
                '%s #%s: %s' % (m.name, m.discriminator, m.id)
                for m in server.members
            ]

        elif cur_cat == 'roles':
            data.append("\nRole IDs:")
            rawudata = ['%s: %s' % (r.name, r.id) for r in server.roles]

        elif cur_cat == 'channels':
            data.append("\nText Channel IDs:")
            tchans = [c for c in server.channels if c.type == ChannelType.text]
            rawudata = ['%s: %s' % (c.name, c.id) for c in tchans]

            rawudata.append("\nVoice Channel IDs:")
            vchans = [
                c for c in server.channels if c.type == ChannelType.voice
            ]
            rawudata.extend('%s: %s' % (c.name, c.id) for c in vchans)

        if rawudata:
            data.extend(rawudata)

    with BytesIO() as sdata:
        sdata.writelines(d.encode('utf8') + b'\n' for d in data)
        sdata.seek(0)

        # TODO: Fix naming (Discord20API-ids.txt)
        await self.send_file(author,
                             sdata,
                             filename='%s-ids-%s.txt' %
                             (server.name.replace(' ', '_'), cat))

    return Response(":mailbox_with_mail:", delete_after=20)
예제 #20
0
async def cmd_queue(self, channel, player, sendas=None):
    """
    Usage:
        {command_prefix}queue

    Prints the current song queue.
    """

    if sendas:
        sendall = (sendas.lower() in ['file', 'full', 'all'])
    else:
        sendall = False

    lines = []
    unlisted = 0
    andmoretext = '* ... and %s more*' % ('x' * len(player.playlist.entries))

    if player.current_entry:
        song_progress = str(timedelta(seconds=player.progress)).lstrip('0').lstrip(':')
        song_total = str(timedelta(seconds=player.current_entry.duration)).lstrip('0').lstrip(':')
        prog_str = '`[%s/%s]`' % (song_progress, song_total)

        if player.current_entry.meta.get('channel', False) and player.current_entry.meta.get('author', False):
            lines.append("Now Playing: **%s** added by **%s** %s\n" % (
                player.current_entry.title, player.current_entry.meta['author'].name, prog_str))
        else:
            lines.append("Now Playing: **%s** %s\n" % (player.current_entry.title, prog_str))

    for i, item in enumerate(player.playlist, 1):
        if item.meta.get('channel', False) and item.meta.get('author', False):
            nextline = '`{}.` **{}** added by **{}**'.format(i, item.title, item.meta['author'].name).strip()
        else:
            nextline = '`{}.` **{}**'.format(i, item.title).strip()

        currentlinesum = sum(len(x) + 1 for x in lines)  # +1 is for newline char

        if (currentlinesum + len(nextline) + len(andmoretext) > DISCORD_MSG_CHAR_LIMIT) and not sendall:
            if currentlinesum + len(andmoretext):
                unlisted += 1
                continue

        lines.append(nextline)

    if unlisted:
        lines.append('\n*... and %s more*' % unlisted)

    if not lines:
        lines.append(
            'There are no songs queued! Queue something with {}play.'.format(self.config.command_prefix))

    message = '\n'.join(lines)

    if sendall:
        with BytesIO() as data:
            data.writelines(x.encode('utf8') + b'\n' for x in lines)
            data.seek(0)
            return await self.send_file(
                channel,
                data,
                filename='musicbot-full-queue.txt'
            )

    return Response(message, delete_after=30)
예제 #21
0
async def cmd_disconnect(self, server):
    await self.disconnect_voice_client(server)
    return Response(":hear_no_evil:", delete_after=20)
예제 #22
0
async def cmd_skip(self, player, channel, author, message, permissions, voice_channel):
    """
    Usage:
        {command_prefix}skip

    Skips the current song when enough votes are cast, or by the bot owner.
    """

    if player.is_stopped:
        raise CommandError("Can't skip! The player is not playing!", expire_in=20)

    if not player.current_entry:
        if player.playlist.peek():
            if player.playlist.peek()._is_downloading:
                return Response("The next song (%s) is downloading, please wait." % player.playlist.peek().title)

            elif player.playlist.peek().is_downloaded:
                log.info("The next song will be played shortly.  Please wait.")
            else:
                log.info("Something odd is happening.  "
                         "You might want to restart the bot if it doesn't start working.")
        else:
            log.info("Something strange is happening.  "
                     "You might want to restart the bot if it doesn't start working.")

    if permissions.instaskip or author == player.current_entry.meta.get("author", None):
        player.skip()
        await self._manual_delete_check(message)
        return

    # TODO: ignore person if they're deaf or take them out of the list or something?
    # Currently is recounted if they vote, deafen, then vote

    num_voice = sum(1 for m in voice_channel.voice_members if not (
        m.deaf or m.self_deaf or m.id in [self.user.id]))

    num_skips = player.skip_state.add_skipper(author.id, message)

    skips_remaining = min(
        self.config.skips_required,
        sane_round_int(num_voice * self.config.skip_ratio_required)
    ) - num_skips

    if skips_remaining <= 0:
        player.skip()
        return Response(
            'your skip for **{title}** was acknowledged.'
            '\nThe vote to skip has been passed.{extra}'.format(
                title=player.current_entry.title,
                extra=' Next song coming up!' if player.playlist.peek() else ''
            ),
            reply=True,
            delete_after=20
        )

    else:
        # TODO: When a song gets skipped, delete the old x needed to skip messages
        return Response(
            'your skip for **{title}** was acknowledged.'
            '\n**{remaining}** more {votes} required to vote to skip this song.'.format(
                title=player.current_entry.title,
                remaining=skips_remaining,
                votes='person is' if skips_remaining == 1 else 'people are'
            ),
            reply=True,
            delete_after=20
        )
예제 #23
0
async def cmd_search(self, player, channel, author, permissions, leftover_args):
    """
    Usage:
        {command_prefix}search [service] [number] query

    Searches a service for a video and adds it to the queue.
    - service: any one of the following services:
        - youtube (yt) (default if unspecified)
        - soundcloud (sc)
        - yahoo (yh)
    - number: return a number of video results and waits for user to choose one
      - defaults to 1 if unspecified
      - note: If your search query starts with a number,
              you must put your query in quotes
        - ex: {command_prefix}search 2 "I ran seagulls"
    """

    if permissions.max_songs and player.playlist.count_for_user(author) > permissions.max_songs:
        raise PermissionsError(
            "You have reached your enqueued song limit (%s)" % permissions.max_songs,
            expire_in=30
        )

    def argcheck():
        if not leftover_args:
            raise CommandError(
                "Please specify a search query.\n%s" % dedent(
                    cmd_search.__doc__.format(command_prefix=self.config.command_prefix)),
                expire_in=60
            )

    argcheck()

    try:
        leftover_args = shlex.split(' '.join(leftover_args))
    except ValueError:
        raise CommandError("Please quote your search query properly.", expire_in=30)

    service = 'youtube'
    items_requested = 3
    max_items = 10  # this can be whatever, but since ytdl uses about 1000, a small number might be better
    services = {
        'youtube': 'ytsearch',
        'soundcloud': 'scsearch',
        'yahoo': 'yvsearch',
        'yt': 'ytsearch',
        'sc': 'scsearch',
        'yh': 'yvsearch'
    }

    if leftover_args[0] in services:
        service = leftover_args.pop(0)
        argcheck()

    if leftover_args[0].isdigit():
        items_requested = int(leftover_args.pop(0))
        argcheck()

        if items_requested > max_items:
            raise CommandError("You cannot search for more than %s videos" % max_items)

    # Look jake, if you see this and go "what the f**k are you doing"
    # and have a better idea on how to do this, i'd be delighted to know.
    # I don't want to just do ' '.join(leftover_args).strip("\"'")
    # Because that eats both quotes if they're there
    # where I only want to eat the outermost ones
    if leftover_args[0][0] in '\'"':
        lchar = leftover_args[0][0]
        leftover_args[0] = leftover_args[0].lstrip(lchar)
        leftover_args[-1] = leftover_args[-1].rstrip(lchar)

    search_query = '%s%s:%s' % (services[service], items_requested, ' '.join(leftover_args))

    search_msg = await self.send_message(channel, "Searching for videos...")

    try:
        info = await self.downloader.extract_info(player.playlist.loop, search_query, download=False, process=True)

    except Exception as e:
        await self.safe_edit_message(search_msg, str(e), send_if_fail=True)
        return
    else:
        await self.safe_delete_message(search_msg)

    if not info:
        return Response("No videos found.", delete_after=30)

    def check(m):
        return (
            m.content.lower()[0] in 'yn' or
            # hardcoded function name weeee
            m.content.lower().startswith('{}{}'.format(self.config.command_prefix, 'search')) or
            m.content.lower().startswith('exit'))

    for e in info['entries']:
        result_message = await self.safe_send_message(channel, "Result %s/%s: %s" % (
            info['entries'].index(e) + 1, len(info['entries']), e['webpage_url']))

        confirm_message = await self.safe_send_message(channel, "Is this ok? Type `y`, `n` or `exit`")
        response_message = await self.wait_for_message(30, author=author, channel=channel, check=check)

        if not response_message:
            await self.safe_delete_message(result_message)
            await self.safe_delete_message(confirm_message)
            return Response("Ok nevermind.", delete_after=30)

        # They started a new search query so lets clean up and bugger off
        elif response_message.content.startswith(self.config.command_prefix) or \
                response_message.content.lower().startswith('exit'):

            await self.safe_delete_message(result_message)
            await self.safe_delete_message(confirm_message)
            return

        if response_message.content.lower().startswith('y'):
            await self.safe_delete_message(result_message)
            await self.safe_delete_message(confirm_message)
            await self.safe_delete_message(response_message)

            await cmd_play(self, player, channel, author, permissions, [], e['webpage_url'])

            return Response("Alright, coming right up!", delete_after=30)
        else:
            await self.safe_delete_message(result_message)
            await self.safe_delete_message(confirm_message)
            await self.safe_delete_message(response_message)

    return Response("Oh well :frowning:", delete_after=30)
예제 #24
0
async def play_playlist_async(self, player, channel, author, permissions, playlist_url, extractor_type):
    """
    Secret handler to use the async wizardry to make playlist queuing non-"blocking"
    """

    info = await self.downloader.extract_info(player.playlist.loop, playlist_url, download=False, process=False)

    if not info:
        raise CommandError("That playlist cannot be played.")

    num_songs = sum(1 for _ in info['entries'])
    t0 = time.time()

    busymsg = await self.safe_send_message(
        channel, "Processing %s songs..." % num_songs)  # TODO: From playlist_title

    entries_added = 0
    if extractor_type == 'youtube:playlist':
        try:
            entries_added = await player.playlist.async_process_youtube_playlist(
                playlist_url, channel=channel, author=author)
            # TODO: Add hook to be called after each song
            # TODO: Add permissions

        except Exception:
            traceback.print_exc()
            self.sentry.captureException()
            raise CommandError('Error handling playlist %s queuing.' % playlist_url, expire_in=30)

    elif extractor_type.lower() in ['soundcloud:set', 'bandcamp:album']:
        try:
            entries_added = await player.playlist.async_process_sc_bc_playlist(
                playlist_url, channel=channel, author=author)
            # TODO: Add hook to be called after each song
            # TODO: Add permissions

        except Exception:
            traceback.print_exc()
            self.sentry.captureException()

            raise CommandError('Error handling playlist %s queuing.' % playlist_url, expire_in=30)

    songs_processed = len(entries_added)
    drop_count = 0
    skipped = False

    if permissions.max_song_length:
        for e in entries_added.copy():
            if e.duration > permissions.max_song_length:
                try:
                    player.playlist.entries.remove(e)
                    entries_added.remove(e)
                    drop_count += 1
                except:
                    pass

        if drop_count:
            log.info("Dropped %s songs" % drop_count)

        if player.current_entry and player.current_entry.duration > permissions.max_song_length:
            await self.safe_delete_message(self.server_specific_data[channel.server]['last_np_msg'])
            self.server_specific_data[channel.server]['last_np_msg'] = None
            skipped = True
            player.skip()
            entries_added.pop()

    await self.safe_delete_message(busymsg)

    songs_added = len(entries_added)
    tnow = time.time()
    ttime = tnow - t0
    wait_per_song = 1.2
    # TODO: actually calculate wait per song in the process function and return that too

    # This is technically inaccurate since bad songs are ignored but still take up time
    log.info("Processed {}/{} songs in {} seconds at {:.2f}s/song, {:+.2g}/song from expected ({}s)".format(
        songs_processed,
        num_songs,
        fixg(ttime),
        ttime / num_songs,
        ttime / num_songs - wait_per_song,
        fixg(wait_per_song * num_songs))
    )

    if not songs_added:
        basetext = "No songs were added, all songs were over max duration (%ss)" % permissions.max_song_length
        if skipped:
            basetext += "\nAdditionally, the current song was skipped for being too long."

        raise CommandError(basetext, expire_in=30)

    return Response("Enqueued {} songs to be played in {} seconds".format(
        songs_added, fixg(ttime, 1)), delete_after=30)
예제 #25
0
async def cmd_play(self, player, channel, author, permissions, leftover_args, song_url):
    """
    Usage:
        {command_prefix}play song_link
        {command_prefix}play text to search for

    Adds the song to the playlist.  If a link is not provided, the first
    result from a youtube search is added to the queue.
    """

    song_url = song_url.strip('<>')

    if permissions.max_songs and player.playlist.count_for_user(author) >= permissions.max_songs:
        raise PermissionsError(
            "You have reached your enqueued song limit (%s)" % permissions.max_songs, expire_in=30
        )

    if leftover_args:
        song_url = ' '.join([song_url, *leftover_args])

    if song_url.startswith("prepend:"):
        song_url = song_url.lstrip("prepend:")
        prepend = True
    else:
        prepend = False

    try:
        info = await self.downloader.extract_info(player.playlist.loop, song_url, download=False, process=False)
    except Exception as e:
        raise CommandError(e, expire_in=30)

    if not info:
        raise CommandError("That video cannot be played.", expire_in=30)

    # abstract the search handling away from the user
    # our ytdl options allow us to use search strings as input urls
    if info.get('url', '').startswith('ytsearch'):
        # log.info("[Command:play] Searching for \"%s\"" % song_url)
        info = await self.downloader.extract_info(
            player.playlist.loop,
            song_url,
            download=False,
            process=True,    # ASYNC LAMBDAS WHEN
            on_error=lambda e: asyncio.ensure_future(
                self.safe_send_message(channel, "```\n%s\n```" % e, expire_in=120), loop=self.loop),
            retry_on_error=True
        )

        if not info:
            raise CommandError(
                "Error extracting info from search string, youtubedl returned no data.  "
                "You may need to restart the bot if this continues to happen.", expire_in=30
            )

        if not all(info.get('entries', [])):
            # empty list, no data
            return

        song_url = info['entries'][0]['webpage_url']
        info = await self.downloader.extract_info(player.playlist.loop, song_url, download=False, process=False)
        # Now I could just do: return await self.cmd_play(player, channel, author, song_url)
        # But this is probably fine

    # TODO: Possibly add another check here to see about things like the bandcamp issue
    # TODO: Where ytdl gets the generic extractor version with no processing, but finds two different urls

    if 'entries' in info:
        # I have to do exe extra checks anyways because you can request an arbitrary number of search results
        if not permissions.allow_playlists and ':search' in info['extractor'] and len(info['entries']) > 1:
            raise PermissionsError("You are not allowed to request playlists", expire_in=30)

        # The only reason we would use this over `len(info['entries'])` is if we add `if _` to this one
        num_songs = sum(1 for _ in info['entries'])

        if permissions.max_playlist_length and num_songs > permissions.max_playlist_length:
            raise PermissionsError(
                "Playlist has too many entries (%s > %s)" % (num_songs, permissions.max_playlist_length),
                expire_in=30
            )

        # This is a little bit weird when it says (x + 0 > y), I might add the other check back in
        if permissions.max_songs and player.playlist.count_for_user(author) + num_songs > permissions.max_songs:
            raise PermissionsError(
                "Playlist entries + your already queued songs reached limit (%s + %s > %s)" % (
                    num_songs, player.playlist.count_for_user(author), permissions.max_songs),
                expire_in=30
            )

        if info['extractor'].lower() in ['youtube:playlist', 'soundcloud:set', 'bandcamp:album']:
            try:
                return await play_playlist_async(self, player, channel, author, permissions, song_url, info['extractor'])
            except CommandError:
                raise
            except Exception as e:
                traceback.print_exc()
                self.sentry.captureException()
                raise CommandError("Error queuing playlist:\n%s" % e, expire_in=30)

        t0 = time.time()

        # My test was 1.2 seconds per song, but we maybe should fudge it a bit, unless we can
        # monitor it and edit the message with the estimated time, but that's some ADVANCED SHIT
        # I don't think we can hook into it anyways, so this will have to do.
        # It would probably be a thread to check a few playlists and get the speed from that
        # Different playlists might download at different speeds though
        wait_per_song = 1.2

        procmesg = await self.safe_send_message(
            channel,
            'Gathering playlist information for {} songs{}'.format(
                num_songs,
                ', ETA: {} seconds'.format(fixg(
                    num_songs * wait_per_song)) if num_songs >= 10 else '.'))

        # TODO: I can create an event emitter object instead, add event functions, and every play list might be asyncified
        #       Also have a "verify_entry" hook with the entry as an arg and returns the entry if its ok

        entry_list, position = await player.playlist.import_from(song_url, channel=channel, author=author)

        tnow = time.time()
        ttime = tnow - t0
        listlen = len(entry_list)
        drop_count = 0

        if permissions.max_song_length:
            for e in entry_list.copy():
                if e.duration > permissions.max_song_length:
                    player.playlist.entries.remove(e)
                    entry_list.remove(e)
                    drop_count += 1
                    # Im pretty sure there's no situation where this would ever break
                    # Unless the first entry starts being played, which would make this a race condition
            if drop_count:
                log.info("Dropped %s songs" % drop_count)

        try:
            log.info("Processed {} songs in {} seconds at {:.2f}s/song, {:+.2g}/song from expected ({}s)".format(
                listlen,
                fixg(ttime),
                ttime / listlen,
                ttime / listlen - wait_per_song,
                fixg(wait_per_song * num_songs))
            )
        except ZeroDivisionError:
            pass

        await self.safe_delete_message(procmesg)

        if not listlen - drop_count:
            raise CommandError(
                "No songs were added, all songs were over max duration (%ss)" % permissions.max_song_length,
                expire_in=30
            )

        reply_text = "Enqueued **%s** songs to be played. Position in queue: %s"
        btext = str(listlen - drop_count)

    else:
        if permissions.max_song_length and info.get('duration', 0) > permissions.max_song_length:
            raise PermissionsError(
                "Song duration exceeds limit (%s > %s)" % (info['duration'], permissions.max_song_length),
                expire_in=30
            )

        try:
            entry, position = await player.playlist.add_entry(song_url, channel=channel, author=author, prepend=prepend)
        except RetryPlay:
            new_url = song_url.replace("/", " ")
            return await cmd_play(self, player, channel, author, permissions, leftover_args, new_url)
        except WrongEntryTypeError as e:
            if e.use_url == song_url:
                log.info("[Warning] Determined incorrect entry type, but suggested url is the same.  Help.")

            return await cmd_play(self, player, channel, author, permissions, leftover_args, e.use_url)

        reply_text = "Enqueued **%s** to be played. Position in queue: %s"
        btext = entry.title

    if position == 1 and player.is_stopped:
        position = 'Up next!'
        reply_text %= (btext, position)

    else:
        try:
            time_until = await player.playlist.estimate_time_until(position, player)
            reply_text += ' - estimated time until playing: %s'
        except:
            traceback.print_exc()
            self.sentry.captureException()
            time_until = ''

        reply_text %= (btext, position, time_until)

    return Response(reply_text, delete_after=30)
예제 #26
0
async def cmd_clean(self, message, channel, server, author, search_range=50):
    """
    Usage:
        {command_prefix}clean [range]

    Removes up to [range] messages the bot has posted in chat. Default: 50, Max: 1000
    """

    try:
        float(search_range)  # lazy check
        search_range = min(int(search_range), 1000)
    except:
        return Response(
            "enter a number.  NUMBER.  That means digits.  `15`.  Etc.",
            reply=True,
            delete_after=8)

    await self.safe_delete_message(message, quiet=True)

    def is_possible_command_invoke(entry):
        valid_call = any(
            entry.content.startswith(prefix)
            for prefix in [self.config.command_prefix])  # can be expanded
        return valid_call and not entry.content[1:2].isspace()

    delete_invokes = True
    delete_all = channel.permissions_for(
        author).manage_messages or self.config.owner_id == author.id

    def check(message):
        if is_possible_command_invoke(message) and delete_invokes:
            return delete_all or message.author == author
        return message.author == self.user

    if channel.permissions_for(server.me).manage_messages:
        deleted = await self.purge_from(channel,
                                        check=check,
                                        limit=search_range,
                                        before=message)
        return Response('Cleaned up {} message{}.'.format(
            len(deleted), 's' * bool(deleted)),
                        delete_after=15)

    deleted = 0
    async for entry in self.logs_from(channel, search_range, before=message):
        if entry == self.server_specific_data[channel.server]['last_np_msg']:
            continue

        if entry.author == self.user:
            await self.safe_delete_message(entry)
            deleted += 1
            await asyncio.sleep(0.21)

        if is_possible_command_invoke(entry) and delete_invokes:
            if delete_all or entry.author == author:
                try:
                    await self.delete_message(entry)
                    await asyncio.sleep(0.21)
                    deleted += 1

                except Forbidden:
                    delete_invokes = False
                except HTTPException:
                    pass

    return Response('Cleaned up {} message{}.'.format(deleted,
                                                      's' * bool(deleted)),
                    delete_after=15)