4
3

ディスコード音楽ボットの作成

Posted at

Pythonを使用してディスコードで音楽を再生するボットを作成するプロセスを紹介します。まずは、ディスコードボットの作成とGoogle Cloud PlatformでYouTube APIキーを取得する方法から始めます。

ディスコードボットの作成方法

  1. ディスコード開発者ポータルへのアクセス: Discord Developer Portalにアクセスします。
  2. 新しいアプリケーションの作成: 「New Application」ボタンをクリックして新しいアプリケーションを作成します。
  3. ボットの追加: 「Bot」セクションに移動し、「Add Bot」をクリックしてボットをアプリケーションに追加します。
  4. トークンの取得: ボットのトークンを取得し、安全な場所に保管します。このトークンは後ほどコード内で使用します。

GCPでYouTube APIキーを取得

  1. Google Cloud Platformにログイン: Google Cloud Consoleにアクセスしてログインします。
  2. 新しいプロジェクトの作成: 「プロジェクトを作成」ボタンをクリックして新しいプロジェクトを作成します。
  3. YouTube Data API v3の有効化: 「APIとサービス」ダッシュボードで「APIとサービスを有効化」をクリックし、YouTube Data API v3を検索して有効化します。
  4. APIキーの作成: 「認証情報」ページに移動し、「認証情報を作成」ボタンからAPIキーを作成します。

既に作成されているプロジェクトから「YouTube Data API v3」を有効化した場合、APIキーが使えなくなる場合があり、その際には新しいプロジェクトからAPIキーを作成してやり直す

FFmpegのインストール

FFmpegは、さまざまなオーディオおよびビデオ形式を扱うために必要な無料のソフトウェアです。ディスコードボットで音楽を再生するには、FFmpegがシステムにインストールされている必要があります。

ライブラリのインストール

  1. discord.py : pythonを利用してディスコードボットを作る際に必要
  2. google-api-python-client : youtube APIとの通信のために必要
  3. PyNaCl : pythonからネットワーク通信のセキュリティするためのライブラリ、ディスコードボットから音声機能を使用するために必要
  4. yt-dpl : youtube urlから音楽をダウンロードするために必要
  5. 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使用

python
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の設定

python
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
python
intents = discord.Intents.default()  # intents active
  • discord.Intents.default()は、Discordのイベントに反応するために必要なデフォルトのIntentsオブジェクトを作成します。Intentsはボットがどの種類のイベント(例えばメッセージ、リアクション、メンバーの更新など)に反応するかを制御するために使用されます。
python
intents.messages = True  # set react to message event
  • この行はボットがメッセージイベントに反応することを許可します。つまり、ユーザーが送信したメッセージに対してボットが反応できるようになります
python
intents.guilds = True  # set react to server(guild) event
  • ここでは、ボットがサーバー(ギルド)のイベントに反応できるように設定しています。サーバーの作成、削除、更新などのイベントがこれに含まれます。
python
intents.message_content = True  # set read message
  • この行はボットにメッセージの内容を読む権限を与えます。これにより、ボットは送信されたメッセージの内容を解析し、特定のコマンドやキーワードに基づいてアクションを取ることができます。

ボットの初期化

python
bot = commands.Bot(command_prefix='!', intents=intents)
  • この行はcommands.Botクラスのインスタンスを作成し、ボットを初期化します。ここで設定されるcommand_prefix='!'は、ボットのコマンドがどの文字列で始まるかを定義します。この例では、ユーザーが!を先頭に付けたメッセージをボットコマンドとして認識します。
  • intents=intentsは、上記で設定したIntentsオブジェクトをボットに適用します。これにより、ボットは設定したイベントに対してのみ反応するようになります。

YouTube API検索関数の定義

python
# 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
python
async def youtube_search(ctx, query):
  • ここで定義されているyoutube_search関数は、非同期(async)関数です。これは、関数が非同期操作を行うことを意味し、主にネットワークリクエストなどの待ち時間が発生する処理に適しています。
  • ctxはコマンドが実行されたコンテキスト(Discordのメッセージやチャンネルなど)を表し、queryはYouTubeで検索するためのクエリ文字列です。
python
youtube = build('youtube', 'v3', developerKey=youtube_api_key)
  • この行は、googleapiclient.discoveryモジュールのbuild関数を使用して、YouTube Data API v3のクライアントを作成します。
  • developerKey=youtube_api_keyは、環境変数から取得したYouTube APIキーを使用して、APIリクエストの認証を行います。
python
request = youtube.search().list(
    q=query,
    part='snippet',
    type='music',
    maxResults=1
)
  • youtube.search().listメソッドを使って、検索リクエストを構築します。
  • q=queryは検索クエリを指定します。
  • part='snippet'は、検索結果に含まれるデータの種類を指定します。ここでは、動画の概要(タイトル、説明、サムネイルなど)を取得します。
  • type='music'は、検索結果を音楽カテゴリの動画に限定します。
  • maxResults=1は、検索結果を1つだけ取得することを指定します。
python
response = request.execute()
  • request.execute()メソッドで検索リクエストを実行し、結果をresponseに格納します。
python
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 プレイリストに追加メソッド

python
# 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')

python
@bot.command(name='add')
async def search(ctx, *, query):
  • ここで定義されているsearch関数は、Discordボットのaddコマンドを処理します。
  • ctxはコマンドが実行されたコンテキストを表し、queryはYouTubeで検索するためのクエリ文字列です。
  • この関数は非同期です。これは、関数が非同期操作(ネットワークリクエストなど)を含むことを意味します。
python
video_id = await youtube_search(ctx, query)
  • youtube_search関数を非同期的に呼び出して、与えられたqueryでYouTubeを検索します。検索結果から動画のIDを取得します。
python
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動画のタイトルに基づいています。
python

            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 プレイリストに追加メソッド

python
# 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')

python
# play music
@bot.command(name='play')
async def play(ctx):
  • ここで定義されているplay関数は、Discordボットのplayコマンドを処理します。
  • ctxはコマンドが実行されたコンテキストを表します。
  • この関数も非同期です。つまり、関数内の処理が非同期操作を含むことを意味します。
python
if not music_list.empty():
  • この行は、プレイリストmusic_listに音楽データがあるかどうかを確認します。
python
if not ctx.voice_client:
    await ctx.author.voice.channel.connect()
  • この部分では、ボットがすでにボイスチャンネルに接続していないかをチェックします。
  • もしボットがボイスチャンネルに接続していなければ、コマンドを実行したユーザーがいるボイスチャンネルに接続します。
python
else:
    await ctx.send('Failed... already played')
    return
  • もしボットがすでにボイスチャンネルに接続している場合、エラーメッセージを送信して処理を終了します
python
await start_playback(ctx)
  • start_playback関数を非同期で呼び出して、プレイリストからの音楽再生を開始します。

音楽を再生メソッド(start_playback)

このコードは、Discordボットが音楽を再生するためのstart_playback関数です。この関数は、プレイリストにある音楽を順番に再生し、各曲が終わった後に次の曲を再生します。それぞれの部分について詳しく説明します。

python
# 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')
python
while ctx.voice_client.is_playing():
            await asyncio.sleep(1)
  • while ctx.voice_client.is_playing(): このループは、音楽が再生中である間続きます。await asyncio.sleep(1)はループを1秒ごとに一時停止し、サーバーへの負荷を軽減します。

全体コード

python
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)
4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3