1. 개요
동아리 사람들과 디스코드를 하다가 오랜만에 뮤직봇을 사용했다.
근데 너무너무너무 제대로 작동하지가 않고 오류가 많았다.
그래서 그냥 Tave 뮤직 봇을 만들기로 했다.
2. 디스코드 봇 제작
Discord Developers에 들어가서 Application을 만든 다음. Application ID와 Token Key를 기억하고 있으면 된다.
Token은 외부 노출을 막기 위해 환경변수 파일(.env)를 만들고 주입했다.
또한 패키지를 설치해야한다.
pip install yt_dlp
pip install discord
pip install dotenv
# pip 설치 권한 해제
python3 -m pip config set global.break-system-packages true
# pip 설치 권한 해제 후 실행
pip install commands
pip install pynacl
그러고 파이썬 IDE 아무거나 사용해서 다음 코드를 작성한다. (VS Code, PyCharm etc...)
import discord
from discord.ext import commands
from yt_dlp import YoutubeDL
from dotenv import load_dotenv
import os
import asyncio
# Load token
load_dotenv()
TOKEN = os.getenv("DISCORD_TOKEN")
# 봇 설정
intents = discord.Intents.default()
intents.message_content = True
intents.voice_states = True
bot = commands.Bot(command_prefix='!', intents=intents)
# 전역 변수
queue = []
now_playing = None
# 강화된 FFmpeg 옵션
ffmpeg_options = {
'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 -probesize 20M -analyzeduration 20M',
'options': '-vn -b:a 128k -bufsize 1M -af "volume=0.5"'
}
# 개선된 yt-dlp 옵션
ytdl_opts = {
'format': 'bestaudio/best',
'quiet': True,
'no_warnings': True,
'ignoreerrors': True,
'noplaylist': True,
'extract_flat': False,
'socket_timeout': 10,
'retries': 10,
'http_chunk_size': 1048576, # 1MB
'extractor_args': {
'youtube': {
'skip': ['hls', 'dash']
}
}
}
# 봇 로그인 확인
@bot.event
async def on_ready():
print(f"✅ 봇 로그인됨: {bot.user}")
# 유튜브 검색 또는 URL -> 오디오 URL, 제목 (에러 처리 강화)
def get_audio_url(query_or_url):
try:
query = query_or_url if query_or_url.startswith("http") else f"ytsearch1:{query_or_url}"
with YoutubeDL(ytdl_opts) as ydl:
info = ydl.extract_info(query, download=False)
if 'entries' in info:
info = info['entries'][0]
return info['url'], info['title']
except Exception as e:
print(f"오디오 URL 가져오기 오류: {e}")
return None, None
# 다음 노래 재생 (에러 처리 강화)
async def play_next(ctx, voice_client):
global now_playing
if not queue:
now_playing = None
await voice_client.disconnect()
return
try:
query = queue.pop(0)
stream_url, title = get_audio_url(query)
if not stream_url:
await ctx.send("⚠️ 음원을 로드하는 데 실패했습니다. 다음 곡으로 넘어갑니다.")
return await play_next(ctx, voice_client)
now_playing = title
def after_callback(error):
if error:
print(f"재생 후 오류 발생: {error}")
fut = play_next(ctx, voice_client)
asyncio.run_coroutine_threadsafe(fut, bot.loop)
# FFmpegPCMAudio 대신 PCMAudioTransformer 사용
source = discord.FFmpegPCMAudio(
stream_url,
**ffmpeg_options,
stderr=open('ffmpeg.log', 'a') # 오류 로깅
)
# 볼륨 조절 가능한 오디오 소스
voice_client.play(source, after=after_callback)
await ctx.send(f"▶️ Now playing: {title}")
except Exception as e:
print(f"재생 중 오류: {e}")
await ctx.send("⚠️ 재생 중 오류가 발생했습니다. 다음 곡으로 넘어갑니다.")
await play_next(ctx, voice_client)
# 공통 재생 명령
async def play_command(ctx, query):
try:
voice_channel = ctx.author.voice.channel if ctx.author.voice else None
if not voice_channel:
await ctx.send("❗ 먼저 음성 채널에 접속해주세요.")
return
voice_client = ctx.guild.voice_client
stream_url, title = get_audio_url(query)
if not stream_url:
await ctx.send("⚠️ 해당 음원을 찾을 수 없습니다.")
return
if voice_client and voice_client.is_playing():
queue.append(query)
await ctx.send(f"🎶 대기열에 추가됨: {title}")
else:
if not voice_client:
voice_client = await voice_channel.connect()
queue.insert(0, query)
await play_next(ctx, voice_client)
except Exception as e:
print(f"재생 명령 오류: {e}")
await ctx.send("⚠️ 음악 재생 중 오류가 발생했습니다.")
# 기존 명령어들 유지 (p, play, queue, nowplaying, remove, skip)
@bot.command(name='p')
@commands.guild_only()
async def p(ctx, *, query):
await play_command(ctx, query)
@bot.command(name='play')
@commands.guild_only()
async def play(ctx, *, query):
await play_command(ctx, query)
# !queue
@bot.command(name='queue')
async def show_queue(ctx):
if not now_playing and not queue:
await ctx.send("🎧 현재 재생 중인 노래가 없습니다.")
return
msg = f"▶️ Now Playing: {now_playing}\n"
for i, q in enumerate(queue, start=1):
_, title = get_audio_url(q)
msg += f"{i}. {title}\n"
await ctx.send(msg)
# !nowplaying
@bot.command(name='nowplaying')
async def now_playing_cmd(ctx):
if now_playing:
await ctx.send(f"🎧 현재 재생 중: {now_playing}")
else:
await ctx.send("⏹️ 현재 재생 중인 노래가 없습니다.")
# !remove [번호]
@bot.command(name='remove')
async def remove(ctx, index: int):
if 1 <= index <= len(queue):
removed = queue.pop(index - 1)
_, title = get_audio_url(removed)
await ctx.send(f"❌ 대기열에서 제거됨: {title}")
else:
await ctx.send("🚫 잘못된 번호입니다.")
# !skip
@bot.command(name='skip')
async def skip(ctx):
voice_client = ctx.guild.voice_client
if voice_client and voice_client.is_playing():
voice_client.stop()
await ctx.send("⏭️ 다음 곡으로 넘어갑니다.")
else:
await ctx.send("❌ 현재 재생 중인 노래가 없어요.")
# 실행
bot.run(TOKEN)
기능은 다음과 같다.
!p 노래 제목 or 유튜브 URL // 노래 실행
!play 노래 제목 or 유튜브 URL // 노래 실행
!skip // 다음 노래로 넘긴다. + 아무 노래도 없으면 나간다.
!queue // 대기 목록 확인
!nowplaying // 현재 실행중인 노래 정보를 가져온다.
!remove + [대기 번호] // 대기 목록에 있는 노래를 제거한다.
잘 동작하는 걸 확인할 수 있다.
3. 배포하기
백엔드 배포 경험이 있어서 그렇게 어렵지 않다.
구글링을 해보니 헤로쿠? 라는 걸 사용해서 24시간 배포를 하는 방법이 대표적인 거 같다.
나는 항상 AWS를 사용해왔고 GC(구글 클라우드)에 대해서 한 번 경험 해보고 싶어서 나는 구글 클라우드로 배포를 해봤다.
방법은 다음과 같다.
1. Github Repository에 코드 업로드
2. GC에서 VM을 만든다
3. 해당 VM에서 Git clone으로 코드를 가져온다. (이때 Password 방식이 안된다면 Token 방식으로 인증한다.)
4. 가져온 코드를 실행하기 위한 준비를 한다. (pip 설치, .env 생성)
5. .py를 백그라운드로 실행한다. (tmux를 사용한다. 도커도 될 듯?)
배포하는 데 어려움은 두가지가 있었다.
3.1 git clone (with Token)
git clone 'https://~~~.git"으로 코드를 가져올 때 보안을 위해서 github id, password 방식이 아닌 token을 사용한 방식을 사용해야했다.
방법은 다음과 같다.
1. 내 계정 github Setting 으로 들어가서 repo 권한이 있는 토큰을 발급받는다. (classic token -> repo 권한 클릭 후 생성)
2. 다음 처럼 clone 경로에 토큰 값을 넣어주면 된다.
https://koreaioi:[토큰값]@github.com/koreaioi/Tave-Music-Bot.git
3.2 tmux 사용
tmux는 처음 사용해봤는데 백그라운드에서 실행할 수 있는 '세션'을 만드는 것이다.
docker -d 옵션과 비슷하다고 느꼈다. (역시 백그라운드 니까..!)
# 이름이 tave_discord_bot인 tmux 세션을 만든다.
tmux new -s tave_discord_bot
# tmux에 다시 들어가기
tmux attach -t tave_discord_bot
# main.py 경로로 이동 후 파이썬 main.py 실행
python3 main.py
# tumx 세션 리스트 보기
tmux ls
# tmux 세션에서 나오는 방법
Ctrl + B를 누른 후 D를 눌러 세션에서 분리한다
4. 결과
이렇게 디스코드 봇 만들고 24시간 활성화하기 위해서 GC에 배포까지 완료했다.
해당 봇을 오래 사용할 경우 유튜브 측에서 IP를 끊어버리는 문제가 있어 오래 사용하기는 힘들지만, 그래도 완성했다는 점에서 의미가 있다!
'프로젝트에서 일어난 일' 카테고리의 다른 글
S3 PreSignedUrl로 이미지 업로드하기 (0) | 2025.05.04 |
---|---|
Spring Cloud Gateway에서 인증/인가 구현하기 (0) | 2025.04.25 |
DataGrip에서 SSH 터널링으로 MySQL Container 접속하기 (0) | 2025.04.18 |
라즈베리파이 + MSA(Spring Cloud) + CI/CD 배포 (1) | 2025.04.17 |
다수 데이터 Insert 시 성능 개선하기(37.06% 개선) (1) | 2025.04.16 |