Finishing to add the ThreadPlayer to the Bot, allowing the user to switch between the versions

This commit is contained in:
Rafael Vargas 2023-02-20 01:52:59 -03:00
parent 1f45b64a62
commit a34a6a78d7
5 changed files with 125 additions and 77 deletions

View File

@ -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:

View File

@ -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:

View File

@ -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():

View File

@ -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:

View File

@ -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)