diff --git a/Config/Configs.py b/Config/Configs.py index 363e5e1..682d251 100644 --- a/Config/Configs.py +++ b/Config/Configs.py @@ -11,7 +11,11 @@ class VConfigs(Singleton): self.SHOULD_AUTO_DISCONNECT_WHEN_ALONE = False # Recommended to be True, except in cases when your Bot is present in thousands servers, in that case # the delay to start a new Python process for the playback is too much, and to avoid that you set as False - self.SONG_PLAYBACK_IN_SEPARATE_PROCESS = False + # This feature is for now in testing period, for a more stable version, keep this boolean = True + self.SONG_PLAYBACK_IN_SEPARATE_PROCESS = True + # Maximum of songs that will be downloaded at once, the higher this number is, the faster the songs will be all available + # but the slower will be the others commands of the Bot during the downloading time, for example, the playback quality + self.MAX_DOWNLOAD_SONGS_AT_A_TIME = 5 self.BOT_PREFIX = '!' try: diff --git a/Handlers/PlayHandler.py b/Handlers/PlayHandler.py index 2aae0d3..1ee3d77 100644 --- a/Handlers/PlayHandler.py +++ b/Handlers/PlayHandler.py @@ -79,14 +79,8 @@ class PlayHandler(AbstractHandler): return response else: # If multiple songs added - # If more than 10 songs, download and load the first 5 to start the play right away - if len(songs) > 10: - fiveFirstSongs = songs[0:5] - songs = songs[5:] - await self.__downloadSongsAndStore(fiveFirstSongs, playersManager) - # Trigger a task to download all songs and then store them in the playlist - asyncio.create_task(self.__downloadSongsAndStore(songs, playersManager)) + asyncio.create_task(self.__downloadSongsInLots(songs, playersManager)) embed = self.embeds.SONGS_ADDED(len(songs)) return HandlerResponse(self.ctx, embed) @@ -95,7 +89,7 @@ class PlayHandler(AbstractHandler): embed = self.embeds.DOWNLOADING_ERROR() return HandlerResponse(self.ctx, embed, error) except Exception as error: - print(f'ERROR IN PLAYHANDLER -> {traceback.format_exc()}', {type(error)}) + print(f'[ERROR IN PLAYHANDLER] -> {traceback.format_exc()}', {type(error)}) if isinstance(error, VulkanError): embed = self.embeds.CUSTOM_ERROR(error) else: @@ -104,33 +98,40 @@ class PlayHandler(AbstractHandler): return HandlerResponse(self.ctx, embed, error) - async def __downloadSongsAndStore(self, songs: List[Song], playersManager: AbstractPlayersManager) -> None: + async def __downloadSongsInLots(self, songs: List[Song], playersManager: AbstractPlayersManager) -> None: + """ + To avoid having a lot of tasks delaying the song playback we will lock the maximum songs downloading at a time + """ playlist = playersManager.getPlayerPlaylist(self.guild) playCommand = VCommands(VCommandsType.PLAY, None) - tooManySongs = len(songs) > 100 + maxDownloads = self.config.MAX_DOWNLOAD_SONGS_AT_A_TIME - # Trigger a task for each song to be downloaded - tasks: List[asyncio.Task] = [] - for index, song in enumerate(songs): - # If there is a lot of songs being downloaded, force a sleep to try resolve the Http Error 429 "To Many Requests" - # Trying to fix the issue https://github.com/RafaelSolVargas/Vulkan/issues/32 - if tooManySongs and index % 3 == 0: - await asyncio.sleep(0.5) - task = asyncio.create_task(self.__down.download_song(song)) - tasks.append(task) + while len(songs) > 0: + # Verify how many songs will be downloaded in this lot and extract from the songs list + songsQuant = min(maxDownloads, len(songs)) + # Get the first quantInLot songs + songsInLot = songs[:songsQuant] + # Remove the first quantInLot songs from the songs + songs = songs[songsQuant:] - for index, task in enumerate(tasks): - await task - song = songs[index] - if not song.problematic: # If downloaded add to the playlist and send play command - playerLock = playersManager.getPlayerLock(self.guild) - acquired = playerLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) - if acquired: - playlist.add_song(song) - await playersManager.sendCommandToPlayer(playCommand, self.guild) - playerLock.release() - else: - playersManager.resetPlayer(self.guild, self.ctx) + # Create task to download the songs in the lot + tasks: List[asyncio.Task] = [] + for index, song in enumerate(songsInLot): + task = asyncio.create_task(self.__down.download_song(song)) + tasks.append(task) + + for index, task, in enumerate(tasks): + await task + song = songsInLot[index] + if not song.problematic: # If downloaded add to the playlist and send play command + playerLock = playersManager.getPlayerLock(self.guild) + acquired = playerLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) + if acquired: + playlist.add_song(song) + await playersManager.sendCommandToPlayer(playCommand, self.guild) + playerLock.release() + else: + playersManager.resetPlayer(self.guild, self.ctx) def __isUserConnected(self) -> bool: if self.ctx.author.voice: diff --git a/Parallelism/ProcessExecutor.py b/Parallelism/ProcessExecutor.py index 8d8f007..4979bdd 100644 --- a/Parallelism/ProcessExecutor.py +++ b/Parallelism/ProcessExecutor.py @@ -1,5 +1,5 @@ from typing import List -from discord import Button, TextChannel +from discord import Button, Guild, TextChannel from discord.ui import View from Config.Emojis import VEmojis from Messages.MessagesCategory import MessagesCategory @@ -21,6 +21,11 @@ from Handlers.QueueHandler import QueueHandler class ProcessCommandsExecutor: + MESSAGES = Messages() + EMBEDS = VEmbeds() + EMOJIS = VEmojis() + MSG_MANAGER = MessagesManager() + def __init__(self, bot: VulkanBot, guildID: int) -> None: self.__bot = bot self.__guildID = guildID @@ -29,6 +34,56 @@ class ProcessCommandsExecutor: self.__embeds = VEmbeds() self.__emojis = VEmojis() + @classmethod + async def sendNowPlayingToGuild(cls, bot: VulkanBot, playlist: Playlist, channel: TextChannel, song: Song, guild: Guild) -> None: + # Get the lock of the playlist + if playlist.isLoopingOne(): + title = cls.MESSAGES.ONE_SONG_LOOPING + else: + title = cls.MESSAGES.SONG_PLAYING + + # Create View and Embed + embed = cls.EMBEDS.SONG_INFO(song.info, title) + view = cls.__getPlayerViewForGuild(channel, guild.id, bot) + # Send Message and add to the MessagesManager + message = await channel.send(embed=embed, view=view) + await cls.MSG_MANAGER.addMessageAndClearPrevious(guild.id, MessagesCategory.NOW_PLAYING, message, view) + + # Set in the view the message witch contains the view + view.set_message(message=message) + + @classmethod + def __getPlayerViewForGuild(cls, channel: TextChannel, guildID: int, bot: VulkanBot) -> View: + buttons = cls.__getPlayerButtonsForGuild(channel, guildID, bot) + view = BasicView(bot, buttons) + return view + + @classmethod + def __getPlayerButtonsForGuild(cls, textChannel: TextChannel, guildID: int, bot: VulkanBot) -> List[Button]: + """Create the Buttons to be inserted in the Player View""" + buttons: List[Button] = [] + + buttons.append(HandlerButton(bot, PrevHandler, cls.EMOJIS.BACK, + textChannel, guildID, MessagesCategory.PLAYER, "Back")) + buttons.append(HandlerButton(bot, PauseHandler, cls.EMOJIS.PAUSE, + textChannel, guildID, MessagesCategory.PLAYER, "Pause")) + buttons.append(HandlerButton(bot, ResumeHandler, cls.EMOJIS.PLAY, + textChannel, guildID, MessagesCategory.PLAYER, "Play")) + buttons.append(HandlerButton(bot, StopHandler, cls.EMOJIS.STOP, + textChannel, guildID, MessagesCategory.PLAYER, "Stop")) + buttons.append(HandlerButton(bot, SkipHandler, cls.EMOJIS.SKIP, + textChannel, guildID, MessagesCategory.PLAYER, "Skip")) + buttons.append(HandlerButton(bot, QueueHandler, cls.EMOJIS.QUEUE, + textChannel, guildID, MessagesCategory.QUEUE, "Songs")) + buttons.append(HandlerButton(bot, LoopHandler, cls.EMOJIS.LOOP_ONE, + textChannel, guildID, MessagesCategory.LOOP, "Loop One", 'One')) + buttons.append(HandlerButton(bot, LoopHandler, cls.EMOJIS.LOOP_OFF, + textChannel, guildID, MessagesCategory.LOOP, "Loop Off", 'Off')) + buttons.append(HandlerButton(bot, LoopHandler, cls.EMOJIS.LOOP_ALL, + textChannel, guildID, MessagesCategory.LOOP, "Loop All", 'All')) + + return buttons + async def sendNowPlaying(self, playlist: Playlist, channel: TextChannel, song: Song) -> None: # Get the lock of the playlist if playlist.isLoopingOne(): diff --git a/Parallelism/ThreadPlayer.py b/Parallelism/ThreadPlayer.py index 3111def..3358f9b 100644 --- a/Parallelism/ThreadPlayer.py +++ b/Parallelism/ThreadPlayer.py @@ -5,7 +5,7 @@ from discord import VoiceClient from asyncio import AbstractEventLoop from threading import RLock, Thread from multiprocessing import Lock -from typing import Callable, Coroutine +from typing import Callable from discord import Guild, FFmpegPCMAudio, VoiceChannel from Music.Playlist import Playlist from Music.Song import Song @@ -33,10 +33,11 @@ class ThreadPlayer(Thread): def __init__(self, bot: VulkanBot, guild: Guild, name: str, voiceChannel: VoiceChannel, playlist: Playlist, lock: Lock, guildID: int, voiceID: int, callbackToSendCommand: Callable, exitCB: Callable) -> None: Thread.__init__(self, name=name, group=None, target=None, args=(), kwargs={}) + print(f'Starting Player Thread for Guild {self.name}') # Synchronization objects self.__playlist: Playlist = playlist self.__playlistLock: Lock = lock - self.__loop: AbstractEventLoop = None + self.__loop: AbstractEventLoop = bot.loop self.__playerLock: RLock = RLock() # Discord context ID self.__voiceChannelID = voiceID @@ -48,30 +49,13 @@ class ThreadPlayer(Thread): self.__callback = callbackToSendCommand self.__exitCB = exitCB self.__bot = bot + self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) self.__playing = False self.__forceStop = False self.FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', 'options': '-vn'} - def run(self) -> None: - """This method is called automatically when the Thread starts""" - try: - print(f'Starting Player Thread for Guild {self.name}') - self.__loop = self.__bot.loop - self.__loop.run_until_complete(self._run()) - - except Exception as e: - print(f'[Error in Process {self.name}] -> {e}') - - async def _run(self) -> None: - # Connect to voice Channel - await self.__connectToVoiceChannel() - # Start the timeout function - self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) - # Start a Task to play songs - self.__loop.create_task(self.__playPlaylistSongs()) - def __verifyIfIsPlaying(self) -> bool: if self.__voiceClient is None: return False @@ -89,8 +73,7 @@ class ThreadPlayer(Thread): song = self.__playlist.next_song() if song is not None: - print('Criando song') - self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') + await self.__playSong(song) self.__playing = True async def __playSong(self, song: Song) -> None: @@ -132,7 +115,7 @@ class ThreadPlayer(Thread): self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) nowPlayingCommand = VCommands(VCommandsType.NOW_PLAYING, song) - await self.__callback(nowPlayingCommand, self.__guild.id, song) + await self.__callback(nowPlayingCommand, self.__guild, song) except Exception as e: print(f'[ERROR IN PLAY SONG FUNCTION] -> {e}, {type(e)}') self.__playNext(None) @@ -157,7 +140,8 @@ class ThreadPlayer(Thread): self.__songPlaying = None self.__playing = False # Send a command to the main process to kill this thread - self.__exitCB(self.__guild.id) + + self.__exitCB(self.__guild) def __verifyIfSongAvailable(self, song: Song) -> bool: """Verify the song source to see if it's already expired""" @@ -214,12 +198,12 @@ class ThreadPlayer(Thread): self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') async def receiveCommand(self, command: VCommands) -> None: - type = command.getType() - args = command.getArgs() - print(f'Player Thread {self.__guild.name} received command {type}') - try: self.__playerLock.acquire() + type = command.getType() + args = command.getArgs() + # print(f'Player Thread {self.__guild.name} received command {type}') + if type == VCommandsType.PAUSE: self.__pause() elif type == VCommandsType.RESUME: diff --git a/Parallelism/ThreadPlayerManager.py b/Parallelism/ThreadPlayerManager.py index 2e8a13c..f90e145 100644 --- a/Parallelism/ThreadPlayerManager.py +++ b/Parallelism/ThreadPlayerManager.py @@ -1,4 +1,4 @@ -from multiprocessing import Lock +from threading import RLock from typing import Any, Dict, Union from Config.Singleton import Singleton from discord import Guild, Interaction, TextChannel @@ -8,6 +8,7 @@ from Music.Song import Song from Music.Playlist import Playlist from Parallelism.Commands import VCommands, VCommandsType from Music.VulkanBot import VulkanBot +from Parallelism.ProcessExecutor import ProcessCommandsExecutor from Parallelism.ThreadPlayer import ThreadPlayer @@ -16,7 +17,7 @@ class ThreadPlayerInfo: Class to store the reference to all structures to maintain a player thread """ - def __init__(self, thread: ThreadPlayer, playlist: Playlist, lock: Lock, textChannel: TextChannel) -> None: + def __init__(self, thread: ThreadPlayer, playlist: Playlist, lock: RLock, textChannel: TextChannel) -> None: self.__thread = thread self.__playlist = playlist self.__lock = lock @@ -28,7 +29,7 @@ class ThreadPlayerInfo: def getPlaylist(self) -> Playlist: return self.__playlist - def getLock(self) -> Lock: + def getLock(self) -> RLock: return self.__lock def getTextChannel(self) -> TextChannel: @@ -55,20 +56,20 @@ class ThreadPlayerManager(Singleton, AbstractPlayersManager): await player.receiveCommand(command) - async def __receiveCommand(self, command: VCommands, guildID: int, args: Any) -> None: + async def __receiveCommand(self, command: VCommands, guild: Guild, args: Any) -> None: commandType = command.getType() if commandType == VCommandsType.NOW_PLAYING: - await self.showNowPlaying(guildID, args) + await self.showNowPlaying(guild, args) else: print( - f'[ERROR] -> Command not processable received from Thread {guildID}: {commandType}') + f'[ERROR] -> Command not processable received from Thread {guild.name}: {commandType}') def getPlayerPlaylist(self, guild: Guild) -> Playlist: playerInfo = self.__getRunningPlayerInfo(guild) if playerInfo: return playerInfo.getPlaylist() - def getPlayerLock(self, guild: Guild) -> Lock: + def getPlayerLock(self, guild: Guild) -> RLock: playerInfo = self.__getRunningPlayerInfo(guild) if playerInfo: return playerInfo.getLock() @@ -118,7 +119,7 @@ class ThreadPlayerManager(Singleton, AbstractPlayersManager): voiceChannel = self.__bot.get_channel(voiceID) playlist = Playlist() - lock = Lock() + lock = RLock() player = ThreadPlayer(self.__bot, context.guild, context.guild.name, voiceChannel, playlist, lock, guildID, voiceID, self.__receiveCommand, self.__deleteThread) playerInfo = ThreadPlayerInfo(player, playlist, lock, context.channel) @@ -126,13 +127,14 @@ class ThreadPlayerManager(Singleton, AbstractPlayersManager): return playerInfo - def __deleteThread(self, guildID: int) -> None: + def __deleteThread(self, guild: Guild) -> None: """Tries to delete the thread and removes all the references to it""" - playerInfo = self.__playersThreads[guildID] + print(f'[THREAD MANAGER] -> Deleting Thread for guild {guild.name}') + playerInfo = self.__playersThreads[guild.id] if playerInfo: thread = playerInfo.getPlayer() + self.__playersThreads.pop(guild.id) del thread - self.__playersThreads.popitem(thread) def __recreateThread(self, guild: Guild, context: Union[Context, Interaction]) -> ThreadPlayerInfo: self.__stopPossiblyRunningProcess(guild) @@ -145,7 +147,7 @@ class ThreadPlayerManager(Singleton, AbstractPlayersManager): voiceChannel = self.__bot.get_channel(voiceID) playlist = self.__playersThreads[guildID].getPlaylist() - lock = Lock() + lock = RLock() player = ThreadPlayer(self.__bot, context.guild, context.guild.name, voiceChannel, playlist, lock, guildID, voiceID, self.__receiveCommand, self.__deleteThread) playerInfo = ThreadPlayerInfo(player, playlist, lock, context.channel) @@ -153,7 +155,9 @@ class ThreadPlayerManager(Singleton, AbstractPlayersManager): return playerInfo - async def showNowPlaying(self, guildID: int, song: Song) -> None: - commandExecutor = self.__playersCommandsExecutor[guildID] - processInfo = self.__playersThreads[guildID] - await commandExecutor.sendNowPlaying(processInfo, song) + async def showNowPlaying(self, guild: Guild, song: Song) -> None: + processInfo = self.__playersThreads[guild.id] + playlist = processInfo.getPlaylist() + txtChannel = processInfo.getTextChannel() + + await ProcessCommandsExecutor.sendNowPlayingToGuild(self.__bot, playlist, txtChannel, song, guild)