mirror of
https://github.com/RafaelSolVargas/Vulkan.git
synced 2025-10-29 16:57:23 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4e9e46d6d | ||
|
|
2ffbab86eb | ||
|
|
75de60470f | ||
|
|
afb223eadd | ||
|
|
8dfa3579ae | ||
|
|
5cdc4e9a53 | ||
|
|
7310eda1a1 | ||
|
|
a72c4c7d8d |
@@ -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')
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}')
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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}')
|
||||
|
||||
@@ -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):
|
||||
|
||||
13
README.md
13
README.md
@@ -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
|
||||
|
||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
Reference in New Issue
Block a user