Skip to content

Commit

Permalink
Merge pull request #17 from jaimegarjr/hardening-music-commands
Browse files Browse the repository at this point in the history
Hardening music commands
  • Loading branch information
jaimegarjr authored Feb 2, 2025
2 parents dae49dd + d194fdf commit 3fbaac5
Show file tree
Hide file tree
Showing 8 changed files with 491 additions and 148 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/main-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,4 @@ jobs:
run: |
gcloud compute instances update-container meowbot-instance-vm \
--container-image gcr.io/meowbot-448505/meow-bot:latest \
--zone us-central1-c \
--container-arg="--log-driver=gcplogs"
--zone us-central1-c
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ To install and actually run meowBot on your local machine, perform the following
```
BOT_TOKEN = "your_bot_token"
CLIENT_ID = "your_client_id"
```
5. To run the bot, execute ```poetry run python -m meowbot.bot.meowbot``` in your terminal.


Expand Down
176 changes: 176 additions & 0 deletions meowbot/application/services/voice_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import discord
from discord.ext import tasks
from meowbot.application.models.ytdl_source import YTDLSource
from meowbot.utils.logger import logging, setup_logger
from meowbot.application.models.music_queue import MusicQueue
from itertools import islice


class VoiceService:
def __init__(self, bot):
self.bot = bot
self.logger = setup_logger(name="service.voice", level=logging.INFO)
self.check_leave.start()
self.music_queue = MusicQueue()

def is_user_in_voice_channel(self, ctx):
return ctx.message.author.voice is not None

def is_bot_in_voice_channel(self, ctx):
return ctx.voice_client is not None

async def send_message(self, ctx, content):
await ctx.send(content)
self.logger.info(content)

@tasks.loop(minutes=8)
async def check_leave(self):
voice_lists = self.bot.voice_clients
for vl in voice_lists:
if vl.is_connected() and not vl.is_playing():
await vl.disconnect()
self.logger.info(f"meowBot left {vl.channel}.")

async def join_voice_channel(self, ctx):
if not self.is_user_in_voice_channel(ctx):
await self.send_message(ctx, "You are not in a voice channel!")
return

channel = ctx.message.author.voice.channel
voice_client = ctx.voice_client

if voice_client:
await voice_client.move_to(channel)
await self.send_message(ctx, f"Bot moved to {channel}.")
else:
self.check_leave.restart()
await channel.connect()
await self.send_message(ctx, f"Joined {channel}! ")

async def leave_voice_channel(self, ctx):
if not self.is_bot_in_voice_channel(ctx):
await self.send_message(ctx, "I'm not in a voice channel!")
return

channel = ctx.voice_client.channel
self.music_queue.clear()
await ctx.voice_client.disconnect()
await self.send_message(ctx, f"Bot left {channel}.")

async def pause_song(self, ctx):
if ctx.voice_client.is_playing():
ctx.voice_client.pause()
await self.send_message(ctx, "Song has been paused!")

async def resume_song(self, ctx):
if ctx.voice_client.is_paused():
ctx.voice_client.resume()
await self.send_message(ctx, "Song has resumed playing!")

async def stop_song(self, ctx):
ctx.voice_client.stop()
await self.send_message(ctx, "Song has stopped playing!")

async def skip_song(self, ctx):
if ctx.voice_client.is_playing():
ctx.voice_client.stop()
await self.send_message(ctx, "Song has been skipped!")

async def get_queue(self, ctx):
if self.music_queue.is_empty():
await self.send_message(ctx, "Queue is empty!")
return

embed = discord.Embed(
title="**Current Queue**",
description="List of songs in queue:",
color=discord.Colour.teal(),
)
length = len(self.music_queue.queue)

for i, song in enumerate(islice(self.music_queue.queue, 10)):
embed.add_field(name=f"Song {i+1}: ", value=song.title, inline=True)

if length > 10:
embed.set_footer(text=f"...and {length - 10} more songs.")

await ctx.send(embed=embed)

async def clear_queue(self, ctx):
self.music_queue.clear()
await self.send_message(ctx, "Queue has been cleared!")

async def play_song(self, ctx, url):
if not self.is_user_in_voice_channel(ctx):
await self.send_message(ctx, "You are not in a voice channel!")
return

channel = ctx.message.author.voice.channel

if not self.is_bot_in_voice_channel(ctx):
await channel.connect()

async with ctx.typing():
try:
player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True)
if not player:
self.send_message(ctx, "Failed to retrieve audio source.")
return
except Exception as e:
self.logger.error(f"Error when trying to play {url}: {e}")
return

embed = self.add_song_to_queue(ctx, player, url)
await ctx.channel.send(embed=embed)

def add_song_to_queue(self, ctx, player, url):
position = self.music_queue.add_to_queue(player)

if position == 1 and not ctx.voice_client.is_playing():
self.start_playback(ctx.voice_client, player)
embed_title = "**Current Song Playing!**"
embed_desc = f"Playing: {player.title}"
else:
embed_title = "**Song Added to Queue!**"
embed_desc = f"Added: {player.title}"

embed = discord.Embed(
title=embed_title, description=embed_desc, color=discord.Colour.teal()
)
embed.add_field(name="```User Input```", value=f"Input: {url}", inline=False)
if position > 1:
embed.add_field(
name="```Position in Queue```", value=f"Position: {position}", inline=True
)

return embed

def start_playback(self, voice_client, player):
if not player or not isinstance(player, discord.AudioSource):
self.logger.error("Invalid audio source.")
return

def after_playback(error):
if error:
self.logger.error(f"Error while playing: {error}")
else:
self.logger.info(f"Finished playing: {player.title}")
self.play_next(voice_client)

voice_client.play(player, after=after_playback)
voice_client.source.volume = 0.10
self.logger.info(f"Now playing: {player.title}")

def play_next(self, voice_client):
next_song = self.music_queue.remove_from_queue()

if not next_song:
self.logger.info("Queue is empty.")
if voice_client.is_playing():
voice_client.stop()
self.logger.info("Stopped playing.")
self.check_leave.restart()
return

self.start_playback(voice_client, next_song)
self.logger.info(f"Now playing: {next_song.title}")
160 changes: 15 additions & 145 deletions meowbot/bot/cogs/voice.py
Original file line number Diff line number Diff line change
@@ -1,182 +1,52 @@
import discord
from discord.ext import commands, tasks
from meowbot.application.models.ytdl_source import YTDLSource
from discord.ext import commands
from meowbot.utils.logger import logging, setup_logger
from meowbot.application.models.music_queue import MusicQueue
from meowbot.application.services.voice_service import VoiceService


class Voice(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.logger = setup_logger(name="cog.voice", level=logging.INFO)
self.check_leave.start()
self.music_queue = MusicQueue()
self.voice_service = VoiceService(bot)

@commands.hybrid_command(name="join", description="Joins a voice channel")
async def join(self, ctx):
try:
if ctx.message.author.voice is None:
await ctx.send("You are not in a voice channel!")
self.logger.error("User is not in a voice channel.")
return
channel = ctx.message.author.voice.channel
if ctx.voice_client is not None:
await ctx.voice_client.move_to(channel)
self.logger.info(f"Bot moved to {channel}.")

else:
self.check_leave.restart()
await channel.connect()
self.logger.info(f"Bot connected to {channel}.")

except AttributeError:
await ctx.send("Join a voice channel first! -.-")
self.logger.error("User did not join a voice channel before attempting to join.")

await ctx.send(f"Joined {channel}! ")
self.logger.info(f"Bot joined {channel}.")
await self.voice_service.join_voice_channel(ctx)

@commands.hybrid_command(name="leave", description="Leaves a voice channel")
async def leave(self, ctx):
if ctx.voice_client:
channel = ctx.voice_client.channel
self.music_queue.clear()
await ctx.voice_client.disconnect()
self.logger.info(f"Bot left {channel}.")
else:
await ctx.send("I'm not in a voice channel!")
self.logger.error("Bot is not in a voice channel.")

@tasks.loop(minutes=8)
async def check_leave(self):
voice_lists = self.bot.voice_clients
for x in voice_lists:
if x.is_connected() and not x.is_playing():
await x.disconnect()
self.logger.info(f"meowBot left {x.channel}.")
await self.voice_service.leave_voice_channel(ctx)

@commands.hybrid_command(name="pause", description="Pauses the current song")
async def pause(self, ctx):
if ctx.voice_client.is_playing():
ctx.voice_client.pause()
await ctx.send("Song has been paused!")
self.logger.info("Song has been paused.")
await self.voice_service.pause_song(ctx)

@commands.hybrid_command(name="resume", description="Resumes the current song")
async def resume(self, ctx):
if ctx.voice_client.is_paused():
ctx.voice_client.resume()
await ctx.send("Song has resumed playing!")
self.logger.info("Song has resumed playing.")
await self.voice_service.resume_song(ctx)

@commands.hybrid_command(name="stop", description="Stops the current song")
async def stop(self, ctx):
ctx.voice_client.stop()
await ctx.send("Song has stopped playing!")
self.logger.info("Song has stopped playing.")
await self.voice_service.stop_song(ctx)

@commands.hybrid_command(name="play", description="Plays a song from youtube given a URL")
async def play(self, ctx, *, url):
if ctx.message.author.voice is None:
await ctx.send("You are not in a voice channel!")
self.logger.error("User is not in a voice channel.")
return

channel = ctx.message.author.voice.channel

if not ctx.voice_client:
await channel.connect()

async with ctx.typing():
try:
player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True)
if not player:
await ctx.send("Failed to retrieve audio source.")
self.logger.error(f"Failed to retrieve audio source for {url}.")
return
except Exception as e:
self.logger.error(f"Error when trying to play {url}: {e}")
return

position = self.music_queue.add_to_queue(player)
if position == 1 and not ctx.voice_client.is_playing():
self.start_playback(ctx.voice_client, player)
else:
self.logger.info(f"Added {player.title} to queue at position {position}.")

embed = discord.Embed(
title="**Current Song Playing!**",
description=f"Playing: {player.title}",
color=discord.Colour.teal(),
)
embed.add_field(name="```User Input```", value=f"Input: {url}", inline=False)
await ctx.channel.send(embed=embed)
self.logger.info(f"Playing {player.title} in {channel}.")

def start_playback(self, voice_client, player):
if not player or not isinstance(player, discord.AudioSource):
self.logger.error("Invalid audio source.")
return

self.logger.info(f"Type of player: {type(player)}")

def after_playback(error):
self.logger.info(f"After playback triggered. Error: {error} (Type: {type(error)})")

if error:
self.logger.error(f"Error while playing: {error}")
else:
self.logger.info(f"Finished playing: {player.title}")
self.play_next(voice_client)

voice_client.play(player, after=after_playback)
voice_client.source.volume = 0.10
self.logger.info(f"Now playing: {player.title}")

def play_next(self, voice_client):
if not self.music_queue.is_empty():
next_song = self.music_queue.remove_from_queue()
if next_song:
self.start_playback(voice_client, next_song)
self.logger.info(f"Now playing: {next_song.title}")
else:
self.logger.error("Failed to retrieve next song.")
else:
self.logger.info("Queue is empty.")
if voice_client.is_playing():
voice_client.stop()
self.logger.info("Stopped playing.")
self.check_leave.restart()
await self.voice_service.play_song(ctx, url)

@commands.hybrid_command(name="queue", description="Displays the current queue")
async def queue(self, ctx):
queue = self.music_queue.queue
if not queue:
await ctx.send("Queue is empty!")
return

embed = discord.Embed(
title="**Current Queue**",
description="List of songs in queue",
color=discord.Colour.teal(),
)
for i, song in enumerate(queue):
embed.add_field(name=f"Song {i+1}", value=song.title, inline=False)
await ctx.send(embed=embed)
self.logger.info("Queue displayed.")
await self.voice_service.get_queue(ctx)

@commands.hybrid_command(name="clear", description="Clears the current queue")
async def clear(self, ctx):
self.music_queue.clear()
await ctx.send("Queue has been cleared!")
self.logger.info("Queue has been cleared.")
await self.voice_service.clear_queue(ctx)

@commands.hybrid_command(name="skip", description="Skips the current song")
async def skip(self, ctx):
if ctx.voice_client.is_playing():
ctx.voice_client.stop()
await ctx.send("Song has been skipped!")
self.logger.info("Song has been skipped.")
await self.voice_service.skip_song(ctx)

def cog_unload(self):
self.logger.info("Voice cog unloaded.")


async def setup(bot):
Expand Down
Loading

0 comments on commit 3fbaac5

Please sign in to comment.