1/(1+e^(-ax))

ポケモンとかPCとか

ポケサーのbotに求められる機能の傾向と対策(1/2)

暇だったので書きました。

にほんばれのdiscord鯖に常駐させているbotで「?(コマンド名)」で扱える各コマンドの使用率を調べてみました。
表にするとこんな感じ。


f:id:Sigmoid_poke:20210705221119p:plain


左上からコマンドが作成された順。斜線はdiscordでの検索が難しかったもの。
同じコマンドを同時に使っていたりするので、あくまでおおよその数です。
使用率が高いコマンドの傾向から、需要のありそうなコマンドを作成したいですね。

とりあえず使用率上位のコマンドを見ていきます。


1位:sev

覚える技からポケモンを検索するコマンド。順当ですね。
サークルOBの人が変な型のポケモンを見つけてくる原因の1割くらいはここにあるのかもしれない。

f:id:Sigmoid_poke:20210705220452p:plain
f:id:Sigmoid_poke:20210705220529p:plain


2位:jet

指定したポケモンが覚える、指定したタイプの技を探すコマンド。ダイジェットが強い今作を象徴してますね。
あと不意の草技は強い。

f:id:Sigmoid_poke:20210705221426p:plain
f:id:Sigmoid_poke:20210705221519p:plain


3位:moves

ポケモンが覚える技の一覧を表示するコマンド。jetコマンドや、後述のmoveclコマンドのほうが便利な気がしますが、初期に作られたコマンドということで使用率が伸びてそうな気がします。

f:id:Sigmoid_poke:20210705222030p:plain


4位:movecl

move classの略(だったはず)で、ポケモンが覚える技のうち、「物理/特殊/変化」を指定できるコマンド。物理/特殊のうち片方しか使わないポケモンが多いことや、ダイウォール媒体を探すためによく使われている印象ですね。

f:id:Sigmoid_poke:20210705222348p:plain


5位:type

タイプからポケモンを検索します。タイプ自体は知っている人も多いので、他のコマンドと併用されることが多い印象。


f:id:Sigmoid_poke:20210705222539p:plain





~結論~


ただの戦闘狂サークル


変則ルールの仲間大会を見つけると、数分後には対応するコマンドが作られているあたり末期感ある。



とはいえ、しりとり用のコマンドや、タマゴ技の遺伝経路を探すコマンドもそれなりに使われているので、
コマンド作成の敷居を下げられるように努力するなどしたい所存です。

(それはそうと、そろそろポケモンHOMEの持ち物とかもここのコマンドにも対応させたい)




気が向いたら後半(!から始まるコマンドのほう)書きます。
数えるまでもなく!stが1位なんですが()。

SDS(ソーシャルディスタンスシングル)使用構築

岡山大・山口大・九州ポケサー連盟合同オンライン企画での特別ルール「ソーシャルディスタンスシングル」で使った構築です。


f:id:Sigmoid_poke:20210314173811j:plain



改めて見ると特に目新しいポケモンもいないですね。



ルールの詳細はこちら

簡単に説明すると
接触技、音技、粉技及び粉と名の付く持ち物、火炎玉禁止。
・特性「えんかく」のジュナイパー接触技を使用可能。
ダイマックス技に関しては物理技や音技をベースとした技も使用可能。
・密っぽいポケモンは使用禁止



考察過程

・弱体化されるのが基本的に物理ポケモンなので、f:id:Sigmoid_poke:20210108113310p:plain等の現環境で強いポケモンは当然強いままと予想。
・物理だとf:id:Sigmoid_poke:20210108113310p:plainに強いf:id:Sigmoid_poke:20210314191221p:plainf:id:Sigmoid_poke:20210108113456p:plainといった岩地面技持ち、地面技繋がりでf:id:Sigmoid_poke:20210314191347p:plainf:id:Sigmoid_poke:20210314191443p:plainが強そう。
・特殊が多いのでf:id:Sigmoid_poke:20210314191805p:plainf:id:Sigmoid_poke:20210314191806p:plain、変化技が大半を占める受け要員も強いのでは?

などと普通のことを考えていた時にとあるツイートを見かけた。


f:id:Sigmoid_poke:20210314174700p:plain



確かに接触技だし負けやなぁって思ってたのだが



~約5分後~








スカーフゴチルゼルやばくね?



というわけで私的tier1ポケモン
f:id:Sigmoid_poke:20210314174909p:plain
ゴチルゼル@拘りスカーフ
特性:かげふみ
努力値:HDベース
確定枠:トリック
以下選択:まもる/コスモパワー/ねむる/いちゃもん/アシストパワー


スカーフトリックをした後にいちゃもんを撃つorPP切れが起こると、ダイマックス3ターンを枯らして悪あがきさせて判定勝ちができる。



ルール発案サークルとしてこれ使うのダメでしょって感じだったが、
これに負けるのはあまりにも癪なので、特性ねんちゃくのポケモンを採用することに決定した。

f:id:Sigmoid_poke:20210314175949p:plain



僕「トリトドンかなぁ…(アギルダーゴチルゼルに逃げられるので)」



f:id:Sigmoid_poke:20210314180145j:plain:h180:w320
トリトドン@ウイの実
特性:ねんちゃく
努力値:呑気HB  だと思ってたら何故か勇敢だった


スカーフトリックをしてくるf:id:Sigmoid_poke:20210314204025p:plainを破壊するためだけのねんちゃくトリトドン
f:id:Sigmoid_poke:20210314204025p:plainにしか投げないつもりだった。
瞑想があればそっちを使ったが瞑想を覚えないので鈍い+地震
f:id:Sigmoid_poke:20210314204025p:plainにコスモパワーがあると頭を抱えることになるのでクリアスモッグ
とりあえず入れておいて損はない自己再生


当たった人誰もゴチルゼル使ってなくて草(うちのサークルメンバーが一人使ってたらしい)
が、実際はけっこう選出した。何もかもこの環境に草技が少なすぎるのが悪い。
一度、f:id:Sigmoid_poke:20210314192013p:plainと対面したときに頭を抱えていたが、
相手視点は呼び水なのでなんとか勝てた。



f:id:Sigmoid_poke:20210314183627j:plain:h180:w320
ウツロイド@とつげきチョッキ
努力値:臆病H84 C172 S252


サンダー対策として採用
ビーストブーストが発動した時にSが上がるシングルの流用個体。
パワフルハーブを持たせたかったのに後述のf:id:Sigmoid_poke:20210108114916p:plainに奪われた。



f:id:Sigmoid_poke:20210314185009j:plain:h180:w320
テッカグヤ@パワフルハーブ
努力値:控え目CS


f:id:Sigmoid_poke:20210314191347p:plainf:id:Sigmoid_poke:20210314191221p:plainf:id:Sigmoid_poke:20210314191443p:plainといった地面の一貫を切るために採用。
f:id:Sigmoid_poke:20210108114637p:plainは宿り木を入れればどうとでもなるでしょの精神だったが、世界に想像以上に存在してなかったので関係なかった。



f:id:Sigmoid_poke:20210314192533j:plain:h180:w320
カビゴン@フィラの実
特性:くいしんぼう、キョダイ個体
努力値:意地っ張り H104 A252 B 104 D 4 S44


調整意図不明の流用個体。
物理ノーマル技で自爆が使えることを知って大喜びしながら採用した。
f:id:Sigmoid_poke:20210314201430p:plainに強めなのも○
技はコンセプトの腹太鼓自爆f:id:Sigmoid_poke:20210314201430p:plainに撃つ地震、地面タイプに撃つダイアイスの元技となる冷凍パンチ

大体の試合で選出した。というかこいつがいないとパワーが低すぎる。
ダイマックスしないと浮いてるポケモンにまともに攻撃できないことを除けば完璧だった。



f:id:Sigmoid_poke:20210314194001j:plain:h180:w320
パンプジン@オボンの実
特性:ふみんにしようと思ったけどお見通し個体しか持ってなかった
努力値:呑気HB


f:id:Sigmoid_poke:20210314201300p:plainを使う上でのトリル展開要員。
耐久よりのポケモンなのでゴチルゼルから逃げられるゴーストタイプなのは偉い。
ボックスにいたから適当に連れてきたが、残飯をf:id:Sigmoid_poke:20210108120620p:plainに強奪された。
マジフレの枠は元々ゴーストダイブにしていたが、非接触技一覧の中から適当に選んだ。
あまり選出しなかったし、選出しても活躍する前に試合が終わってた。
想像以上にダイジェット要員が多かったので反省。



f:id:Sigmoid_poke:20210314200132j:plain:h180:w320
カプ・レヒレ@残飯
特性:ミストメイカ
努力値:控え目 H108 C220 S180


例によって調整意図不明の個体。7世代の頃に使ってた調整の使い回し
受けポケモンがやや重いので、スカーフトリックも考えたが性に合わないのでこの構成になった。
ラッキーがちきゅうなげを使えないのがかなりの追い風。
毒づきを持たないf:id:Sigmoid_poke:20210314201640g:plain程度なら対面から勝てる。

想像以上にフェアリー技の通りが悪かったので全然選出しなかった。


終結

残ポケ数と直接対決により2位!
負けた試合はけんさんにプレミを咎められてぴったり負けたので順当といった感じ。
それ以外は零度を何回も避けたり認識のズレで拾った試合も多いので思ったより勝てて満足

f:id:Sigmoid_poke:20210314202004p:plain



ここまで読んでくださりありがとうございました~

Discord Botで音楽再生用Botを作る(完結編)

Top→Discord Botの作り方 - 1/(1+e^(-ax))
prev→Discord Botでファイル構造を送信する - 1/(1+e^(-ax))
next→未定


今回が最後のつもりでしたが思ってたよりもやることが残っていたので手っ取り早くやります。

フォルダを指定して再生する

次の項目と絡んでいるのでまとめてやります。

botのステータスに曲名を表示する、ループ再生を可能にする

以下のように修正します。

  • AudioStatusに曲のファイルパスと、ループ再生するか否かの値を保存します。

class AudioQueue()のadd_audioplaying_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 (フォルダ名)」と撃つとそのフォルダの中身が表示されるはずです。
f:id:Sigmoid_poke:20210308190812p:plain
f:id:Sigmoid_poke:20210308190819p:plain

次回は細かい修正を行って完成としたいとおもいます。
お疲れ様でした。

next→Discord Botで音楽再生用Botを作る(完結編) - 1/(1+e^(-ax))

リストを埋め込み形式で送信する

Top→Discord Botの作り方 - 1/(1+e^(-ax))
prev→特定のフォルダにあるファイルの一覧を作成する - 1/(1+e^(-ax))
next→Discord Botでファイル構造を送信する - 1/(1+e^(-ax))

bgmの入ったファイル構造や、再生キューの状態を送信するために、リストを簡単に送信する関数を作ります。
リストをそのまま送るのは見づらいため、まずはリストを文字列に変換する関数を作ります。


↓埋め込み(Embed)はこんな感じのやつです。↓
f:id:Sigmoid_poke:20210308154949p:plain

基本的には要素ごとに改行で区切りますが、リストの中にリストがある場合は複数の区切り文字を設定するようにします。

送信:[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',','])

という処理を行ってみると、先ほど例で上げたような埋め込みが送信されると思います。


次回は、音楽ファイルが入っているファイル構造の送信を行ってみたいと思います。
お疲れ様でした。

next→Discord Botでファイル構造を送信する - 1/(1+e^(-ax))

特定のフォルダにあるファイルの一覧を作成する

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コマンドを使ったことがある人は、コマンドをグループ分けしているものだと思ってもらえばいいと思います。
f:id:Sigmoid_poke:20210305175321p:plain


これの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)


挙動は前回と全く同じはずです。
次回は、ファイル名を指定して再生する方法を考えます。
短いですが今回はここまでです。お疲れ様でした。


next→特定のフォルダにあるファイルの一覧を作成する - 1/(1+e^(-ax))