- Final Project: Discord Bot - Music Bot 종료. - Final Project 회고 - Django 복습 및 실습
Discord Bot - Music Bot
어느덧 한 달 여간의 파이널 프로젝트가 끝나간다. 프로젝트 시작하고 처음으로 디스코드 봇의 존재를 알고나서 뮤직봇을 만들기 위해서 디스코드API에 대해 이것저것 동영상도 보고 공식문서도 찾아가며 열심히 공부를 했다. 처음으로 뮤직봇을 만들고 명령어로 음악도 재생시켜보고 일시정지, 다시재생, 음악종료 기능도 구현해 보았다. 크롤링과 셀레니움을 같이 사용해 가면서 기능 구현을 할 수 있었다. 그리고 노래의 정보를 긁어와서 임베드 형식으로 출력해 주는 것도 구현을 하였다. 뮤직봇 사용을 위한 설명서
# 임베드 생성을 위한 크롤링 및 셀레니움 코드
# 크롬웹드라이버 세팅
chrome_options.binary_location = os.environ.get("GOOGLE_CHROME_BIN")
driver = webdriver.Chrome(executable_path=os.environ.get("CHROMEDRIVER_PATH"), chrome_options=chrome_options)
driver.get("https://www.youtube.com/results?search_query="+msg+"+lyrics") # 웹드라이버로 가져올 url
source = driver.page_source # 드라이버에서 가져온 html 중에서 page_source 만 가져온다.
bs = BeautifulSoup(source, 'lxml') # 가져온 source 를 lxml 로 파싱한다.
entire = bs.find_all('a', {'id': 'video-title'}) # <a> 중 id 가 video-title 인 애들만 찾아서 entire 변수에 담는다 : 검색해서 가져온 결과들
entireNum = entire[0] # 검색해서 가져온 결과들 중 [0] 첫 번째 데이터만 가져온다.
entireText = entireNum.text.strip() # 첫 번째 데이터의 text 를 가져오고 .strip 을 해서 여백을 삭제한다.
musicurl = entireNum.get('href') # 데이터 중에 href 만 가져와서 musicurl 에 담는다.
# 썸네일 이미지 가져오기
thumbnail = bs.find_all('img', {'class':'style-scope yt-img-shadow'}) # 썸네일 img 태그 찾아오기
thumbnail_img = thumbnail[1] #
thumbnail_img_src = thumbnail_img.get('src') # 이미지 src
# 조회수 가져오기
views = bs.find_all('span', {'class': 'style-scope ytd-video-meta-block'}) # 유튜브 views
views_num = views[0].text
# 유튜브 채널명 가져오기
channel = bs.find_all('a', {'class':'yt-simple-endpoint style-scope yt-formatted-string'}) # 노래 찾아온 유튜브 채널명
channel_name = channel[0].text
# 노래 플레이타임 가져오기.
playtime = driver.find_element(by=By.XPATH, value=('//*[@id="video-title"]')).get_attribute('aria-label').split()
sec = playtime[-4]
min = playtime[-6]
play_time = f'{min}분:{sec}초'
# 노래정보 임베드 기능 구현
music_embed = discord.Embed(title= f"{entireText}\n", description=f"검색어 [{msg}] 를 재생중입니다.", url=url, color = 0x00ff00) # 포맷이 생성되면 embed 출력
music_embed.set_thumbnail(url=f'{thumbnail_img_src}')
music_embed.add_field(name="CHANNEL", value=f'{channel_name}\n',inline=True )
music_embed.add_field(name="VIEWS", value=f'{views_num}\n',inline=True )
music_embed.add_field(name="PLAYTIME", value=f'{play_time}',inline=True )
music_embed.set_footer(text="Information requested by : Merlin bot dev-team", icon_url="merlin.png")
await ctx.send(embed=music_embed)
뮤직봇 play, pause, resume, stop 기능 구현
# play 기능 구현
# 크롬웹드라이버 및 FFMPEG 음원 기본 세팅
FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', 'options': '-vn'}
YDL_OPTIONS = {'format': 'bestaudio'} # 노래 재생을 위한 YDL 의 옵션 포맷 : bestaudio
# 크롬웹드라이버 세팅
chrome_options.binary_location = os.environ.get("GOOGLE_CHROME_BIN")
driver = webdriver.Chrome(executable_path=os.environ.get("CHROMEDRIVER_PATH"), chrome_options=chrome_options)
driver.get("https://www.youtube.com/results?search_query="+msg+"+lyrics") # 웹드라이버로 가져올 url
source = driver.page_source # 드라이버에서 가져온 html 중에서 page_source 만 가져온다.
bs = BeautifulSoup(source, 'lxml') # 가져온 source 를 lxml 로 파싱한다.
entire = bs.find_all('a', {'id': 'video-title'}) # <a> 중 id 가 video-title 인 애들만 찾아서 entire 변수에 담는다 : 검색해서 가져온 결과들
entireNum = entire[0] # 검색해서 가져온 결과들 중 [0] 첫 번째 데이터만 가져온다.
entireText = entireNum.text.strip() # 첫 번째 데이터의 text 를 가져오고 .strip 을 해서 여백을 삭제한다.
musicurl = entireNum.get('href') # 데이터 중에 href 만 가져와서 musicurl 에 담는다.
url = 'https://www.youtube.com'+musicurl # 실제 실행하게될 url 주소를 url 변수에 담는다.
driver.quit() # 크롬웹드라이버를 종료한다.
with youtube_dl.YoutubeDL(YDL_OPTIONS) as ydl: # 위에서 정의한 YDL_OPTIONS 를 사용하여 youtube_dl 을 실행하고 앞으로 이름은 ydl 로 정의한다.
info = ydl.extract_info(url, download=False) # url 에서 정보를 추출해서 info 변수에 담는다.
URL = info['formats'][0]['url'] # 추출한 info 의 ['formats'] 의 첫 번째 정보의 ['url'] 정보를 URL 변수에 담는다.
go = await discord.FFmpegOpusAudio.from_probe(URL,**FFMPEG_OPTIONS) # 추출한 URL 을 디스코드의 음원 포맷 기능을 사용하여 포맷 후 go 변수에 담는다.
ctx.voice_client.play(go) # 위에서 추출해 온 음악 정보인 go 를 보이스 채널에서 노래를 재생한다.
# pause, resume stop 기능 구현
# 멀린 플레이어 일시 중지.
@commands.command(name='pause')
async def music_pause(self, ctx):
if ctx.voice_client.is_playing():
ctx.voice_client.pause()
await ctx.send('Your Youtube Music is Paused ⏸️ !')
else:
await ctx.send("you're not listening to music now")
# 멀린 플레이어 다시 재생.
@commands.command(name='resume')
async def music_resume(self, ctx):
if not ctx.voice_client.is_playing():
ctx.voice_client.resume()
await ctx.send('Your Youtube Music is re-play ➡️ !')
else:
await ctx.send("you're not listening to music now")
# 멀린 플레이어 재생 종료.
@commands.command(name='stop')
async def music_stop(self, ctx):
if ctx.voice_client.is_playing():
ctx.voice_client.stop()
await ctx.send('Your Youtube Music is stoped :stop_button: !')
ctx.voice_client.disconnect()
else:
await ctx.send("you're not listening to music now")
추가적으로 플레이리스트를 만들어서 리스트에 음악을 담기도 해보고 삭제하고 리스트의 음악을 재생시키는 기능도 구현해 보았다. 뮤직봇 플레이리스트 기능 add, show 구현
# 플레이리스트 기능 구현
# 목록
@commands.command(name='show.playlist')
async def play_list(self, ctx):
if len(musictitle) == 0:
await ctx.send("등록된 노래가 없습니다.")
else:
global Text
Text = ""
for i in range(len(musictitle)):
Text = Text + "\n" + str(i + 1) + ". " + str(musictitle[i])
await ctx.send(embed = discord.Embed(title="노래목록", description = Text.strip(), color = 0x00ff00))
# 대기열 추가
@commands.command(name='add.playlist')
async def music_list(self, ctx, *, msg):
user.append(msg)
result, URLTEST = self.title(msg)
song_queue.append(URLTEST)
await ctx.send(result + "를 재생목록에 추가했어요!")
# 대기열 초기화
@commands.command(name='del.playlist')
async def delete_list(self, ctx):
try:
ex = len(musicnow) - len(user)
del user[:]
del musictitle[:]
del song_queue[:]
while True:
try:
del musicnow[ex]
except:
break
await ctx.send(embed = discord.Embed(title= "목록초기화", description = """목록이 정상적으로 초기화되었습니다. 이제 노래를 등록해볼까요?""", color = 0x00ff00))
except:
await ctx.send("아직 아무노래도 등록하지 않았습니다.")
# 목록재생
@commands.command(name="play.playlist")
async def list_play(self, ctx):
# 사용자가 보이스 채널에 있는지 확인하는 구간, 없으면 You're not in a voice channel 메시지 보낸다.
if ctx.author.voice is None:
await ctx.send("You're not in a voice channel!")
# 사용자가 있는 보이스채널 = voice_channel
voice_channel = ctx.author.voice.channel
if ctx.voice_client is None: # ctx.voice_client 가 None 이라면
await voice_channel.connect() # 봇을 보이스채널에 접속시킨다.
await ctx.send('마법사 멀린이 음성 채널에 접속했어요 ! 🔌💥 !') # 봇이 보이스채널에 접속되면 메세지 전송
else: # ctx.voice_client 가 None 이 아니라면
await ctx.voice_client.move_to(voice_channel)
if ctx.voice_client.is_playing(): # 봇이 노래를 재생중이라면
ctx.voice_client.stop() # 노래를 중단하고 셀레니움을 진행한다.
YDL_OPTIONS = {'format': 'bestaudio', 'noplaylist':'True'}
FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', 'options': '-vn'}
if len(user) == 0:
await ctx.send("아직 아무노래도 등록하지 않았어요.")
else:
if len(musicnow) - len(user) >= 1:
for i in range(len(musicnow) - len(user)):
del musicnow[0]
if not ctx.voice_client.is_playing():
self.play(ctx)
else:
await ctx.send("노래가 이미 재생되고 있어요!")
뮤직봇 기능 구현 이후 깃허브로 올린 뮤직봇 레포지토리를 헤로쿠와 연결하여 헤로쿠를 통한 배포를 진행하였고, 배포하면서 라이브러리 버전으로 인한 약간의 트러블이 발생하였지만, requirements.txt 로 잘 극복할 수 있었다.
느낀점
마지막으로 배포 후 다른 조 사람들에게 우리 프로젝트의 결과물을 보여주고 테스트해 보면서 피드백도 받았다. 너무 고맙게도 많은 사람들이 우리 서비스에 대해서 호평을 해 주었고, 몇몇 기능에 대해서 피드백을 해 주어서 수정사항을 따로 만들기도 하였다. 피드백 사항들을 보고 프로젝트 이후로도 뮤직봇을 따로 수정해서 기능을 좀 수정하고 추가해 볼 생각이다. 약 한 달여 동안 팀원들 모두 너무 고생했고, 고생한 만큼 좋은 결과물이 나온것 같아서 다행이다. 이제 파이널 프로젝트가 모두 끝났으니 취업준비를 해야 하는데 1조 팀원들 모두 좋은 결과가 있었으면 좋겠다!