discord developer

discord.js 를 이용한 디스코드 음악 봇 만들기 (ytdl-core 심화편)

yjlee06 2023. 7. 30. 05:20
반응형

THUMBNAIL

 

디스코드 음악봇을 만들기 위해 인터넷을 찾아보면

대표적으로 3가지의 선택지를 발견하실 수 있습니다.

 

1. 다른 사람이 개발한 youtube player 라이브러리 사용하기

2. Lavalink 또는 ytdl-core를 사용하여 만들기

 

대부분 쉬운 방법을 선택하신다면

다른 개발자분들이 만든 라이브러리를 사용합니다. 

 

하지만 이러한 방법은 오류가 발생하였을 때

수정하기 매우 까다로우며 / 자신이 원하는 방향으로 개발하기 힘들 수 있기에

저는 YTDL-CORE를 사용하는 것을 적극적으로 추천합니다.

 

그래서 YTDL-CORE가 뭐야?

YTDL-CORE는 유튜브 영상 다운로드 라이브러리인 youtube-dl를 업그레이드하여 제작한 라이브러리입니다.

 

해당 라이브러리는 유튜브를 이용할 시 얻을 수 있는 모든 정보를

가져올 수 있으며 대표적으로 추천영상, 좋아요, 아티스트 프로필, 영상정보... 등등 있습니다.

 

또한 유튜브 서버를 이용하여 영상 오디오를 받아오기에

끊김이 거의 없으며 안정적입니다.

 

그러면 유튜브에 있는 정보를 허락 없이 이용하니까 저작권위반이지 않아?

유튜브 썸네일 링크, 제목, 추천영상을 이용하거나

유튜브 서버를 이용하여  영상의 오디오를 재생하는 것은 법적인 문제가 되지 않습니다. (이해 자료)

하지만

영리 목적으로 이용한다면 법적인 문제가 발생할 수 있습니다.

(해당 문제는 모든 음악봇이 겪는 문제이며 유튜브 측에서 영리 목적으로 이용하는 봇은

이용을 하지 못하도록 하기에 이점은 크게 문제가 되지 않습니다.)

 

1. ytdl-core를 이용하여 음악봇 만들기

빠르게 코드를 보시려면 아래의 블로그를 이용해 주세요!

이동하기!

 

디스코드 봇 라이브러리 npm install discord.js
음악 재생 라이브러리 npm install ytdl-core
디지털 음성 스트림 변환 라이브러리 npm install ffmpeg-static
유튜브 음성채널을 이용할수있게 하는 라이브러리와
디지털 음성 스트림 변환 라이브러리
npm install @discordjs/voice libsodium-wrappers

 

파일 구조

.
└── discord Bot
    ├── index.js
    └── command
        ├── play.js
        ├── stream
        │   ├── resource.js
        │   ├── connection.js
        │   └── stream.js
        │   └── streamEvent.js
        ├── skip.js
        ├── pause.js
        ├── resume.js
        └── clear.js

 

slash commands 봇을 만드는 자세한 내용은 아래 블로그를 참조하시기 바랍니다!

2023.06.27 - [discord developer] - Slash command 디스코드 봇 제작 [discord.js] (+ interaction 오류 처리 & 안정적인 interaction 반응)

 

connection.js

디스코드 통화방에 연결시킵니다.

const { joinVoiceChannel } = require('@discordjs/voice');

module.export = function connection(interaction) { 
 return joinVoiceChannel({
  channelId: interaction.member.voice.channel.id, 
  guildId: interaction.member.voice.channel.guild.id, 
  adapterCreator: interaction.member.voice.channel.guild.voiceAdapterCreator,
  selfMute: false,
  selfDeaf: true,
 });
}

resource.js

재생할 음악의 오디오 데이터를 가져옵니다.

const { createAudioResource,StreamType } = require(`@discordjs/voice`);
const ytdl = require('ytdl-core');
module.export = function resource(url) { 
 return createAudioResource(
  ytdl(
   url,
   {
    highWaterMark: 1 << 25,
    quality: 'highestaudio',
    liveBuffer: 4900,
    filter:'audioonly',
   }
 )
}

stream.js

음악을 재생하고 콜백함수로 player, connection을 넘겨줍니다.

const createResource = require('./resource.js');
const createConnection = require('./connection.js');
const queue = require('../index.js');

module.export = function stream(interaction,url, run) {
 let connection;
 let resource;
 let player;
 
 
 if(queue.length == 0) {
  connection = createConnection(interaction);
  player = createAudioPlayer({
  behaviors: {
   noSubscriber: NoSubscriberBehavior.Stop,
  },
 })
 };
 
 player = queue[0].player;
 connection = queue[0].connection;
 resource = createResource(url);
 
 player.play(resource);
 
 connection.subscribe(player);
 
 run(player, connection);
}

 

streamEvent.js

음악이 끝났을 때 만약 queue에 1 초과의 목록 있을 경우

재생했던 목록을 제거 후 stream함수를 이용해 다음음악을 재생합니다.

 

만약 queue에 하나의 목록만 존재할 경우 제거한 다음

"모든 음악이 재생되었습니다."라고  메시지를 보냅니다.

const stream = require('./stream.js')

module.export = function event(interaction) {
player.on('stateChange', async (oldState, newState) => {
  if (oldState.status === 'playing' && newState.status === 'idle') {
   
   if(queue[0].isClear == true) { 
    queue = [];
    await interaction.reply('모든 음악이 재생되었습니다!');
   } else if(queue.size > 1) {
    queue.shift();
    await interaction.reply('음악이 끝났습니다. 다음음악이 재생됩니다.');
    
    stream(interaction,queue[0].url, (({player, connection}) => {
     queue.shift();
     queue.push({
     url: url,
     player: player,
     connection: connection,
     isClear: false
      })
      
   } else {
    queue = [];
    await interaction.reply('모든 음악이 재생되었습니다!');
   }
  }
 })
}

index.js

전역변수로 queue(재생목록)을 생성합니다.

const queue = new Array();

module.export = {
 queue
}

...

 

 

play.js

string 옵션으로 받은 url을 stream 함수에 넘겨줍니다.

 

또한 첫 번째로 입력된 url과 player, connection 정보는

기존에 url만 존재하던 queue의 목록을 제거하고 추가해 줍니다.

const { SlashCommandBuilder } = require('discord.js');
const { queue } = require('../index.js');

module.exports = {
 data: new SlashCommandBuilder()
  .setName('play') 
  .setDescription('음악을 재생합니다.')
  .addStringOption((otpion) => {
   return otpion.setName('url')
   .setDescription('유튜브 링크를 입력하세요.')
   .setRequired(true);
  }),
 async execute(interaction) { 
  if(interaction.member.voice.channel) { 
   const url = interaction.options.data[0].value;
   
    queue.push({
     url: url
    })
     
    if(queue.length == 0) {
   	 stream(interaction,url, (({player, connection}) => {
      queue.shift();
      queue.push({
       url: url,
       player: player,
       connection: connection,
       isClear: false
      })
      
      await interaction.reply('음악이 추가되었습니다.');
     });
    }
  } else {
   await interaction.reply('음성채널에 접속해주세요.');
  }
 },
};

 

위와 같은 형식으로 skip 또는 resume, pause를 만들 수 있습니다!

EX. skip.js

const {queue} = require('../index.js');

...
 async execute(interaction) {
  if(queue.length > 0) {
   queue[0].player.stop();
   await interaction.reply('음악이 스킵되었습니다.');
  } else {
   await interaction.reply('음성채널에 접속해주세요.');
  }
 }
}

 

나머지 명령어는 아래의 함수를 참고하여 만드시면 됩니다.

음악 일시정지 player.pause()
일시정지 해제 player.unpause()
  통화방 연결 삭제 connection.destroy()

추가적으로 clear 명령어를 만드실 때에는 

queue [0]. isClear = true라는 구문과 connection.destroy() 또는 player.stop()이 꼭 들어가야 합니다!

 

(음악을 종료 후 streamEvent.js에서 이벤트를 받게 되는데

이때 isClear 프로퍼티를 통해 초기화를 할 것인지 확인하기 때문입니다!)

위의 코드는 ytdl-core로 만든 간단한 음악봇 소스코드입니다.

변수와 구조는 이해하기 쉽게 임의로 지정한 것이며 복붙이 아닌 참고하는 용도로 이용하시는 것을 추천합니다!