Published: Tue, Jul 08, 25

Download YouTube videos or audio straight from Discord commands.


This Python-powered bot lets Discord users download YouTube videos or extract audio directly from chat. Enter a simple command, and the bot fetches the media, processes it, and shares the file back in the channel. Ideal for grabbing music, educational clips, or saving content for offline use—all without leaving Discord.


import discord
from discord import app_commands
from discord.ext import commands
import yt_dlp
import os
import re
import asyncio
from functools import partial

from dotenv import load_dotenv

load_dotenv()
TOKEN = os.getenv("DISCORD_DOWNLOADER")

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

# Ensure downloads folder exists
os.makedirs("downloads", exist_ok=True)


class YTDLSource:
    @staticmethod
    def is_youtube_url(url):
        youtube_regex = r"(https?://)?(www\.)?(youtube\.com|youtu\.be)/"
        return re.match(youtube_regex, url) is not None

    @staticmethod
    def search(query):
        ydl_opts = {
            'quiet': True,
            'skip_download': True,
            'extract_flat': 'in_playlist',
            'default_search': 'ytsearch5',
            'format': 'bestaudio/best'
        }
        try:
            with yt_dlp.YoutubeDL(ydl_opts) as ydl:
                info = ydl.extract_info(query, download=False)
                if 'entries' in info:
                    return info['entries']
                else:
                    return [info]
        except yt_dlp.utils.DownloadError as e:
            print(f"[ERROR] yt-dlp search error: {e}")
            return None
        except Exception as e:
            print(f"[ERROR] yt-dlp unknown search error: {e}")
            return None

    @staticmethod
    def extract_from_url(url):
        ydl_opts = {
            'quiet': True,
            'skip_download': True,
            'extract_flat': 'in_playlist',
        }
        try:
            with yt_dlp.YoutubeDL(ydl_opts) as ydl:
                info = ydl.extract_info(url, download=False)

                if info.get('_type') == 'playlist':
                    entries = info['entries']
                    return entries
                else:
                    return [info]
        except yt_dlp.utils.DownloadError as e:
            print(f"[ERROR] yt-dlp extract error: {e}")
            return None
        except Exception as e:
            print(f"[ERROR] yt-dlp unknown extract error: {e}")
            return None

    @staticmethod
    def download_audio(url):
        output_path = "downloads/%(title)s.%(ext)s"
        ydl_opts = {
            'format': 'bestaudio/best',
            'outtmpl': output_path,
            'quiet': True,
            'noplaylist': True,
            'cookiefile': 'cookies.txt',
            'user_agent': 'Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
            'postprocessors': [{
                'key': 'FFmpegExtractAudio',
                'preferredcodec': 'mp3',
                'preferredquality': '128',
            }],
            'postprocessor_args': ['-t', '300'],
        }

        try:
            with yt_dlp.YoutubeDL(ydl_opts) as ydl:
                info = ydl.extract_info(url, download=True)
                filename = ydl.prepare_filename(info)
                audio_file = os.path.splitext(filename)[0] + ".mp3"
                return audio_file
        except yt_dlp.utils.DownloadError as e:
            print(f"[ERROR] yt-dlp download error: {e}")
            raise e
        except Exception as e:
            print(f"[ERROR] yt-dlp unknown download error: {e}")
            raise e


async def run_in_executor(func, *args, **kwargs):
    loop = asyncio.get_running_loop()
    return await loop.run_in_executor(None, partial(func, *args, **kwargs))


@bot.event
async def on_ready():
    print(f"Bot connected as {bot.user}")
    try:
        synced = await bot.tree.sync()
        print(f"Synced {len(synced)} commands")
        for cmd in synced:
            print(f"- {cmd.name}: {cmd.description}")
    except Exception as e:
        print(e)


@bot.tree.command(name="discord-dl", description="Search YouTube or paste a link to download audio")
@app_commands.describe(query="Search term or YouTube link")
async def discord_dl(interaction: discord.Interaction, query: str):
    await interaction.response.defer(thinking=True)

    # Check if query is a direct YouTube link
    if YTDLSource.is_youtube_url(query):
        results = YTDLSource.extract_from_url(query)
    else:
        results = YTDLSource.search(query)

    if not results:
        await interaction.followup.send("❌ No results found or an error occurred while searching.")
        return

    # Clip options to max 25 to avoid Discord error
    results = results[:25]

    options = []
    for video in results:
        title = video.get("title", "Unknown Title")
        video_id = video.get("id")
        if not video_id:
            continue
        url = f"https://www.youtube.com/watch?v={video_id}"
        options.append(
            discord.SelectOption(
                label=title[:100],
                value=url,
                description=url[:100]
            )
        )

    if not options:
        await interaction.followup.send("❌ No valid videos found.")
        return

    class VideoSelect(discord.ui.Select):
        def __init__(self):
            super().__init__(
                placeholder="Choose a video or playlist item...",
                options=options,
                min_values=1,
                max_values=1,
            )

        async def callback(self, select_interaction: discord.Interaction):
            chosen_url = self.values[0]
            await select_interaction.response.defer(thinking=True)

            entries = YTDLSource.extract_from_url(chosen_url)
            if entries is None:
                await select_interaction.followup.send(
                    f"❌ Failed to extract data for: <{chosen_url}>"
                )
                return

            if len(entries) > 1:
                await download_queue(select_interaction, entries)
            else:
                try:
                    audio_file = await run_in_executor(
                        YTDLSource.download_audio, chosen_url
                    )
                except yt_dlp.utils.DownloadError as e:
                    await select_interaction.followup.send(
                        f"❌ Download error:\n```{str(e)}```"
                    )
                    return
                except Exception as e:
                    await select_interaction.followup.send(
                        f"❌ Unknown error occurred:\n```{str(e)}```"
                    )
                    return

                await select_interaction.followup.send(
                    content=f"✅ **Done:** <{chosen_url}>",
                    file=discord.File(audio_file)
                )
                os.remove(audio_file)

    view = discord.ui.View()
    view.add_item(VideoSelect())

    await interaction.followup.send(
        "Select the video you want to download audio from:",
        view=view
    )


async def download_queue(interaction, video_entries):
    # clip large playlists to avoid overwhelming Discord
    video_entries = video_entries[:25]

    for video in video_entries:
        title = video.get("title", "Untitled")
        video_id = video.get("id")
        if not video_id:
            continue
        url = f"https://www.youtube.com/watch?v={video_id}"

        await interaction.followup.send(f"▶️ **Downloading:** {title}")

        try:
            audio_file = await run_in_executor(
                YTDLSource.download_audio, url
            )
        except yt_dlp.utils.DownloadError as e:
            await interaction.followup.send(
                f"❌ Download error for {title}:\n```{str(e)}```"
            )
            continue
        except Exception as e:
            await interaction.followup.send(
                f"❌ Unknown error for {title}:\n```{str(e)}```"
            )
            continue

        await interaction.followup.send(
            content=f"✅ **Done:** {title}\n<{url}>",
            file=discord.File(audio_file)
        )
        os.remove(audio_file)

    await interaction.followup.send("✅ **All playlist downloads finished!**")


bot.run(TOKEN)