ポケサーのbotに求められる機能の傾向と対策(1/2)
暇だったので書きました。
にほんばれのdiscord鯖に常駐させているbotで「?(コマンド名)」で扱える各コマンドの使用率を調べてみました。
表にするとこんな感じ。
左上からコマンドが作成された順。斜線はdiscordでの検索が難しかったもの。
同じコマンドを同時に使っていたりするので、あくまでおおよその数です。
使用率が高いコマンドの傾向から、需要のありそうなコマンドを作成したいですね。
とりあえず使用率上位のコマンドを見ていきます。
3位:moves
ポケモンが覚える技の一覧を表示するコマンド。jetコマンドや、後述のmoveclコマンドのほうが便利な気がしますが、初期に作られたコマンドということで使用率が伸びてそうな気がします。
4位:movecl
move classの略(だったはず)で、ポケモンが覚える技のうち、「物理/特殊/変化」を指定できるコマンド。物理/特殊のうち片方しか使わないポケモンが多いことや、ダイウォール媒体を探すためによく使われている印象ですね。
~結論~
ただの戦闘狂サークル
変則ルールの仲間大会を見つけると、数分後には対応するコマンドが作られているあたり末期感ある。
とはいえ、しりとり用のコマンドや、タマゴ技の遺伝経路を探すコマンドもそれなりに使われているので、
コマンド作成の敷居を下げられるように努力するなどしたい所存です。
(それはそうと、そろそろポケモンHOMEの持ち物とかもここのコマンドにも対応させたい)
気が向いたら後半(!から始まるコマンドのほう)書きます。
数えるまでもなく!stが1位なんですが()。
SDS(ソーシャルディスタンスシングル)使用構築
岡山大・山口大・九州ポケサー連盟合同オンライン企画での特別ルール「ソーシャルディスタンスシングル」で使った構築です。
改めて見ると特に目新しいポケモンもいないですね。
ルールの詳細はこちら
簡単に説明すると
・接触技、音技、粉技及び粉と名の付く持ち物、火炎玉禁止。
・特性「えんかく」のジュナイパーは接触技を使用可能。
・ダイマックス技に関しては物理技や音技をベースとした技も使用可能。
・密っぽいポケモンは使用禁止
考察過程
・弱体化されるのが基本的に物理ポケモンなので、等の現環境で強いポケモンは当然強いままと予想。
・物理だとに強いといった岩地面技持ち、地面技繋がりでやが強そう。
・特殊が多いので、変化技が大半を占める受け要員も強いのでは?
などと普通のことを考えていた時にとあるツイートを見かけた。
確かに接触技だし負けやなぁって思ってたのだが
~約5分後~
スカーフゴチルゼルやばくね?
というわけで私的tier1ポケモン
ゴチルゼル@拘りスカーフ
特性:かげふみ
努力値:HDベース
確定枠:トリック
以下選択:まもる/コスモパワー/ねむる/いちゃもん/アシストパワー
スカーフトリックをした後にいちゃもんを撃つorPP切れが起こると、ダイマックス3ターンを枯らして悪あがきさせて判定勝ちができる。
ルール発案サークルとしてこれ使うのダメでしょって感じだったが、
これに負けるのはあまりにも癪なので、特性ねんちゃくのポケモンを採用することに決定した。
僕「トリトドンかなぁ…(アギルダーはゴチルゼルに逃げられるので)」
トリトドン@ウイの実
特性:ねんちゃく
努力値:呑気HB だと思ってたら何故か勇敢だった
スカーフトリックをしてくるを破壊するためだけのねんちゃくトリトドン。
にしか投げないつもりだった。
瞑想があればそっちを使ったが瞑想を覚えないので鈍い+地震、
にコスモパワーがあると頭を抱えることになるのでクリアスモッグ、
とりあえず入れておいて損はない自己再生
当たった人誰もゴチルゼル使ってなくて草(うちのサークルメンバーが一人使ってたらしい)
が、実際はけっこう選出した。何もかもこの環境に草技が少なすぎるのが悪い。
一度、と対面したときに頭を抱えていたが、
相手視点は呼び水なのでなんとか勝てた。
ウツロイド@とつげきチョッキ
努力値:臆病H84 C172 S252
サンダー対策として採用
ビーストブーストが発動した時にSが上がるシングルの流用個体。
パワフルハーブを持たせたかったのに後述のに奪われた。
といった地面の一貫を切るために採用。
は宿り木を入れればどうとでもなるでしょの精神だったが、世界に想像以上に存在してなかったので関係なかった。
カビゴン@フィラの実
特性:くいしんぼう、キョダイ個体
努力値:意地っ張り H104 A252 B 104 D 4 S44
調整意図不明の流用個体。
物理ノーマル技で自爆が使えることを知って大喜びしながら採用した。
に強めなのも○
技はコンセプトの腹太鼓自爆、に撃つ地震、地面タイプに撃つダイアイスの元技となる冷凍パンチ
大体の試合で選出した。というかこいつがいないとパワーが低すぎる。
ダイマックスしないと浮いてるポケモンにまともに攻撃できないことを除けば完璧だった。
パンプジン@オボンの実
特性:ふみんにしようと思ったけどお見通し個体しか持ってなかった
努力値:呑気HB
を使う上でのトリル展開要員。
耐久よりのポケモンなのでゴチルゼルから逃げられるゴーストタイプなのは偉い。
ボックスにいたから適当に連れてきたが、残飯をに強奪された。
マジフレの枠は元々ゴーストダイブにしていたが、非接触技一覧の中から適当に選んだ。
あまり選出しなかったし、選出しても活躍する前に試合が終わってた。
想像以上にダイジェット要員が多かったので反省。
カプ・レヒレ@残飯
特性:ミストメイカー
努力値:控え目 H108 C220 S180
例によって調整意図不明の個体。7世代の頃に使ってた調整の使い回し
受けポケモンがやや重いので、スカーフトリックも考えたが性に合わないのでこの構成になった。
ラッキーがちきゅうなげを使えないのがかなりの追い風。
毒づきを持たない程度なら対面から勝てる。
想像以上にフェアリー技の通りが悪かったので全然選出しなかった。
Discord Botで音楽再生用Botを作る(完結編)
Top→Discord Botの作り方 - 1/(1+e^(-ax))
prev→Discord Botでファイル構造を送信する - 1/(1+e^(-ax))
next→未定
今回が最後のつもりでしたが思ってたよりもやることが残っていたので手っ取り早くやります。
- フォルダを指定して再生する
- botのステータスに曲名を表示する、ループ再生を可能にする
- フォルダの表示をembedの文字数上限に合わせる
- 音楽を制御する
- 現在再生中の曲の詳細情報(ファイルの位置)を表示する
- 一定時間何もしないとbotが勝手にボイスチャンネルから落ちる
フォルダを指定して再生する
次の項目と絡んでいるのでまとめてやります。
botのステータスに曲名を表示する、ループ再生を可能にする
以下のように修正します。
- AudioStatusに曲のファイルパスと、ループ再生するか否かの値を保存します。
class AudioQueue()のadd_audioとplaying_task()を次のようにします。
#曲の追加 async def add_audio(self, title, path, isloop = False): await self.queue.put([title, path, isloop]) #曲の再生(再生にはffmpegが必要) async def playing_task(self): while True: self.playing.clear() try: title, path, isloop = await asyncio.wait_for(self.queue.get(), timeout = 100) except asyncio.TimeoutError: asyncio.create_task(self.leave()) selfpath = os.path.dirname(__file__) self.vc.play(discord.FFmpegPCMAudio(executable=selfpath+"/bin/ffmpeg.exe", source=path), after = self.play_next) if (isloop): await self.add_audio(title, path, isloop = True) activity = discord.Activity(name=title, type=discord.ActivityType.listening) #アクティビティの作成 self.bgminfo = path #後で使います await bot.change_presence(activity=activity) #アクティビティの更新 await self.playing.wait()
- bgmコマンドを次のように変更します。
@commands.command() async def bgm(self, ctx, *name): """play music""" if (ctx.author.voice is None): #送信者がボイスチャンネルにいなければエラーを返す await ctx.send(f'{ctx.author.mention} ボイスチャンネルが見つかりません') return if ((self.audio_status is None) or (self.audio_status.vc is None)): #botがボイスチャンネルに入っていなければ voice_channel = ctx.author.voice.channel.id #送信者の入っているボイスチャンネルのID vc = await bot.get_channel(voice_channel).connect() #ボイスチャンネルに入る self.audio_status = AudioStatus(vc) #filenameの作成(nameの連結) filename = '' for s in name: filename += s + ' ' filename = filename[:-1] if (len(filename) == 0): #引数無しなら全曲を追加 for i in range(len(self.music_titles)): await self.audio_status.add_audio(self.music_titles[i], self.music_pathes[i]) elif filename in self.music_titles: #指定された曲がある場合 idx = self.music_titles.index(filename) #リストの何番目にあるかを探す await self.audio_status.add_audio(filename, self.music_pathes[idx]) #対応する絶対パスを再生キューに追加 elif (filename in self.mdir_name): #ディレクトリ名に一致した場合,該当するディレクトリ下にある全ての曲を再生キューに追加 idx = self.mdir_name.index(filename) cur_path = os.getcwd() os.chdir(self.path + os.sep + self.music_dirs[idx]) #指定したフォルダに移動 music_pathes = [p for p in glob('**', recursive=True) if os.path.isfile(p)] #音楽ファイル一覧 music_titles = [os.path.splitext(os.path.basename(path))[0] for path in music_pathes] os.chdir(cur_path) #カレントディレクトリを戻す length = len(music_titles) for i in range(length): #トラック番号の除去 if (re.fullmatch(r'[0-9][0-9] .*', music_titles[i])): music_titles[i] = (music_titles[i])[3:] numbers = len(music_pathes) for i in range(numbers): await self.audio_status.add_audio(music_titles[i], self.path + os.sep + self.music_dirs[idx] + os.sep + music_pathes[i]) else: #それ以外 await ctx.send('Audio File Not Found') return
ループ再生を行いたい場合は、await add_audio()の引数にisloop=Trueを加えればループ再生されるようになります(bgmコマンドを別名のコマンドとしてコピーしてadd_audio()にisloop=Trueを加えるのが簡単)。
フォルダの表示をembedの文字数上限に合わせる
前回触れませんでしたが、make_filetree関数の引数にnestというものがあるのでこれを使います。
再帰呼び出しを利用するため__BGMにsend_tree関数を作成し、bgmlistコマンドも少し変更します。
send_tree()
#embedに収まる範囲でファイル構造を送信する #path:送るファイル構造の頂点のファイルパス #nest:何階層分を送信するか async def send_tree(self, ctx, path, nest = -1): if (nest == 0): await ctx.send('エラー:該当するデータが多すぎます') if (nest == -1): #とりあえずネスト上限無しで送信してみる tree = cmd_bgm.make_filetree(path) nest = cmd_bgm.depth(tree) else: tree = cmd_bgm.make_filetree(path, nest = nest) result = await send_list(ctx.send, '', tree, delimiter = ['\n'+'....'*i+'├' for i in range(nest)], senderr = False) if (result is False): await self.send_tree(ctx, path, nest = nest-1) #ネストを1つ浅くしてやり直し return
bgmlist()
@commands.command() async def bgmlist(self, ctx, *dir_name): """一覧""" cur_path = os.getcwd() MUSIC_PATH = os.path.dirname(__file__) os.chdir(MUSIC_PATH) #カレントディレクトリの移動 dirname = '' for s in dir_name: #引数を1つの文字列に纏める dirname += s + ' ' dirname = dirname[:-1] if (len(dirname) == 0): #引数無しなら全てのディレクトリを表示 await self.send_tree(ctx=ctx, path=MUSIC_PATH+os.sep+'bgm') else: for f in self.music_dirs: if (dirname == pathlib.Path(f).parts[-1]): #ディレクトリ名と引数が一致した場合,表示 current = f.split(os.sep)[1:][0] tree = make_filetree(MUSIC_PATH+os.sep+f[:-1*len(os.sep)]) if (len(tree) != 1): #該当ディレクトリの下にディレクトリがあった場合は木構造を表示 result = await send_list(ctx.send, '', tree, delimiter = ['\n'+'....'*i+'├' for i in range(10)]) if (result is None): nest = depth(tree) await self.send_tree(ctx=ctx, path=MUSIC_PATH+os.sep+f, nest = nest-1) else: #ディレクトリを持たなければオーディオファイルの一覧を表示 os.chdir(MUSIC_PATH + os.sep + f) music_titles = [os.path.splitext(os.path.basename(p))[0] for p in glob('*', recursive=True) if os.path.isfile(p)] length = len(music_titles) for i in range(length): if (re.fullmatch(r'[0-9][0-9] .*', music_titles[i])): music_titles[i] = (music_titles[i])[3:] await send_list(ctx.send, '', music_titles) break os.chdir(cur_path) #カレントディレクトリを戻す return
音楽を制御する
それぞれが短いのでまとめて書きます。それぞれの場所に追記してください。
AudioQueue
#曲が再生中ならtrue def is_playing(self): return self.vc.is_playing()
__BGM
@commands.command() async def pause(self, ctx): """再生中のbgmの一時停止""" if (self.audio_status.is_playing()): self.audio_status.vc.pause() await ctx.message.delete() return @commands.command() async def resume(self, ctx): """再生中のbgmの再開""" self.audio_status.vc.resume() await ctx.message.delete() return @commands.command() async def stop(self, ctx): """再生中のbgmの中断""" if (self.audio_status.is_playing()): self.audio_status.vc.stop() return @commands.command() async def clear(self, ctx): """再生キューのリセット""" self.audio_status.queue.reset() return @commands.command() async def queue(self, ctx): """再生キューの表示""" await send_list(ctx.send, '', [x[0] for x in self.audio_status.queue], title = '再生キュー', isembed = True, half = True) return
現在再生中の曲の詳細情報(ファイルの位置)を表示する
AudioQueue
def playing_info(self): if (self.bgminfo is None): return 'This bot is not playing an Audio File' return self.bgminfo
__BGM
@commands.command() async def bgminfo(self, ctx): """再生中のBGMのパスを表示""" if (self.audio_status is None): await ctx.send(f'{ctx.author.mention} This bot is not playing an Audio File') else: await ctx.send(f'{ctx.author.mention} {self.audio_status.playing_info()[len(self.path)+1:]}') return
一定時間何もしないとbotが勝手にボイスチャンネルから落ちる
仕様っぽいです。アクティビティ等が更新されなかったり面倒なことになるので、更新用の関数を置いておきます。bgmコマンドやremoveコマンドの最初あたりで呼び出してやってください。
AudioQueue
#再生する曲が無くなる等でweb socketが切断されていればtrue def is_closed(self): return (self.vc is None or (self.vc.is_connected() == False))
__BGM
#再生キューが空の状態で放置しているとvcから切断されるため,bgm及びremoveコマンドで現在の接続状況を再読み込みする async def reload_state(self): if (self.audio_status == None or self.audio_status.is_closed()): #vcから切断済みである場合 global voice, now_vc activity = discord.Activity(name='Python', type=discord.ActivityType.playing) #アクティビティも修正する await bot.change_presence(activity=activity) now_vc = None voice = None return
これくらいあれば足りると思います。
音楽ファイルをbotに投げつけたり、youtubeからDLするなどで追加することもできますが、ここでは扱わないことにします。
シャッフル再生等はお好みでやってください。
このページの一番下に一応ここまでのものをまとめたバージョンを置いておきます。
ここまでお疲れ様でした。
next→需要があれば
音楽再生bot(サンプル)
import discord from discord.ext import commands import os import asyncio from glob import glob import pathlib import re TOKEN = "token" #トークン PREFIX = '!' #prefix=接頭辞 #botの作成 bot = commands.Bot(command_prefix=PREFIX) #bgmコマンドで使う再生キュー class AudioQueue(asyncio.Queue): def __init__(self): super().__init__(0) #再生キューの上限を設定しない def __getitem__(self, idx): return self._queue[idx] #idx番目を取り出し def to_list(self): return list(self._queue) #キューをリスト化 def reset(self): self._queue.clear() #キューのリセット #bgmコマンドで使う,現在の再生状況を管理するクラス class AudioStatus: def __init__(self, vc): self.vc = vc #自分が今入っているvc self.queue = AudioQueue() #再生キュー self.playing = asyncio.Event() asyncio.create_task(self.playing_task()) #曲の追加 async def add_audio(self, title, path, isloop = False): await self.queue.put([title, path, isloop]) #曲の再生(再生にはffmpegが必要) async def playing_task(self): while True: self.playing.clear() try: title, path, isloop = await asyncio.wait_for(self.queue.get(), timeout = 100) except asyncio.TimeoutError: asyncio.create_task(self.leave()) selfpath = os.path.dirname(__file__) self.vc.play(discord.FFmpegPCMAudio(executable=selfpath+"/bin/ffmpeg.exe", source=path), after = self.play_next) if (isloop): await self.add_audio(title, path, isloop = True) activity = discord.Activity(name=title, type=discord.ActivityType.listening) #アクティビティの作成 self.bgminfo = path #後で使います await bot.change_presence(activity=activity) #アクティビティの更新 await self.playing.wait() #playing_taskの中で呼び出される #再生が終わると次の曲を再生する def play_next(self, err=None): self.bgminfo = None self.playing.set() return #vcから切断 async def leave(self): self.queue.reset() #キューのリセット if self.vc: await self.vc.disconnect() self.vc = None return #曲が再生中ならtrue def is_playing(self): return self.vc.is_playing() def playing_info(self): if (self.bgminfo is None): return 'This bot is not playing an Audio File' return self.bgminfo #再生する曲が無くなる等でweb socketが切断されていればtrue def is_closed(self): return (self.vc is None or (self.vc.is_connected() == False)) #多重リストを区切り文字で展開する def list2str(list_, delimiter): result = '' #区切り文字が存在しなければスペースを区切り文字とする if (len(delimiter) == 0): d = ' ' #区切り文字 for s in list_: result += str(s) + d return result[:-1] #区切り文字=delimiterの第一要素 d = delimiter[0] for s in list_: #list_の中にリストがあれば再帰呼び出し if (type(s) is list): result += list2str(s, delimiter[1:]) + d else: result += str(s) + d return result[:-1*len(d)] def make_filetree(path, layer=0, is_last=False, nest = -1): if (nest == 0): return '' if (nest == 1): is_last = True d = [] #pathが相対パスなら絶対パスに直す if not pathlib.Path(path).is_absolute(): path = str(pathlib.Path(path).resolve()) # カレントディレクトリの表示 current = path.split(os.sep)[::-1][0] d.append(pathlib.Path(current).parts[-1]) # 下の階層のパスを取得 paths = [p for p in glob(path+'/*') if os.path.isdir(p) or os.path.isfile(p)] def is_last_path(i): return i == len(paths)-1 # 再帰的に表示 for i, p in enumerate(paths): if os.path.isdir(p): #フォルダなら自身を再帰呼び出し d.append(make_filetree(p, layer=layer+1, is_last=is_last_path(i), nest = nest-1)) if (nest == 1): break return d #フォルダの一覧をリストで返す def depth(k): if not k: return 0 else: if isinstance(k, list): return 1 + max(depth(i) for i in k) else: return 0 async def send_list(send_method, mention, mes, title = 'Result', delimiter = ['\n'], isembed = True, senderr = True, half = False, ishalf = False): if (len(mes) == 0): #listの長さ=0 await send_method(f'{mention} 該当するデータがありません') else: #listを文字列に変換 reply = list2str(mes, delimiter) if (ishalf): reply += "\n………" if (isembed): try: embed = discord.Embed(title=title, description=reply) await send_method(f'{mention} ', embed=embed) except: if (half and len(mes) > 1): result = await send_list(send_method, mention, mes[:int(len(mes)/2)], title, delimiter, isembed, senderr, half, ishalf = True) return result if (senderr): await send_method(f'{mention} エラー:該当するデータが多すぎます') return False else: await send_method(f'{mention} \n'+reply) return True class __BGM(commands.Cog, name= 'BGM'): #BGMという名前でCogを定義する def search_audiofiles(self): cur_path = os.getcwd() #カレントディレクトリ MUSIC_PATH = os.path.dirname(__file__) os.chdir(MUSIC_PATH) #オーディオファイルがある場所の頂点に移動 self.music_pathes = [p for p in glob('bgm/**', recursive=True) if os.path.isfile(p)] #オーディオファイルの検索(相対パス) self.music_titles = [os.path.splitext(os.path.basename(path))[0] for path in self.music_pathes] #オーディオファイルの名前から拡張子とパスを除去したリストを作成 self.music_pathes = [MUSIC_PATH + os.sep + p for p in self.music_pathes] #絶対パスに変換 self.music_dirs = glob(os.path.join('bgm', '**' + os.sep), recursive=True) #ディレクトリの一覧を作成(相対パス) self.mdir_name = [pathlib.Path(f).parts[-1] for f in self.music_dirs] os.chdir(cur_path) #カレントディレクトリを戻す return def __init__(self, bot): super().__init__() self.bot = bot self.audio_status = None self.path = os.path.dirname(__file__) #このファイルが置いてあるディレクトリまでのファイルパス self.search_audiofiles() #embedに収まる範囲でファイル構造を送信する #path:送るファイル構造の頂点のファイルパス #nest:何階層分を送信するか async def send_tree(self, ctx, path, nest = -1): if (nest == 0): await ctx.send('エラー:該当するデータが多すぎます') if (nest == -1): #とりあえずネスト上限無しで送信してみる tree = make_filetree(path) nest = depth(tree) else: tree = make_filetree(path, nest = nest) result = await send_list(ctx.send, '', tree, delimiter = ['\n'+'....'*i+'├' for i in range(nest)], senderr = False) if (result is False): await self.send_tree(ctx, path, nest = nest-1) #ネストを1つ浅くしてやり直し return #再生キューが空の状態で放置しているとvcから切断されるため,bgm及びremoveコマンドで現在の接続状況を再読み込みする async def reload_state(self): if (self.audio_status == None or self.audio_status.is_closed()): #vcから切断済みである場合 global voice, now_vc activity = discord.Activity(name='Python', type=discord.ActivityType.playing) #アクティビティも修正する await bot.change_presence(activity=activity) now_vc = None voice = None return @commands.command() async def bgm(self, ctx, *name): """play music""" await self.reload_state() if (ctx.author.voice is None): #送信者がボイスチャンネルにいなければエラーを返す await ctx.send(f'{ctx.author.mention} ボイスチャンネルが見つかりません') return if ((self.audio_status is None) or (self.audio_status.vc is None)): #botがボイスチャンネルに入っていなければ voice_channel = ctx.author.voice.channel.id #送信者の入っているボイスチャンネルのID vc = await bot.get_channel(voice_channel).connect() #ボイスチャンネルに入る self.audio_status = AudioStatus(vc) #filenameの作成(nameの連結) filename = '' for s in name: filename += s + ' ' filename = filename[:-1] if (len(filename) == 0): #引数無しなら全曲を追加 for i in range(len(self.music_titles)): await self.audio_status.add_audio(self.music_titles[i], self.music_pathes[i]) elif filename in self.music_titles: #指定された曲がある場合 idx = self.music_titles.index(filename) #リストの何番目にあるかを探す await self.audio_status.add_audio(filename, self.music_pathes[idx]) #対応する絶対パスを再生キューに追加 elif (filename in self.mdir_name): #ディレクトリ名に一致した場合,該当するディレクトリ下にある全ての曲を再生キューに追加 idx = self.mdir_name.index(filename) cur_path = os.getcwd() os.chdir(self.path + os.sep + self.music_dirs[idx]) #指定したフォルダに移動 music_pathes = [p for p in glob('**', recursive=True) if os.path.isfile(p)] #音楽ファイル一覧 music_titles = [os.path.splitext(os.path.basename(path))[0] for path in music_pathes] os.chdir(cur_path) #カレントディレクトリを戻す length = len(music_titles) for i in range(length): #トラック番号の除去 if (re.fullmatch(r'[0-9][0-9] .*', music_titles[i])): music_titles[i] = (music_titles[i])[3:] numbers = len(music_pathes) for i in range(numbers): await self.audio_status.add_audio(music_titles[i], self.path + os.sep + self.music_dirs[idx] + os.sep + music_pathes[i]) else: #それ以外 await ctx.send('Audio File Not Found') return @commands.command() async def loopbgm(self, ctx, *name): """loop music""" await self.reload_state() if (ctx.author.voice is None): #送信者がボイスチャンネルにいなければエラーを返す await ctx.send(f'{ctx.author.mention} ボイスチャンネルが見つかりません') return if ((self.audio_status is None) or (self.audio_status.vc is None)): #botがボイスチャンネルに入っていなければ voice_channel = ctx.author.voice.channel.id #送信者の入っているボイスチャンネルのID vc = await bot.get_channel(voice_channel).connect() #ボイスチャンネルに入る self.audio_status = AudioStatus(vc) #filenameの作成(nameの連結) filename = '' for s in name: filename += s + ' ' filename = filename[:-1] if (len(filename) == 0): #引数無しなら全曲を追加 for i in range(len(self.music_titles)): await self.audio_status.add_audio(self.music_titles[i], self.music_pathes[i], isloop = True) elif filename in self.music_titles: #指定された曲がある場合 idx = self.music_titles.index(filename) #リストの何番目にあるかを探す await self.audio_status.add_audio(filename, self.music_pathes[idx], isloop = True) #対応する絶対パスを再生キューに追加 elif (filename in self.mdir_name): #ディレクトリ名に一致した場合,該当するディレクトリ下にある全ての曲を再生キューに追加 idx = self.mdir_name.index(filename) cur_path = os.getcwd() os.chdir(self.path + os.sep + self.music_dirs[idx]) #指定したフォルダに移動 music_pathes = [p for p in glob('**', recursive=True) if os.path.isfile(p)] #音楽ファイル一覧 music_titles = [os.path.splitext(os.path.basename(path))[0] for path in music_pathes] os.chdir(cur_path) #カレントディレクトリを戻す length = len(music_titles) for i in range(length): #トラック番号の除去 if (re.fullmatch(r'[0-9][0-9] .*', music_titles[i])): music_titles[i] = (music_titles[i])[3:] numbers = len(music_pathes) for i in range(numbers): await self.audio_status.add_audio(music_titles[i], self.path + os.sep + self.music_dirs[idx] + os.sep + music_pathes[i], isloop = True) else: #それ以外 await ctx.send('Audio File Not Found') return #botをボイスチャンネルから切断する @commands.command() async def remove(self, ctx): await self.audio_status.leave() await self.reload_state() return @commands.command() async def bgmlist(self, ctx, *dir_name): """一覧""" cur_path = os.getcwd() MUSIC_PATH = os.path.dirname(__file__) os.chdir(MUSIC_PATH) #カレントディレクトリの移動 dirname = '' for s in dir_name: #引数を1つの文字列に纏める dirname += s + ' ' dirname = dirname[:-1] if (len(dirname) == 0): #引数無しなら全てのディレクトリを表示 await self.send_tree(ctx=ctx, path=MUSIC_PATH+os.sep+'bgm') else: for f in self.music_dirs: if (dirname == pathlib.Path(f).parts[-1]): #ディレクトリ名と引数が一致した場合,表示 current = f.split(os.sep)[1:][0] tree = make_filetree(MUSIC_PATH+os.sep+f[:-1*len(os.sep)]) if (len(tree) != 1): #該当ディレクトリの下にディレクトリがあった場合は木構造を表示 result = await send_list(ctx.send, '', tree, delimiter = ['\n'+'....'*i+'├' for i in range(10)]) if (result is None): nest = depth(tree) await self.send_tree(ctx=ctx, path=MUSIC_PATH+os.sep+f, nest = nest-1) else: #ディレクトリを持たなければオーディオファイルの一覧を表示 os.chdir(MUSIC_PATH + os.sep + f) music_titles = [os.path.splitext(os.path.basename(p))[0] for p in glob('*', recursive=True) if os.path.isfile(p)] length = len(music_titles) for i in range(length): if (re.fullmatch(r'[0-9][0-9] .*', music_titles[i])): music_titles[i] = (music_titles[i])[3:] await send_list(ctx.send, '', music_titles) break os.chdir(cur_path) #カレントディレクトリを戻す return @commands.command() async def pause(self, ctx): """再生中のbgmの一時停止""" if (self.audio_status.is_playing()): self.audio_status.vc.pause() return @commands.command() async def resume(self, ctx): """再生中のbgmの再開""" self.audio_status.vc.resume() return @commands.command() async def stop(self, ctx): """再生中のbgmの中断""" if (self.audio_status.is_playing()): self.audio_status.vc.stop() return @commands.command() async def clear(self, ctx): """再生キューのリセット""" self.audio_status.queue.reset() return @commands.command() async def queue(self, ctx): """再生キューの表示""" await send_list(ctx.send, '', [x[0] for x in self.audio_status.queue], title = '再生キュー', isembed = True, half = True) return @commands.command() async def bgminfo(self, ctx): """再生中のBGMのパスを表示""" if (self.audio_status is None): await ctx.send(f'{ctx.author.mention} This bot is not playing an Audio File') else: await ctx.send(f'{ctx.author.mention} {self.audio_status.playing_info()[len(self.path)+1:]}') return bot.add_cog(__BGM(bot=bot)) bot.run(TOKEN)
Discord Botでフォルダの階層構造を送信する
Top→Discord Botの作り方 - 1/(1+e^(-ax))
prev→リストを埋め込み形式で送信する - 1/(1+e^(-ax))
next→Discord Botで音楽再生用Botを作る(完結編) - 1/(1+e^(-ax))
ファイル構造をリストで取得した後に、それを前回作ったsend_list関数で送信するようにします。
ファイル構造を取得するmake_filetree関数は次のようになります。
import pathlib def make_filetree(path, layer=0, is_last=False, nest = -1): if (nest == 0): return '' if (nest == 1): is_last = True d = [] #pathが相対パスなら絶対パスに直す if not pathlib.Path(path).is_absolute(): path = str(pathlib.Path(path).resolve()) # カレントディレクトリの表示 current = path.split(os.sep)[::-1][0] d.append(pathlib.Path(current).parts[-1]) # 下の階層のパスを取得 paths = [p for p in glob(path+'/*') if os.path.isdir(p) or os.path.isfile(p)] def is_last_path(i): return i == len(paths)-1 # 再帰的に表示 for i, p in enumerate(paths): if os.path.isdir(p): #フォルダなら自身を再帰呼び出し d.append(make_filetree(p, layer=layer+1, is_last=is_last_path(i), nest = nest-1)) if (nest == 1): break return d #フォルダの一覧をリストで返す
処理の流れとしては指定したパスにあるフォルダ全てに対して、もう一度make_filetree()関数を適用することで最下層までのフォルダを探索しています。
また、後々使うことになる、リストの深さを返すdepth()関数も作成しておきます。
def depth(k): if not k: return 0 else: if isinstance(k, list): return 1 + max(depth(i) for i in k) else: return 0
次に、フォルダの指定を受け付けるために、音楽ファイルの一覧を作成したのと同様にフォルダ一覧も作成しておきます。
search_audiofiles()関数を次のように修正してください。
def search_audiofiles(self): cur_path = os.getcwd() #カレントディレクトリ MUSIC_PATH = os.path.dirname(__file__) os.chdir(MUSIC_PATH) #オーディオファイルがある場所の頂点に移動 self.music_pathes = [p for p in glob('bgm/**', recursive=True) if os.path.isfile(p)] #オーディオファイルの検索(相対パス) self.music_titles = [os.path.splitext(os.path.basename(path))[0] for path in self.music_pathes] #オーディオファイルの名前から拡張子とパスを除去したリストを作成 self.music_pathes = [MUSIC_PATH + os.sep + p for p in self.music_pathes] #絶対パスに変換 self.music_dirs = glob(os.path.join('bgm', '**' + os.sep), recursive=True) #ディレクトリの一覧を作成(相対パス) self.mdir_name = [pathlib.Path(f).parts[-1] for f in self.music_dirs] os.chdir(cur_path) #カレントディレクトリを戻す return
これで準備が整ったので、いよいよ送信してみたいと思います。
class __BGMの中に以下のコマンドを作成します。
@commands.command() async def bgmlist(self, ctx, *dir_name): """一覧""" cur_path = os.getcwd() MUSIC_PATH = os.path.dirname(__file__) os.chdir(MUSIC_PATH) #カレントディレクトリの移動 dirname = '' for s in dir_name: #引数を1つの文字列に纏める dirname += s + ' ' dirname = dirname[:-1] if (len(dirname) == 0): #引数無しなら全てのディレクトリを表示 tree = make_filetree(MUSIC_PATH+os.sep+'bgm') await send_list(ctx.send, '', tree, delimiter = ['\n'+'....'*i+'├' for i in range(10)]) else: for f in self.music_dirs: if (dirname == pathlib.Path(f).parts[-1]): #ディレクトリ名と引数が一致した場合,表示 current = f.split(os.sep)[1:][0] tree = make_filetree(MUSIC_PATH+os.sep+f[:-1*len(os.sep)]) if (len(tree) != 1): #該当ディレクトリの下にディレクトリがあった場合は木構造を表示 result = await send_list(ctx.send, '', tree, delimiter = ['\n'+'....'*i+'├' for i in range(10)]) if (result is None): nest = depth(tree) else: #ディレクトリを持たなければオーディオファイルの一覧を表示 os.chdir(MUSIC_PATH + os.sep + f) music_titles = [os.path.splitext(os.path.basename(p))[0] for p in glob('*', recursive=True) if os.path.isfile(p)] await send_list(ctx.send, '', music_titles) break os.chdir(cur_path) #カレントディレクトリを戻す return
これで「!bgmlist」と打つとフォルダの一覧が、「!bgmlist (フォルダ名)」と撃つとそのフォルダの中身が表示されるはずです。
次回は細かい修正を行って完成としたいとおもいます。
お疲れ様でした。
リストを埋め込み形式で送信する
Top→Discord Botの作り方 - 1/(1+e^(-ax))
prev→特定のフォルダにあるファイルの一覧を作成する - 1/(1+e^(-ax))
next→Discord Botでファイル構造を送信する - 1/(1+e^(-ax))
bgmの入ったファイル構造や、再生キューの状態を送信するために、リストを簡単に送信する関数を作ります。
リストをそのまま送るのは見づらいため、まずはリストを文字列に変換する関数を作ります。
↓埋め込み(Embed)はこんな感じのやつです。↓
基本的には要素ごとに改行で区切りますが、リストの中にリストがある場合は複数の区切り文字を設定するようにします。
例
送信:[A, [B, C], [D, E], F]
区切り文字['\n', '、']
※'\n'は改行の意
↓
ーーーーーーーーー
A
B、C
D、E
F
ーーーーーーーーー
再帰関数で作ります。fを今回作成する関数として、
先ほどの例だと
f([A, [B, C], [D, E], F]), 区切り文字='\n'
ーーーーーーーーーーーーーーーーーー
f('A')+f( [B, C] )+f( [D, E] ) + f('F')
ーーーーーーーーーーーーーーーーーー
A
f('B') + f('C')
f('D') + f('E')
F
区切り文字='、'
ーーーーーーーーーーーーーーーーーー
A
B、C
D、E
F
ーーーーーーーーーーーーーーーーーー
のような流れとします。
#多重リストを区切り文字で展開する def list2str(list_, delimiter): result = '' #区切り文字が存在しなければスペースを区切り文字とする if (len(delimiter) == 0): d = ' ' #区切り文字 for s in list_: result += str(s) + d return result[:-1] #区切り文字=delimiterの第一要素 d = delimiter[0] for s in list_: #list_の中にリストがあれば再帰呼び出し if (type(s) is list): result += list2str(s, delimiter[1:]) + d else: result += str(s) + d return result[:-1*len(d)]
埋め込みを使ったリストの送信は次のようになります。
Embedには文字数制限があるので、文字数がオーバーした際に文字数過多のメッセージを送るか、送るメッセージを減らすかを引数halfとsenderrで設定しています。
async def send_list(send_method, mention, mes, title = 'Result', delimiter = ['\n'], isembed = True, senderr = True, half = False, ishalf = False): if (len(mes) == 0): #listの長さ=0 await send_method(f'{mention} 該当するデータがありません') else: #listを文字列に変換 reply = list2str(mes, delimiter) if (ishalf): reply += "\n………" if (isembed): try: embed = discord.Embed(title=title, description=reply) await send_method(f'{mention} ', embed=embed) except: if (half and len(mes) > 1): result = await send_list(send_method, mention, mes[:int(len(mes)/2)], title, delimiter, isembed, senderr, half, ishalf = True) return result if (senderr): await send_method(f'{mention} エラー:該当するデータが多すぎます') return False else: await send_method(f'{mention} \n'+reply) return True
戻り値は、正しく送信できればTrueであり、senderr=Trueのときに文字数オーバーだった場合はFalseが返ります。
試しに、適当なコマンドを作成して
await send_list(ctx.send, ctx.author.mention, ['A',['B','C'],['D','E'],'F'], delimiter = ['\n',','])
という処理を行ってみると、先ほど例で上げたような埋め込みが送信されると思います。
次回は、音楽ファイルが入っているファイル構造の送信を行ってみたいと思います。
お疲れ様でした。
特定のフォルダにあるファイルの一覧を作成する
Top→Discord Botの作り方 - 1/(1+e^(-ax))
prev→Discord BotでCogを使ってみる - 1/(1+e^(-ax))
next→リストを埋め込み形式で送信する - 1/(1+e^(-ax))
前回の冒頭で少し触れましたが、コマンドを入力するたびに音楽ファイルの一覧を作るのは処理が遅くなってしまうので、起動時にファイルの一覧を作成することにします。
今回はglobという標準ライブラリを使って、手元の音楽ファイルの一覧を作る関数を実装します。
ファイルの先頭にfrom glob import globという一行を追加しておいてください。
長くなるので作成する関数のみを置いておきます。
前回作成した__BGMの中に置くことを想定して書いています。
def search_audiofiles(self): cur_path = os.getcwd() #カレントディレクトリ MUSIC_PATH = os.path.dirname(__file__) os.chdir(MUSIC_PATH) #オーディオファイルがある場所の頂点に移動 self.music_pathes = [p for p in glob('bgm/**', recursive=True) if os.path.isfile(p)] #オーディオファイルの検索(相対パス) self.music_pathes = [MUSIC_PATH + os.sep + p for p in self.music_pathes] #絶対パスに変換 os.chdir(cur_path) #カレントディレクトリを戻す return
これを__BGMの中に置いて、__init__の最後でself.search_files()で呼び出すことで、音楽ファイルの一覧を取得できます。
ただ、このままだと絶対パスしか使えないのでファイル名の一覧も作成します。
ファイル名一覧は次のようにして作成できます。
self.music_titles = [os.path.splitext(os.path.basename(path))[0] for path in self.music_pathes]
ファイル名びのついでに、拡張子も削除しています。
また、これは絶対パスの一覧と対応しているので、ここで作成したリストにある曲名とファイル名が一致したらその曲を再生キューに入れるように書き換えます。
__BGMの全体図は以下の通りです。
class __BGM(commands.Cog, name= 'BGM'): #BGMという名前でCogを定義する def search_audiofiles(self): cur_path = os.getcwd() #カレントディレクトリ MUSIC_PATH = os.path.dirname(__file__) os.chdir(MUSIC_PATH) #オーディオファイルがある場所の頂点に移動 self.music_pathes = [p for p in glob('bgm/**', recursive=True) if os.path.isfile(p)] #オーディオファイルの検索(相対パス) self.music_titles = [os.path.splitext(os.path.basename(path))[0] for path in self.music_pathes] #オーディオファイルの名前から拡張子とパスを除去したリストを作成 print(self.music_titles) self.music_pathes = [MUSIC_PATH + os.sep + p for p in self.music_pathes] #絶対パスに変換 os.chdir(cur_path) #カレントディレクトリを戻す return def __init__(self, bot): super().__init__() self.bot = bot self.audio_status = None self.path = os.path.dirname(__file__) #このファイルが置いてあるディレクトリまでのファイルパス self.search_audiofiles() #予め決めておいた音楽ファイルを再生する @commands.command() async def bgm(self, ctx, filename): """play music""" if (ctx.author.voice is None): #送信者がボイスチャンネルにいなければエラーを返す await ctx.send(f'{ctx.author.mention} ボイスチャンネルが見つかりません') return if ((self.audio_status is None) or (self.audio_status.vc is None)): #botがボイスチャンネルに入っていなければ voice_channel = ctx.author.voice.channel.id #送信者の入っているボイスチャンネルのID vc = await bot.get_channel(voice_channel).connect() #ボイスチャンネルに入る self.audio_status = AudioStatus(vc) if (len(filename) == 0): #引数無しなら全曲を追加 for f in self.music_pathes: await self.audio_status.add_audio(f) elif filename in self.music_titles: #指定された曲がある場合 idx = self.music_titles.index(filename) #リストの何番目にあるかを探す await self.audio_status.add_audio(self.music_pathes[idx]) #対応する絶対パスを再生キューに追加 else: #それ以外 await ctx.send('Audio File Not Found') return #botをボイスチャンネルから切断する @commands.command() async def remove(self, ctx): await self.audio_status.leave() return
これでファイル名を指定して再生できるようになりました。
bgmフォルダの中に適当にフォルダを作成して、その中に音楽ファイルを置いて実行してみてください。
「!bgm filename」(filenameは拡張子を除くファイル名)
次回はファイル一覧の表示をするための準備を行います。
お疲れ様でした。
next→リストを埋め込み形式で送信する - 1/(1+e^(-ax))
オマケ(拡張子だけでなくCDのトラック番号も除去する)
ファイルの先頭に「import re」、self.music_titlesの定義の後に次の4行を追加してください。
length = len(self.music_titles) for i in range(length): #トラック番号も除去 if (re.fullmatch(r'[0-9][0-9] .*', self.music_titles[i])): self.music_titles[i] = (self.music_titles[i])[3:]
これでCDのトラック番号も除去できます。
Discord BotでCogを使ってみる
Top→Discord Botの作り方 - 1/(1+e^(-ax))
prev→Discord Botで予め準備しておいた音楽を連続再生する - 1/(1+e^(-ax))
next→特定のフォルダにあるファイルの一覧を作成する - 1/(1+e^(-ax))
前回で連続再生が可能になったので今回は曲の指定…といきたいところなのですが、曲の一覧をグローバル変数に保存しておいたり毎回読み込むのもアレなのでCogというものを活用してみたいと思います。
Cogとは
Discord botでhelpコマンドを使ったことがある人は、コマンドをグループ分けしているものだと思ってもらえばいいと思います。
これのBGM管理にあたるグループですね。
プログラムで言うとクラスのようなものになります(実際クラスで宣言してますし)。
主な違いとしては、第一引数にselfが入ることと、
@bot.command()→@commands.command()となることです。
下に、前回のプログラムをCogを使って書き直したものを置いておきます。
ついでにAudio_queueもインスタンス変数(self.audio_status)として書き直しています。
最後のbot.add_cog()で作成したcogをbotに付与しています。
import discord from discord.ext import commands import os import asyncio TOKEN = "token" #トークン PREFIX = '!' #prefix=接頭辞 #bgmコマンドで使う再生キュー class AudioQueue(asyncio.Queue): def __init__(self): super().__init__(0) #再生キューの上限を設定しない def __getitem__(self, idx): return self._queue[idx] #idx番目を取り出し def to_list(self): return list(self._queue) #キューをリスト化 def reset(self): self._queue.clear() #キューのリセット #bgmコマンドで使う,現在の再生状況を管理するクラス class AudioStatus: def __init__(self, vc): self.vc = vc #自分が今入っているvc self.queue = AudioQueue() #再生キュー self.playing = asyncio.Event() asyncio.create_task(self.playing_task()) #曲の追加 async def add_audio(self, path): await self.queue.put(path) #曲の再生(再生にはffmpegが必要) async def playing_task(self): while True: self.playing.clear() try: path = await asyncio.wait_for(self.queue.get(), timeout = 100) except asyncio.TimeoutError: asyncio.create_task(self.leave()) selfpath = os.path.dirname(__file__) self.vc.play(discord.FFmpegPCMAudio(executable=selfpath+"/bin/ffmpeg.exe", source=path), after = self.play_next) await self.playing.wait() #playing_taskの中で呼び出される #再生が終わると次の曲を再生する def play_next(self, err=None): self.bgminfo = None self.playing.set() return #vcから切断 async def leave(self): self.queue.reset() #キューのリセット if self.vc: await self.vc.disconnect() self.vc = None return #botの作成 bot = commands.Bot(command_prefix=PREFIX) class __BGM(commands.Cog, name= 'BGM'): #BGMという名前でCogを定義する #初期化 def __init__(self, bot): super().__init__() self.bot = bot self.audio_status = None self.path = os.path.dirname(__file__) #このファイルが置いてあるディレクトリまでのファイルパス #予め決めておいた音楽ファイルを再生する @commands.command() async def bgm(self, ctx): """play music""" global Audio_queue #この関数内ではVCはグローバル変数のVCを指す if (ctx.author.voice is None): #送信者がボイスチャンネルにいなければエラーを返す await send_message(ctx.send, ctx.author.mention, 'ボイスチャンネルが見つかりません') return if ((self.audio_status is None) or (self.audio_status.vc is None)): #botがボイスチャンネルに入っていなければ voice_channel = ctx.author.voice.channel.id #送信者の入っているボイスチャンネルのID vc = await bot.get_channel(voice_channel).connect() #ボイスチャンネルに入る self.audio_status = AudioStatus(vc) #music.mp3をキューに追加 await self.audio_status.add_audio(self.path+'/bgm/music.mp3') return #botをボイスチャンネルから切断する @commands.command() async def remove(self, ctx): await self.audio_status.leave() return bot.add_cog(__BGM(bot=bot)) bot.run(TOKEN)
挙動は前回と全く同じはずです。
次回は、ファイル名を指定して再生する方法を考えます。
短いですが今回はここまでです。お疲れ様でした。