Modifying play and prev commands to work with Process

This commit is contained in:
Rafael Vargas 2022-07-23 15:51:13 -03:00
parent 7efed8ab89
commit 56456bf2ed
20 changed files with 178 additions and 150 deletions

View File

@ -21,7 +21,7 @@ class Configs(Singleton):
self.MAX_PLAYLIST_LENGTH = 50
self.MAX_PLAYLIST_FORCED_LENGTH = 5
self.MAX_PRELOAD_SONGS = 10
self.MAX_PRELOAD_SONGS = 15
self.MAX_SONGS_HISTORY = 15
self.INVITE_MESSAGE = """To invite Vulkan to your own server, click [here]({}).

View File

@ -12,11 +12,11 @@ class ClearHandler(AbstractHandler):
async def run(self) -> HandlerResponse:
# Get the current process of the guild
processManager = ProcessManager()
processContext = processManager.getRunningPlayerContext(self.guild)
if processContext:
processInfo = processManager.getRunningPlayerInfo(self.guild)
if processInfo:
# Clear the playlist
playlist = processContext.getPlaylist()
with processContext.getLock():
playlist = processInfo.getPlaylist()
with processInfo.getLock():
playlist.clear()
return HandlerResponse(self.ctx)

View File

@ -13,10 +13,10 @@ class HistoryHandler(AbstractHandler):
async def run(self) -> HandlerResponse:
# Get the current process of the guild
processManager = ProcessManager()
processContext = processManager.getRunningPlayerContext(self.guild)
if processContext:
with processContext.getLock():
playlist = processContext.getPlaylist()
processInfo = processManager.getRunningPlayerInfo(self.guild)
if processInfo:
with processInfo.getLock():
playlist = processInfo.getPlaylist()
history = playlist.getSongsHistory()
else:
history = []

View File

@ -13,15 +13,15 @@ class LoopHandler(AbstractHandler):
async def run(self, args: str) -> HandlerResponse:
# Get the current process of the guild
processManager = ProcessManager()
processContext = processManager.getRunningPlayerContext(self.guild)
if not processContext:
processInfo = processManager.getRunningPlayerInfo(self.guild)
if not processInfo:
embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error)
playlist = processContext.getPlaylist()
playlist = processInfo.getPlaylist()
with processContext.getLock():
with processInfo.getLock():
if args == '' or args is None:
playlist.loop_all()
embed = self.embeds.LOOP_ALL_ACTIVATED()

View File

@ -14,19 +14,19 @@ class MoveHandler(AbstractHandler):
async def run(self, pos1: str, pos2: str) -> HandlerResponse:
processManager = ProcessManager()
processContext = processManager.getRunningPlayerContext(self.guild)
if not processContext:
processInfo = processManager.getRunningPlayerInfo(self.guild)
if not processInfo:
embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error)
with processContext.getLock():
with processInfo.getLock():
error = self.__validateInput(pos1, pos2)
if error:
embed = self.embeds.ERROR_EMBED(error.message)
return HandlerResponse(self.ctx, embed, error)
playlist = processContext.getPlaylist()
playlist = processInfo.getPlaylist()
pos1, pos2 = self.__sanitizeInput(playlist, pos1, pos2)
if not playlist.validate_position(pos1) or not playlist.validate_position(pos2):

View File

@ -14,12 +14,12 @@ class NowPlayingHandler(AbstractHandler):
async def run(self) -> HandlerResponse:
# Get the current process of the guild
processManager = ProcessManager()
processContext = processManager.getRunningPlayerContext(self.guild)
if not processContext:
processInfo = processManager.getRunningPlayerInfo(self.guild)
if not processInfo:
embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed)
playlist = processContext.getPlaylist()
playlist = processInfo.getPlaylist()
if playlist.getCurrentSong() is None:
embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed)

View File

@ -12,11 +12,11 @@ class PauseHandler(AbstractHandler):
async def run(self) -> HandlerResponse:
processManager = ProcessManager()
processContext = processManager.getRunningPlayerContext(self.guild)
if processContext:
processInfo = processManager.getRunningPlayerInfo(self.guild)
if processInfo:
# Send Pause command to be execute by player process
command = VCommands(VCommandsType.PAUSE, None)
queue = processContext.getQueue()
queue = processInfo.getQueue()
queue.put(command)
return HandlerResponse(self.ctx)

View File

@ -1,3 +1,5 @@
import asyncio
from typing import List
from Config.Exceptions import DownloadingError, InvalidInput, VulkanError
from discord.ext.commands import Context
from discord import Client
@ -8,6 +10,7 @@ from Music.Downloader import Downloader
from Music.Searcher import Searcher
from Music.Song import Song
from Parallelism.ProcessManager import ProcessManager
from Parallelism.ProcessInfo import ProcessInfo
from Parallelism.Commands import VCommands, VCommandsType
@ -27,55 +30,56 @@ class PlayHandler(AbstractHandler):
return HandlerResponse(self.ctx, embed, error)
try:
musics = await self.__searcher.search(track)
if musics is None or len(musics) == 0:
# Search for musics and get the name of each song
musicsInfo = await self.__searcher.search(track)
if musicsInfo is None or len(musicsInfo) == 0:
raise InvalidInput(self.messages.INVALID_INPUT, self.messages.ERROR_TITLE)
for music in musics:
song = Song(music, self.player.playlist, requester)
self.player.playlist.add_song(song)
quant = len(musics)
songs_preload = self.player.playlist.getSongsToPreload()
await self.__down.preload(songs_preload)
if quant == 1:
pos = len(self.player.playlist)
song = self.__down.finish_one_song(song)
if song.problematic:
embed = self.embeds.SONG_PROBLEMATIC()
error = DownloadingError()
response = HandlerResponse(self.ctx, embed, error)
elif not self.player.playing:
embed = self.embeds.SONG_ADDED(song.title)
response = HandlerResponse(self.ctx, embed)
else:
embed = self.embeds.SONG_ADDED_TWO(song.info, pos)
response = HandlerResponse(self.ctx, embed)
else:
embed = self.embeds.SONGS_ADDED(quant)
response = HandlerResponse(self.ctx, embed)
# Get the process context for the current guild
manager = ProcessManager()
processContext = manager.getPlayerContext(self.guild, self.ctx)
# Add the downloaded song to the process playlist
# All access to shared memory should be protect by acquire the Lock
with processContext.getLock():
processContext.getPlaylist().add_song(song)
# If process already started send a command to the player process by queue
process = processContext.getProcess()
queue = processContext.getQueue()
if process.is_alive():
command = VCommands(VCommandsType.PLAY)
queue.put(command)
else:
# Start the process
processInfo = manager.getPlayerContext(self.guild, self.ctx)
playlist = processInfo.getPlaylist()
process = processInfo.getProcess()
if not process.is_alive(): # If process has not yet started, start
print('Starting process')
process.start()
return response
# Create the Songs objects
songs: List[Song] = []
for musicInfo in musicsInfo:
songs.append(Song(musicInfo, playlist, requester))
if len(songs) == 1:
# If only one music, download it directly
song = self.__down.finish_one_song(songs[0])
if song.problematic: # If error in download song return
embed = self.embeds.SONG_PROBLEMATIC()
error = DownloadingError()
return HandlerResponse(self.ctx, embed, error)
# If not playing
if not playlist.getCurrentSong():
embed = self.embeds.SONG_ADDED(song.title)
response = HandlerResponse(self.ctx, embed)
else: # If already playing
pos = len(playlist.getSongs())
embed = self.embeds.SONG_ADDED_TWO(song.info, pos)
response = HandlerResponse(self.ctx, embed)
# Add the unique song to the playlist and send a command to player process
with processInfo.getLock():
playlist.add_song(song)
queue = processInfo.getQueue()
playCommand = VCommands(VCommandsType.PLAY, None)
queue.put(playCommand)
return response
else: # If multiple songs added
# Trigger a task to download all songs and then store them in the process playlist
asyncio.create_task(self.__downloadSongsAndStore(songs, processInfo))
embed = self.embeds.SONGS_ADDED(len(songs))
return HandlerResponse(self.ctx, embed)
except Exception as err:
if isinstance(err, VulkanError): # If error was already processed
@ -89,6 +93,25 @@ class PlayHandler(AbstractHandler):
return HandlerResponse(self.ctx, embed, error)
async def __downloadSongsAndStore(self, songs: List[Song], processInfo: ProcessInfo) -> None:
playlist = processInfo.getPlaylist()
queue = processInfo.getQueue()
playCommand = VCommands(VCommandsType.PLAY, None)
# Trigger a task for each song to be downloaded
tasks: List[asyncio.Task] = []
for song in songs:
task = asyncio.create_task(self.__down.download_song(song))
tasks.append(task)
# In the original order, await for the task and then if successfully downloaded add in the playlist
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
with processInfo.getLock():
playlist.add_song(song)
queue.put(playCommand)
def __isUserConnected(self) -> bool:
if self.ctx.author.voice:
return True

View File

@ -1,8 +1,10 @@
from discord.ext.commands import Context
from discord import Client
from Handlers.AbstractHandler import AbstractHandler
from Config.Exceptions import BadCommandUsage, ImpossibleMove, UnknownError
from Config.Exceptions import BadCommandUsage, ImpossibleMove
from Handlers.HandlerResponse import HandlerResponse
from Parallelism.ProcessManager import ProcessManager
from Parallelism.Commands import VCommands, VCommandsType
class PrevHandler(AbstractHandler):
@ -10,7 +12,15 @@ class PrevHandler(AbstractHandler):
super().__init__(ctx, bot)
async def run(self) -> HandlerResponse:
if len(self.player.playlist.history()) == 0:
processManager = ProcessManager()
processInfo = processManager.getRunningPlayerInfo(self.guild)
if not processInfo:
embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error)
playlist = processInfo.getPlaylist()
if len(playlist.getHistory()) == 0:
error = ImpossibleMove()
embed = self.embeds.NOT_PREVIOUS_SONG()
return HandlerResponse(self.ctx, embed, error)
@ -20,41 +30,18 @@ class PrevHandler(AbstractHandler):
embed = self.embeds.NO_CHANNEL()
return HandlerResponse(self.ctx, embed, error)
if not self.__is_connected():
success = await self.__connect()
if not success:
error = UnknownError()
embed = self.embeds.UNKNOWN_ERROR()
return HandlerResponse(self.ctx, embed, error)
if self.player.playlist.isLoopingAll() or self.player.playlist.isLoopingOne():
if playlist.isLoopingAll() or playlist.isLoopingOne():
error = BadCommandUsage()
embed = self.embeds.FAIL_DUE_TO_LOOP_ON()
return HandlerResponse(self.ctx, embed, error)
await self.player.play_prev(self.ctx)
# Send a prev command, together with the user voice channel
prevCommand = VCommands(VCommandsType.PREV, self.ctx.author.voice.channel.id)
queue = processInfo.getQueue()
queue.put(prevCommand)
def __user_connected(self) -> bool:
if self.ctx.author.voice:
return True
else:
return False
def __is_connected(self) -> bool:
try:
voice_channel = self.guild.voice_client.channel
if not self.guild.voice_client.is_connected():
return False
else:
return True
except:
return False
async def __connect(self) -> bool:
# if self.guild.voice_client is None:
try:
await self.ctx.author.voice.channel.connect(reconnect=True, timeout=None)
return True
except:
return False

View File

@ -16,14 +16,14 @@ class QueueHandler(AbstractHandler):
async def run(self) -> HandlerResponse:
# Retrieve the process of the guild
process = ProcessManager()
processContext = process.getRunningPlayerContext(self.guild)
if not processContext: # If no process return empty list
processInfo = process.getRunningPlayerInfo(self.guild)
if not processInfo: # If no process return empty list
embed = self.embeds.EMPTY_QUEUE()
return HandlerResponse(self.ctx, embed)
# Acquire the Lock to manipulate the playlist
with processContext.getLock():
playlist = processContext.getPlaylist()
with processInfo.getLock():
playlist = processInfo.getPlaylist()
if playlist.isLoopingOne():
song = playlist.getCurrentSong()
@ -31,6 +31,7 @@ class QueueHandler(AbstractHandler):
return HandlerResponse(self.ctx, embed)
songs_preload = playlist.getSongsToPreload()
allSongs = playlist.getSongs()
if len(songs_preload) == 0:
embed = self.embeds.EMPTY_QUEUE()
return HandlerResponse(self.ctx, embed)
@ -43,7 +44,7 @@ class QueueHandler(AbstractHandler):
title = self.messages.QUEUE_TITLE
total_time = Utils.format_time(sum([int(song.duration if song.duration else 0)
for song in songs_preload]))
for song in allSongs]))
total_songs = len(playlist.getSongs())
text = f'📜 Queue length: {total_songs} | ⌛ Duration: `{total_time}` downloaded \n\n'

View File

@ -15,14 +15,14 @@ class RemoveHandler(AbstractHandler):
async def run(self, position: str) -> HandlerResponse:
# Get the current process of the guild
processManager = ProcessManager()
processContext = processManager.getRunningPlayerContext(self.guild)
if not processContext:
processInfo = processManager.getRunningPlayerInfo(self.guild)
if not processInfo:
# Clear the playlist
embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error)
playlist = processContext.getPlaylist()
playlist = processInfo.getPlaylist()
if playlist.getCurrentSong() is None:
embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage()
@ -60,5 +60,5 @@ class RemoveHandler(AbstractHandler):
position = int(position)
if position == -1:
position = len(playlist)
position = len(playlist.getSongs())
return position

View File

@ -13,10 +13,10 @@ class ResetHandler(AbstractHandler):
async def run(self) -> HandlerResponse:
# Get the current process of the guild
processManager = ProcessManager()
processContext = processManager.getRunningPlayerContext(self.guild)
if processContext:
processInfo = processManager.getRunningPlayerInfo(self.guild)
if processInfo:
command = VCommands(VCommandsType.RESET, None)
queue = processContext.getQueue()
queue = processInfo.getQueue()
queue.put(command)
return HandlerResponse(self.ctx)

View File

@ -12,11 +12,11 @@ class ResumeHandler(AbstractHandler):
async def run(self) -> HandlerResponse:
processManager = ProcessManager()
processContext = processManager.getRunningPlayerContext(self.guild)
if processContext:
processInfo = processManager.getRunningPlayerInfo(self.guild)
if processInfo:
# Send Resume command to be execute by player process
command = VCommands(VCommandsType.RESUME, None)
queue = processContext.getQueue()
queue = processInfo.getQueue()
queue.put(command)
return HandlerResponse(self.ctx)

View File

@ -12,11 +12,11 @@ class ShuffleHandler(AbstractHandler):
async def run(self) -> HandlerResponse:
processManager = ProcessManager()
processContext = processManager.getRunningPlayerContext(self.guild)
if processContext:
processInfo = processManager.getRunningPlayerInfo(self.guild)
if processInfo:
try:
with processContext.getLock():
playlist = processContext.getPlaylist()
with processInfo.getLock():
playlist = processInfo.getPlaylist()
playlist.shuffle()
embed = self.embeds.SONGS_SHUFFLED()

View File

@ -13,9 +13,9 @@ class SkipHandler(AbstractHandler):
async def run(self) -> HandlerResponse:
processManager = ProcessManager()
processContext = processManager.getRunningPlayerContext(self.guild)
if processContext: # Verify if there is a running process
playlist = processContext.getPlaylist()
processInfo = processManager.getRunningPlayerInfo(self.guild)
if processInfo: # Verify if there is a running process
playlist = processInfo.getPlaylist()
if playlist.isLoopingOne():
embed = self.embeds.ERROR_DUE_LOOP_ONE_ON()
error = BadCommandUsage()
@ -23,6 +23,6 @@ class SkipHandler(AbstractHandler):
# Send a command to the player process to skip the music
command = VCommands(VCommandsType.SKIP, None)
queue = processContext.getQueue()
queue = processInfo.getQueue()
queue.put(command)
return HandlerResponse(self.ctx)

View File

@ -12,11 +12,11 @@ class StopHandler(AbstractHandler):
async def run(self) -> HandlerResponse:
processManager = ProcessManager()
processContext = processManager.getRunningPlayerContext(self.guild)
if processContext:
processInfo = processManager.getRunningPlayerInfo(self.guild)
if processInfo:
# Send command to player process stop
command = VCommands(VCommandsType.STOP, None)
queue = processContext.getQueue()
queue = processInfo.getQueue()
queue.put(command)
return HandlerResponse(self.ctx)

View File

@ -133,7 +133,7 @@ class Playlist:
return song
def history(self) -> list:
def getHistory(self) -> list:
titles = []
for song in self.__songs_history:
title = song.title if song.title else 'Unknown'

View File

@ -3,7 +3,7 @@ from typing import Tuple
class VCommandsType(Enum):
PLAY_PREV = 'Play Prev'
PREV = 'Prev'
SKIP = 'Skip'
PAUSE = 'Pause'
RESUME = 'Resume'

View File

@ -6,7 +6,6 @@ from multiprocessing import Process, Queue
from threading import Lock, Thread
from typing import Callable, List
from discord import Client, Guild, FFmpegPCMAudio, VoiceChannel, TextChannel
from discord.ext.commands import Context
from Music.Playlist import Playlist
from Music.Song import Song
from Config.Configs import Configs
@ -30,12 +29,12 @@ class TimeoutClock:
class PlayerProcess(Process):
"""Process that will play songs, receive commands from the main process by a Queue"""
def __init__(self, playlist: Playlist, lock: Lock, queue: Queue, guildID: int, textID: int, voiceID: int, authorID: int) -> None:
def __init__(self, name: str, playlist: Playlist, lock: Lock, queue: Queue, guildID: int, textID: int, voiceID: int, authorID: int) -> None:
"""
Start a new process that will have his own bot instance
Due to pickle serialization, no objects are stored, the values initialization are being made in the run method
"""
Process.__init__(self, group=None, target=None, args=(), kwargs={})
Process.__init__(self, name=name, group=None, target=None, args=(), kwargs={})
# Synchronization objects
self.__playlist: Playlist = playlist
self.__lock: Lock = lock
@ -64,6 +63,7 @@ class PlayerProcess(Process):
def run(self) -> None:
"""Method called by process.start(), this will exec the actually _run method in a event loop"""
try:
print(f'Starting Process {self.name}')
self.__loop = asyncio.get_event_loop()
self.__configs = Configs()
@ -99,16 +99,17 @@ class PlayerProcess(Process):
self.__timer.cancel()
async def __playPlaylistSongs(self) -> None:
print(f'Playing: {self.__playing}')
if not self.__playing:
with self.__lock:
print('Next Song Aqui')
song = self.__playlist.next_song()
await self.__playSong(song)
async def __playSong(self, song: Song) -> None:
try:
if song is None:
return
if song.source is None:
return self.__playNext(None)
@ -140,19 +141,38 @@ class PlayerProcess(Process):
else:
self.__playing = False
async def __playPrev(self, voiceChannelID: int) -> None:
with self.__lock:
song = self.__playlist.prev_song()
if song is not None:
if self.__guild.voice_client is None: # If not connect, connect to the user voice channel
self.__voiceChannelID = voiceChannelID
self.__voiceChannel = self.__guild.get_channel(self.__voiceChannelID)
self.__connectToVoiceChannel()
# If already playing, stop the current play
if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused():
# Will forbidden next_song to execute after stopping current player
self.__forceStop = True
self.__guild.voice_client.stop()
self.__playing = False
await self.__playSong(song)
def __commandsReceiver(self) -> None:
while True:
command: VCommands = self.__queue.get()
type = command.getType()
args = command.getArgs()
print(f'Command Received: {type}')
print(f'Process {self.name} receive Command: {type} with Args: {args}')
if type == VCommandsType.PAUSE:
self.__pause()
elif type == VCommandsType.PLAY:
self.__loop.create_task(self.__playPlaylistSongs())
elif type == VCommandsType.PLAY_PREV:
self.__playPrev()
elif type == VCommandsType.PREV:
self.__loop.create_task(self.__playPrev(args))
elif type == VCommandsType.RESUME:
self.__resume()
elif type == VCommandsType.SKIP:
@ -199,18 +219,6 @@ class PlayerProcess(Process):
if self.__guild.voice_client is not None:
self.__guild.voice_client.stop()
async def __playPrev(self, ctx: Context) -> None:
with self.__lock:
song = self.__playlist.prev_song()
if song is not None:
if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused():
# Will forbidden next_song to execute after stopping current player
self.__forceStop = True
self.__guild.voice_client.stop()
self.__playing = False
await self.__playSong(ctx, song)
async def __forceStop(self) -> None:
try:
if self.__guild.voice_client is None:
@ -246,7 +254,7 @@ class PlayerProcess(Process):
await task
self.__loop.create_task(bot.connect(reconnect=True))
# Sleep to wait connection to be established
await asyncio.sleep(2)
await self.__ensureDiscordConnection(bot)
return bot
@ -279,11 +287,19 @@ class PlayerProcess(Process):
except:
return False
async def __ensureDiscordConnection(self, bot: Client) -> None:
"""Await in this point until connection to discord is established"""
guild = None
while guild is None:
guild = bot.get_guild(self.__guildID)
await asyncio.sleep(0.2)
async def __connectToVoiceChannel(self) -> bool:
try:
await self.__voiceChannel.connect(reconnect=True, timeout=None)
return True
except:
except Exception as e:
print(f'[ERROR CONNECTING TO VC] -> {e}')
return False
def __getBotMember(self) -> Member:

View File

@ -38,7 +38,7 @@ class ProcessManager(Singleton):
except Exception as e:
print(f'[Error In GetPlayerContext] -> {e}')
def getRunningPlayerContext(self, guild: Guild) -> ProcessInfo:
def getRunningPlayerInfo(self, guild: Guild) -> ProcessInfo:
"""Return the process info for the guild, if not, return None"""
if guild not in self.__playersProcess.keys():
return None
@ -54,10 +54,11 @@ class ProcessManager(Singleton):
playlist: Playlist = self.__manager.Playlist()
lock = Lock()
queue = Queue()
process = PlayerProcess(playlist, lock, queue, guildID, textID, voiceID, authorID)
processContext = ProcessInfo(process, queue, playlist, lock)
process = PlayerProcess(context.guild.name, playlist, lock, queue,
guildID, textID, voiceID, authorID)
processInfo = ProcessInfo(process, queue, playlist, lock)
return processContext
return processInfo
class VManager(BaseManager):