diff --git a/Config/Embeds.py b/Config/Embeds.py index 6e298a6..113488a 100644 --- a/Config/Embeds.py +++ b/Config/Embeds.py @@ -343,6 +343,13 @@ class VEmbeds: description=self.__messages.PLAYER_NOT_PLAYING, colour=self.__colors.BLUE) return embed + + def VOLUME_CHANGED(self, volume: float) -> Embed: + embed = Embed( + title=self.__messages.SONG_PLAYER, + description=self.__messages.VOLUME_CHANGED.format(volume), + colour=self.__colors.BLUE) + return embed def QUEUE(self, title: str, description: str) -> Embed: embed = Embed( diff --git a/Config/Helper.py b/Config/Helper.py index 22c611c..40e7a5e 100644 --- a/Config/Helper.py +++ b/Config/Helper.py @@ -30,6 +30,8 @@ class Helper(Singleton): self.HELP_SHUFFLE = 'Shuffle the songs playing.' self.HELP_SHUFFLE_LONG = 'Randomly shuffle the songs in the queue.\n\nArguments: None.' self.HELP_PLAY = 'Plays a song from URL' + self.CHANGE_VOLUME = '**[Pre-release]** - Set the volume of the song' + self.CHANGE_VOLUME_LONG = '**[Pre-release]** - Change the volume of the song, expect a number from 0 to 100' self.HELP_PLAY_LONG = 'Play a song in discord. \n\nRequire: You to be connected to a voice channel.\nArguments: Youtube, Spotify or Deezer song/playlist link or the title of the song to be searched in Youtube.' self.HELP_HISTORY = f'Show the history of played songs.' self.HELP_HISTORY_LONG = f'Show the last {config.MAX_SONGS_HISTORY} played songs' diff --git a/Config/Messages.py b/Config/Messages.py index 00f5ea0..58a8a3b 100644 --- a/Config/Messages.py +++ b/Config/Messages.py @@ -16,6 +16,7 @@ class Messages(Singleton): self.SONGINFO_REQUESTER = 'Requester: ' self.SONGINFO_POSITION = 'Position: ' + self.VOLUME_CHANGED = '**[Pre-release]** - Song volume changed to `{}`%' self.SONGS_ADDED = 'Downloading `{}` songs to add to the queue' self.SONG_ADDED = 'Downloading the song `{}` to add to the queue' self.SONG_ADDED_TWO = f'{self.__emojis.MUSIC} Song added to the queue' @@ -56,6 +57,7 @@ class Messages(Singleton): self.ERROR_MOVING = f'{self.__emojis.ERROR} Error while moving the songs' self.LENGTH_ERROR = f'{self.__emojis.ERROR} Numbers must be between 1 and queue length, use -1 for the last song' self.ERROR_NUMBER = f'{self.__emojis.ERROR} This command require a number' + self.ERROR_VOLUME_NUMBER = f'{self.__emojis.ERROR} This command require a number between 0 and 100' self.ERROR_PLAYING = f'{self.__emojis.ERROR} Error while playing songs' self.COMMAND_NOT_FOUND = f'{self.__emojis.ERROR} Command not found, type {configs.BOT_PREFIX}help to see all commands' self.UNKNOWN_ERROR = f'{self.__emojis.ERROR} Unknown Error, if needed, use {configs.BOT_PREFIX}reset to reset the player of your server' diff --git a/DiscordCogs/ControlCog.py b/DiscordCogs/ControlCog.py index 70a98ca..286f183 100644 --- a/DiscordCogs/ControlCog.py +++ b/DiscordCogs/ControlCog.py @@ -21,7 +21,7 @@ class ControlCog(Cog): 'MUSIC': ['resume', 'pause', 'loop', 'stop', 'skip', 'play', 'queue', 'clear', 'np', 'shuffle', 'move', 'remove', - 'reset', 'prev', 'history'], + 'reset', 'prev', 'history', 'volume'], 'RANDOM': ['choose', 'cara', 'random'] } diff --git a/DiscordCogs/MusicCog.py b/DiscordCogs/MusicCog.py index 5d6709b..8802f74 100644 --- a/DiscordCogs/MusicCog.py +++ b/DiscordCogs/MusicCog.py @@ -17,6 +17,7 @@ from Handlers.ResumeHandler import ResumeHandler from Handlers.HistoryHandler import HistoryHandler from Handlers.QueueHandler import QueueHandler from Handlers.LoopHandler import LoopHandler +from Handlers.VolumeHandler import VolumeHandler from Messages.MessagesCategory import MessagesCategory from Messages.Responses.EmoteCogResponse import EmoteCommandResponse from Messages.Responses.EmbedCogResponse import EmbedCommandResponse @@ -64,6 +65,25 @@ class MusicCog(Cog): except Exception as e: print(f'[ERROR IN COG] -> {e}') + @command(name="volume", help=helper.CHANGE_VOLUME, description=helper.CHANGE_VOLUME_LONG, aliases=['v']) + async def volume(self, ctx: Context, *args) -> None: + try: + controller = VolumeHandler(ctx, self.__bot) + + if len(args) > 1: + track = " ".join(args) + else: + track = args[0] + + response = await controller.run(track) + if response is not None: + cogResponser1 = EmbedCommandResponse(response, MessagesCategory.PLAYER) + cogResponser2 = EmoteCommandResponse(response, MessagesCategory.PLAYER) + await cogResponser1.run() + await cogResponser2.run() + except Exception as e: + print(f'[ERROR IN COG] -> {e}') + @command(name="queue", help=helper.HELP_QUEUE, description=helper.HELP_QUEUE_LONG, aliases=['q', 'fila', 'musicas']) async def queue(self, ctx: Context, *args) -> None: try: diff --git a/DiscordCogs/SlashCog.py b/DiscordCogs/SlashCog.py index 5ee2a19..8822d58 100644 --- a/DiscordCogs/SlashCog.py +++ b/DiscordCogs/SlashCog.py @@ -15,6 +15,7 @@ from Handlers.ResumeHandler import ResumeHandler from Handlers.HistoryHandler import HistoryHandler from Handlers.QueueHandler import QueueHandler from Handlers.LoopHandler import LoopHandler +from Handlers.VolumeHandler import VolumeHandler from Messages.MessagesCategory import MessagesCategory from Messages.Responses.SlashEmbedResponse import SlashEmbedResponse from Music.VulkanBot import VulkanBot @@ -237,6 +238,22 @@ class SlashCommands(Cog): except Exception: print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}') + @slash_command(name='volume', description=helper.CHANGE_VOLUME_LONG) + async def move(self, ctx: ApplicationContext, + volume: Option(float, "The new volume of the song", min_value=1, default= 100)) -> None: + if not self.__bot.listingSlash: + return + try: + await ctx.defer() + + controller = VolumeHandler(ctx, self.__bot) + + response = await controller.run(f'{volume}') + cogResponser = SlashEmbedResponse(response, ctx, MessagesCategory.PLAYER) + await cogResponser.run() + except Exception: + print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}') + @slash_command(name='remove', description=helper.HELP_REMOVE) async def remove(self, ctx: ApplicationContext, position: Option(int, "The song position to remove", min_value=1)) -> None: diff --git a/Handlers/VolumeHandler.py b/Handlers/VolumeHandler.py new file mode 100644 index 0000000..6059010 --- /dev/null +++ b/Handlers/VolumeHandler.py @@ -0,0 +1,62 @@ +from Config.Exceptions import BadCommandUsage, NumberRequired, VulkanError +from Parallelism.AbstractProcessManager import AbstractPlayersManager +from Parallelism.Commands import VCommands, VCommandsType +from Handlers.AbstractHandler import AbstractHandler +from Handlers.HandlerResponse import HandlerResponse +from discord.ext.commands import Context +from Music.VulkanBot import VulkanBot +from discord import Interaction +from typing import Union + + +class VolumeHandler(AbstractHandler): + def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: + super().__init__(ctx, bot) + + async def run(self, args: str) -> HandlerResponse: + if args is None or args.strip() == '': + error = BadCommandUsage() + return HandlerResponse(self.ctx, embed, error) + + error = self.__validateInput(args) + if error: + embed = self.embeds.ERROR_EMBED(error.message) + return HandlerResponse(self.ctx, embed, error) + + playersManager: AbstractPlayersManager = self.config.getPlayersManager() + if not playersManager.verifyIfPlayerExists(self.guild): + embed = self.embeds.NOT_PLAYING() + error = BadCommandUsage() + return HandlerResponse(self.ctx, embed, error) + + playerLock = playersManager.getPlayerLock(self.guild) + acquired = playerLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) + volume = self.__convert_input_to_volume(args) + if acquired: + volumeCommand = VCommands(VCommandsType.VOLUME, volume) + await playersManager.sendCommandToPlayer(volumeCommand, self.guild, self.ctx) + + playerLock.release() + + embed = self.embeds.VOLUME_CHANGED(volume) + return HandlerResponse(self.ctx, embed) + else: + playersManager.resetPlayer(self.guild, self.ctx) + + embed = self.embeds.PLAYER_RESTARTED() + return HandlerResponse(self.ctx, embed) + + def __convert_input_to_volume(self, input_volume: str) -> float: + volume = float(input_volume) + if volume < 0: + volume = 0 + if volume > 100: + volume = 100 + + return volume + + def __validateInput(self, volume: str) -> Union[VulkanError, None]: + try: + _ = float(volume) + except: + return NumberRequired(self.messages.ERROR_VOLUME_NUMBER) \ No newline at end of file diff --git a/Parallelism/Commands.py b/Parallelism/Commands.py index 9a18a09..d80703c 100644 --- a/Parallelism/Commands.py +++ b/Parallelism/Commands.py @@ -13,6 +13,7 @@ class VCommandsType(Enum): RESET = 'Reset' NOW_PLAYING = 'Now Playing' TERMINATE = 'Terminate' + VOLUME = 'Volume' SLEEPING = 'Sleeping' diff --git a/Parallelism/ProcessPlayer.py b/Parallelism/ProcessPlayer.py index 0f83728..b17b4fe 100644 --- a/Parallelism/ProcessPlayer.py +++ b/Parallelism/ProcessPlayer.py @@ -2,7 +2,7 @@ import asyncio from time import sleep, time from urllib.parse import parse_qs, urlparse from Music.VulkanInitializer import VulkanInitializer -from discord import VoiceClient +from discord import PCMVolumeTransformer, VoiceClient from asyncio import AbstractEventLoop, Semaphore, Queue from multiprocessing import Process, RLock, Lock, Queue from threading import Thread @@ -54,6 +54,7 @@ class ProcessPlayer(Process): self.__voiceChannel: VoiceChannel = None self.__voiceClient: VoiceClient = None + self.__currentSongChangeVolume = False self.__playing = False self.__forceStop = False self.__botCompletedLoad = False @@ -98,6 +99,31 @@ class ProcessPlayer(Process): # In this point the process should finalize self.__timer.cancel() + def __set_volume(self, volume: float) -> None: + """Set the volume of the player, must be values between 0 and 100""" + try: + if self.__voiceClient is None: + return + + if not isinstance(volume, float): + print('[PROCESS ERROR] -> Volume instance must be float') + return + + if volume < 0: + volume = 0 + if volume > 100: + volume = 100 + + volume = volume / 100 + + if not self.__currentSongChangeVolume: + print('[PROCESS ERROR] -> Cannot change the volume of this song') + return + + self.__voiceClient.source.volume = volume + except Exception as e: + print(e) + def __verifyIfIsPlaying(self) -> bool: if self.__voiceClient is None: return False @@ -151,6 +177,10 @@ class ProcessPlayer(Process): self.__songPlaying = song player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS) + if not player.is_opus(): + player = PCMVolumeTransformer(player, 1) + self.__currentSongChangeVolume = True + self.__voiceClient.play(player, after=lambda e: self.__playNext(e)) self.__timer.cancel() @@ -169,6 +199,8 @@ class ProcessPlayer(Process): print(f'[PROCESS PLAYER -> ERROR PLAYING SONG] -> {error}') with self.__playlistLock: with self.__playerLock: + self.__currentSongChangeVolume = False + if self.__forceStop: # If it's forced to stop player self.__forceStop = False return None @@ -271,6 +303,8 @@ class ProcessPlayer(Process): asyncio.run_coroutine_threadsafe(self.__reset(), self.__loop) elif type == VCommandsType.STOP: asyncio.run_coroutine_threadsafe(self.__stop(), self.__loop) + elif type == VCommandsType.VOLUME: + self.__set_volume(args) else: print(f'[PROCESS PLAYER ERROR] -> Unknown Command Received: {command}') except Exception as e: diff --git a/Parallelism/ThreadPlayer.py b/Parallelism/ThreadPlayer.py index 0ae2367..44b2620 100644 --- a/Parallelism/ThreadPlayer.py +++ b/Parallelism/ThreadPlayer.py @@ -1,7 +1,7 @@ import asyncio from time import time from urllib.parse import parse_qs, urlparse -from discord import VoiceClient +from discord import PCMVolumeTransformer, VoiceClient from asyncio import AbstractEventLoop from threading import RLock, Thread from multiprocessing import Lock @@ -45,6 +45,7 @@ class ThreadPlayer(Thread): self.__voiceChannel: VoiceChannel = voiceChannel self.__voiceClient: VoiceClient = None + self.__currentSongChangeVolume = False self.__downloader = Downloader() self.__callback = callbackToSendCommand self.__exitCB = exitCB @@ -56,6 +57,31 @@ class ThreadPlayer(Thread): self.FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', 'options': '-vn'} + def __set_volume(self, volume: float) -> None: + """Set the volume of the player, must be values between 0 and 100""" + try: + if self.__voiceClient is None: + return + + if not isinstance(volume, float): + print('[THREAD ERROR] -> Volume instance must be float') + return + + if volume < 0: + volume = 0 + if volume > 100: + volume = 100 + + volume = volume / 100 + + if not self.__currentSongChangeVolume: + print('[THREAD ERROR] -> Cannot change the volume of this song') + return + + self.__voiceClient.source.volume = volume + except Exception as e: + print(e) + def __verifyIfIsPlaying(self) -> bool: if self.__voiceClient is None: return False @@ -109,6 +135,9 @@ class ThreadPlayer(Thread): self.__songPlaying = song player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS) + if not player.is_opus(): + player = PCMVolumeTransformer(player, 1) + self.__currentSongChangeVolume = True self.__voiceClient.play(player, after=lambda e: self.__playNext(e)) self.__timer.cancel() @@ -127,6 +156,7 @@ class ThreadPlayer(Thread): print(f'[THREAD PLAYER -> ERROR PLAYING SONG] -> {error}') with self.__playlistLock: with self.__playerLock: + self.__currentSongChangeVolume = False if self.__forceStop: # If it's forced to stop player self.__forceStop = False return None @@ -217,6 +247,8 @@ class ThreadPlayer(Thread): await self.__reset() elif type == VCommandsType.STOP: await self.__stop() + elif type == VCommandsType.VOLUME: + self.__set_volume(args) else: print(f'[THREAD PLAYER ERROR] -> Unknown Command Received: {command}') except Exception as e: