From 7a5d76ffd343e64f412e345104c406791e71be95 Mon Sep 17 00:00:00 2001 From: Rafael Vargas Date: Sun, 19 Feb 2023 00:33:31 -0300 Subject: [PATCH] Refactoring some code and modifying variables and class names --- Config/Configs.py | 8 +- DiscordCogs/MusicCog.py | 2 +- Handlers/ClearHandler.py | 7 +- Handlers/HistoryHandler.py | 3 +- Handlers/JumpMusicHandler.py | 3 +- Handlers/LoopHandler.py | 4 +- Handlers/MoveHandler.py | 3 +- Handlers/NowPlayingHandler.py | 3 +- Handlers/PauseHandler.py | 7 +- Handlers/PlayHandler.py | 9 +- Handlers/PrevHandler.py | 3 +- Handlers/QueueHandler.py | 3 +- Handlers/RemoveHandler.py | 7 +- Handlers/ResetHandler.py | 7 +- Handlers/ResumeHandler.py | 7 +- Handlers/ShuffleHandler.py | 4 +- Handlers/SkipHandler.py | 7 +- Handlers/StopHandler.py | 7 +- Parallelism/AbstractProcessManager.py | 31 +++ Parallelism/PlayerProcess.py | 16 +- Parallelism/PlayerThread.py | 365 ++++++++++++++++++++++++++ Parallelism/ProcessExecutor.py | 4 +- Parallelism/ProcessInfo.py | 4 +- Parallelism/ProcessManager.py | 27 +- Parallelism/ThreadManager.py | 199 ++++++++++++++ 25 files changed, 672 insertions(+), 68 deletions(-) create mode 100644 Parallelism/AbstractProcessManager.py create mode 100644 Parallelism/PlayerThread.py create mode 100644 Parallelism/ThreadManager.py diff --git a/Config/Configs.py b/Config/Configs.py index d9da122..363e5e1 100644 --- a/Config/Configs.py +++ b/Config/Configs.py @@ -1,4 +1,3 @@ -import os from decouple import config from Config.Singleton import Singleton from Config.Folder import Folder @@ -10,6 +9,9 @@ class VConfigs(Singleton): # You can change this boolean to False if you want to prevent the Bot from auto disconnecting # Resolution for the issue: https://github.com/RafaelSolVargas/Vulkan/issues/33 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 self.BOT_PREFIX = '!' try: @@ -44,8 +46,8 @@ class VConfigs(Singleton): self.MY_ERROR_BAD_COMMAND = 'This string serves to verify if some error was raised by myself on purpose' self.INVITE_URL = 'https://discordapp.com/oauth2/authorize?client_id={}&scope=bot' - def getProcessManager(self): + def getPlayersManager(self): return self.__manager - def setProcessManager(self, newManager): + def setPlayersManager(self, newManager): self.__manager = newManager diff --git a/DiscordCogs/MusicCog.py b/DiscordCogs/MusicCog.py index 153a844..c84293e 100644 --- a/DiscordCogs/MusicCog.py +++ b/DiscordCogs/MusicCog.py @@ -38,7 +38,7 @@ class MusicCog(Cog): def __init__(self, bot: VulkanBot) -> None: self.__bot: VulkanBot = bot self.__embeds = VEmbeds() - VConfigs().setProcessManager(ProcessManager(bot)) + VConfigs().setPlayersManager(ProcessManager(bot)) @command(name="play", help=helper.HELP_PLAY, description=helper.HELP_PLAY_LONG, aliases=['p', 'tocar']) async def play(self, ctx: Context, *args) -> None: diff --git a/Handlers/ClearHandler.py b/Handlers/ClearHandler.py index 216e030..f79d4a8 100644 --- a/Handlers/ClearHandler.py +++ b/Handlers/ClearHandler.py @@ -4,7 +4,8 @@ from discord.ext.commands import Context from Music.VulkanBot import VulkanBot from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse -from Parallelism.ProcessInfo import ProcessInfo +from Parallelism.AbstractProcessManager import AbstractPlayersManager +from Parallelism.ProcessInfo import PlayerInfo class ClearHandler(AbstractHandler): @@ -13,8 +14,8 @@ class ClearHandler(AbstractHandler): async def run(self) -> HandlerResponse: # Get the current process of the guild - processManager = self.config.getProcessManager() - processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild) + processManager: AbstractPlayersManager = self.config.getPlayersManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: # Clear the playlist playlist = processInfo.getPlaylist() diff --git a/Handlers/HistoryHandler.py b/Handlers/HistoryHandler.py index 864d90e..0c337c3 100644 --- a/Handlers/HistoryHandler.py +++ b/Handlers/HistoryHandler.py @@ -2,6 +2,7 @@ from discord.ext.commands import Context from Music.VulkanBot import VulkanBot from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse +from Parallelism.AbstractProcessManager import AbstractPlayersManager from Utils.Utils import Utils from typing import Union from discord import Interaction @@ -13,7 +14,7 @@ class HistoryHandler(AbstractHandler): async def run(self) -> HandlerResponse: # Get the current process of the guild - processManager = self.config.getProcessManager() + processManager: AbstractPlayersManager = self.config.getPlayersManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: processLock = processInfo.getLock() diff --git a/Handlers/JumpMusicHandler.py b/Handlers/JumpMusicHandler.py index 76e30fd..3ab6c95 100644 --- a/Handlers/JumpMusicHandler.py +++ b/Handlers/JumpMusicHandler.py @@ -6,6 +6,7 @@ from discord import Interaction from Handlers.HandlerResponse import HandlerResponse from Music.Playlist import Playlist from Music.VulkanBot import VulkanBot +from Parallelism.AbstractProcessManager import AbstractPlayersManager from Parallelism.Commands import VCommands, VCommandsType @@ -16,7 +17,7 @@ class JumpMusicHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self, musicPos: str) -> HandlerResponse: - processManager = self.config.getProcessManager() + processManager: AbstractPlayersManager = self.config.getPlayersManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if not processInfo: embed = self.embeds.NOT_PLAYING() diff --git a/Handlers/LoopHandler.py b/Handlers/LoopHandler.py index 065334a..17d9ee9 100644 --- a/Handlers/LoopHandler.py +++ b/Handlers/LoopHandler.py @@ -6,6 +6,8 @@ from Config.Exceptions import BadCommandUsage from typing import Union from discord import Interaction +from Parallelism.AbstractProcessManager import AbstractPlayersManager + class LoopHandler(AbstractHandler): def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: @@ -13,7 +15,7 @@ class LoopHandler(AbstractHandler): async def run(self, args: str) -> HandlerResponse: # Get the current process of the guild - processManager = self.config.getProcessManager() + processManager: AbstractPlayersManager = self.config.getPlayersManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if not processInfo: embed = self.embeds.NOT_PLAYING() diff --git a/Handlers/MoveHandler.py b/Handlers/MoveHandler.py index 0f36260..27d004c 100644 --- a/Handlers/MoveHandler.py +++ b/Handlers/MoveHandler.py @@ -7,6 +7,7 @@ from Config.Exceptions import BadCommandUsage, VulkanError, InvalidInput, Number from Music.Playlist import Playlist from typing import Union from discord import Interaction +from Parallelism.AbstractProcessManager import AbstractPlayersManager class MoveHandler(AbstractHandler): @@ -14,7 +15,7 @@ class MoveHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self, pos1: str, pos2: str) -> HandlerResponse: - processManager = self.config.getProcessManager() + processManager: AbstractPlayersManager = self.config.getPlayersManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if not processInfo: embed = self.embeds.NOT_PLAYING() diff --git a/Handlers/NowPlayingHandler.py b/Handlers/NowPlayingHandler.py index 81dc414..194314c 100644 --- a/Handlers/NowPlayingHandler.py +++ b/Handlers/NowPlayingHandler.py @@ -2,6 +2,7 @@ from discord.ext.commands import Context from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Music.VulkanBot import VulkanBot +from Parallelism.AbstractProcessManager import AbstractPlayersManager from Utils.Cleaner import Cleaner from typing import Union from discord import Interaction @@ -14,7 +15,7 @@ class NowPlayingHandler(AbstractHandler): async def run(self) -> HandlerResponse: # Get the current process of the guild - processManager = self.config.getProcessManager() + processManager: AbstractPlayersManager = self.config.getPlayersManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if not processInfo: embed = self.embeds.NOT_PLAYING() diff --git a/Handlers/PauseHandler.py b/Handlers/PauseHandler.py index 386997c..7cbda92 100644 --- a/Handlers/PauseHandler.py +++ b/Handlers/PauseHandler.py @@ -1,8 +1,9 @@ from discord.ext.commands import Context from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse +from Parallelism.AbstractProcessManager import AbstractPlayersManager from Parallelism.Commands import VCommands, VCommandsType -from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus +from Parallelism.ProcessInfo import PlayerInfo, ProcessStatus from Music.VulkanBot import VulkanBot from typing import Union from discord import Interaction @@ -13,8 +14,8 @@ class PauseHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - processManager = self.config.getProcessManager() - processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild) + processManager: AbstractPlayersManager = self.config.getPlayersManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: if processInfo.getStatus() == ProcessStatus.SLEEPING: embed = self.embeds.NOT_PLAYING() diff --git a/Handlers/PlayHandler.py b/Handlers/PlayHandler.py index 8816cf9..5dcd58e 100644 --- a/Handlers/PlayHandler.py +++ b/Handlers/PlayHandler.py @@ -9,7 +9,8 @@ from Handlers.HandlerResponse import HandlerResponse from Music.Downloader import Downloader from Music.Searcher import Searcher from Music.Song import Song -from Parallelism.ProcessInfo import ProcessInfo +from Parallelism.AbstractProcessManager import AbstractPlayersManager +from Parallelism.ProcessInfo import PlayerInfo from Parallelism.Commands import VCommands, VCommandsType from Music.VulkanBot import VulkanBot from typing import Union @@ -37,7 +38,7 @@ class PlayHandler(AbstractHandler): raise InvalidInput(self.messages.INVALID_INPUT, self.messages.ERROR_TITLE) # Get the process context for the current guild - processManager = self.config.getProcessManager() + processManager: AbstractPlayersManager = self.config.getPlayersManager() processInfo = processManager.getOrCreatePlayerInfo(self.guild, self.ctx) playlist: Playlist = processInfo.getPlaylist() process = processInfo.getProcess() @@ -109,7 +110,7 @@ class PlayHandler(AbstractHandler): return HandlerResponse(self.ctx, embed, error) - async def __downloadSongsAndStore(self, songs: List[Song], processInfo: ProcessInfo) -> None: + async def __downloadSongsAndStore(self, songs: List[Song], processInfo: PlayerInfo) -> None: playlist = processInfo.getPlaylist() queue = processInfo.getQueueToPlayer() playCommand = VCommands(VCommandsType.PLAY, None) @@ -126,7 +127,7 @@ class PlayHandler(AbstractHandler): tasks.append(task) # In the original order, await for the task and then, if successfully downloaded, add to the playlist - processManager = self.config.getProcessManager() + processManager: AbstractPlayersManager = self.config.getPlayersManager() for index, task in enumerate(tasks): await task song = songs[index] diff --git a/Handlers/PrevHandler.py b/Handlers/PrevHandler.py index 72dd4fc..13e81b6 100644 --- a/Handlers/PrevHandler.py +++ b/Handlers/PrevHandler.py @@ -2,6 +2,7 @@ from discord.ext.commands import Context from Handlers.AbstractHandler import AbstractHandler from Config.Exceptions import BadCommandUsage, ImpossibleMove from Handlers.HandlerResponse import HandlerResponse +from Parallelism.AbstractProcessManager import AbstractPlayersManager from Parallelism.Commands import VCommands, VCommandsType from Music.VulkanBot import VulkanBot from typing import Union @@ -18,7 +19,7 @@ class PrevHandler(AbstractHandler): embed = self.embeds.NO_CHANNEL() return HandlerResponse(self.ctx, embed, error) - processManager = self.config.getProcessManager() + processManager: AbstractPlayersManager = self.config.getPlayersManager() processInfo = processManager.getOrCreatePlayerInfo(self.guild, self.ctx) if not processInfo: embed = self.embeds.NOT_PLAYING() diff --git a/Handlers/QueueHandler.py b/Handlers/QueueHandler.py index 180402b..5ffefad 100644 --- a/Handlers/QueueHandler.py +++ b/Handlers/QueueHandler.py @@ -4,6 +4,7 @@ from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Handlers.JumpMusicHandler import JumpMusicHandler from Messages.MessagesCategory import MessagesCategory +from Parallelism.AbstractProcessManager import AbstractPlayersManager from UI.Views.BasicView import BasicView from Utils.Utils import Utils from Music.VulkanBot import VulkanBot @@ -22,7 +23,7 @@ class QueueHandler(AbstractHandler): async def run(self, pageNumber=0) -> HandlerResponse: # Retrieve the process of the guild - processManager = self.config.getProcessManager() + processManager: AbstractPlayersManager = self.config.getPlayersManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if not processInfo: # If no process return empty list embed = self.embeds.EMPTY_QUEUE() diff --git a/Handlers/RemoveHandler.py b/Handlers/RemoveHandler.py index 0d032cd..fd17da6 100644 --- a/Handlers/RemoveHandler.py +++ b/Handlers/RemoveHandler.py @@ -4,7 +4,8 @@ from Handlers.HandlerResponse import HandlerResponse from Config.Exceptions import BadCommandUsage, VulkanError, ErrorRemoving, InvalidInput, NumberRequired from Music.Playlist import Playlist from Music.VulkanBot import VulkanBot -from Parallelism.ProcessInfo import ProcessInfo +from Parallelism.AbstractProcessManager import AbstractPlayersManager +from Parallelism.ProcessInfo import PlayerInfo from typing import Union from discord import Interaction @@ -15,8 +16,8 @@ class RemoveHandler(AbstractHandler): async def run(self, position: str) -> HandlerResponse: # Get the current process of the guild - processManager = self.config.getProcessManager() - processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild) + processManager: AbstractPlayersManager = self.config.getPlayersManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) if not processInfo: embed = self.embeds.NOT_PLAYING() error = BadCommandUsage() diff --git a/Handlers/ResetHandler.py b/Handlers/ResetHandler.py index bb23f6b..2bc72d9 100644 --- a/Handlers/ResetHandler.py +++ b/Handlers/ResetHandler.py @@ -1,7 +1,8 @@ from discord.ext.commands import Context from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse -from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus +from Parallelism.AbstractProcessManager import AbstractPlayersManager +from Parallelism.ProcessInfo import PlayerInfo, ProcessStatus from Parallelism.Commands import VCommands, VCommandsType from Music.VulkanBot import VulkanBot from typing import Union @@ -14,8 +15,8 @@ class ResetHandler(AbstractHandler): async def run(self) -> HandlerResponse: # Get the current process of the guild - processManager = self.config.getProcessManager() - processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild) + processManager: AbstractPlayersManager = self.config.getPlayersManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: if processInfo.getStatus() == ProcessStatus.SLEEPING: embed = self.embeds.NOT_PLAYING() diff --git a/Handlers/ResumeHandler.py b/Handlers/ResumeHandler.py index 158a688..da7867b 100644 --- a/Handlers/ResumeHandler.py +++ b/Handlers/ResumeHandler.py @@ -1,7 +1,8 @@ from discord.ext.commands import Context from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse -from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus +from Parallelism.AbstractProcessManager import AbstractPlayersManager +from Parallelism.ProcessInfo import PlayerInfo, ProcessStatus from Parallelism.Commands import VCommands, VCommandsType from Music.VulkanBot import VulkanBot from typing import Union @@ -13,8 +14,8 @@ class ResumeHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - processManager = self.config.getProcessManager() - processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild) + processManager: AbstractPlayersManager = self.config.getPlayersManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: if processInfo.getStatus() == ProcessStatus.SLEEPING: embed = self.embeds.NOT_PLAYING() diff --git a/Handlers/ShuffleHandler.py b/Handlers/ShuffleHandler.py index 3f38b8b..7445e9d 100644 --- a/Handlers/ShuffleHandler.py +++ b/Handlers/ShuffleHandler.py @@ -6,13 +6,15 @@ from Music.VulkanBot import VulkanBot from typing import Union from discord import Interaction +from Parallelism.AbstractProcessManager import AbstractPlayersManager + class ShuffleHandler(AbstractHandler): def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - processManager = self.config.getProcessManager() + processManager: AbstractPlayersManager = self.config.getPlayersManager() processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: try: diff --git a/Handlers/SkipHandler.py b/Handlers/SkipHandler.py index 8170b94..9de6589 100644 --- a/Handlers/SkipHandler.py +++ b/Handlers/SkipHandler.py @@ -3,7 +3,8 @@ from Handlers.AbstractHandler import AbstractHandler from Config.Exceptions import BadCommandUsage, ImpossibleMove from Handlers.HandlerResponse import HandlerResponse from Music.VulkanBot import VulkanBot -from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus +from Parallelism.AbstractProcessManager import AbstractPlayersManager +from Parallelism.ProcessInfo import PlayerInfo, ProcessStatus from Parallelism.Commands import VCommands, VCommandsType from typing import Union from discord import Interaction @@ -19,8 +20,8 @@ class SkipHandler(AbstractHandler): embed = self.embeds.NO_CHANNEL() return HandlerResponse(self.ctx, embed, error) - processManager = self.config.getProcessManager() - processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild) + processManager: AbstractPlayersManager = self.config.getPlayersManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: # Verify if there is a running process if processInfo.getStatus() == ProcessStatus.SLEEPING: embed = self.embeds.NOT_PLAYING() diff --git a/Handlers/StopHandler.py b/Handlers/StopHandler.py index ddc9199..dd5ea7a 100644 --- a/Handlers/StopHandler.py +++ b/Handlers/StopHandler.py @@ -2,7 +2,8 @@ from discord.ext.commands import Context from Handlers.AbstractHandler import AbstractHandler from Handlers.HandlerResponse import HandlerResponse from Music.VulkanBot import VulkanBot -from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus +from Parallelism.AbstractProcessManager import AbstractPlayersManager +from Parallelism.ProcessInfo import PlayerInfo, ProcessStatus from Parallelism.Commands import VCommands, VCommandsType from typing import Union from discord import Interaction @@ -13,8 +14,8 @@ class StopHandler(AbstractHandler): super().__init__(ctx, bot) async def run(self) -> HandlerResponse: - processManager = self.config.getProcessManager() - processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild) + processManager: AbstractPlayersManager = self.config.getPlayersManager() + processInfo = processManager.getRunningPlayerInfo(self.guild) if processInfo: if processInfo.getStatus() == ProcessStatus.SLEEPING: embed = self.embeds.NOT_PLAYING() diff --git a/Parallelism/AbstractProcessManager.py b/Parallelism/AbstractProcessManager.py new file mode 100644 index 0000000..ecc2849 --- /dev/null +++ b/Parallelism/AbstractProcessManager.py @@ -0,0 +1,31 @@ +from abc import ABC, abstractmethod +from typing import Union +from discord.ext.commands import Context +from discord import Guild, Interaction +from Music.Song import Song +from Parallelism.ProcessInfo import PlayerInfo + + +class AbstractPlayersManager(ABC): + def __init__(self, bot) -> None: + pass + + @abstractmethod + def setPlayerInfo(self, guild: Guild, info: PlayerInfo): + pass + + @abstractmethod + def getOrCreatePlayerInfo(self, guild: Guild, context: Union[Context, Interaction]) -> PlayerInfo: + pass + + @abstractmethod + def resetProcess(self, guild: Guild, context: Context) -> None: + pass + + @abstractmethod + def getRunningPlayerInfo(self, guild: Guild) -> PlayerInfo: + pass + + @abstractmethod + async def showNowPlaying(self, guildID: int, song: Song) -> None: + pass diff --git a/Parallelism/PlayerProcess.py b/Parallelism/PlayerProcess.py index de09f39..7e2979a 100644 --- a/Parallelism/PlayerProcess.py +++ b/Parallelism/PlayerProcess.py @@ -2,19 +2,17 @@ import asyncio from time import sleep, time from urllib.parse import parse_qs, urlparse from Music.VulkanInitializer import VulkanInitializer -from discord import User, Member, Message, VoiceClient +from discord import Member, VoiceClient from asyncio import AbstractEventLoop, Semaphore, Queue from multiprocessing import Process, RLock, Lock, Queue from threading import Thread from typing import Callable, List -from discord import Guild, FFmpegPCMAudio, VoiceChannel, TextChannel +from discord import Guild, FFmpegPCMAudio, VoiceChannel from Music.Playlist import Playlist from Music.Song import Song from Config.Configs import VConfigs -from Config.Messages import Messages from Music.VulkanBot import VulkanBot from Music.Downloader import Downloader -from Config.Embeds import VEmbeds from Parallelism.Commands import VCommands, VCommandsType @@ -57,14 +55,7 @@ class PlayerProcess(Process): self.__bot: VulkanBot = None self.__voiceChannel: VoiceChannel = None self.__voiceClient: VoiceClient = None - self.__textChannel: TextChannel = None - self.__author: User = None - self.__botMember: Member = None - self.__configs: VConfigs = None - self.__embeds: VEmbeds = None - self.__messages: Messages = None - self.__messagesToDelete: List[Message] = [] self.__playing = False self.__forceStop = False self.FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5', @@ -78,9 +69,6 @@ class PlayerProcess(Process): self.__loop = asyncio.get_event_loop_policy().new_event_loop() asyncio.set_event_loop(self.__loop) - self.__configs = VConfigs() - self.__messages = Messages() - self.__embeds = VEmbeds() self.__downloader = Downloader() self.__semStopPlaying = Semaphore(0) diff --git a/Parallelism/PlayerThread.py b/Parallelism/PlayerThread.py new file mode 100644 index 0000000..eb26c7d --- /dev/null +++ b/Parallelism/PlayerThread.py @@ -0,0 +1,365 @@ +import asyncio +from time import sleep, time +from urllib.parse import parse_qs, urlparse +from Music.VulkanInitializer import VulkanInitializer +from discord import Member, VoiceClient +from asyncio import AbstractEventLoop, Semaphore +from threading import RLock, Thread +from multiprocessing import Lock +from typing import Callable, List +from discord import Guild, FFmpegPCMAudio, VoiceChannel +from Music.Playlist import Playlist +from Music.Song import Song +from Config.Configs import VConfigs +from Music.VulkanBot import VulkanBot +from Music.Downloader import Downloader +from Parallelism.Commands import VCommands, VCommandsType + + +class TimeoutClock: + def __init__(self, callback: Callable, loop: asyncio.AbstractEventLoop): + self.__callback = callback + self.__task = loop.create_task(self.__executor()) + + async def __executor(self): + await asyncio.sleep(VConfigs().VC_TIMEOUT) + await self.__callback() + + def cancel(self): + self.__task.cancel() + + +class PlayerThread(Thread): + """Player Thread to control the song playback in the same Process of the Main Process""" + + def __init__(self, bot: VulkanBot, guild: Guild, name: str, voiceChannel: VoiceChannel, playlist: Playlist, lock: Lock, guildID: int, textID: int, voiceID: int, authorID: int) -> None: + Thread.__init__(self, name=name, group=None, target=None, args=(), kwargs={}) + # Synchronization objects + self.__playlist: Playlist = playlist + self.__playlistLock: Lock = lock + self.__loop: AbstractEventLoop = None + self.__playerLock: RLock = RLock() + # Discord context ID + self.__guildID = guildID + self.__voiceChannelID = voiceID + self.__guild: Guild = guild + self.__bot: VulkanBot = bot + self.__voiceChannel: VoiceChannel = voiceChannel + self.__voiceClient: VoiceClient = None + + self.__downloader = Downloader() + + 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 = asyncio.get_event_loop_policy().new_event_loop() + asyncio.set_event_loop(self.__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 + if not self.__voiceClient.is_connected(): + return False + return self.__voiceClient.is_playing() or self.__voiceClient.is_paused() + + async def __playPlaylistSongs(self) -> None: + """If the player is not running trigger to play a new song""" + self.__playing = self.__verifyIfIsPlaying() + if not self.__playing: + song = None + with self.__playlistLock: + with self.__playerLock: + song = self.__playlist.next_song() + + if song is not None: + self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') + self.__playing = True + + async def __playSong(self, song: Song) -> None: + """Function that will trigger the player to play the song""" + try: + self.__playerLock.acquire() + if song is None: + return + + if song.source is None: + return self.__playNext(None) + + # If not connected, connect to bind channel + if self.__voiceClient is None: + await self.__connectToVoiceChannel() + + # If the voice channel disconnect for some reason + if not self.__voiceClient.is_connected(): + print('[VOICE CHANNEL NOT NULL BUT DISCONNECTED, CONNECTING AGAIN]') + await self.__connectToVoiceChannel() + # If the player is connected and playing return the song to the playlist + elif self.__voiceClient.is_playing(): + print('[SONG ALREADY PLAYING, RETURNING]') + self.__playlist.add_song_start(song) + return + + songStillAvailable = self.__verifyIfSongAvailable(song) + if not songStillAvailable: + print('[SONG NOT AVAILABLE ANYMORE, DOWNLOADING AGAIN]') + song = self.__downloadSongAgain(song) + + self.__playing = True + self.__songPlaying = song + + player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS) + self.__voiceClient.play(player, after=lambda e: self.__playNext(e)) + + self.__timer.cancel() + self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) + + nowPlayingCommand = VCommands(VCommandsType.NOW_PLAYING, song) + self.__queueSend.put(nowPlayingCommand) + except Exception as e: + print(f'[ERROR IN PLAY SONG FUNCTION] -> {e}, {type(e)}') + self.__playNext(None) + finally: + self.__playerLock.release() + + def __playNext(self, error) -> None: + if error is not None: + print(f'[ERROR PLAYING SONG] -> {error}') + with self.__playlistLock: + with self.__playerLock: + if self.__forceStop: # If it's forced to stop player + self.__forceStop = False + return None + + song = self.__playlist.next_song() + + if song is not None: + self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') + else: + self.__playlist.loop_off() + self.__songPlaying = None + self.__playing = False + # Send a command to the main process put this one to sleep + sleepCommand = VCommands(VCommandsType.SLEEPING) + self.__queueSend.put(sleepCommand) + # Release the semaphore to finish the process + self.__semStopPlaying.release() + + def __verifyIfSongAvailable(self, song: Song) -> bool: + """Verify the song source to see if it's already expired""" + try: + parsedUrl = urlparse(song.source) + + if 'expire' not in parsedUrl.query: + # If already passed 5 hours since the download + if song.downloadTime + 18000 < int(time()): + return False + return True + + # If the current time plus the song duration plus 10min exceeds the expirationValue + expireValue = parse_qs(parsedUrl.query)['expire'][0] + if int(time()) + song.duration + 600 > int(str(expireValue)): + return False + return True + except Exception as e: + print(f'[ERROR VERIFYING SONG AVAILABILITY] -> {e}') + return False + + def __downloadSongAgain(self, song: Song) -> Song: + """Force a download to be executed again, one use case is when the song.source expired and needs to refresh""" + return self.__downloader.finish_one_song(song) + + async def __playPrev(self, voiceChannelID: int) -> None: + with self.__playlistLock: + song = self.__playlist.prev_song() + + with self.__playerLock: + if song is not None: + # If not connect, connect to the user voice channel, may change the channel + if self.__voiceClient is None or not self.__voiceClient.is_connected(): + self.__voiceChannelID = voiceChannelID + self.__voiceChannel = self.__guild.get_channel(self.__voiceChannelID) + await self.__connectToVoiceChannel() + + # If already playing, stop the current play + if self.__verifyIfIsPlaying(): + # Will forbidden next_song to execute after stopping current player + self.__forceStop = True + self.__voiceClient.stop() + self.__playing = False + + self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}') + + async def __restartCurrentSong(self) -> None: + song = self.__playlist.getCurrentSong() + if song is None: + song = self.__playlist.next_song() + if song is None: + return + + 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() + if type == VCommandsType.PAUSE: + self.__pause() + elif type == VCommandsType.RESUME: + await self.__resume() + elif type == VCommandsType.SKIP: + await self.__skip() + elif type == VCommandsType.PLAY: + await self.__playPlaylistSongs() + elif type == VCommandsType.PREV: + await self.__playPrev(args) + elif type == VCommandsType.RESET: + await self.__reset() + elif type == VCommandsType.STOP: + await self.__stop() + else: + print(f'[ERROR] -> Unknown Command Received: {command}') + except Exception as e: + print(f'[ERROR IN COMMAND RECEIVER] -> {type} - {e}') + finally: + self.__playerLock.release() + + def __pause(self) -> None: + if self.__voiceClient is not None: + if self.__voiceClient.is_connected(): + if self.__voiceClient.is_playing(): + self.__voiceClient.pause() + + async def __reset(self) -> None: + if self.__voiceClient is None: + return + + if not self.__voiceClient.is_connected(): + await self.__connectToVoiceChannel() + if self.__songPlaying is not None: + await self.__restartCurrentSong() + + async def __stop(self) -> None: + if self.__voiceClient is not None: + if self.__voiceClient.is_connected(): + with self.__playlistLock: + self.__playlist.loop_off() + self.__playlist.clear() + + self.__voiceClient.stop() + await self.__voiceClient.disconnect() + + self.__songPlaying = None + self.__playing = False + self.__voiceClient = None + # If the voiceClient is not None we finish things + else: + await self.__forceBotDisconnectAndStop() + + async def __resume(self) -> None: + # Lock to work with Player + with self.__playerLock: + if self.__voiceClient is not None: + # If the player is paused then return to play + if self.__voiceClient.is_paused(): + return self.__voiceClient.resume() + # If there is a current song but the voice client is not playing + elif self.__songPlaying is not None and not self.__voiceClient.is_playing(): + await self.__playSong(self.__songPlaying) + + async def __skip(self) -> None: + self.__playing = self.__verifyIfIsPlaying() + # Lock to work with Player + with self.__playerLock: + if self.__playing: + self.__playing = False + self.__voiceClient.stop() + # If for some reason the Bot has disconnect but there is still songs to play + elif len(self.__playlist.getSongs()) > 0: + print('[RESTARTING CURRENT SONG]') + await self.__restartCurrentSong() + + async def __forceBotDisconnectAndStop(self) -> None: + # Lock to work with Player + with self.__playerLock: + if self.__voiceClient is None: + return + self.__playing = False + self.__songPlaying = None + try: + self.__voiceClient.stop() + await self.__voiceClient.disconnect(force=True) + except Exception as e: + print(f'[ERROR FORCING BOT TO STOP] -> {e}') + finally: + self.__voiceClient = None + with self.__playlistLock: + self.__playlist.clear() + self.__playlist.loop_off() + + async def __timeoutHandler(self) -> None: + try: + if self.__voiceClient is None: + return + + # If the bot should not disconnect when alone + if not VConfigs().SHOULD_AUTO_DISCONNECT_WHEN_ALONE: + return + + if self.__voiceClient.is_connected(): + if self.__voiceClient.is_playing() or self.__voiceClient.is_paused(): + if not self.__isBotAloneInChannel(): # If bot is not alone continue to play + self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop) + return + + # Finish the process + with self.__playerLock: + with self.__playlistLock: + self.__playlist.loop_off() + await self.__forceBotDisconnectAndStop() + except Exception as e: + print(f'[ERROR IN TIMEOUT] -> {e}') + + def __isBotAloneInChannel(self) -> bool: + try: + if len(self.__voiceClient.channel.members) <= 1: + return True + else: + return False + except Exception as e: + print(f'[ERROR IN CHECK BOT ALONE] -> {e}') + return False + + async def __connectToVoiceChannel(self) -> bool: + try: + print('[CONNECTING TO VOICE CHANNEL]') + if self.__voiceClient is not None: + try: + await self.__voiceClient.disconnect(force=True) + except Exception as e: + print(f'[ERROR FORCING DISCONNECT] -> {e}') + self.__voiceClient = await self.__voiceChannel.connect(reconnect=True, timeout=None) + return True + except Exception as e: + print(f'[ERROR CONNECTING TO VC] -> {e}') + return False diff --git a/Parallelism/ProcessExecutor.py b/Parallelism/ProcessExecutor.py index ba6ab1e..a5f7843 100644 --- a/Parallelism/ProcessExecutor.py +++ b/Parallelism/ProcessExecutor.py @@ -4,7 +4,7 @@ from discord.ui import View from Config.Emojis import VEmojis from Messages.MessagesCategory import MessagesCategory from Music.VulkanBot import VulkanBot -from Parallelism.ProcessInfo import ProcessInfo +from Parallelism.ProcessInfo import PlayerInfo from Config.Messages import Messages from Music.Song import Song from Config.Embeds import VEmbeds @@ -29,7 +29,7 @@ class ProcessCommandsExecutor: self.__embeds = VEmbeds() self.__emojis = VEmojis() - async def sendNowPlaying(self, processInfo: ProcessInfo, song: Song) -> None: + async def sendNowPlaying(self, processInfo: PlayerInfo, song: Song) -> None: # Get the lock of the playlist playlist = processInfo.getPlaylist() if playlist.isLoopingOne(): diff --git a/Parallelism/ProcessInfo.py b/Parallelism/ProcessInfo.py index 50ed1b0..d1a0d89 100644 --- a/Parallelism/ProcessInfo.py +++ b/Parallelism/ProcessInfo.py @@ -9,9 +9,9 @@ class ProcessStatus(Enum): SLEEPING = 'Sleeping' -class ProcessInfo: +class PlayerInfo: """ - Class to store the reference to all structures to maintain a player process + Class to store the reference to all structures to maintain a song player """ def __init__(self, process: Process, queueToPlayer: Queue, queueToMain: Queue, playlist: Playlist, lock: Lock, textChannel: TextChannel) -> None: diff --git a/Parallelism/ProcessManager.py b/Parallelism/ProcessManager.py index dad23de..0664935 100644 --- a/Parallelism/ProcessManager.py +++ b/Parallelism/ProcessManager.py @@ -7,19 +7,20 @@ from typing import Dict, Tuple, Union from Config.Singleton import Singleton from discord import Guild, Interaction from discord.ext.commands import Context +from Parallelism.AbstractProcessManager import AbstractPlayersManager from Parallelism.ProcessExecutor import ProcessCommandsExecutor from Music.Song import Song from Parallelism.PlayerProcess import PlayerProcess from Music.Playlist import Playlist -from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus +from Parallelism.ProcessInfo import PlayerInfo, ProcessStatus from Parallelism.Commands import VCommands, VCommandsType from Music.VulkanBot import VulkanBot -class ProcessManager(Singleton): +class ProcessManager(Singleton, AbstractPlayersManager): """ Manage all running player process, creating and storing them for future calls - Deal with the creation of shared memory + Deals with the creation of shared memory """ def __init__(self, bot: VulkanBot = None) -> None: @@ -28,14 +29,14 @@ class ProcessManager(Singleton): VManager.register('Playlist', Playlist) self.__manager = VManager() self.__manager.start() - self.__playersProcess: Dict[int, ProcessInfo] = {} + self.__playersProcess: Dict[int, PlayerInfo] = {} self.__playersListeners: Dict[int, Tuple[Thread, bool]] = {} self.__playersCommandsExecutor: Dict[int, ProcessCommandsExecutor] = {} - def setPlayerInfo(self, guild: Guild, info: ProcessInfo): + def setPlayerInfo(self, guild: Guild, info: PlayerInfo): self.__playersProcess[guild.id] = info - def getOrCreatePlayerInfo(self, guild: Guild, context: Union[Context, Interaction]) -> ProcessInfo: + def getOrCreatePlayerInfo(self, guild: Guild, context: Union[Context, Interaction]) -> PlayerInfo: """Return the process info for the guild, the user in context must be connected to a voice_channel""" try: if guild.id not in self.__playersProcess.keys(): @@ -62,7 +63,7 @@ class ProcessManager(Singleton): newProcessInfo.getQueueToPlayer().put(playCommand) self.__playersProcess[guild.id] = newProcessInfo - def getRunningPlayerInfo(self, guild: Guild) -> ProcessInfo: + def getRunningPlayerInfo(self, guild: Guild) -> PlayerInfo: """Return the process info for the guild, if not, return None""" if guild.id not in self.__playersProcess.keys(): print('Process Info not found') @@ -70,7 +71,7 @@ class ProcessManager(Singleton): return self.__playersProcess[guild.id] - def __createProcessInfo(self, guild: Guild, context: Context) -> ProcessInfo: + def __createProcessInfo(self, guild: Guild, context: Context) -> PlayerInfo: guildID: int = context.guild.id textID: int = context.channel.id voiceID: int = context.author.voice.channel.id @@ -82,8 +83,8 @@ class ProcessManager(Singleton): queueToSend = Queue() process = PlayerProcess(context.guild.name, playlist, lock, queueToSend, queueToListen, guildID, textID, voiceID, authorID) - processInfo = ProcessInfo(process, queueToSend, queueToListen, - playlist, lock, context.channel) + processInfo = PlayerInfo(process, queueToSend, queueToListen, + playlist, lock, context.channel) # Create a Thread to listen for the queue coming from the Player Process, this will redirect the Queue to a async thread = Thread(target=self.__listenToCommands, @@ -110,7 +111,7 @@ class ProcessManager(Singleton): except Exception as e: print(f'[ERROR STOPPING PROCESS] -> {e}') - def __recreateProcess(self, guild: Guild, context: Union[Context, Interaction]) -> ProcessInfo: + def __recreateProcess(self, guild: Guild, context: Union[Context, Interaction]) -> PlayerInfo: """Create a new process info using previous playlist""" self.__stopPossiblyRunningProcess(guild) @@ -129,8 +130,8 @@ class ProcessManager(Singleton): queueToSend = Queue() process = PlayerProcess(context.guild.name, playlist, lock, queueToSend, queueToListen, guildID, textID, voiceID, authorID) - processInfo = ProcessInfo(process, queueToSend, queueToListen, - playlist, lock, context.channel) + processInfo = PlayerInfo(process, queueToSend, queueToListen, + playlist, lock, context.channel) # Create a Thread to listen for the queue coming from the Player Process, this will redirect the Queue to a async thread = Thread(target=self.__listenToCommands, diff --git a/Parallelism/ThreadManager.py b/Parallelism/ThreadManager.py new file mode 100644 index 0000000..2df8aff --- /dev/null +++ b/Parallelism/ThreadManager.py @@ -0,0 +1,199 @@ +import asyncio +from multiprocessing import Lock, Queue +from multiprocessing.managers import BaseManager, NamespaceProxy +from queue import Empty +from threading import Thread +from typing import Dict, Tuple, Union +from Config.Singleton import Singleton +from discord import Guild, Interaction +from discord.ext.commands import Context +from Parallelism.AbstractProcessManager import AbstractPlayersManager +from Parallelism.ProcessExecutor import ProcessCommandsExecutor +from Music.Song import Song +from Parallelism.PlayerProcess import PlayerProcess +from Music.Playlist import Playlist +from Parallelism.ProcessInfo import PlayerInfo, ProcessStatus +from Parallelism.Commands import VCommands, VCommandsType +from Music.VulkanBot import VulkanBot + + +class ThreadManager(Singleton, AbstractPlayersManager): + """ + Manage all running player threads, creating and storing them for future calls + """ + + def __init__(self, bot: VulkanBot = None) -> None: + if not super().created: + self.__bot = bot + self.__playersProcess: Dict[int, Thread] = {} + + def setPlayerInfo(self, guild: Guild, info: PlayerInfo): + self.__playersProcess[guild.id] = info + + def getOrCreatePlayerInfo(self, guild: Guild, context: Union[Context, Interaction]) -> PlayerInfo: + """Return the process info for the guild, the user in context must be connected to a voice_channel""" + try: + if guild.id not in self.__playersProcess.keys(): + self.__playersProcess[guild.id] = self.__createProcessInfo(guild, context) + else: + # If the process has ended create a new one + if not self.__playersProcess[guild.id].getProcess().is_alive(): + self.__playersProcess[guild.id] = self.__recreateProcess(guild, context) + + return self.__playersProcess[guild.id] + except Exception as e: + print(f'[Error In GetPlayerContext] -> {e}') + + def resetProcess(self, guild: Guild, context: Context) -> None: + """Restart a running process, already start it to return to play""" + if guild.id not in self.__playersProcess.keys(): + return None + + # Recreate the process keeping the playlist + newProcessInfo = self.__recreateProcess(guild, context) + newProcessInfo.getProcess().start() # Start the process + # Send a command to start the play again + playCommand = VCommands(VCommandsType.PLAY) + newProcessInfo.getQueueToPlayer().put(playCommand) + self.__playersProcess[guild.id] = newProcessInfo + + def getRunningPlayerInfo(self, guild: Guild) -> PlayerInfo: + """Return the process info for the guild, if not, return None""" + if guild.id not in self.__playersProcess.keys(): + print('Process Info not found') + return None + + return self.__playersProcess[guild.id] + + def __createProcessInfo(self, guild: Guild, context: Context) -> PlayerInfo: + guildID: int = context.guild.id + textID: int = context.channel.id + voiceID: int = context.author.voice.channel.id + authorID: int = context.author.id + + playlist: Playlist = self.__manager.Playlist() + lock = Lock() + queueToListen = Queue() + queueToSend = Queue() + process = PlayerProcess(context.guild.name, playlist, lock, queueToSend, + queueToListen, guildID, textID, voiceID, authorID) + processInfo = PlayerInfo(process, queueToSend, queueToListen, + playlist, lock, context.channel) + + # Create a Thread to listen for the queue coming from the Player Process, this will redirect the Queue to a async + thread = Thread(target=self.__listenToCommands, + args=(queueToListen, guild), daemon=True) + self.__playersListeners[guildID] = (thread, False) + thread.start() + + # Create a Message Controller for this player + self.__playersCommandsExecutor[guildID] = ProcessCommandsExecutor(self.__bot, guildID) + + return processInfo + + def __stopPossiblyRunningProcess(self, guild: Guild): + try: + if guild.id in self.__playersProcess.keys(): + playerProcess = self.__playersProcess[guild.id] + process = playerProcess.getProcess() + process.close() + process.kill() + playerProcess.getQueueToMain().close() + playerProcess.getQueueToMain().join_thread() + playerProcess.getQueueToPlayer().close() + playerProcess.getQueueToPlayer().join_thread() + except Exception as e: + print(f'[ERROR STOPPING PROCESS] -> {e}') + + def __recreateProcess(self, guild: Guild, context: Union[Context, Interaction]) -> PlayerInfo: + """Create a new process info using previous playlist""" + self.__stopPossiblyRunningProcess(guild) + + guildID: int = context.guild.id + textID: int = context.channel.id + if isinstance(context, Interaction): + authorID: int = context.user.id + voiceID: int = context.user.voice.channel.id + else: + authorID: int = context.author.id + voiceID: int = context.author.voice.channel.id + + playlist: Playlist = self.__playersProcess[guildID].getPlaylist() + lock = Lock() + queueToListen = Queue() + queueToSend = Queue() + process = PlayerProcess(context.guild.name, playlist, lock, queueToSend, + queueToListen, guildID, textID, voiceID, authorID) + processInfo = PlayerInfo(process, queueToSend, queueToListen, + playlist, lock, context.channel) + + # Create a Thread to listen for the queue coming from the Player Process, this will redirect the Queue to a async + thread = Thread(target=self.__listenToCommands, + args=(queueToListen, guild), daemon=True) + self.__playersListeners[guildID] = (thread, False) + thread.start() + + return processInfo + + def __listenToCommands(self, queue: Queue, guild: Guild) -> None: + guildID = guild.id + while True: + shouldEnd = self.__playersListeners[guildID][1] + if shouldEnd: + break + + try: + command: VCommands = queue.get(timeout=5) + commandType = command.getType() + args = command.getArgs() + + print(f'Process {guild.name} sended command {commandType}') + if commandType == VCommandsType.NOW_PLAYING: + asyncio.run_coroutine_threadsafe(self.showNowPlaying( + guild.id, args), self.__bot.loop) + elif commandType == VCommandsType.TERMINATE: + # Delete the process elements and return, to finish task + self.__terminateProcess(guildID) + return + elif commandType == VCommandsType.SLEEPING: + # The process might be used again + self.__sleepingProcess(guildID) + return + else: + print(f'[ERROR] -> Unknown Command Received from Process: {commandType}') + except Empty: + continue + except Exception as e: + print(f'[ERROR IN LISTENING PROCESS] -> {guild.name} - {e}') + + def __terminateProcess(self, guildID: int) -> None: + # Delete all structures associated with the Player + del self.__playersProcess[guildID] + del self.__playersCommandsExecutor[guildID] + threadListening = self.__playersListeners[guildID] + threadListening._stop() + del self.__playersListeners[guildID] + + def __sleepingProcess(self, guildID: int) -> None: + # Disable all process structures, except Playlist + queue1 = self.__playersProcess[guildID].getQueueToMain() + queue2 = self.__playersProcess[guildID].getQueueToPlayer() + queue1.close() + queue1.join_thread() + queue2.close() + queue2.join_thread() + # Set the status of this process as sleeping, only the playlist object remains + self.__playersProcess[guildID].setStatus(ProcessStatus.SLEEPING) + + async def showNowPlaying(self, guildID: int, song: Song) -> None: + commandExecutor = self.__playersCommandsExecutor[guildID] + processInfo = self.__playersProcess[guildID] + await commandExecutor.sendNowPlaying(processInfo, song) + + +class VManager(BaseManager): + pass + + +class VProxy(NamespaceProxy): + _exposed_ = ('__getattribute__', '__setattr__', '__delattr__')