【discord.js v13 + Discord Player】よくあるmusic botを作成する

# はじめに
こんにちは、こんばんは、ナマステ
今までdiscord.js+@discordjs/voice+ytdl-coreでmusic botを動かしていたのですが、
[Discord Player](https://discord-player.js.org)を使用したところかなり便利だったのとQiitaにこれを使ってる記事が無かったので、備忘録がてら基本的な使い方を書きます
[Discord Developer Portal](https://discord.com/developers/applications)からアプリケーションを作成して～みたいな初っ端の手順は、他の記事が大量に転がってるので端折ります

# 前提
この辺知らないと途中で混乱するかも、知ってても文章がヘタクソだから混乱するかもゴメン
- JavaScript/Node.jsがとりあえず書ける
- discord.jsを触ったことがある
- discordのスラッシュコマンドを知っている([この記事べんり](https://qiita.com/gaato/items/55b32bc4777905ac162a))

# 環境構築
FFmpegが必要なので[ダウンロードサイト](https://www.ffmpeg.org/download.html)からインスコしておく

適当に作業用ディレクトリを作成して`npm init -y`

以下のコマンドで今回使うパッケージを一括で拾ってくる
`npm i discord.js @discordjs/builders @discordjs/opus @discordjs/rest discord-player dotenv`

そしたら必要なディレクトリとファイルを作成、ディレクトリ構造は以下の通り
```
discord-bot/
  ├ commands/
  │　├ play.js
  │　└ quit.js
  ├ .env
  ├ index.js
  ├ register.js
  ├ package.json
  └ package-lock.json
```

とりあえず.envにトークンとクライアントIDとギルドIDを記述する
トークンとクライアントIDは[Discord Developer Portal](https://discord.com/developers/applications)から取得、ギルドIDはbotを動かしたいサーバを右クリ→IDをコピーで取得できる
``` .env
TOKEN=xxx
CLIENT_ID=xxx
GUILD_ID=xxx
```

# スラッシュコマンド登録用のコードを書く
この辺は[discord.js公式](https://discord.js.org/#/docs/discord.js/stable/general/welcome)のサンプルとほぼ同じ、commandsディレクトリ下の.jsファイルを取得してスラッシュコマンドを登録するだけです
``` javascript:register.js
const { REST } = require("@discordjs/rest");
const { Routes } = require("discord-api-types/v9");
const dotenv = require("dotenv");
const fs = require("fs");

dotenv.config();

const commands = [];

// commandsディレクトリのファイルを全取得
const commandFiles = fs
  .readdirSync("./commands")
  .filter((file) => file.endsWith(".js"));

// コマンドを配列に突っ込む
for (const file of commandFiles) {
  const command = require(`./commands/${file}`);
  commands.push(command.data.toJSON());
}

const rest = new REST({ version: "9" }).setToken(process.env.TOKEN);

// コマンドをdiscordサーバに登録
(async () => {
  try {
    console.log("スラッシュコマンド登録");
    await rest.put(
      Routes.applicationGuildCommands(
        process.env.CLIENT_ID,
        process.env.GUILD_ID
      ),
      { body: commands }
    );
  } catch (err) {
    console.error(err);
  }
})();
```

# 起動用のコードを書く
こいつもほぼテンプレ、クライアントを生成してトークン渡して起動、スラッシュコマンドが送信されたらコマンドを取得して実行するだけ、簡単ですね
playerが今回のテーマのDiscord Playerのインスタンス、ここではインスタンスを生成するだけなんで特に細かいことは書きません
``` javascript:index.js
const { Client, Intents } = require("discord.js");
const { Player } = require("discord-player");
const dotenv = require("dotenv");
const fs = require("fs");

dotenv.config();

const client = new Client({
  intents: [
    Intents.FLAGS.GUILDS,
    Intents.FLAGS.GUILD_MESSAGES,
    Intents.FLAGS.GUILD_VOICE_STATES,
  ],
});

client.slashcommands = {};
client.player = new Player(client);

// commandsディレクトリのファイルを全取得
const commandFiles = fs
  .readdirSync("./commands")
  .filter((file) => file.endsWith(".js"));

for (const file of commandFiles) {
  const command = require(`./commands/${file}`);
  client.slashcommands[command.data.name] = command;
}

client.once("ready", () => {
  console.log("ready!");
});

client.on("interactionCreate", async (interaction) => {
  if (!interaction.isCommand()) return;

  // コマンド名からコマンドを取得して実行
  const command = client.slashcommands[interaction.commandName];
  if (!command) {
    await interaction.reply({
      content: "コマンドが存在しません",
      ephemeral: true,
    });
  }
  await command.run({ client, interaction });
});

// bot起動
client.login(process.env.TOKEN);
```

# 音楽再生用コマンドのコードを書く
この辺からようやく本題
`player.createQueue`で[キュー](https://discord-player.js.org/docs/main/master/class/Queue)を生成、既に生成されている場合は取得してくれます
キューとは言いつつもただのデータ構造ではなくVCへの接続やら再生中音楽の停止、スキップ、詳細表示などなど色々やってくれます、便利ですね

`player.search`で音楽の検索、第2引数で渡してるオプションのQueryTypeに応じて検索方法が変わります
今回はYouTubeの動画を対象に検索を行ってますが、YouTubeのプレイリストを取得して動画を一括で拾ってきたりSoundCloudとかSpotifyも検索対象にできるっぽいです
詳しいことは[公式ドキュメント](https://discord-player.js.org/docs/main/master/typedef/QueryType)を読むのが一番手っ取り早いです

`queue.addTrack`で取得した音楽を追加、`queue.play`で再生
音楽を2曲追加した場合、1曲目が終わった時点で自動的に2曲目を再生し、2曲目が終了した時点でVCから退出しconnectionを破棄してくれるなど、再生終了時の処理を特に書かなくても自動でやってくれるのが便利ですね

``` javascript:play.js
const { SlashCommandBuilder } = require("@discordjs/builders");
const { QueryType } = require("discord-player");

module.exports = {
  data: new SlashCommandBuilder()
    .setName("play")
    .setDescription("音楽を再生します")
    .addStringOption((option) =>
      option.setName("url").setDescription("YouTube URL").setRequired(true)
    ),

  run: async ({ client, interaction }) => {
    if (!interaction.member.voice.channelId) {
      return await interaction.reply({
        content: "ボイスチャンネルに参加してください",
        ephemeral: true,
      });
    }

    if (
      interaction.guild.me.voice.channelId &&
      interaction.member.voice.channelId !==
        interaction.guild.me.voice.channelId
    ) {
      return await interaction.reply({
        content: "botと同じボイスチャンネルに参加してください",
        ephemeral: true,
      });
    }

    // キューを生成
    const queue = client.player.createQueue(interaction.guild, {
      metadata: {
        channel: interaction.channel,
      },
    });

    try {
      // VCに入ってない場合、VCに参加する
      if (!queue.connection) {
        await queue.connect(interaction.member.voice.channel);
      }
    } catch {
      queue.destroy();
      return await interaction.reply({
        content: "ボイスチャンネルに参加できませんでした",
        ephemeral: true,
      });
    }

    await interaction.deferReply();

    const url = interaction.options.getString("url");
    // 入力されたURLからトラックを取得
    const track = await client.player
      .search(url, {
        requestedBy: interaction.user,
        searchEngine: QueryType.YOUTUBE_VIDEO,
      })
      .then((x) => x.tracks[0]);

    if (!track) {
      return await interaction.followUp({
        content: "動画が見つかりませんでした",
      });
    }

    // キューにトラックを追加
    await queue.addTrack(track);

    // 音楽が再生中ではない場合、再生
    if (!queue.playing) {
      queue.play();
    }

    return await interaction.followUp({
      content: `音楽をキューに追加しました **${track.title}**`,
    });
  },
};
```

# 再生停止用コマンドのコードを書く
再生ができたら停止もできないと不便なのでチャチャっと書きましょう
`queue.destroy`を呼ぶだけです
```javascript:quit.js
const { SlashCommandBuilder } = require("@discordjs/builders");

module.exports = {
  data: new SlashCommandBuilder()
    .setName("quit")
    .setDescription("再生を停止してbotを終了します"),

  run: async ({ client, interaction }) => {
    const queue = client.player.getQueue(interaction.guildId);

    if (!queue) {
      return await interaction.reply({
        content: "音楽が再生されていません",
        ephemeral: true,
      });
    }

    queue.destroy();

    await interaction.reply({
      content: "botを終了しました",
    });
  },
};
```

# スラッシュコマンドの登録
最低限の機能(音楽の再生/停止)の実装ができたのでとりあえず動かしてみます
とはいえ作成したスラッシュコマンドをサーバに登録する必要があるので
`node register.js`
を実行し、目的のサーバのチャットでスラッシュを入力した時に作成したplayとquitのコマンドが出たらOKです

# botの起動/動作確認
`node index.js`
を実行し、botがオンラインになったのを確認
適当なVCに参加した状態でplayコマンドを叩いてみましょう
目的の音楽が再生されましたか？されてたら完成です、おめでとう、𝐶𝑜𝑛𝑔𝑟𝑎𝑡𝑢𝑙𝑎𝑡𝑖𝑜𝑛

# 終わりに
とりあえず最低限の機能を持ったmusic botが完成しました
機能を追加したい場合は`/commands`ディレクトリ配下にコマンドを追加していきましょう
因みに、よくある機能に関しては既にフレームワーク側で実装されてることが殆どなので、メソッド呼び出すだけで大抵作れます、[公式ドキュメント](https://discord-player.js.org/docs/main/master/general/welcome)を読みましょう

# 参考
[discord.js 公式ドキュメント](https://discord.js.org/#/docs/discord.js/stable/general/welcome)
[Discord Player 公式ドキュメント](https://discord-player.js.org/docs/main/master/general/welcome)