본문 바로가기

code/Python

디스코드 노래방 봇 만들기

참고 블로그: https://kante-kante.tistory.com/36
이 코드 그대로 했더니 명령어 인식을 못해서 수정 조금 함.
또, 한번에 노래 하나씩만 재생 가능해서 플리 기능 추가했습니다. (하나 이상 노래 추가하면 대기열에 추가됨)

 
봇 사용설명서

  !join   음성 채널 입장 (= !입장)
  !playlist   대기열(큐) 목록 출력 (= !플리)
  !pause  음악을 일시정지 (= !일시정지)
  !play   대기열(큐)에 노래 추가 & 노래가 없으면 최근 노래 재생 (= !재생)
  !remove 대기열(큐)에 있는 곡 삭제. 사용법: !remove 1 (= !삭제 1)
  !resume 일시정지된 음악을 다시 재생 (= !다시재생)
  !skip   현재 재생중인 노래 스킵 (= !스킵)
  !stop   음성 채널 퇴장 (= !퇴장)
  !volume 볼륨 조정 (불완전함) 사용법: !volume 50 (= !볼륨 50)

  !help   도움말 출력 (이 내용이 나옵니다)
 
 
volume 명령어가 제대로 적용이 안되던데 어차피 음량 바꾸는거라 상관없을것같아서 그냥 뒀습니다.
mp3으로 바꾼 뒤 재생하는 방식이라 새로 노래를 추가할 때 조금 버벅거리는 현상 있습니다.
 
테스트를 완벽하게는 못 했습니다.
수정사항, 오류, 추가했으면 하는 명령어 등 댓글 환영입니다.
 

 


0. 목차

코딩에 아예 지식이 없는 분을 위해 하나하나 설명합니다. 소스코드만 필요하신 분들은 페이지 내 검색 기능 활용해주세요.

https://code-shabushabu.tistory.com/20

밑의 내용은 VS코드로 실행할 때만 봇 구동됩니다.

24시간 구동하고 싶으시면 목차 1, 3번만 보시고 위 링크 글 참고해주세요. (3번은 각각 py파일로 만들어두면 됩니다.)

 

1. 봇 생성&서버에 추가

2. VS Code (비쥬얼 스튜디오 코드)

3. 소스코드

4. 실행

 


1. 봇 생성&서버에 추가

https://discord.com/developers/applications

링크 들어가서 New Application 으로 봇 생성

 

Bot - MESSAGE CONTENT INTENT 체크

 

 

OAuth2 > 화면에 보이는 대로 권한 체크 후 밑으로 내리면 링크가 있습니다. 이 링크로 서버에 봇을 추가합니다.


2. VS Code (비쥬얼 스튜디오 코드)

[사전 작업]

파이썬 설치
파이썬 설치 경로에 ffmpeg 파일 넣기(exe 파일 3개)

 

cmd(명령 프롬프트) 창에 복붙
pip install discord yt_dlp pynacl

 

 

1. VS 코드를 설치하고 실행한 뒤, 아래 그림과 같이 파이썬을 설치합니다.

 

2. 탐색기에서 폴더를 지정 후 엽니다. > '우클릭' - '새 파일' 로 py 파일을 2개 만듭니다. 아래의 소스코드를 복붙해서 내용 채우면 됩니다.

폴더에 다른거 있으면 다른거도 다 뜨니까 열 폴더는 새로 만들어놔주세요. 안에 py파일 두개만 있는 게 보기 편합니다.


3. 소스코드

await ctx.send(' ') 안의 내용 수정하면 봇 대사 바꿀 수 있습니다.
 
dico_token.py

Token="(여기에 토큰 복붙)"

이 내용 복붙하면 됩니다

 
bot.py

import asyncio
import discord
import yt_dlp as youtube_dl
from discord.ext import commands
from dico_token import Token

# Suppress noise about console usage from errors
youtube_dl.utils.bug_reports_message = lambda: ''

ytdl_format_options = {
    'format': 'bestaudio/best',
    'outtmpl': '%(extractor)s-%(id)s-%(title)s.%(ext)s',
    'restrictfilenames': True,
    'noplaylist': True,
    'nocheckcertificate': True,
    'ignoreerrors': False,
    'logtostderr': False,
    'quiet': True,
    'no_warnings': True,
    'default_search': 'auto',
    'source_address': '0.0.0.0',  # bind to ipv4 since ipv6 addresses cause issues sometimes
}

ffmpeg_options = {
    'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
    'options': '-vn',
}

ytdl = youtube_dl.YoutubeDL(ytdl_format_options)

class YTDLSource(discord.PCMVolumeTransformer):
    def __init__(self, source, *, data, volume=0.5):
        super().__init__(source, volume)
        self.data = data
        self.title = data.get('title')
        self.url = data.get('url')

    @classmethod
    async def from_url(cls, url, *, loop=None, stream=False):
        loop = loop or asyncio.get_event_loop()
        data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=not stream))

        if 'entries' in data:
            data = data['entries'][0]

        filename = data['url'] if stream else ytdl.prepare_filename(data)
        return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data)

class Music(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
        self.queue = asyncio.Queue()
        self.current = None
        self.is_playing = False

    @commands.command(aliases=['입장'])
    async def join(self, ctx):
        """음성 채널 입장 (= !입장)"""

        if ctx.author.voice and ctx.author.voice.channel:
            channel = ctx.author.voice.channel
            await ctx.send("봇이 {0.author.voice.channel} 채널에 입장합니다.".format(ctx))
            await channel.connect()
            print("음성 채널 정보: {0.author.voice}".format(ctx))
            print("음성 채널 이름: {0.author.voice.channel}".format(ctx))
        else:
            await ctx.send("음성 채널에 유저가 존재하지 않습니다. 1명 이상 입장해 주세요.")
            return await ctx.voice_client.move_to(channel)

    @commands.command(aliases=['재생'])
    async def play(self, ctx, *, url):
        """대기열(큐)에 노래 추가 & 노래가 없으면 최근 노래 재생 (= !재생)"""

        async with ctx.typing():
            player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True)
            if player is None:
                await ctx.send("노래를 가져오는데 문제 발생. URL을 확인해주세요.")
                return

            await self.queue.put(player)
            position = self.queue.qsize()
            await ctx.send(f'{player.title}, #{position}번째로 대기열에 추가.')

            # 현재 노래가 재생 중이 아니면 다음 곡 재생
            if not self.is_playing and not ctx.voice_client.is_paused():
                await self.play_next(ctx)

    async def play_next(self, ctx):
        if not self.queue.empty():
            self.current = await self.queue.get()
            self.is_playing = True
            ctx.voice_client.play(self.current, after=lambda e: self.bot.loop.create_task(self.play_next_after(ctx, e)))
            await ctx.send(f'Now playing: {self.current.title}')
        else:
            self.current = None
            self.is_playing = False

    async def play_next_after(self, ctx, error):
        if error:
            print(f'에러: {error}')
        self.is_playing = False
        await self.play_next(ctx)
    
    @commands.command(aliases=['스킵'])
    async def skip(self, ctx):
        """현재 재생중인 노래 스킵 (= !스킵)"""
        if ctx.voice_client and ctx.voice_client.is_playing():
            ctx.voice_client.stop()
            await ctx.send("현재 노래를 건너뜁니다.")
            await self.play_next(ctx)
        else:
            await ctx.send("현재 재생 중인 노래가 없습니다.")

    @commands.command(aliases=['볼륨'])
    async def volume(self, ctx, volume: int):
        """볼륨 조정 (불완전함) 사용법: !volume 50 (= !볼륨 50)"""
        if ctx.author.voice and ctx.author.voice.channel:
            if ctx.voice_client and ctx.voice_client.source:
                ctx.voice_client.source.volume = volume / 100
                await ctx.send(f"스피커 음량을 {volume}%로 변경")
            else:
                await ctx.send("No audio is currently playing.")
        else:
            return await ctx.send("음성 채널과 연결 불가능")

    @commands.command(aliases=['퇴장'])
    async def stop(self, ctx):
        """음성 채널 퇴장 (= !퇴장)"""
        
        self.queue = asyncio.Queue()
        if ctx.voice_client and ctx.voice_client.is_playing():
            ctx.voice_client.stop()
            
        await ctx.send("봇이 {0.author.voice.channel} 채널을 나갑니다.".format(ctx))
        await ctx.voice_client.disconnect()

    @commands.command(aliases=['일시정지'])
    async def pause(self, ctx):
        ''' 음악을 일시정지 (= !일시정지)'''
        if ctx.voice_client.is_paused() or not ctx.voice_client.is_playing():
            await ctx.send("음악이 이미 일시 정지 중이거나 재생 중이지 않습니다.")
        else:
            ctx.voice_client.pause()
            await ctx.send("음악이 일시 정지되었습니다.")

    @commands.command(aliases=['다시재생'])
    async def resume(self, ctx):
        ''' 일시정지된 음악을 다시 재생 (= !다시재생)'''
        if ctx.voice_client.is_playing() or not ctx.voice_client.is_paused():
            await ctx.send("음악이 이미 재생 중이거나 재생할 음악이 존재하지 않습니다.")
        else:
            ctx.voice_client.resume()
            await ctx.send("음악이 다시 재생됩니다.")

    @commands.command(aliases=['플리'])
    async def playlist(self, ctx):
        """대기열(큐) 목록 출력 (= !플리)"""
        if not self.queue.empty():
            message = '플레이리스트:\n'
            temp_queue = list(self.queue._queue)
            for idx, player in enumerate(temp_queue, start=1):
                message += f'{idx}. {player.title}\n'
            await ctx.send(message)
        else:
            await ctx.send("대기열이 비어 있습니다.")

    @commands.command(aliases=['삭제'])
    async def remove(self, ctx, index: int):
        """대기열(큐)에 있는 곡 삭제. 사용법: !remove 1 (= !삭제 1)"""
        if not self.queue.empty():
            temp_queue = list(self.queue._queue)  # Convert the queue to a list to access it
            if 0 < index <= len(temp_queue):
                removed = temp_queue.pop(index - 1)
                await ctx.send(f'삭제: {removed.title}')
                # Rebuild the queue
                self.queue = asyncio.Queue()
                for item in temp_queue:
                    await self.queue.put(item)
            else:
                await ctx.send("유효한 번호를 입력하세요.")
        else:
            await ctx.send("대기열이 비어 있습니다.")
            
    @play.before_invoke
    async def ensure_voice(self, ctx):
        if not (ctx.author.voice and ctx.author.voice.channel):
            await ctx.send("You are not connected to a voice channel.")
            raise commands.CommandError("Author not connected to a voice channel.")
        elif ctx.voice_client is None:
            if ctx.author.voice:
                await ctx.author.voice.channel.connect()
            else:
                await ctx.send("You are not connected to a voice channel.")
                raise commands.CommandError("Author not connected to a voice channel.")


intents = discord.Intents.default()
intents.message_content = True

bot = commands.Bot(
    command_prefix=commands.when_mentioned_or("!"),
    description='봇 사용설명서',
    intents=intents,
)


@bot.event
async def on_ready():
    print(f'{bot.user} 봇 실행!! (ID: {bot.user.id})')
    print('------')


async def main():
    async with bot:
        await bot.add_cog(Music(bot))
        await bot.start(Token)


asyncio.run(main())

 


4. 실행

[F5] > Python 파일 선택하면 실행됩니다.

 
아래의 터미널에 봇 등장 메시지가 뜨면 디코에서 봇 사용 가능합니다.

 
코드 실행하는 동안만 봇 구동합니다. 실행 멈추고 싶으면 정지 버튼 눌러주세요.