본문 바로가기
개발

GPT로 디스코드 노래 봇 만들기

by wafla 2024. 9. 26.

1. 들어가며

이 글에서는 ChatGPT를 활용해 유튜브 링크를 받아 노래를 재생하는 디스코드 노래 봇을 만드는 과정을 다룹니다. 디스코드 봇 개발이 처음이라면, 이 글에서 기본적인 절차를 배우는 데 큰 도움이 될 것입니다.

 

인공지능이 제공하는 코드가 항상 완벽하지 않기에, 오류를 해결하고 직접 디버깅하는 과정 또한 중요한 능력입니다. 따라서 제가 어떻게 오류를 해결했는지 과정을 서술하며 작성했습니다.

 

2. ChatGPT에게 첫 요청

디스코드 봇 개발에 필요한 파이썬 라이브러리인 pycord를 이용하여, 유튜브 링크를 제공하면 노래를 재생해주는 봇을 만들고자 GPT에게 다음과 같은 코드를 요청했습니다.

import discord
from discord.ext import commands
import youtube_dl

intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix='!', intents=intents)

@bot.command(name='play')
async def play(ctx, url: str):
    voice_channel = ctx.author.voice.channel
    if not voice_channel:
        await ctx.send("You need to be in a voice channel to play music.")
        return

    voice_client = await voice_channel.connect()

    ydl_opts = {
        'format': 'bestaudio/best',
        'postprocessors': [{
            'key': 'FFmpegExtractAudio',
            'preferredcodec': 'mp3',
            'preferredquality': '192',
        }],
    }

    with youtube_dl.YoutubeDL(ydl_opts) as ydl:
        info = ydl.extract_info(url, download=False)
        url2 = info['formats'][0]['url']
        voice_client.play(discord.FFmpegPCMAudio(url2))

@bot.command(name='stop')
async def stop(ctx):
    await ctx.voice_client.disconnect()
    
bot.run('YOUR_BOT_TOKEN')

 

하지만 youtube_dl이 더 이상 지원되지 않아, 이를 yt-dlp로 대체해야 한다는 정보를 검색을 통해 얻었습니다. (나무위키)

 

3. yt-dlp를 사용한 코드 수정

youtube_dl 대신 yt-dlp를 사용하여 새로운 코드를 작성했습니다. 이로 인해 코드는 개선되었지만, 새로운 문제가 발생했습니다. AttributeError: 'FFmpegPCMAudio' object has no attribute '_process' 오류가 나타난 것입니다.

import discord
from discord.ext import commands
import yt_dlp

bot = commands.Bot(command_prefix='!')

@bot.slash_command(name='play')
async def play(ctx, url: str):
    if ctx.author.voice is None or ctx.author.voice.channel is None:
        await ctx.respond("You need to be in a voice channel to play music.", ephemeral=True)
        return

    voice_channel = ctx.author.voice.channel
    voice_client = await voice_channel.connect()

    ydl_opts = {
        'format': 'bestaudio/best',
        'postprocessors': [{
            'key': 'FFmpegExtractAudio',
            'preferredcodec': 'mp3',
            'preferredquality': '192',
        }],
    }

    with yt_dlp.YoutubeDL(ydl_opts) as ydl:
        info = ydl.extract_info(url, download=False)
        url2 = info['formats'][0]['url']
        voice_client.play(discord.FFmpegPCMAudio(url2))

    await ctx.respond("Playing music!")

@bot.slash_command(name='stop')
async def stop(ctx):
    if ctx.voice_client:
        await ctx.voice_client.disconnect()
        await ctx.respond("Stopped playing music.")
    else:
        await ctx.respond("The bot is not connected to a voice channel.", ephemeral=True)

bot.run('YOUR_BOT_TOKEN')

 

 

GPT한테 물어보니 오디오와 비디오를 처리하고 변환하는 오픈 소스 멀티미디어 프레임워크인 FFmpeg가 설치되어있지 않아 나타나는 문제라고 합니다.

 

4. FFmpeg 설치

이 사이트에 접속해서 FFmpeg 파일을 다운받습니다.

 

압축을 풀고 나온 폴더에서 bin 폴더에 들어가면 다음과 같이 3개의 파일들이 있습니다. 이 파일의 경로를 환경 변수에 등록해줘야 합니다. 저는 C드라이브에 옮긴 후에 환경 변수에 등록 했습니다.

 

5. 환경 변수 설정

환경 변수에 등록하는 방법은 다음과 같습니다.

 

1. 환경 변수 검색 후 '시스템 환경 변수 편집' 클릭

 

2. 환경 변수 클릭

 

3. Path 선택 후 편집 클릭

 

4. 새로 만들기 클릭 후 bin 폴더의 파일 경로를 입력하고 확인을 누르면 됩니다.

 

5. 환경 변수 창에서도 다시 확인을 눌러주고 적용을 하기 위해 컴퓨터를 다시시작 합니다.

 

이제 FFmpeg 설치가 끝났습니다. 디스코드에 접속해서 음성 채널에 들어간 뒤 /play 명령어로 노래를 재생시켜 봅시다.

 

6. 문제 해결: 스트리밍 URL 확인

코드는 잘 동작하는 것 같지만 노래는 나오지 않습니다. GPT의 도움을 받아 로컬에 저장된 노래를 재생시켜 봤는데 로컬에 저장된 노래는 잘 됩니다. url 링크에 문제가 있는 것 같아 url 주소를 출력시켜 봤습니다.

 with yt_dlp.YoutubeDL(ydl_opts) as ydl:
        info = ydl.extract_info(url, download=False)
        url2 = info['formats'][0]['url']  # 스트리밍 URL 추출
        title = info.get('title', 'Unknown Title')
        print(url2)

 

출력된 링크에 접속해보니 노래가 아닌 사진이 나옵니다. 사진 링크를 걸어놨기 때문에 노래가 나오지 않는 것이었습니다.

 

유튜브 영상에 대한 정보를 잘 가져오는지 info를 출력해서 확인해봐야겠습니다.

 with yt_dlp.YoutubeDL(ydl_opts) as ydl:
        info = ydl.extract_info(url, download=False) # 유튜브 영상에 관한 json파일 반환
        url2 = info['formats'][0]['url']
        title = info.get('title', 'Unknown Title')
        print(info)

 

json 형식으로 텍스트가 출력됐습니다. 정렬이 되어있지 않아 보기 힘들기 때문에 정렬을 시켜서 무슨 내용이 있는지 검색해 봅시다. 저는 Online Parser 사이트에서 확인했습니다. 왼쪽에 알아보기 힘든 문서가 오른쪽과 같이 잘 정리 됐습니다.

 

url2가 info['formats'][0]['url']로 지정됐었으니 해당 경로로 가봅시다.

 

위에서 바로 확인할 수 있었습니다. format_id가 sb2라고 돼있는데 sb는 스토리보드를 의미한다고 합니다. 애초에 음악 파일이 아닌 곳에서 url 링크를 가져오고 있었습니다. 음악 url이 없나 살펴보니 밑 쪽에 있었습니다.

 

GPT에게 오디오 파일 링크를 가져와달라고 했더니 코드를 다음과 같이 수정해줬습니다.

with yt_dlp.YoutubeDL(ydl_opts) as ydl:
    info = ydl.extract_info(url, download=False)  # 다운로드 하지 않고 정보만 추출
    # 'formats'에서 오디오만 포함된 형식을 찾습니다.
    audio_formats = [f for f in info['formats'] if f.get('acodec') and 'audio' in f['acodec']]

    # 가장 좋은 품질의 오디오 형식을 선택합니다.
    if audio_formats:
        best_audio = audio_formats[0]  # 첫 번째 오디오 형식 선택 (또는 품질을 기준으로 선택 가능)
        url2 = best_audio['url']  # 오디오 파일 URL
        title = info.get('title', 'Unknown Title')  # 노래 제목 추출
        voice_client.play(discord.FFmpegPCMAudio(url2, options="-vn"))
    else:
        print("오디오 형식을 찾을 수 없습니다.")

 

이제 노래가 정상적으로 재생이 됩니다! 만약 소리가 터지는 것 같다 싶으면 봇을 우클릭해서 볼륨을 줄여주면 해결될겁니다. 또 노래 속도가 느려졌다 빨라졌다 하는 것 같다면 다음 코드를 추가하면 됩니다.

ffmpeg_options = {
        'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',  # Network reconnection options
        'options': '-vn'  # Audio only
    }

 

하지만 이제 노래만 나오는게 전부입니다. 다른 노래를 추가하고 싶어도 재생 중인 노래를 끊고 새로 노래를 틀어버립니다. 따라서 큐를 생성하여 대기열을 관리하고 /play, /skip, /stop 명령어를 만들어 대기열을 관리합시다.

 

GPT에게 부탁했더니 skip, stop 명령어는 잘 됐지만 play 명령어는 마음에 들지 않았습니다. url을 등록하면 해당 정보를 불러오느라 재생 중인 노래가 잠시 끊겼고 노래를 재생할 때 다시 정보를 다운로드하는 비효율적인 구조였기 때문입니다.

from discord import FFmpegPCMAudio

# Create a queue to hold song URLs
song_queue = []

@bot.slash_command(name='play')
async def play(ctx, url: str):
    await ctx.defer()

    if ctx.author.voice is None or ctx.author.voice.channel is None:
        await ctx.respond("You need to be in a voice channel to play music.", ephemeral=True)
        return

    voice_channel = ctx.author.voice.channel
    if ctx.voice_client is None or not ctx.voice_client.is_connected():
        voice_client = await voice_channel.connect()
    else:
        voice_client = ctx.voice_client

    ydl_opts = {
        'format': 'bestaudio/best',
        'noplaylist': True,
    }

    ffmpeg_options = {
        'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',  # Network retry options
        'options': '-vn'  # Remove video, only play audio
    }

    with yt_dlp.YoutubeDL(ydl_opts) as ydl:
        info = ydl.extract_info(url, download=False)
        
        # Select the best audio format (opus or aac)
        best_audio_format = next(
            (f for f in info['formats'] if f.get('acodec') in ['opus', 'aac'] and f.get('vcodec') == 'none'),
            None
        )
        
        if best_audio_format:
            song_url = best_audio_format['url']
        else:
            song_url = info['formats'][0]['url']
        
        title = info.get('title', 'Unknown Title')

    # Add the song to the queue
    song_queue.append((song_url, title))

    # If no song is currently playing, play the song immediately
    if not voice_client.is_playing():
        await play_next_song(ctx, voice_client)

    await ctx.respond(f"Added to queue: {title}")


async def play_next_song(ctx, voice_client):
    if song_queue:
        song_url, title = song_queue.pop(0)
        voice_client.play(FFmpegPCMAudio(song_url, **ffmpeg_options), after=lambda e: bot.loop.create_task(play_next_song(ctx, voice_client)))
        await ctx.respond(f"Now playing: {title}")
    else:
        await ctx.respond("The queue is empty.")

 

따라서 제가 직접 아래와 같이 코드를 수정했습니다.

6. 큐 기능 추가

기존 코드에서는 한 곡이 끝나면 다음 곡을 자동으로 재생할 수 없었습니다. 이를 해결하기 위해 큐 시스템을 추가했습니다. GPT에게 도움을 요청했으나, 재생 중 노래가 끊기는 비효율적인 구조가 발생했습니다. 이를 개선하기 위해 두 개의 함수를 만들었습니다:

  1. play: 노래를 큐에 추가하는 기능
  2. play_next_song: 큐에 있는 노래를 순차적으로 재생하는 기능

7. 성능 개선: pytube 라이브러리 사용

play 명령어를 실행하면 영상 제목과 함께 큐에 추가됐다는 응답을 보내줍니다. 하지만 yt-dlp는 노래 정보와 함께 불필요한 데이터를 가져와 재생 속도가 느려지는 문제가 있었습니다. 이를 해결하기 위해 pytube 라이브러리를 사용하여 노래 제목만 추출하도록 수정했습니다. 

 

8. 최종 코드

다음은 최종적으로 완성된 코드입니다:

import discord
from discord.ext import commands
from pytube import YouTube
import yt_dlp

# 봇 초기화 및 권한 설정
intents = discord.Intents.default()
intents.message_content = True
intents.guilds = True
intents.members = True

bot = commands.Bot(command_prefix="!", intents=intents)

@bot.event
async def on_ready():
    print(f'{bot.user} has connected to Discord!')

url_queue = []   # URL 저장

@bot.slash_command(name='play') # 대기열에 추가
async def play(ctx, url: str):
    await ctx.defer()

    if ctx.author.voice is None or ctx.author.voice.channel is None:
        await ctx.respond("You need to be in a voice channel to play music.", ephemeral=True)
        return

    voice_channel = ctx.author.voice.channel
    if ctx.voice_client is None or not ctx.voice_client.is_connected():
        voice_client = await voice_channel.connect()
    else:
        voice_client = ctx.voice_client

    video = YouTube(url)
    title = video.title
        
    if url not in url_queue:
        url_queue.append(url)
        await ctx.respond(f"Added to queue: {title}")
    else:
        await ctx.respond(f"URL already in queue: {title}")

    if not voice_client.is_playing():
        await play_next_song(ctx, voice_client);
    
async def play_next_song(ctx, voice_client): # 노래 재생
    ydl_opts = {
            'format': 'bestaudio/best',
            'noplaylist': True,
        }
    
    ffmpeg_options = {
        'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',  # Network reconnection options
        'options': '-vn'  # Audio only
    } # 지연 설정

    if url_queue:
        url = url_queue.pop(0)
        with yt_dlp.YoutubeDL(ydl_opts) as ydl:
            info = ydl.extract_info(url, download=False)
            best_audio_format = next(
                (f for f in info['formats'] if f.get('acodec') in ['opus', 'aac'] and f.get('vcodec') == 'none'),
                None
            )
            if best_audio_format:
                song_url = best_audio_format['url']
            else:
                song_url = info['formats'][0]['url']
                
            title = info.get('title', 'Unknown Title')

            print(best_audio_format)
            print(song_url)

            # 다음 곡 재생
            voice_client.play(discord.FFmpegPCMAudio(song_url, **ffmpeg_options), after=lambda e: bot.loop.create_task(play_next_song(ctx, voice_client)))

            await ctx.respond(f"Now playing: {title}")
    else:
        await ctx.respond(f"Queue is empty!")

@bot.slash_command(name='skip')
async def skip(ctx):
    """Skips the current song."""
    if ctx.voice_client and ctx.voice_client.is_playing():
        ctx.voice_client.stop()  # This will trigger the after callback to play the next song
        await ctx.respond("Song skipped.")
    else:
        await ctx.respond("No song is playing to skip.")

@bot.slash_command(name='stop')
async def stop(ctx):
    if ctx.voice_client:
        await ctx.voice_client.disconnect()
        await ctx.respond("Stopped playing music.")
    else:
        await ctx.respond("The bot is not connected to a voice channel.", ephemeral=True) # ephemeral은 명령어를 사용한 사용자에게만 응답 메시지를 보여줍니다.

bot.run("YOUR_TOKEN_HERE")

9. 개선할 점

이 코드에서는 여러 가지 개선이 가능합니다:

  1. 재생 시간 지연 문제: 노래 다운로드 및 재생 시간이 다소 길 수 있습니다.
  2. 멀티 서버 지원: 현재는 하나의 서버에서만 사용 가능한 큐 시스템입니다. 여러 서버에서 개별 큐를 할당하려면 객체화를 통해 서버마다 별도의 큐를 제공해야 합니다.

10. 결론

이 글에서는 ChatGPT를 통해 디스코드 노래 봇을 제작하고, 발생하는 여러 오류와 성능 문제를 해결하는 과정을 설명했습니다. 이와 같은 방법으로 인공지능과 협력하여 원하는 결과를 얻을 수 있습니다.