본문 바로가기

개발일지/디스코드 봇

디스코드 봇 개발 일지 2023-01-05 - 음악 봇

 

※ 작성자가 작성한 내용이 일부 틀릴 수도 있음 주의

 

※ 작성자가 코드 쓰다가 계속 코드 수정함 주의

 

※ 혹시나 유튜브 정책 관련 문제 발생시 공지 없이 삭제될 수도 있음

 

 

 

그냥 노래 하나만 트는 것은 쉬운데...

 

노래 하나 트는 것은 저번에 참고했던 글을 따라하면

 

문제 없이 거의 바로 된다.

 

하지만 큐에 url (현재는) 을 저장하여 바로바로 다음곡을 트는 것은

 

생각보다 문제가 복잡했다.

 

중간 과정도 헷갈리고 해결하는 데에 골머리를 썩히는 바람에

 

현재 완성(계속 수정 예정)된 기준으로 설명하겠다.

 

 

 

ffmpeg

 

영상/음악 등의 파일을 인코딩/디코딩 하는 프로그램 또는 프로젝트 정도로 생각하면 된다.

 

 

https://www.gyan.dev/ffmpeg/builds/

 

Builds - CODEX FFMPEG @ gyan.dev

FFmpeg is a widely-used cross-platform multimedia framework which can process almost all common and many uncommon media formats. It has over 1000 internal components to capture, decode, encode, modify, combine, stream media, and it can make use of dozens o

www.gyan.dev

 

 

여기에서 ffmpeg 파일을 다운로드 받을 수 있다.

 

사용자에 따라 다른 걸 받아도 되지만, 일단 여기서 사용한 것을 기준으로 설명한다.

 

여기서 ffmpeg-release-essentials.zip 을 다운받았다.

 

윈도우 기준이며, 굳이 비교적 불안정한 git 최신 branch 버전을 다운받을 필요는 없어서 선택했다.

 

 

압축파일을 적당한 위치에 풀어준 후, 환경변수의 Path에 추가해주어야 한다.

 

윈도우 왼쪽 아래 검색 -> 시스템 환경 변수 편집 -> 환경 변수 -> 아래 시스템 변수 중 "Path" 들어가준 후

 

새로 만들기 -> bin 폴더 위치 (Ex : C:\ffmpeg-5.1.2-essentials_build\bin ) 를 입력해주고 확인을 눌러준다.

 

이렇게 해도 문제가 일어나는 경우가 있지만, 후술하도록 하겠다.

 

 

 

 

youtube_dl

 

유튜브에서 영상/음악 파일을 불러오려면 필수다.

 

cmd 창을 켜서

 

pip install youtube_dl

 

입력해주자.

 

 

그리고 디코 봇 파일 맨 위에

 

import youtube_dl

 

추가해주자.

 

 

 

 

음악 재생하기 (메인 play 함수)

 

@bot.command()
async def play(message,*vars):

    global voiceChannel
    global messageChannel
    voiceChannel = message.author.voice.channel
    messageChannel = message.channel
    
    if len(bot.voice_clients) == 0:
        await voiceChannel.connect()
        await messageChannel.send("봇이 노래 틀려고 입장했습니다")

    voice = bot.voice_clients[0]
    
    if len(vars) == 0:
        if len(playList) <= playNumber:
            await messageChannel.send("플레이리스트 끝이거나, 비어있습니다")
        elif voice.is_playing():
            await messageChannel.send("노래가 이미 재생중입니다")
        elif voice.is_paused():
            await voice.resume()
        else:
            await play_list(voice)

    elif len(vars) == 1 and vars[0][0:23] == "https://www.youtube.com":
        playList.append(vars[0])
        await messageChannel.send("큐에 추가")
        if voice.is_paused():
            await messageChannel.send("큐에 노래를 추가했지만, 노래 재생은 멈춘 상태입니다. :(play 나 :(resume 으로 재생해주세요")
        elif voice.is_playing() == False:
            await play_list(voice)

 

 

함수/변수의 이름이 직관적이라 생각하기 때문에 길게 설명하지는 않겠다.

 

아래는 pseudo-code 다. 여기서 play_list(voice) 만 직접 작성한 함수다.

 

pseudo-code

if 봇이 음성 채널에 없음:
	음성 채널에 접속

if :(play 이후 변수 없음:
	if 플레이리스트 끝이거나 없음:
    	아무것도 안하고 로그 출력
    elif 이미 재생중:
    	아무것도 안하고 로그 출력
    elif 일시 정지된 상태:
    	다시 재생
    else: #그냥 멈춘 상태
    	노래 재생
        
elif url을 포함:
	큐에 노래 추가 #다른 노래 재생 중인 경우 큐에 노래 추가만 실시
    if 일시 정지:
    	큐에 노래만 넣고 노래 재생은 안 함
    elif 노래 재생 안 하는 상태:
    	노래 재생

 

VoiceClient.play() : 오디오 재생
VoiceClient.is_playing() : 오디오 재생 중일 때 true 반환
VoiceClient.is_paused() : 오디오가 일시 정지 상태일 때 true 반환
VoiceClient.stop() : 오디오 재생 중지
VoiceClient.pause() : 오디오 일시 중지
VoiceClient.resume() : 오디오 다시 재생
VoiceClient.disconnect() : 음성 채널에서 퇴장

 

 

play_list(VoiceClient voice) 함수 및 옵션 튜플

 

노래를 재생해주는 play_list 함수다.

 

ydl_opts = {
    'format': 'bestaudio/best',
    'postprocessors': [{
    	'key': 'FFmpegExtractAudio',
    	'preferredcodec': 'mp3',
    	'preferredquality': '192',
    }]  
} # 비어있으면 일반 영상 형태

FFMPEG_OPTIONS = {
    'executable': 'C:/ffmpeg-5.1.2-essentials_build/bin/ffmpeg.exe',
    'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
    'options': '-vn'
}

async def play_list(voice):
    global playNumber
    global messageChannel
    
    try:
        with youtube_dl.YoutubeDL(ydl_opts) as ydl:
            #ydl.download([vars]) # 영상을 다운로드
            info = ydl.extract_info(playList[playNumber], download = False)
            url = info['formats'][0]['url']
        playNumber = playNumber + 1
        voice.play(discord.FFmpegPCMAudio(url, **FFMPEG_OPTIONS),after = my_after)
    except Exception as e:
        print(e)

 

ydl_opts 와 FFMPEG_OPTIONS 는 영상/음악 및 파일 변환을 위한 옵션 튜플이다.

 

이름은 추후에 바뀔 수도 있다. 임시로 참고한 것들을 그대로 방치해둔 흔적이다.

 

대략 ydl_opts는 192k mp3 음악 파일로 변환해주며,

 

 

상술했던 ffmpeg에 관한 오류에 대한 해결 방법으로 FFMPEG_OPTIONS의 'executable'에

 

아까 설치한 ffmpeg 폴더의 ffmpeg.exe의 주소를 넣는 걸 추천한다.

 

 

그렇게 play_list 함수에서

 

큐에 넣은 url을 바탕으로 음악 파일을 뽑아내서 VoiceClient.play() 로 재생해준다.

 

이게 play() 안의 after에 대해 설명하려한다.

 

 

 

 

VoiceClient.play(source, after)

 

def my_after(error):
    try:
        fut = None
        if playNumber < len(playList):
            fut = asyncio.run_coroutine_threadsafe(play_list(bot.voice_clients[0]), bot.loop)
        fut.result()
    except Exception as e:
        print(e)

 

말하자면, play() 로 재생한 오디오가 종료되었을 때,

 

다음 행동을 지정하는 함수를 인자로 넣는다고 생각하면 된다.

 

 

지금 내 코드의 경우, 큐의 끝에 도달하지 않은 경우에 대해서

 

coroutine을 발생시켜서 다음 노래로 넘어가게 한다.

 

 

노래가 다음으로 안 넘어가고 asyncio가 없다는 에러가 발생하면 

 

pip install asyncio

-> 파일에 import asyncio 추가

 

추가해주자.

 

 

 

 

 

나머지 기능 함수

 

재생 외에 정지, 일시 정지, 다시 재생, 큐(임시 확인용)를 확인하는 함수는 아래와 같이 짰다.

 

@bot.command()
async def stop(message):
    if message.channel == messageChannel:
        await bot.voice_clients[0].disconnect()
        await message.channel.send("NAGA")

@bot.command()
async def leave(message):
    if message.channel == messageChannel:
        await bot.voice_clients[0].disconnect()
        await message.channel.send("NAGA")

@bot.command()
async def reset(message):
    if message.channel == messageChannel:
        bot.voice_clients[0].stop()
        global playNumber
        playNumber = 0
        playList.clear()
        await message.channel.send("큐를 초기화했습니다")

@bot.command()
async def pause(message):
    if message.channel == messageChannel:
        bot.voice_clients[0].pause()
        await message.channel.send("노래를 일시정지했습니다")

@bot.command()
async def resume(message):
    if message.channel == messageChannel:
        bot.voice_clients[0].resume()
        await message.channel.send("노래를 계속 재생합니다")

@bot.command()
async def queue(message):
    if message.channel == messageChannel:
        await message.channel.send(f'{playNumber} / {len(playList)}')

 

 

일단은 임시로 만든게 많아서 나중에 많이 수정될 것 같다.

 

코드 수정하는 데에 너무나 많은 에너지를 소모해서

 

글에서 자세히 설명을 못한 것 같다.

 

 

 

양해 바랍니다.

 

관련해서 질문사항이 있을 경우,

 

댓글로 남겨주시면 최대한 성심껏 답변드리겠습니다.

 

 

728x90