Pythonを使用してディスコードで音楽を再生するボットを作成するプロセスを紹介します。まずは、ディスコードボットの作成とGoogle Cloud PlatformでYouTube APIキーを取得する方法から始めます。
ディスコードボットの作成方法
- ディスコード開発者ポータルへのアクセス: Discord Developer Portalにアクセスします。
- 新しいアプリケーションの作成: 「New Application」ボタンをクリックして新しいアプリケーションを作成します。
- ボットの追加: 「Bot」セクションに移動し、「Add Bot」をクリックしてボットをアプリケーションに追加します。
- トークンの取得: ボットのトークンを取得し、安全な場所に保管します。このトークンは後ほどコード内で使用します。
GCPでYouTube APIキーを取得
- Google Cloud Platformにログイン: Google Cloud Consoleにアクセスしてログインします。
- 新しいプロジェクトの作成: 「プロジェクトを作成」ボタンをクリックして新しいプロジェクトを作成します。
- YouTube Data API v3の有効化: 「APIとサービス」ダッシュボードで「APIとサービスを有効化」をクリックし、YouTube Data API v3を検索して有効化します。
- APIキーの作成: 「認証情報」ページに移動し、「認証情報を作成」ボタンからAPIキーを作成します。
既に作成されているプロジェクトから「YouTube Data API v3」を有効化した場合、APIキーが使えなくなる場合があり、その際には新しいプロジェクトからAPIキーを作成してやり直す
FFmpegのインストール
FFmpegは、さまざまなオーディオおよびビデオ形式を扱うために必要な無料のソフトウェアです。ディスコードボットで音楽を再生するには、FFmpegがシステムにインストールされている必要があります。
ライブラリのインストール
-
discord.py
: pythonを利用してディスコードボットを作る際に必要 -
google-api-python-client
: youtube APIとの通信のために必要 -
PyNaCl
: pythonからネットワーク通信のセキュリティするためのライブラリ、ディスコードボットから音声機能を使用するために必要 -
yt-dpl
: youtube urlから音楽をダウンロードするために必要 -
python-dontenv
: トークンや環境変数の管理のために必要
pip install discord.py google-api-python-client yt-dpl PyNaCl python-dotenv
.envファイル生成
プロジェクトのルートディレクトリに.env
ファイルを作成し、環境変数を設定
DISCORD_TOKEN=your_discord_bot_token
YOUTUBE_API_KEY=your_youtube_api_key
FFMPEG_PATH=path_to_ffmpeg(ffmpeg.exeファイルがあるディレクトリ)
pythonから.env使用
import os
from dotenv import load_dotenv
load_dotenv()
discord_token = os.getenv('DISCORD_TOKEN')
youtube_api_key = os.getenv('YOUTUBE_API_KEY')
ffmpeg_path = os.getenv('FFMPEG_PATH')
dl_path = os.getenv('MUSIC_PATH')
-
.env
ファイルは敏感な情報が含まれる可能性があるため、絶対に公開ストレージにアップロードしないでください。 - 境変数は、重要な設定情報やパスワードなどを保存するために使用されます。
Discord Intentsの設定
intents = discord.Intents.default() # intents active
intents.messages = True # set react to message event
intents.guilds = True # set react to server(guild) event
intents.message_content = True # set read message
intents = discord.Intents.default() # intents active
-
discord.Intents.default()
は、Discordのイベントに反応するために必要なデフォルトのIntents
オブジェクトを作成します。Intents
はボットがどの種類のイベント(例えばメッセージ、リアクション、メンバーの更新など)に反応するかを制御するために使用されます。
intents.messages = True # set react to message event
- この行はボットがメッセージイベントに反応することを許可します。つまり、ユーザーが送信したメッセージに対してボットが反応できるようになります
intents.guilds = True # set react to server(guild) event
- ここでは、ボットがサーバー(ギルド)のイベントに反応できるように設定しています。サーバーの作成、削除、更新などのイベントがこれに含まれます。
intents.message_content = True # set read message
- この行はボットにメッセージの内容を読む権限を与えます。これにより、ボットは送信されたメッセージの内容を解析し、特定のコマンドやキーワードに基づいてアクションを取ることができます。
ボットの初期化
bot = commands.Bot(command_prefix='!', intents=intents)
- この行は
commands.Bot
クラスのインスタンスを作成し、ボットを初期化します。ここで設定されるcommand_prefix='!'
は、ボットのコマンドがどの文字列で始まるかを定義します。この例では、ユーザーが!
を先頭に付けたメッセージをボットコマンドとして認識します。 -
intents=intents
は、上記で設定したIntents
オブジェクトをボットに適用します。これにより、ボットは設定したイベントに対してのみ反応するようになります。
YouTube API検索関数の定義
# function , YouTube api search
async def youtube_search(ctx, query):
youtube = build('youtube', 'v3', developerKey=youtube_api_key)
request = youtube.search().list(
q=query,
part='snippet',
type='music',
maxResults=1
)
response = request.execute()
if response['items']:
return f'https://www.youtube.com/watch?v={response["items"][0]["id"]["videoId"]}'
else:
await ctx.send(f"Failed... no search result")
return None
async def youtube_search(ctx, query):
- ここで定義されている
youtube_search
関数は、非同期(async)関数です。これは、関数が非同期操作を行うことを意味し、主にネットワークリクエストなどの待ち時間が発生する処理に適しています。 -
ctx
はコマンドが実行されたコンテキスト(Discordのメッセージやチャンネルなど)を表し、query
はYouTubeで検索するためのクエリ文字列です。
youtube = build('youtube', 'v3', developerKey=youtube_api_key)
- この行は、
googleapiclient.discovery
モジュールのbuild
関数を使用して、YouTube Data API v3のクライアントを作成します。 -
developerKey=youtube_api_key
は、環境変数から取得したYouTube APIキーを使用して、APIリクエストの認証を行います。
request = youtube.search().list(
q=query,
part='snippet',
type='music',
maxResults=1
)
-
youtube.search().list
メソッドを使って、検索リクエストを構築します。 -
q=query
は検索クエリを指定します。 -
part='snippet'
は、検索結果に含まれるデータの種類を指定します。ここでは、動画の概要(タイトル、説明、サムネイルなど)を取得します。 -
type='music'
は、検索結果を音楽カテゴリの動画に限定します。 -
maxResults=1
は、検索結果を1つだけ取得することを指定します。
response = request.execute()
-
request.execute()
メソッドで検索リクエストを実行し、結果をresponse
に格納します。
if response['items']:
return f'https://www.youtube.com/watch?v={response["items"][0]["id"]["videoId"]}'
else:
await ctx.send(f"Failed... no search result")
return None
-
if response['items']:
で検索結果が存在するかを確認します。 - 結果がある場合、最初の動画(
response["items"][0]
)のURLを生成して返します。 - 結果がない場合、
await ctx.send
を使用してDiscordのチャンネルに「検索結果が見つかりません」というメッセージを送信し、None
を返します。
!add
プレイリストに追加メソッド
# add music from YouTube
@bot.command(name='add')
async def search(ctx, *, query):
video_id = await youtube_search(ctx, query)
if video_id:
ydl_opts = {
'headers': {
'User-Agent': 'Mozilla/5.0'
},
'format': 'bestaudio/best',
'outtmpl': f'{dl_path}/%(title)s.%(ext)s'
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(video_id, download=True)
info = ydl.sanitize_info(info)
if info['title'] and info['ext']:
music_path = f"{dl_path}/{info['title']}.{info['ext']}"
musicData = {'name': query, 'url': music_path}
music_list.put(musicData)
await ctx.send(f"add music {query}")
else:
await ctx.send("Failed... to add music")
else:
await ctx.send('Failed... no search data')
@bot.command(name='add')
async def search(ctx, *, query):
- ここで定義されている
search
関数は、Discordボットのadd
コマンドを処理します。 -
ctx
はコマンドが実行されたコンテキストを表し、query
はYouTubeで検索するためのクエリ文字列です。 - この関数は非同期です。これは、関数が非同期操作(ネットワークリクエストなど)を含むことを意味します。
video_id = await youtube_search(ctx, query)
-
youtube_search
関数を非同期的に呼び出して、与えられたquery
でYouTubeを検索します。検索結果から動画のIDを取得します。
if video_id:
ydl_opts = {
'headers': {
'User-Agent': 'Mozilla/5.0'
},
'format': 'bestaudio/best',
'outtmpl': f'{dl_path}/%(title)s.%(ext)s'
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(video_id, download=True)
info = ydl.sanitize_info(info)
...
-
if video_id
: は、動画IDが存在するかどうかをチェックします。 -
ydl_opts
は、yt_dlpに渡すオプションを格納する辞書です。 -
headers
: {'User-Agent': 'Mozilla/5.0'} は、ダウンロードリクエストにユーザーエージェントを設定します。これは、YouTubeがリクエストを通常のブラウザからのものとして認識するのに役立ちます。 -
format
: 'bestaudio/best' は、利用可能な最高音質のオーディオをダウンロードすることを指定します。 -
outtmpl
: f'{dl_path}/%(title)s.%(ext)s' は、ダウンロードされたファイルの保存形式とパスを指定します。dl_pathはダウンロードパスで、ファイル名はYouTube動画のタイトルに基づいています。
if info['title'] and info['ext']:
music_path = f"{dl_path}/{info['title']}.{info['ext']}"
musicData = {'name': query, 'url': music_path}
music_list.put(musicData)
await ctx.send(f"add music {query}")
else:
await ctx.send("Failed... to add music")
このコードは、ユーザーが指定したYouTube動画から音声をダウンロードし、その音声をプレイリストに追加する機能を提供します。エラーハンドリングも含まれており、何らかの問題が発生した場合には適切なメッセージでユーザーに通知されます。
!play
プレイリストに追加メソッド
# play music
@bot.command(name='play')
async def play(ctx):
if not music_list.empty():
if not ctx.voice_client:
await ctx.author.voice.channel.connect()
else:
await ctx.send('Failed... already played')
return
await start_playback(ctx)
else:
await ctx.send('Failed... no data')
# play music
@bot.command(name='play')
async def play(ctx):
- ここで定義されているplay関数は、Discordボットのplayコマンドを処理します。
-
ctx
はコマンドが実行されたコンテキストを表します。 - この関数も非同期です。つまり、関数内の処理が非同期操作を含むことを意味します。
if not music_list.empty():
- この行は、プレイリスト
music_list
に音楽データがあるかどうかを確認します。
if not ctx.voice_client:
await ctx.author.voice.channel.connect()
- この部分では、ボットがすでにボイスチャンネルに接続していないかをチェックします。
- もしボットがボイスチャンネルに接続していなければ、コマンドを実行したユーザーがいるボイスチャンネルに接続します。
else:
await ctx.send('Failed... already played')
return
- もしボットがすでにボイスチャンネルに接続している場合、エラーメッセージを送信して処理を終了します
await start_playback(ctx)
-
start_playback
関数を非同期で呼び出して、プレイリストからの音楽再生を開始します。
音楽を再生メソッド(start_playback)
このコードは、Discordボットが音楽を再生するためのstart_playback関数です。この関数は、プレイリストにある音楽を順番に再生し、各曲が終わった後に次の曲を再生します。それぞれの部分について詳しく説明します。
# play music
async def start_playback(ctx):
while music_list.qsize() > 0:
music_data = music_list.get()
audio_source = discord.FFmpegPCMAudio(executable=ffmpeg_path, source=music_data.get('url', 'noData'))
ctx.voice_client.play(audio_source)
await ctx.send(f"play music: {music_data.get('name', 'noData')}")
while ctx.voice_client.is_playing():
await asyncio.sleep(1)
ctx.voice_client.stop()
os.remove(music_data['url'])
await ctx.voice_client.disconnect()
await ctx.send('End play music')
while ctx.voice_client.is_playing():
await asyncio.sleep(1)
- while ctx.voice_client.is_playing(): このループは、音楽が再生中である間続きます。await asyncio.sleep(1)はループを1秒ごとに一時停止し、サーバーへの負荷を軽減します。
全体コード
import asyncio
import discord
import yt_dlp
from discord.ext import commands
import os
from dotenv import load_dotenv
from googleapiclient.discovery import build
from queue import Queue
# dot env load
load_dotenv()
# get token
discord_token = os.getenv('DISCORD_BOT_TOKEN')
youtube_api_key = os.getenv('YOUTUBE_API_KEY')
ffmpeg_path = os.getenv('FFMPEG_Path')
dl_path = os.getenv('MUSIC_PATH')
if not discord_token or not youtube_api_key or not ffmpeg_path or not dl_path:
print("Error .env")
exit(1)
# play list
music_list = Queue()
# discord
intents = discord.Intents.default() # intents active
intents.messages = True # set react to message event
intents.guilds = True # set react to server(guild) event
intents.message_content = True # set read message
# ! + query
bot = commands.Bot(command_prefix='!', intents=intents)
# function , YouTube api search
async def youtube_search(ctx, query):
youtube = build('youtube', 'v3', developerKey=youtube_api_key)
request = youtube.search().list(
q=query,
part='snippet',
type='music',
maxResults=1
)
response = request.execute()
if response['items']:
return f'https://www.youtube.com/watch?v={response["items"][0]["id"]["videoId"]}'
else:
await ctx.send(f"Failed... no search result")
return None
# play music
async def start_playback(ctx):
while music_list.qsize() > 0:
music_data = music_list.get()
audio_source = discord.FFmpegPCMAudio(executable=ffmpeg_path, source=music_data.get('url', 'noData'))
ctx.voice_client.play(audio_source)
await ctx.send(f"play music: {music_data.get('name', 'noData')}")
while ctx.voice_client.is_playing():
await asyncio.sleep(1)
ctx.voice_client.stop()
os.remove(music_data['url'])
await ctx.voice_client.disconnect()
await ctx.send('End play music')
# client setting
@bot.event
async def on_ready():
print(f'Logged in as {bot.user.name}')
# add reaction to hello
@bot.command(name='hello')
async def hello(ctx):
await ctx.send('hello!')
# add music from YouTube
@bot.command(name='add')
async def search(ctx, *, query):
video_id = await youtube_search(ctx, query)
if video_id:
ydl_opts = {
'headers': {
'User-Agent': 'Mozilla/5.0'
},
'format': 'bestaudio/best',
'outtmpl': f'{dl_path}/%(title)s.%(ext)s'
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(video_id, download=True)
info = ydl.sanitize_info(info)
if info['title'] and info['ext']:
music_path = f"{dl_path}/{info['title']}.{info['ext']}"
musicData = {'name': query, 'url': music_path}
music_list.put(musicData)
await ctx.send(f"add music {query}")
else:
await ctx.send("Failed... to add music")
else:
await ctx.send('Failed... no search data')
# play music
@bot.command(name='play')
async def play(ctx):
if not music_list.empty():
if not ctx.voice_client:
await ctx.author.voice.channel.connect()
else:
await ctx.send('Failed... already played')
return
await start_playback(ctx)
else:
await ctx.send('Failed... no data')
bot.run(discord_token)