DiscordのMusicBotに新しくコマンドを追加する&インストール時の注意点

discord、ゲームしながら通話するのに使っているあれのことです。

ちょっと前にMusicBotという通話している中に音楽を流すBotが流行りましたが最近になってそれを入れまして、追加で欲しいコマンドがあったため1回も触ったことがないPythonでなんとか動かせたのでメモを残しておこうかと思います。

##注意点(追記17/12/17）
apl all をここではプレイリストの取得を行うコマンドとしていますが、プレイリストの中にアーティストの公式PV（特に洋楽系）が入っているとDL防止とかそっち系の保護機能に引っかかるようでエラー吐いてBOTを再起動するまで全てのコマンドが使えなくなります。
今後暇な時にその辺のエラーハンドリングを追加してみようと思いますが、とりあえずここに貼っているプログラムではエラーが起きますのでお気を付けください。

##環境

Raspberry pi3とOSX
Python3.5.3
[MusicBot](https://github.com/Just-Some-Bots/MusicBot)

##導入

導入の記事は比較的新しく、詳細に書かれた記事がすでにありますのでこちらを
[Macでdiscordで音楽botを導入する](https://qiita.com/siro_live_0501/items/218099a457e6dbb7f3a8)

Raspberry piの方は公式ドキュメント通りにやるのが一番いいです（簡単な英語しかない）
[Guide for Raspbian（公式ガイド）](https://github.com/Just-Some-Bots/MusicBot/wiki/Guide-for-Raspbian)

##導入の注意点
OSXでMusicBotの公式の最新の情報を見ながらそれに従えばうまく行きますが、他のサイトなどの古い情報を見ながらやるとPythonのバージョンがMusicBotが想定しているバージョンとずれることがあります。
具体的にはDLが終わり最後MusicBotを起動する際に

```
./update_macdeps.command
./runbot_mac.command
```

を実行しますが、その際にrunbot_mac.commandに描かれているPythonのバージョンが古い場合があります。
一度runbot_mac.txtなどにリネームし、最後の行のpython*.* run.pyを自身のPythonのバージョンに合わせて変更してから再度runbot_mac.commandにリネームして実行して下さい（無理して.command使わなくてもいいけど一応。。。）

```
#!/bin/bash

cd "$(dirname "$BASH_SOURCE")" || {
	echo "Python 3.5 doesn't seem to be installed" >&2
exit 1
}

python3.5 run.py
```

##MusicBotのコマンドの追加方法
まず今回私が追加したコマンドはautoplaylistを操作できるようにするコマンドです。
複数人が毎回playコマンドを使いまくると面倒ですし、youtubeの再生リストを共有してdiscordで流してもニコニコ動画の音声は流せない・・・
そこでautoplaylist.txtを皆んなでいじれるようにして自動でみんなが追加した曲を流し続けてくれるようにする方法でした。
具体的にはMusicBot/musicbot/以下にある*.pyのソースコードに手を加えます。
musicbot/以下にあるbot.pyがメインのソースコードになっていて、コマンドはそれぞれ***cmd_**で始まる関数になっており、その書式に沿って関数を追加することで実装できます。

関数名を**cmd_apl**にすると!aplでこの関数が呼ばれるようになる。
ちなみに!helpでもaplが表示されるようになった
![スクリーンショット 2017-11-11 11.54.59.png](https://qiita-image-store.s3.amazonaws.com/0/204184/75a65d56-afb4-a3ef-1a77-0233b51d5f1e.png)


```bot.py
    #Discordで!aplと実行するとBotが "テスト" と発言する
    async def cmd_apl(self, player, channel, author, option, MES=None):

        """
        Usage:
            {command_prefix}apl [ + | - | add | remove | all] URL

            URLを指定してオートプレイリストに追加、消去、一覧表示ができます
        """

        return Response("テスト",reply=True, delete_after=10)
```

初めてPythonに触ったのもあり、基本的には!blacklistや!helpなど他の関数を見ながらオリジナルのコマンドを作るのが基本になりましたがなんとか完成はしました。
需要ないとは思いますが一応載せておきます、動くの優先で作ったので無駄な変数、書き方があるかもしれませんがスルーして下さい。

```bot.py
    async def cmd_apl(self, player, channel, author, option, MES=None):

        """
        Usage:
            {command_prefix}apl [ + | - | add | remove | all] URL

            URLを指定してオートプレイリストに追加、消去、一覧表示ができます
        """

        if option not in ['+', '-', 'add', 'remove','all']:
            raise exceptions.CommandError(
                '"%s" は無効なオプションです, +, -, add, remove, all を利用して' % option, expire_in=20
            )

        old_len = len(self.blacklist)

        if option in ['+', 'add']:
            
            if not MES:
                return Response('URLがないお',reply=True, delete_after=10)  
            if MES in self.autoplaylist:
                return Response('もう既にあります', reply=True, delete_after=10)

            self.autoplaylist.append(MES)

            write_file(self.config.auto_playlist_file, self.autoplaylist)

            try:
                entry, position = await player.playlist.add_entry(MES, channel=channel, author=author)
            except exceptions.WrongEntryTypeError as e:
                return await self.cmd_play(player, channel, author, permissions, leftover_args, e.use_url)

            return Response(
            '%s をプレイリストに曲を追加しました'%entry.title,
            reply=True, delete_after=10
            )
                

        elif option in ['-', 'remove']:
            
            if not MES:
                return Response('URLがありません',reply=True, delete_after=10)
            if MES not in self.autoplaylist:
                return Response('存在しません', reply=True, delete_after=10)

            self.autoplaylist.remove(MES)
            write_file(self.config.auto_playlist_file, self.autoplaylist)

            try:
                entry, position = await player.playlist.add_entry(MES, channel=channel, author=author)
            except exceptions.WrongEntryTypeError as e:
                return await self.cmd_play(player, channel, author, permissions, leftover_args, e.use_url)

            return Response(
                'プレイリストから %s を消去しました'%entry.title,
                reply=True, delete_after=10
            )

        elif option in ['all']:
            APList = "```"
            for url in self.autoplaylist:
                try:
                    entry = await player.playlist.getEntry(url, channel=channel, author=author)
                except exceptions.WrongEntryTypeError as e:
                    return await self.cmd_play(player, channel, author, permissions, leftover_args, e.use_url)
                
                APList += '・' + entry.title + '\n  ' + url + '\n'

            APList += "```"

            return Response(
                    APList,
                    reply=True, delete_after=10
                )
```

```playlist.py
async def getEntry(self, song_url, **meta):
        """
            Validates and adds a song_url to be played. This does not start the download of the song.

            Returns the entry & the position it is in the queue.

            :param song_url: The song url to add to the playlist.
            :param meta: Any additional metadata to add to the playlist entry.
        """

        try:
            info = await self.downloader.extract_info(self.loop, song_url, download=False)
        except Exception as e:
            raise ExtractionError('Could not extract information from {}\n\n{}'.format(song_url, e))

        if not info:
            raise ExtractionError('Could not extract information from %s' % song_url)

        # TODO: Sort out what happens next when this happens
        if info.get('_type', None) == 'playlist':
            raise WrongEntryTypeError("This is a playlist.", True, info.get('webpage_url', None) or info.get('url', None))

        if info['extractor'] in ['generic', 'Dropbox']:
            try:
                # unfortunately this is literally broken
                # https://github.com/KeepSafe/aiohttp/issues/758
                # https://github.com/KeepSafe/aiohttp/issues/852
                content_type = await get_header(self.bot.aiosession, info['url'], 'CONTENT-TYPE')
                print("Got content type", content_type)

            except Exception as e:
                print("[Warning] Failed to get content type for url %s (%s)" % (song_url, e))
                content_type = None

            if content_type:
                if content_type.startswith(('application/', 'image/')):
                    if '/ogg' not in content_type:  # How does a server say `application/ogg` what the actual fuck
                        raise ExtractionError("Invalid content type \"%s\" for url %s" % (content_type, song_url))

                elif not content_type.startswith(('audio/', 'video/')):
                    print("[Warning] Questionable content type \"%s\" for url %s" % (content_type, song_url))

        entry = URLPlaylistEntry(
            self,
            song_url,
            info.get('title', 'Untitled'),
            info.get('duration', 0) or 0,
            self.downloader.ytdl.prepare_filename(info),
            **meta
        )
        return entry
```

!apl all は現在登録されているautoplaylist.txtの中身のURLとそのタイトルを一覧表示するコマンドです。

実行した画面
![スクリーンショット 2017-11-11 11.53.10.png](https://qiita-image-store.s3.amazonaws.com/0/204184/49eb2f24-724b-2852-3b79-ee26e6bea918.png)

おわり