discord允許我們提供音檔來讓機器人播放聲音
如果音檔為音樂,則可以實現播放音樂的功能
環境準備
播放音樂以及抓取音檔需要額外的套件,請一樣依照自己的電腦環境來替換開頭:
1
| pip install yt-dlp discord.py[voice] python-ffmpeg
|
yt-dlp
是提取Youtube影片資料的
discord.py[voice]
是discord.py的語音擴充
python-ffmpeg
可以把音檔轉換成二進制檔案供Discord使用
屬性介紹
voice屬性
之前我們講過使用者有許多屬性,其中voice
沒有提到,今天簡單介紹我們需要用到的部分
user.voice
屬性本身屬於discord.VoiceState
類別,代表目前該使用者的語音狀態,如果沒有在語音狀態則是None
如果存在的話會有以下常用屬性(非全部)
deaf
: 是否拒聽
mute
: 是否靜音
channel
: 連線到的頻道
self_stream
: 是不是正在直播
我們今天會需要他的channel
屬性
voice_clients
這個屬性是bot
所有的語音連線狀態
與使用者不同,一個機器人可以同時在不同伺服器連線到語音頻道(不過一個伺服器一樣限制一個頻道)
bot.voice_clients
列出了所有機器人連線到的語音用戶端(可以想成一個專屬語音的虛擬使用者),屬於discord.VoiceClient
類別
有channel
跟guild
(頻道/伺服器)屬性可以給我們取用,還能進行其他播放的操作
discord.utils.get()
這是一個特殊的函數,是discord提供的方便函數
其用法為
1
| result = discord.utils.get(一個列表, 屬性=值)
|
意思是在一個列表裡面一一尋找,直到找到某一個項目的屬性符合值,舉例來說
1
| result = discord.utils.get(社團列表, name="電算社")
|
這時候result就會找到一個物件,其name
屬性為"電算社"
的並返回給result
這時候我們就可以拿result
來用
但如果沒有找到的話result
會是None
如果搭配voice_clients
就可以尋找機器人所有語音用戶端內,是當前這個伺服器的那個項目
加入與退出
加入
我們將請使用者先加入一個語音頻道,然後使機器人也加入該頻道
這時候就需要考慮到使用者如果還沒進去一個頻道,就要請他進去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @bot.tree.command() async def join(interaction: discord.Interaction):
v = interaction.user.voice vc = discord.utils.get(bot.voice_clients, guild=interaction.guild)
if v and not vc:
await v.channel.connect() await interaction.response.send_message(f"已經連線至 {v.channel.mention}") elif not v: await interaction.response.send_message("請先加入一個語音頻道") else: await interaction.response.send_message(f"機器人已經連線了")
|
退出
退出時一樣要考慮機器人是不是真的在一個頻道內
1 2 3 4 5 6 7 8 9 10
| @bot.tree.command(description="讓機器人離開語音頻道") async def leave(interaction: discord.Interaction): vc = discord.utils.get(bot.voice_clients, guild=interaction.guild) if vc: await vc.disconnect(force=True) await interaction.response.send_message("已經離線") else: await interaction.response.send_message("機器人不在語音頻道內")
|
播放本地音訊
跟圖片的概念一樣,讓使用者輸入一個音檔的名稱,就可以播放
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @bot.tree.command(description="播放音訊") async def play(interaction: discord.Interaction, name: str): await interaction.response.defer() vc = discord.utils.get(bot.voice_clients, guild=interaction.guild) if not vc: await interaction.followup.send("機器人不在語音頻道內") return
audio = discord.FFmpegPCMAudio(f"./{name}")
vc.play(audio) await interaction.followup.send(f"正在播放 {name}")
|
暫停、繼續和停止
暫停
這裡還需要多考慮一件事:機器人有沒有在播音樂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @bot.tree.command(name="pause", description="暫停播放音樂") async def pause(interaction: discord.Interaction):
vc: discord.VoiceClient = discord.utils.get(bot.voice_clients, guild=interaction.guild) if not vc: await interaction.response.send_message("機器人不在語音頻道內") return
if vc.is_playing(): vc.pause() await interaction.response.send_message("音樂已暫停") else: await interaction.response.send_message("目前沒有音樂在播放")
|
繼續
同理,繼續之前需要檢查機器人是不是暫停狀態
1 2 3 4 5 6 7 8 9 10 11 12
| @bot.tree.command(name="resume", description="繼續播放音樂") async def resume(interaction: discord.Interaction): vc: discord.VoiceClient = discord.utils.get(bot.voice_clients, guild=interaction.guild) if not vc: await interaction.response.send_message("機器人不在語音頻道內") return
if vc.is_paused(): vc.resume() await interaction.response.send_message("音樂已繼續播放") else: await interaction.response.send_message("音樂未被暫停")
|
停止
1 2 3 4 5 6 7 8 9 10 11 12
| @bot.tree.command(name="stop", description="停止播放音樂") async def stop(interaction: discord.Interaction): vc: discord.VoiceClient = discord.utils.get(bot.voice_clients, guild=interaction.guild) if not vc: await interaction.response.send_message("機器人不在語音頻道內") return
if vc.is_playing(): vc.stop() await interaction.response.send_message("已經停止") else: await interaction.response.send_message("沒有在播音樂")
|
簡化確認流程
從上面的例子來看,每一次都要確認機器人的話是有點麻煩,我們可以寫一個函數來做這個重複性的內容
1 2 3 4 5 6 7 8 9
| async def check(interaction:discord.Interaction) -> discord.VoiceClient|None: vc: discord.VoiceClient = discord.utils.get(bot.voice_clients, guild=interaction.guild) if not vc:
await interaction.response.send_message("機器人不在語音頻道內") return None
return vc
|
然後指令就改成
1 2 3 4 5 6 7 8 9 10 11
| @bot.tree.command(description="播放音訊") async def play(interaction: discord.Interaction, name: str): await interaction.response.defer() vc = await check(interaction) if not vc: return
audio = discord.FFmpegPCMAudio(f"./{name}")
vc.play(audio) await interaction.followup.send(f"正在播放 {name}")
|
當然這部分並不是必要的,如果你不想改也可以
提取Youtube資訊
單一網址測試
首先,先建立一個新的檔案來做測試,並輸入以下程式碼
1 2 3 4 5 6 7 8 9 10 11 12
| from yt_dlp import YoutubeDL import json
url = input('url> ')
ydl = YoutubeDL({'format':'ba/b'})
data = ydl.extract_info(url, download=False)
with open('result.json', 'w', encoding='utf8') as file: json.dump(data, file, ensure_ascii=False, indent=4)
|
打開result.json
,你會看到很多資訊(請善用vscode的收合功能來收合很長的內容)
有formats、thumbnail、title等資訊,其中我們需要取得最上面那一層的url(通常在最底下的區塊)
點開他,可以看到我們指定格式的音訊網址,這個就是我們需要的路徑
要注意的是,這個連結會過期,所以不能永久保存
由此可以看到,只要我們提取完資訊之後取得['url']
的內容,我們就有一個可以播放的音訊了
也可以看看其他屬性都是怎麼儲存的,比如說標題、網址等等
搜尋測試
如果要用關鍵字搜尋,可以把url參數的部分改成ytsearch:關鍵字
比如說
1 2 3
| keyword = input() found = ydl.extract_info('ytsearch:'+keyword, download=False) data = found['entries'][0]
|
也可以修改剛剛的程式,把結果丟進去result.json
看看長什麼樣子
播放Youtube
現在我們稍微更改一下play指令,讓機器人可以去抓取youtube的網址
播放
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @bot.tree.command(description="播放Youtube") async def play(interaction: discord.Interaction, url: str): await interaction.response.defer() vc = await check(interaction) if not vc: return
ydl = YoutubeDL({'format':'ba/b'})
data = ydl.extract_info(url, download=False)
audio_url = data['url'] title = data['title']
audio = discord.FFmpegPCMAudio(audio_url)
vc.play(audio) await interaction.followup.send(f"正在播放 {title}")
|
搜尋
到這裡,我們就可以讓機器人播放想要的音樂了,我們也可以把搜尋功能融入進來
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @bot.tree.command(description="播放Youtube") async def play(interaction: discord.Interaction, query: str): await interaction.response.defer() vc = await check(interaction) if not vc: return
ydl = YoutubeDL({'format':'ba/b'})
data = ydl.extract_info(f"ytsearch:{query}", download=False)['entries'][0]
audio_url = data['url'] title = data['title']
audio = discord.FFmpegPCMAudio(audio_url)
vc.play(audio) await interaction.followup.send(f"正在播放 {title}")
|