8 Commits
v15.1 ... v15.2

Author SHA1 Message Date
Rafael Vargas
f4e9e46d6d Fixing error 34 2023-01-28 10:42:07 -03:00
Rafael Vargas
2ffbab86eb Upgrading Player Stability 2023-01-27 10:22:07 -03:00
Rafael Vargas
75de60470f Adding a verification to error when playing song 2023-01-25 13:27:41 -03:00
Rafael Vargas
afb223eadd Fixing error when stop and return too fast, because of that there may be some threads downloading songs that will try to put songs in a already closed queue 2023-01-25 13:07:48 -03:00
Rafael Vargas
8dfa3579ae Updating requiremetns 2023-01-24 19:53:17 -03:00
Rafael Vargas
5cdc4e9a53 Fixing error in starting playing songs that the Player lost reference to the current playing song 2023-01-24 19:42:57 -03:00
Rafael Vargas
7310eda1a1 Resolving issue 33 2023-01-23 10:35:52 -03:00
Rafael Vargas
a72c4c7d8d Trying to fix issue 32 2023-01-22 15:06:55 -03:00
19 changed files with 269 additions and 101 deletions

View File

@@ -7,6 +7,10 @@ from Config.Folder import Folder
class VConfigs(Singleton):
def __init__(self) -> None:
if not super().created:
# 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
self.BOT_PREFIX = '!'
try:
self.BOT_TOKEN = config('BOT_TOKEN')

View File

@@ -1,6 +1,8 @@
<h1 align="center">Configuring Heroku</h1>
Nobody wants to run the Vulkan process on their machine, so we host the process on Heroku, a cloud platform that contains free accounts.<br>
> Heroku doesn't offer free services anymore
Nobody wants to run the Vulkan process on their machine, so we host the process on Heroku, <s>a cloud platform that contains free</s>.<br>
To configure the Vulkan to run in your Heroku account you will need to:
- Create an application project in Heroku.

View File

@@ -1,4 +1,6 @@
from abc import ABC, abstractmethod
from Parallelism.Commands import VCommands
from multiprocessing import Queue
from typing import List, Union
from discord.ext.commands import Context
from discord import Client, Guild, ClientUser, Interaction, Member, User
@@ -27,6 +29,12 @@ class AbstractHandler(ABC):
else:
self.__author = ctx.user
def putCommandInQueue(self, queue: Queue, command: VCommands) -> None:
try:
queue.put(command)
except Exception as e:
print(f'[ERROR PUTTING COMMAND IN QUEUE] -> {e}')
@abstractmethod
async def run(self) -> HandlerResponse:
pass

View File

@@ -50,7 +50,7 @@ class JumpMusicHandler(AbstractHandler):
# Send a command to the player to skip the music
command = VCommands(VCommandsType.SKIP, None)
queue = processInfo.getQueueToPlayer()
queue.put(command)
self.putCommandInQueue(queue, command)
processLock.release()
return HandlerResponse(self.ctx)

View File

@@ -23,7 +23,7 @@ class PauseHandler(AbstractHandler):
# Send Pause command to be execute by player process
command = VCommands(VCommandsType.PAUSE, None)
queue = processInfo.getQueueToPlayer()
queue.put(command)
self.putCommandInQueue(queue, command)
embed = self.embeds.PLAYER_PAUSED()
return HandlerResponse(self.ctx, embed)

View File

@@ -14,6 +14,7 @@ from Parallelism.Commands import VCommands, VCommandsType
from Music.VulkanBot import VulkanBot
from typing import Union
from discord import Interaction
from Music.Playlist import Playlist
class PlayHandler(AbstractHandler):
@@ -38,7 +39,7 @@ class PlayHandler(AbstractHandler):
# Get the process context for the current guild
processManager = self.config.getProcessManager()
processInfo = processManager.getOrCreatePlayerInfo(self.guild, self.ctx)
playlist = processInfo.getPlaylist()
playlist: Playlist = processInfo.getPlaylist()
process = processInfo.getProcess()
if not process.is_alive(): # If process has not yet started, start
process.start()
@@ -74,7 +75,8 @@ class PlayHandler(AbstractHandler):
processLock.release()
queue = processInfo.getQueueToPlayer()
playCommand = VCommands(VCommandsType.PLAY, None)
queue.put(playCommand)
self.putCommandInQueue(queue, playCommand)
else:
processManager.resetProcess(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED()
@@ -82,6 +84,12 @@ 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, processInfo)
# Trigger a task to download all songs and then store them in the process playlist
asyncio.create_task(self.__downloadSongsAndStore(songs, processInfo))
@@ -92,13 +100,10 @@ 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)})
if isinstance(error, VulkanError): # If error was already processed
print(
f'DEVELOPER NOTE -s> PlayController Error: {traceback.format_exc()}', {type(error)})
embed = self.embeds.CUSTOM_ERROR(error)
else:
print(
f'DEVELOPER NOTE -> PlayController Error: {traceback.format_exc()}, {type(error)}')
error = UnknownError()
embed = self.embeds.UNKNOWN_ERROR()
@@ -108,13 +113,19 @@ class PlayHandler(AbstractHandler):
playlist = processInfo.getPlaylist()
queue = processInfo.getQueueToPlayer()
playCommand = VCommands(VCommandsType.PLAY, None)
tooManySongs = len(songs) > 100
# Trigger a task for each song to be downloaded
tasks: List[asyncio.Task] = []
for song in songs:
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)
# In the original order, await for the task and then if successfully downloaded add in the playlist
# In the original order, await for the task and then, if successfully downloaded, add to the playlist
processManager = self.config.getProcessManager()
for index, task in enumerate(tasks):
await task
@@ -125,7 +136,7 @@ class PlayHandler(AbstractHandler):
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired:
playlist.add_song(song)
queue.put(playCommand)
self.putCommandInQueue(queue, playCommand)
processLock.release()
else:
processManager.resetProcess(self.guild, self.ctx)

View File

@@ -44,7 +44,7 @@ class PrevHandler(AbstractHandler):
# Send a prev command, together with the user voice channel
prevCommand = VCommands(VCommandsType.PREV, self.author.voice.channel.id)
queue = processInfo.getQueueToPlayer()
queue.put(prevCommand)
self.putCommandInQueue(queue, prevCommand)
embed = self.embeds.RETURNING_SONG()
return HandlerResponse(self.ctx, embed)

View File

@@ -23,7 +23,7 @@ class ResetHandler(AbstractHandler):
command = VCommands(VCommandsType.RESET, None)
queue = processInfo.getQueueToPlayer()
queue.put(command)
self.putCommandInQueue(queue, command)
return HandlerResponse(self.ctx)
else:

View File

@@ -23,7 +23,7 @@ class ResumeHandler(AbstractHandler):
# Send Resume command to be execute by player process
command = VCommands(VCommandsType.RESUME, None)
queue = processInfo.getQueueToPlayer()
queue.put(command)
self.putCommandInQueue(queue, command)
embed = self.embeds.PLAYER_RESUMED()
return HandlerResponse(self.ctx, embed)

View File

@@ -29,7 +29,7 @@ class SkipHandler(AbstractHandler):
# Send a command to the player process to skip the music
command = VCommands(VCommandsType.SKIP, None)
queue = processInfo.getQueueToPlayer()
queue.put(command)
self.putCommandInQueue(queue, command)
embed = self.embeds.SKIPPING_SONG()
return HandlerResponse(self.ctx, embed)

View File

@@ -23,7 +23,7 @@ class StopHandler(AbstractHandler):
# Send command to player process stop
command = VCommands(VCommandsType.STOP, None)
queue = processInfo.getQueueToPlayer()
queue.put(command)
self.putCommandInQueue(queue, command)
embed = self.embeds.STOPPING_PLAYER()
return HandlerResponse(self.ctx, embed)

View File

@@ -56,8 +56,8 @@ class Downloader:
song.finish_down(song_info)
return song
# Convert yt_dlp error to my own error
except DownloadError:
raise DownloadingError()
except DownloadError as e:
raise DownloadingError(e.msg)
@run_async
def extract_info(self, url: str) -> List[dict]:
@@ -151,6 +151,8 @@ class Downloader:
return {}
if self.__is_multiple_musics(extracted_info):
if len(extracted_info['entries']) == 0:
return {}
return extracted_info['entries'][0]
else:
print(f'DEVELOPER NOTE -> Failed to extract title {title}')

View File

@@ -106,6 +106,10 @@ class Playlist:
self.__queue.append(song)
return song
def add_song_start(self, song: Song) -> Song:
self.__queue.insert(0, song)
return song
def shuffle(self) -> None:
random.shuffle(self.__queue)

View File

@@ -1,15 +1,20 @@
from time import time
class Song:
def __init__(self, identifier: str, playlist, requester: str) -> None:
self.__identifier = identifier
self.__info = {'requester': requester}
self.__problematic = False
self.__playlist = playlist
self.__downloadTime: int = time()
def finish_down(self, info: dict) -> None:
if info is None:
if info is None or info == {}:
self.destroy()
return None
self.__downloadTime = time()
self.__useful_keys = ['duration',
'title', 'webpage_url',
'channel', 'id', 'uploader',
@@ -20,7 +25,8 @@ class Song:
if key in info.keys():
self.__info[key] = info[key]
else:
print(f'DEVELOPER NOTE -> {key} not found in info of music: {self.identifier}')
print(
f'DEVELOPER NOTE -> Required information [{key}] was not found in the music: {self.identifier}')
self.destroy()
return
@@ -34,6 +40,10 @@ class Song:
self.__info['title'] = ''.join(char if char.isalnum() or char ==
' ' else ' ' for char in self.__info['title'])
@property
def downloadTime(self) -> int:
return self.__downloadTime
@property
def source(self) -> str:
if 'url' in self.__info.keys():
@@ -41,6 +51,10 @@ class Song:
else:
return None
@source.setter
def source(self, value) -> None:
self.__info['url'] = value
@property
def title(self) -> str:
if 'title' in self.__info.keys():
@@ -52,19 +66,23 @@ class Song:
def duration(self) -> str:
if 'duration' in self.__info.keys():
return self.__info['duration']
else:
return 0.0
else: # Default minimum duration
return 5.0
@property
def identifier(self) -> str:
return self.__identifier
@identifier.setter
def identifier(self, value) -> None:
self.__identifier = value
@property
def problematic(self) -> bool:
return self.__problematic
def destroy(self) -> None:
print(f'DEVELOPER NOTE -> Music self destroying {self.__identifier}')
print(f'MUSIC ERROR -> Music self destroying {self.__identifier}')
self.__problematic = True
self.__playlist.destroy_song(self)

View File

@@ -9,6 +9,7 @@ from Config.Embeds import VEmbeds
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()
@@ -43,9 +44,11 @@ class VulkanBot(Bot):
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):

View File

@@ -1,6 +1,8 @@
import asyncio
from time import time
from urllib.parse import parse_qs, urlparse
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
@@ -11,6 +13,7 @@ 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
@@ -53,6 +56,7 @@ class PlayerProcess(Process):
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
@@ -69,7 +73,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}')
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)
@@ -77,6 +81,7 @@ class PlayerProcess(Process):
self.__configs = VConfigs()
self.__messages = Messages()
self.__embeds = VEmbeds()
self.__downloader = Downloader()
self.__semStopPlaying = Semaphore(0)
self.__loop.run_until_complete(self._run())
@@ -108,17 +113,25 @@ class PlayerProcess(Process):
# 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}')
self.__playing = True
async def __playSong(self, song: Song) -> None:
"""Function that will trigger the player to play the song"""
@@ -131,18 +144,29 @@ class PlayerProcess(Process):
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 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.__playingSong = song
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)
@@ -150,12 +174,14 @@ class PlayerProcess(Process):
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()
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
@@ -168,7 +194,7 @@ class PlayerProcess(Process):
self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}')
else:
self.__playlist.loop_off()
self.__playingSong = None
self.__songPlaying = None
self.__playing = False
# Send a command to the main process put this one to sleep
sleepCommand = VCommands(VCommandsType.SLEEPING)
@@ -176,40 +202,75 @@ class PlayerProcess(Process):
# 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 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}')
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}')
def __commandsReceiver(self) -> None:
while True:
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:
self.__skip()
asyncio.run_coroutine_threadsafe(self.__skip(), self.__loop)
elif type == VCommandsType.PLAY:
asyncio.run_coroutine_threadsafe(self.__playPlaylistSongs(), self.__loop)
elif type == VCommandsType.PREV:
@@ -226,25 +287,23 @@ class PlayerProcess(Process):
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()
@@ -252,33 +311,54 @@ class PlayerProcess(Process):
# 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.__playingSong = None
await self.__guild.voice_client.disconnect()
self.__semStopPlaying.release()
self.__voiceClient.stop()
await self.__voiceClient.disconnect()
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()
def __skip(self) -> None:
# Lock to work with Player
with self.__playerLock:
if self.__guild.voice_client is not None and self.__playing:
self.__songPlaying = None
self.__playing = False
self.__guild.voice_client.stop()
self.__voiceClient = None
self.__semStopPlaying.release()
# If the voiceClient is not None we finish things
else:
await self.__forceBotDisconnectAndStop()
async def __forceStop(self) -> None:
async def __resume(self) -> None:
# Lock to work with Player
with self.__playerLock:
if self.__guild.voice_client is None:
return
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)
self.__guild.voice_client.stop()
await self.__guild.voice_client.disconnect()
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()
@@ -294,32 +374,36 @@ class PlayerProcess(Process):
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 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 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
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
@@ -336,7 +420,13 @@ class PlayerProcess(Process):
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}')

View File

@@ -28,9 +28,9 @@ class ProcessManager(Singleton):
VManager.register('Playlist', Playlist)
self.__manager = VManager()
self.__manager.start()
self.__playersProcess: Dict[Guild, ProcessInfo] = {}
self.__playersListeners: Dict[Guild, Tuple[Thread, bool]] = {}
self.__playersCommandsExecutor: Dict[Guild, ProcessCommandsExecutor] = {}
self.__playersProcess: Dict[int, ProcessInfo] = {}
self.__playersListeners: Dict[int, Tuple[Thread, bool]] = {}
self.__playersCommandsExecutor: Dict[int, ProcessCommandsExecutor] = {}
def setPlayerInfo(self, guild: Guild, info: ProcessInfo):
self.__playersProcess[guild.id] = info
@@ -65,6 +65,7 @@ class ProcessManager(Singleton):
def getRunningPlayerInfo(self, guild: Guild) -> ProcessInfo:
"""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]
@@ -95,8 +96,24 @@ class ProcessManager(Singleton):
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]) -> ProcessInfo:
"""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):

View File

@@ -69,7 +69,7 @@ BOT_PREFIX=Your_Wanted_Prefix_For_Vulkan
```
### **⚙️ Configs**
The config file is located at ```./config/Configs.py```, it doesn't require any change, but if you can change values to the way you want.
The config file is located at ```./config/Configs.py```, it doesn't require any change, but if you can change values to the way you want. <br>
### **Initialization**
@@ -77,11 +77,20 @@ The config file is located at ```./config/Configs.py```, it doesn't require any
- Run ```python main.py``` in console to start
### **Configuring Auto Disconnect**
As a result of the [Issue 33](https://github.com/RafaelSolVargas/Vulkan/issues/33) now you can configure if the Bot will auto disconnect when being alone in the voice channel, the default configuration is to disconnect within 300 seconds if it finds out no one is currently listing to it.
To change that you must: <br>
- Change the property SHOULD_AUTO_DISCONNECT_WHEN_ALONE of the VConfigs class to False
> The path to the file is ./Config/Configs.py
<br>
<hr>
<br>
## **🚀 Heroku**
## **🚀 Heroku (Not free anymore)**
> *Heroku doesn't offer free host services anymore.* <br>
To deploy and run your Bot in Heroku 24/7, follow the instructions in the [Heroku Instructions](HEROKU.md) page.
## 🧪 Tests

Binary file not shown.