Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ WORKDIR /app

# Copy requirements and bot code
COPY requirements.txt .
COPY discord_yt_bot ./discord_yt_bot
COPY bot.py .

# Install Python dependencies
RUN pip install --upgrade pip && \
pip install -r requirements.txt

# Run the bot
CMD ["python", "bot.py"]

CMD ["python", "-m", "discord_yt_bot.main"]
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
Discord Youtube
---

A simple discord bot to play YT videos on a Discord channel
A simple Discord bot to play YouTube videos in a voice channel.

### Running

Install the dependencies from `requirements.txt` and set the `DISCORD_BOT_TOKEN`
environment variable. Start the bot using:

```
python -m discord_yt_bot.main
```

To build and run the Docker image instead, execute:

```
docker compose up --build
```
123 changes: 2 additions & 121 deletions bot.py
Original file line number Diff line number Diff line change
@@ -1,123 +1,4 @@
import discord
from discord.ext import commands
import yt_dlp
import os
import sys

intents = discord.Intents.default()
intents.message_content = True
intents.reactions = True
intents.voice_states = True # Needed for joining channels

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

@bot.event
async def on_ready():
activity = discord.Game(name="Type !help for commands")
print(f"Logged in as {bot.user}")

last_play_request = {}

@bot.command()
async def help(ctx):
embed = discord.Embed(
title="Bot Commands",
description="Here's what I can do:",
color=discord.Color.blue()
)
embed.add_field(name="!play <YouTube URL>", value="Get a prompt to play a YouTube video in your voice channel.", inline=False)
embed.add_field(name="!stop", value="Stop playback, but stay in the voice channel.", inline=False)
embed.add_field(name="!leave", value="Disconnect from the voice channel.", inline=False)
embed.add_field(name="!help", value="Show this help message.", inline=False)
await ctx.send(embed=embed)

@bot.command()
async def play(ctx, url: str):
if not ("youtube.com" in url or "youtu.be" in url):
await ctx.send("Please provide a valid YouTube URL.")
return

msg = await ctx.send(f"{ctx.author.mention}, press ▶️ to play this video in your voice channel.")
await msg.add_reaction("▶️")

global last_play_request
last_play_request = {
"message_id": msg.id,
"user_id": ctx.author.id,
"url": url,
"guild_id": ctx.guild.id
}

@bot.command()
async def stop(ctx):
"""Stop playing audio, but stay connected to the voice channel."""
vc = ctx.voice_client
if vc and vc.is_playing():
vc.stop()
await ctx.send("Playback stopped.")
else:
await ctx.send("Nothing is playing right now.")

@bot.command()
async def leave(ctx):
if ctx.voice_client:
await ctx.voice_client.disconnect()
await ctx.send("Left the voice channel.")
else:
await ctx.send("I'm not in a voice channel.")

@bot.event
async def on_reaction_add(reaction, user):
if user.bot:
return

global last_play_request
play_req = last_play_request

if (
play_req
and reaction.message.id == play_req.get("message_id")
and user.id == play_req.get("user_id")
and reaction.message.guild.id == play_req.get("guild_id")
and str(reaction.emoji) == "▶️"
):
voice = user.voice
if not voice or not voice.channel:
await reaction.message.channel.send(f"{user.mention}, you must be in a voice channel to play music!")
return

vc = reaction.message.guild.voice_client
if not vc:
vc = await voice.channel.connect()
elif vc.channel != voice.channel:
await vc.move_to(voice.channel)

ydl_opts = {
'format': 'bestaudio/best',
'quiet': True,
'noplaylist': True,
}
try:
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(play_req["url"], download=False)
if 'entries' in info:
info = info['entries'][0]
audio_url = info['url']

if vc.is_playing():
vc.stop()

source = discord.FFmpegPCMAudio(audio_url)
vc.play(source)
await reaction.message.channel.send(f"Now playing: {play_req['url']} in {voice.channel.mention}")

except Exception as e:
await reaction.message.channel.send(f"Failed to play: `{e}`")
from discord_yt_bot.main import main

if __name__ == "__main__":
token = os.getenv("DISCORD_BOT_TOKEN")
if not token:
print("Error: Please set the DISCORD_BOT_TOKEN environment variable!")
sys.exit(1)
bot.run(token)

main()
Empty file added discord_yt_bot/__init__.py
Empty file.
Empty file added discord_yt_bot/cogs/__init__.py
Empty file.
Empty file.
34 changes: 34 additions & 0 deletions discord_yt_bot/cogs/commands/help.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from discord.ext import commands
import discord


class Help(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot

@commands.command()
async def help(self, ctx: commands.Context):
embed = discord.Embed(
title="Bot Commands",
description="Here's what I can do:",
color=discord.Color.blue(),
)
embed.add_field(
name="!play <YouTube URL>",
value="Get a prompt to play a YouTube video in your voice channel.",
inline=False,
)
embed.add_field(
name="!stop",
value="Stop playback, but stay in the voice channel.",
inline=False,
)
embed.add_field(
name="!leave", value="Disconnect from the voice channel.", inline=False
)
embed.add_field(name="!help", value="Show this help message.", inline=False)
await ctx.send(embed=embed)


async def setup(bot: commands.Bot):
await bot.add_cog(Help(bot))
47 changes: 47 additions & 0 deletions discord_yt_bot/cogs/commands/music.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import discord
from discord.ext import commands


class Music(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.last_play_request = {}

@commands.command()
async def play(self, ctx: commands.Context, url: str):
if "youtube.com" not in url and "youtu.be" not in url:
await ctx.send("Please provide a valid YouTube URL.")
return

msg = await ctx.send(
f"{ctx.author.mention}, press \u25B6\uFE0F to play this video in your voice channel."
)
await msg.add_reaction("\u25B6\uFE0F")

self.last_play_request = {
"message_id": msg.id,
"user_id": ctx.author.id,
"url": url,
"guild_id": ctx.guild.id,
}

@commands.command()
async def stop(self, ctx: commands.Context):
"""Stop playing audio, but stay connected to the voice channel."""
vc = ctx.voice_client
if vc and vc.is_playing():
vc.stop()
await ctx.send("Playback stopped.")
else:
await ctx.send("Nothing is playing right now.")

@commands.command()
async def leave(self, ctx: commands.Context):
if ctx.voice_client:
await ctx.voice_client.disconnect()
await ctx.send("Left the voice channel.")
else:
await ctx.send("I'm not in a voice channel.")

async def setup(bot: commands.Bot):
await bot.add_cog(Music(bot))
Empty file.
57 changes: 57 additions & 0 deletions discord_yt_bot/cogs/events/reactions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import discord
from discord.ext import commands
from discord_yt_bot.utils.youtube import extract_audio_url


class ReactionHandler(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot

@commands.Cog.listener()
async def on_reaction_add(self, reaction: discord.Reaction, user: discord.User):
if user.bot:
return

music_cog = self.bot.get_cog("Music")
if not music_cog or not hasattr(music_cog, "last_play_request"):
return

play_req = music_cog.last_play_request

if (
play_req
and reaction.message.id == play_req.get("message_id")
and user.id == play_req.get("user_id")
and reaction.message.guild.id == play_req.get("guild_id")
and str(reaction.emoji) == "\u25B6\uFE0F"
):
voice = user.voice
if not voice or not voice.channel:
await reaction.message.channel.send(
f"{user.mention}, you must be in a voice channel to play music!"
)
return

vc = reaction.message.guild.voice_client
if not vc:
vc = await voice.channel.connect()
elif vc.channel != voice.channel:
await vc.move_to(voice.channel)

try:
audio_url = extract_audio_url(play_req["url"])

if vc.is_playing():
vc.stop()

source = discord.FFmpegPCMAudio(audio_url)
vc.play(source)
await reaction.message.channel.send(
f"Now playing: {play_req['url']} in {voice.channel.mention}"
)
except Exception as e:
await reaction.message.channel.send(f"Failed to play: `{e}`")


async def setup(bot: commands.Bot):
await bot.add_cog(ReactionHandler(bot))
18 changes: 18 additions & 0 deletions discord_yt_bot/cogs/events/ready.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from discord.ext import commands
import discord


class ReadyEvent(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot

@commands.Cog.listener()
async def on_ready(self):
activity = discord.Game(name="Type !help for commands")
if self.bot.user:
print(f"Logged in as {self.bot.user}")
await self.bot.change_presence(activity=activity)


async def setup(bot: commands.Bot):
await bot.add_cog(ReadyEvent(bot))
37 changes: 37 additions & 0 deletions discord_yt_bot/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import os
import sys
import discord
from discord.ext import commands


def create_bot():
intents = discord.Intents.default()
intents.message_content = True
intents.reactions = True
intents.voice_states = True

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


async def load_cogs(bot: commands.Bot):
await bot.load_extension("discord_yt_bot.cogs.commands.help")
await bot.load_extension("discord_yt_bot.cogs.commands.music")
await bot.load_extension("discord_yt_bot.cogs.events.ready")
await bot.load_extension("discord_yt_bot.cogs.events.reactions")


def main():
bot = create_bot()

token = os.getenv("DISCORD_BOT_TOKEN")
if not token:
print("Error: Please set the DISCORD_BOT_TOKEN environment variable!")
sys.exit(1)

bot.loop.create_task(load_cogs(bot))
bot.run(token)


if __name__ == "__main__":
main()
Empty file.
15 changes: 15 additions & 0 deletions discord_yt_bot/utils/youtube.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import yt_dlp


def extract_audio_url(url: str):
"""Return direct audio URL for a YouTube video."""
ydl_opts = {
"format": "bestaudio/best",
"quiet": True,
"noplaylist": True,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(url, download=False)
if "entries" in info:
info = info["entries"][0]
return info["url"]