Skip to content

Commit

Permalink
Upgrading Player Stability
Browse files Browse the repository at this point in the history
  • Loading branch information
RafaelSolVargas committed Jan 27, 2023
1 parent 75de604 commit 2ffbab8
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 63 deletions.
7 changes: 5 additions & 2 deletions Music/VulkanBot.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

class VulkanBot(Bot):
def __init__(self, listingSlash: bool = False, *args, **kwargs):
"""If listing Slash is False then the process is just a Player Process, should not interact with discord commands"""
super().__init__(*args, **kwargs)
self.__listingSlash = listingSlash
self.__configs = VConfigs()
Expand Down Expand Up @@ -43,9 +44,11 @@ async def __connect(self):
await self.connect(reconnect=True)

async def on_ready(self):
print(self.__messages.STARTUP_MESSAGE)
if self.__listingSlash:
print(self.__messages.STARTUP_MESSAGE)
await self.change_presence(status=Status.online, activity=Game(name=f"Vulkan | {self.__configs.BOT_PREFIX}help"))
print(self.__messages.STARTUP_COMPLETE_MESSAGE)
if self.__listingSlash:
print(self.__messages.STARTUP_COMPLETE_MESSAGE)

async def on_command_error(self, ctx, error):
if isinstance(error, MissingRequiredArgument):
Expand Down
159 changes: 99 additions & 60 deletions Parallelism/PlayerProcess.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import asyncio
from Music.VulkanInitializer import VulkanInitializer
from discord import User, Member, Message
from discord import User, Member, Message, VoiceClient
from asyncio import AbstractEventLoop, Semaphore, Queue
from multiprocessing import Process, RLock, Lock, Queue
from threading import Thread
Expand Down Expand Up @@ -53,6 +53,7 @@ def __init__(self, name: str, playlist: Playlist, lock: Lock, queueToReceive: Qu
self.__guild: Guild = None
self.__bot: VulkanBot = None
self.__voiceChannel: VoiceChannel = None
self.__voiceClient: VoiceClient = None
self.__textChannel: TextChannel = None
self.__author: User = None
self.__botMember: Member = None
Expand All @@ -69,7 +70,7 @@ def __init__(self, name: str, playlist: Playlist, lock: Lock, queueToReceive: Qu
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}')
print(f'Starting Player Process for Guild {self.name}')
self.__playerLock = RLock()
self.__loop = asyncio.get_event_loop_policy().new_event_loop()
asyncio.set_event_loop(self.__loop)
Expand Down Expand Up @@ -108,14 +109,21 @@ async def _run(self) -> None:
# In this point the process should finalize
self.__timer.cancel()

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:
if not (self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused()):
song = self.__playlist.next_song()
song = self.__playlist.next_song()

if song is not None:
self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}')
Expand All @@ -132,26 +140,31 @@ async def __playSong(self, song: Song) -> None:
return self.__playNext(None)

# If not connected, connect to bind channel
if self.__guild.voice_client is None:
if self.__voiceClient is None:
await self.__connectToVoiceChannel()

# If the player is already playing return
if self.__guild.voice_client.is_playing():
# 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
elif self.__voiceClient.is_playing():
print('[SONG ALREADY PLAYING, RETURNING]')
return

self.__playing = True
self.__songPlaying = song

player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS)
self.__guild.voice_client.play(player, after=lambda e: self.__playNext(e))
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] -> {e}, {type(e)}')
print(f'[ERROR IN PLAY SONG FUNCTION] -> {e}, {type(e)}')
self.__playNext(None)
finally:
self.__playerLock.release()
Expand Down Expand Up @@ -185,16 +198,17 @@ async def __playPrev(self, voiceChannelID: int) -> None:

with self.__playerLock:
if song is not None:
if self.__guild.voice_client is None: # If not connect, connect to the user voice channel
# 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.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused():
if self.__verifyIfIsPlaying():
# Will forbidden next_song to execute after stopping current player
self.__forceStop = True
self.__guild.voice_client.stop()
self.__voiceClient.stop()
self.__playing = False

self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}')
Expand All @@ -213,13 +227,14 @@ def __commandsReceiver(self) -> None:
command: VCommands = self.__queueReceive.get()
type = command.getType()
args = command.getArgs()
print(f'Player Process {self.__guild.name} received command {type}')

try:
self.__playerLock.acquire()
if type == VCommandsType.PAUSE:
self.__pause()
elif type == VCommandsType.RESUME:
self.__resume()
asyncio.run_coroutine_threadsafe(self.__resume(), self.__loop)
elif type == VCommandsType.SKIP:
asyncio.run_coroutine_threadsafe(self.__skip(), self.__loop)
elif type == VCommandsType.PLAY:
Expand All @@ -238,61 +253,78 @@ def __commandsReceiver(self) -> None:
self.__playerLock.release()

def __pause(self) -> None:
if self.__guild.voice_client is not None:
if self.__guild.voice_client.is_playing():
self.__guild.voice_client.pause()
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.__guild.voice_client is None:
if self.__voiceClient is None:
return
# Reset the bot
self.__guild.voice_client.stop()
await self.__guild.voice_client.disconnect()
self.__playlist.clear()
self.__playlist.loop_off()
await self.__botMember.move_to(None)
# Release semaphore to finish the current player process
self.__semStopPlaying.release()

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.__guild.voice_client is not None:
if self.__guild.voice_client.is_connected():
if self.__voiceClient is not None:
if self.__voiceClient.is_connected():
with self.__playlistLock:
self.__playlist.loop_off()
self.__playlist.clear()

# Send a command to the main process put this to sleep
sleepCommand = VCommands(VCommandsType.SLEEPING)
self.__queueSend.put(sleepCommand)
self.__guild.voice_client.stop()
self.__voiceClient.stop()
await self.__voiceClient.disconnect()

self.__songPlaying = None
await self.__guild.voice_client.disconnect()
self.__playing = False
self.__voiceClient = None
self.__semStopPlaying.release()
# If the voiceClient is not None we finish things
else:
await self.__forceBotDisconnectAndStop()

def __resume(self) -> None:
async def __resume(self) -> None:
# Lock to work with Player
with self.__playerLock:
if self.__guild.voice_client is not None:
if self.__guild.voice_client.is_paused():
self.__guild.voice_client.resume()
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.__guild.voice_client is not None and self.__playing:
if self.__playing:
self.__playing = False
self.__guild.voice_client.stop()
elif len(self.__playlist) > 0: # If for some reason the Bot has disconnect but there is still songs to play
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 __forceStop(self) -> None:
async def __forceBotDisconnectAndStop(self) -> None:
# Lock to work with Player
with self.__playerLock:
if self.__guild.voice_client is None:
if self.__voiceClient is None:
return

self.__guild.voice_client.stop()
await self.__guild.voice_client.disconnect()
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()
Expand All @@ -308,35 +340,36 @@ async def __createBotInstance(self) -> VulkanBot:

async def __timeoutHandler(self) -> None:
try:
if self.__guild.voice_client is None:
# If there is not voiceClient return
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.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused():
if not self.__isBotAloneInChannel(): # If bot is not alone continue to play
self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop)
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
if self.__guild.voice_client.is_connected():
with self.__playerLock:
with self.__playlistLock:
self.__playlist.loop_off()
self.__playing = False
await self.__guild.voice_client.disconnect()
# Send command to main process to finish this one
sleepCommand = VCommands(VCommandsType.SLEEPING)
self.__queueSend.put(sleepCommand)
# Release semaphore to finish process
self.__semStopPlaying.release()
with self.__playerLock:
with self.__playlistLock:
self.__playlist.loop_off()
await self.__forceBotDisconnectAndStop()
# Send command to main process to finish this one
sleepCommand = VCommands(VCommandsType.SLEEPING)
self.__queueSend.put(sleepCommand)
# Release semaphore to finish process
self.__semStopPlaying.release()
except Exception as e:
print(f'[Error in Timeout] -> {e}')
print(f'[ERROR IN TIMEOUT] -> {e}')

def __isBotAloneInChannel(self) -> bool:
try:
if len(self.__guild.voice_client.channel.members) <= 1:
if len(self.__voiceClient.channel.members) <= 1:
return True
else:
return False
Expand All @@ -353,7 +386,13 @@ async def __ensureDiscordConnection(self, bot: VulkanBot) -> None:

async def __connectToVoiceChannel(self) -> bool:
try:
await self.__voiceChannel.connect(reconnect=True, timeout=None)
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}')
Expand Down
2 changes: 1 addition & 1 deletion Parallelism/ProcessManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def __createProcessInfo(self, guild: Guild, context: Context) -> ProcessInfo:
def __stopPossiblyRunningProcess(self, guild: Guild):
try:
if guild.id in self.__playersProcess.keys():
playerProcess = self.__playersProcess.popitem(guild.id)
playerProcess = self.__playersProcess[guild.id]
process = playerProcess.getProcess()
process.close()
process.kill()
Expand Down

0 comments on commit 2ffbab8

Please sign in to comment.