42 Commits

Author SHA1 Message Date
Rafael Vargas
50363cd630 Merge pull request #66 from rivai45/patch-1
Configuring the Bot to be invited as an Administrator to allow further actions
2024-04-02 08:31:28 -03:00
Rivai
d605e6a5a8 Update Configs.py
Add administrator permission for the bot, so the bot can play in hidden or private channel
2024-04-01 12:27:17 +07:00
Rafael Vargas
b3a9920037 Merge pull request #53 from RafaelSolVargas/pr1044-from-someone
Upgrading documentation
2023-07-16 16:17:59 -03:00
Rafael Vargas
6be5c0d27b Upgrading documentation 2023-07-16 16:12:47 -03:00
Rafael Vargas
e3e7f2048a Merge pull request #52 from Paillat-dev/master
Adding docker support
2023-07-16 16:03:31 -03:00
Paillat
634e6560c6 docs(DOCKER.md): update instructions for running the bot in a Docker container
- Add clarification about creating a `.env` file in the root of the project
- Mention the need to change parameters in the `.env` file
- Provide a link to an example `.env` file
- Refer to the [Settings page](.github/Docs/SETTINGS.md) for changing environment file settings
- Add a note about trying the commands without a dash between docker and compose if they fail
2023-07-14 16:55:59 +02:00
Paillat
582e35bce1 chore(.env.example): add example .env file with configurable settings
docs(DOCKER.md): add instructions for running the bot in a Docker container
docs(SETTINGS.md): update instructions for changing bot settings in .env file
docs(README.md): update instructions for running the bot and add Docker instructions
2023-07-14 16:53:55 +02:00
Paillat
a82bf7950b fix(Configs.py): change default BOT_PREFIX to '!' if it is set to 'Your_Wanted_Prefix_For_Vulkan' 2023-07-14 16:53:23 +02:00
Paillat
aa5649bcdc fix(Configs.py): set SPOTIFY_ID and SPOTIFY_SECRET to None if they are set to default values 2023-07-14 16:50:14 +02:00
Paillat
6cfd188c3b feat: add Dockerfile and docker-compose.yaml for containerization
The Dockerfile is added to define the container image for the application. It uses the Python 3.10.0 base image, sets the PYTHONUNBUFFERED environment variable, clones the Vulkan repository, installs the required dependencies, installs ffmpeg, creates a non-root user, and sets the command to run the main.py file.

The docker-compose.yaml file is added to define the Docker Compose configuration for the application. It specifies a service named "vulkan" that builds the image using the Dockerfile and uses the .env file for environment variables.
2023-07-14 16:22:53 +02:00
Paillat
83a864725d fix(Configs.py): update PROJECT_URL to point to the correct GitHub repository 2023-07-14 16:12:38 +02:00
Paillat
f336df43ee chore(requirements.txt): update requirements.txt file 2023-07-13 14:47:59 +02:00
Paillat
d86f014a3a chore(requirements.txt): update requirements.txt file 2023-07-13 14:26:37 +02:00
Paillat
219fef7daa chore(Configs.py): refactor Configs.py to use environment variables instead of decouple config
feat(Configs.py): add support for loading environment variables from .env file using dotenv
feat(Configs.py): add support for SHOULD_AUTO_DISCONNECT_WHEN_ALONE environment variable
feat(Configs.py): add support for SONG_PLAYBACK_IN_SEPARATE_PROCESS environment variable
feat(Configs.py): add support for MAX_DOWNLOAD_SONGS_AT_A_TIME environment variable
feat(Configs.py): add support for BOT_PREFIX environment variable
feat(Configs.py): add support for BOT_TOKEN environment variable
feat(Configs.py): add support for SPOTIFY_ID and SPOTIFY_SECRET environment variables
feat(Configs.py): add support for CLEANER_MESSAGES_QUANT environment variable
feat(Configs.py): add support for ACQUIRE_LOCK_TIMEOUT environment variable
feat(Configs.py): add support for QUEUE_VIEW_TIMEOUT environment variable
feat(Configs.py): add support for COMMANDS
2023-07-13 14:25:38 +02:00
Rafael Vargas
45d27b4c47 Fixing mobile youtube links and volume returning to 100% when changing songs 2023-07-01 16:12:21 -03:00
Rafael Vargas
2114f15840 Adding volume change command, pre-release 2023-06-24 22:05:23 -03:00
Rafael Vargas
fc02cab769 Changing default settings 2023-03-22 22:09:03 -03:00
Rafael Vargas
b659f22863 Upgrading Pycord version, fixing audio issue reported 2023-03-22 21:55:22 -03:00
Rafael Vargas
6c15345eac Fixing passing None to thumbnail in help command 2023-03-19 15:43:40 -03:00
Rafael Vargas
8f18ef3f2e Fixing errors when closing the player info and trying to access it again 2023-03-14 00:25:33 -03:00
Rafael Vargas
db405285ac Adding flag 2023-02-22 19:18:54 -03:00
Rafael Vargas
030e6bd7e5 Modifying prints 2023-02-22 19:16:14 -03:00
Rafael Vargas
97996a1647 Trying to fix error 2023-02-21 19:35:09 -03:00
Rafael Vargas
47cb9ec8f5 Updating readme 2023-02-20 02:58:41 -03:00
Rafael Vargas
ecc0f32447 Updating issue report template 2023-02-20 01:22:48 -04:00
Rafael Vargas
72c65d86d8 Adding issues template 2023-02-20 01:20:45 -04:00
Rafael Vargas
cf8f13d1d9 Merge pull request #37 from RafaelSolVargas/creatingThreadsManager
Adding ThreadPlayer
2023-02-20 01:11:39 -04:00
Rafael Vargas
a34a6a78d7 Finishing to add the ThreadPlayer to the Bot, allowing the user to switch between the versions 2023-02-20 01:52:59 -03:00
Rafael Vargas
1f45b64a62 Sending more code 2023-02-19 13:40:37 -03:00
Rafael Vargas
7d53840f87 Fixing error 2023-02-19 11:53:58 -03:00
Rafael Vargas
72043c4475 Continuing the refactoring 2023-02-19 11:28:33 -03:00
Rafael Vargas
7a5d76ffd3 Refactoring some code and modifying variables and class names 2023-02-19 00:33:31 -03:00
Rafael Vargas
ca75e1823f Upgrading yt-dlp version 2023-02-18 14:52:30 -03:00
Rafael Vargas
8dc68b0898 Fixing error 35 2023-02-08 23:27:57 -03:00
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
49 changed files with 1772 additions and 709 deletions

7
.env.example Normal file
View File

@@ -0,0 +1,7 @@
BOT_TOKEN=Your_Own_Bot_Token
# If you do not want to use spotify, remove the 2 following lines
SPOTIFY_ID=Your_Own_Spotify_ID
SPOTIFY_SECRET=Your_Own_Spotify_Secret
BOT_PREFIX=Your_Wanted_Prefix_For_Vulkan
SHOULD_AUTO_DISCONNECT_WHEN_ALONE=True #all settings can be set like this
#etc... All settings can be set this way

33
.github/Docs/DOCKER.md vendored Normal file
View File

@@ -0,0 +1,33 @@
<h1 align="center">Docker</h1>
This bot can be easily deployed using Docker.
## **Requirements**
To run this project in a Docker container you must have Docker and Docker Compose installed in your machine. Find how to install Docker in your machine [here](https://docs.docker.com/get-docker/) and find how to install Docker Compose in your machine [here](https://docs.docker.com/compose/install/).
Once you have Docker and Docker Compose installed in your machine, clone or download this repository and follow the instructions below.
## **Running the Bot**
To run the bot in a Docker container, you must first create a `.env` file in the root of the project if there isn't one already. You will need to change the parameters in the `.env` file to your own parameters. You can find an example of a `.env` file [here](.env.example). You will also be able to change the settings in that environment file as explained in the [Settings page](.github/Docs/SETTINGS.md).
**If any of the following commands fail, try without a dash between docker and compose.**
To run the bot, simply execute the following command in the root of the project:
```bash
docker-compose up -d
```
This will build the Docker image and run the bot in a Docker container. The `-d` flag is used to run the container in detached mode, which means that the container will run in the background. If you want to see the logs of the container, you can run the following command:
```bash
docker-compose logs -f
```
To stop the container, run the following command:
```bash
docker-compose down
```
## **Updating the Bot**
To update the bot, you must first stop the container as explained in the previous section. Then, you must pull the latest changes from the repository, in any way you want. Finally, you must build the Docker image again and run the container again. To do this, run the following commands:
```bash
docker-compose build
docker-compose up -d
```

View File

@@ -1,6 +1,8 @@
<h1 align="center">Configuring Heroku</h1> <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: To configure the Vulkan to run in your Heroku account you will need to:
- Create an application project in Heroku. - Create an application project in Heroku.

47
.github/Docs/SETTINGS.md vendored Normal file
View File

@@ -0,0 +1,47 @@
<h1 align="center">Settings</h1>
## Important Parameters
- Bot Prefix
- Auto Disconnect When Alone
- Specific Process for each Player
- Maximum songs downloading at a time
- Maximum songs in a Queue Page
All parameters can be modified in the .env file or in an environment variable.
Here is a sample of the .env file:
```env
BOT_TOKEN=Your_Own_Bot_Token
SPOTIFY_ID=Your_Own_Spotify_ID
SPOTIFY_SECRET=Your_Own_Spotify_Secret
BOT_PREFIX=Your_Wanted_Prefix_For_Vulkan
SHOULD_AUTO_DISCONNECT_WHEN_ALONE=True #all settings can be set like this
#etc... All settings can be set this way
```
### **Bot Prefix**
The Bot Prefix is just a string that must be passed as prefix when calling any Bot command from the Discord.
To change that you must: <br>
- Change the property BOT_PREFIX in the .env file or in an environment variable to what you want to.
### **Auto Disconnect**
As a result of the [Issue 33](https://github.com/RafaelSolVargas/Vulkan/issues/33) 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 in the .env file or in an environment variable to False
### **Multiprocessing or Threading**
As a result of the [Issue 35](https://github.com/RafaelSolVargas/Vulkan/issues/35) you can configure if the Bot will create a specific Python Process for each Player (Guild) that he is playing songs or all will happen in the Main Process. The Default behavior is to create a new process.
To change that you must: <br>
- Change the property SONG_PLAYBACK_IN_SEPARATE_PROCESS in the .env file or in an environment variable to False
### **Maximum Downloading Quant**
The download of songs can be very fast or very slow, the faster it is the slower the response time for any command (during the download) is higher, (including the playback quality), because there will be a Task for each song. But it's possible to set up this variable to slow the download and keep the response time better.
To change that you must: <br>
- Change the property MAX_DOWNLOAD_SONGS_AT_A_TIME in the .env file or in an environment variable to what you want to.
### **Maximum Songs In Queue Page**
When the ```Queue``` command is called, the current song playlist is presented in the Discord, you can configure how many songs you will want to show in each page.
To change that you must: <br>
- Change the property MAX_SONGS_IN_PAGE in the .env file or in an environment variable to what you want to.

17
.github/ISSUE_TEMPLATE/bug.md vendored Normal file
View File

@@ -0,0 +1,17 @@
---
name: Report Any Issues
about: Create a report to help us improve
title: Bug
labels: bug
assignees: RafaelSolVargas
---
**Describe your context**
The Vulkan contains some configuration parameters set up in the Configs/Config.py file, tell us what values you are using.
**The path to the bug**
Don't forget to specify how you found the issue, what Discord commands you used and which was the parameters passed.
**Screenshots**
Screenshots of the error thrown or the Bot messages will help us to fix you issue faster.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -1,47 +1,74 @@
import os import os
from decouple import config from dotenv import load_dotenv
from Config.Singleton import Singleton from Config.Singleton import Singleton
from Config.Folder import Folder from Config.Folder import Folder
load_dotenv()
class VConfigs(Singleton): class VConfigs(Singleton):
def __init__(self) -> None: def __init__(self) -> None:
if not super().created: if not super().created:
self.BOT_PREFIX = '!' # You can change this boolean to False if you want to prevent the Bot from auto disconnecting
try: # Resolution for the issue: https://github.com/RafaelSolVargas/Vulkan/issues/33
self.BOT_TOKEN = config('BOT_TOKEN') self.SHOULD_AUTO_DISCONNECT_WHEN_ALONE = os.getenv('SHOULD_AUTO_DISCONNECT_WHEN_ALONE') == 'True'
self.SPOTIFY_ID = config('SPOTIFY_ID')
self.SPOTIFY_SECRET = config('SPOTIFY_SECRET') # Recommended to be True, except in cases when your Bot is present in thousands servers, in that case
self.BOT_PREFIX = config('BOT_PREFIX') # the delay to start a new Python process for the playback is too much, and to avoid that you set as False
except: # This feature is for now in testing period, for a more stable version, keep this boolean = Trued
print( self.SONG_PLAYBACK_IN_SEPARATE_PROCESS = os.getenv('SONG_PLAYBACK_IN_SEPARATE_PROCESS', 'True') == 'True'
'[ERROR] -> You must create and .env file with all required fields, see documentation for help')
self.CLEANER_MESSAGES_QUANT = 5 # Maximum of songs that will be downloaded at once, the higher this number is, the faster the songs will be all available
self.ACQUIRE_LOCK_TIMEOUT = 10 # but the slower will be the others commands of the Bot during the downloading time, for example, the playback quality
self.QUEUE_VIEW_TIMEOUT = 120 self.MAX_DOWNLOAD_SONGS_AT_A_TIME = int(os.getenv('MAX_DOWNLOAD_SONGS_AT_A_TIME', 5))
self.COMMANDS_FOLDER_NAME = 'DiscordCogs'
self.BOT_PREFIX = os.getenv('BOT_PREFIX', '!')
if self.BOT_PREFIX == 'Your_Wanted_Prefix_For_Vulkan':
self.BOT_PREFIX = '!'
self.BOT_TOKEN = os.getenv('BOT_TOKEN')
if self.BOT_TOKEN is None:
raise ValueError('No token was given')
self.SPOTIFY_ID = os.getenv('SPOTIFY_ID')
self.SPOTIFY_SECRET = os.getenv('SPOTIFY_SECRET')
if self.SPOTIFY_ID == "Your_Own_Spotify_ID":
self.SPOTIFY_ID = None
if self.SPOTIFY_SECRET == "Your_Own_Spotify_Secret":
self.SPOTIFY_SECRET = None
if self.SPOTIFY_ID is None or self.SPOTIFY_SECRET is None:
print('Spotify will not work')
self.CLEANER_MESSAGES_QUANT = int(os.getenv('CLEANER_MESSAGES_QUANT', 5))
self.ACQUIRE_LOCK_TIMEOUT = int(os.getenv('ACQUIRE_LOCK_TIMEOUT', 10))
self.QUEUE_VIEW_TIMEOUT = int(os.getenv('QUEUE_VIEW_TIMEOUT', 120))
self.COMMANDS_FOLDER_NAME = os.getenv('COMMANDS_FOLDER_NAME', 'DiscordCogs')
self.COMMANDS_PATH = f'{Folder().rootFolder}{self.COMMANDS_FOLDER_NAME}' self.COMMANDS_PATH = f'{Folder().rootFolder}{self.COMMANDS_FOLDER_NAME}'
self.VC_TIMEOUT = 300 self.VC_TIMEOUT = int(os.getenv('VC_TIMEOUT', 300))
self.CHANCE_SHOW_PROJECT = 15 self.CHANCE_SHOW_PROJECT = int(os.getenv('CHANCE_SHOW_PROJECT', 15))
self.PROJECT_URL = 'https://github.com/RafaelSolVargas/Vulkan' self.PROJECT_URL = os.getenv('PROJECT_URL', 'https://github.com/RafaelSolVargas/Vulkan')
self.SUPPORTING_ICON = 'https://i.pinimg.com/originals/d6/05/b4/d605b4f8c5d1c6ae20dc353ef9f091bd.png' self.SUPPORTING_ICON = os.getenv('SUPPORTING_ICON', 'https://i.pinimg.com/originals/d6/05/b4/d605b4f8c5d1c6ae20dc353ef9f091bd.png')
self.MAX_PLAYLIST_LENGTH = 50 self.MAX_PLAYLIST_LENGTH = int(os.getenv('MAX_PLAYLIST_LENGTH', 50))
self.MAX_PLAYLIST_FORCED_LENGTH = 5 self.MAX_PLAYLIST_FORCED_LENGTH = int(os.getenv('MAX_PLAYLIST_FORCED_LENGTH', 5))
self.MAX_SONGS_IN_PAGE = 10 self.MAX_SONGS_IN_PAGE = int(os.getenv('MAX_SONGS_IN_PAGE', 10))
self.MAX_PRELOAD_SONGS = 15 self.MAX_PRELOAD_SONGS = int(os.getenv('MAX_PRELOAD_SONGS', 15))
self.MAX_SONGS_HISTORY = 15 self.MAX_SONGS_HISTORY = int(os.getenv('MAX_SONGS_HISTORY', 15))
self.INVITE_MESSAGE = """To invite Vulkan to your own server, click [here]({}). self.INVITE_MESSAGE = os.getenv('INVITE_MESSAGE', """To invite Vulkan to your own server, click [here]({}).
Or use this direct URL: {}""" Or use this direct URL: {}""")
self.MY_ERROR_BAD_COMMAND = 'This string serves to verify if some error was raised by myself on purpose' self.MY_ERROR_BAD_COMMAND = os.getenv('MY_ERROR_BAD_COMMAND', 'This string serves to verify if some error was raised by myself on purpose')
self.INVITE_URL = 'https://discordapp.com/oauth2/authorize?client_id={}&scope=bot' self.INVITE_URL = os.getenv('INVITE_URL', 'https://discordapp.com/oauth2/authorize?client_id={}&permissions=8&scope=bot')
def getProcessManager(self): def getPlayersManager(self):
return self.__manager return self.__manager
def setProcessManager(self, newManager): def setPlayersManager(self, newManager):
self.__manager = newManager self.__manager = newManager

View File

@@ -343,6 +343,13 @@ class VEmbeds:
description=self.__messages.PLAYER_NOT_PLAYING, description=self.__messages.PLAYER_NOT_PLAYING,
colour=self.__colors.BLUE) colour=self.__colors.BLUE)
return embed return embed
def VOLUME_CHANGED(self, volume: float) -> Embed:
embed = Embed(
title=self.__messages.SONG_PLAYER,
description=self.__messages.VOLUME_CHANGED.format(volume),
colour=self.__colors.BLUE)
return embed
def QUEUE(self, title: str, description: str) -> Embed: def QUEUE(self, title: str, description: str) -> Embed:
embed = Embed( embed = Embed(

View File

@@ -29,7 +29,9 @@ class Helper(Singleton):
self.HELP_PREV_LONG = 'Play the previous song. If playing, the current song will return to queue.\n\nRequire: Loop to be disable.\nArguments: None.' self.HELP_PREV_LONG = 'Play the previous song. If playing, the current song will return to queue.\n\nRequire: Loop to be disable.\nArguments: None.'
self.HELP_SHUFFLE = 'Shuffle the songs playing.' self.HELP_SHUFFLE = 'Shuffle the songs playing.'
self.HELP_SHUFFLE_LONG = 'Randomly shuffle the songs in the queue.\n\nArguments: None.' self.HELP_SHUFFLE_LONG = 'Randomly shuffle the songs in the queue.\n\nArguments: None.'
self.HELP_PLAY = 'Plays a song from URL' self.HELP_PLAY = 'Plays a song from URL.'
self.CHANGE_VOLUME = 'Set the volume of the song.'
self.CHANGE_VOLUME_LONG = 'Change the volume of the song, expect a number from 0 to 100.'
self.HELP_PLAY_LONG = 'Play a song in discord. \n\nRequire: You to be connected to a voice channel.\nArguments: Youtube, Spotify or Deezer song/playlist link or the title of the song to be searched in Youtube.' self.HELP_PLAY_LONG = 'Play a song in discord. \n\nRequire: You to be connected to a voice channel.\nArguments: Youtube, Spotify or Deezer song/playlist link or the title of the song to be searched in Youtube.'
self.HELP_HISTORY = f'Show the history of played songs.' self.HELP_HISTORY = f'Show the history of played songs.'
self.HELP_HISTORY_LONG = f'Show the last {config.MAX_SONGS_HISTORY} played songs' self.HELP_HISTORY_LONG = f'Show the last {config.MAX_SONGS_HISTORY} played songs'

View File

@@ -16,6 +16,7 @@ class Messages(Singleton):
self.SONGINFO_REQUESTER = 'Requester: ' self.SONGINFO_REQUESTER = 'Requester: '
self.SONGINFO_POSITION = 'Position: ' self.SONGINFO_POSITION = 'Position: '
self.VOLUME_CHANGED = 'Song volume changed to `{}`%'
self.SONGS_ADDED = 'Downloading `{}` songs to add to the queue' self.SONGS_ADDED = 'Downloading `{}` songs to add to the queue'
self.SONG_ADDED = 'Downloading the song `{}` to add to the queue' self.SONG_ADDED = 'Downloading the song `{}` to add to the queue'
self.SONG_ADDED_TWO = f'{self.__emojis.MUSIC} Song added to the queue' self.SONG_ADDED_TWO = f'{self.__emojis.MUSIC} Song added to the queue'
@@ -56,6 +57,7 @@ class Messages(Singleton):
self.ERROR_MOVING = f'{self.__emojis.ERROR} Error while moving the songs' self.ERROR_MOVING = f'{self.__emojis.ERROR} Error while moving the songs'
self.LENGTH_ERROR = f'{self.__emojis.ERROR} Numbers must be between 1 and queue length, use -1 for the last song' self.LENGTH_ERROR = f'{self.__emojis.ERROR} Numbers must be between 1 and queue length, use -1 for the last song'
self.ERROR_NUMBER = f'{self.__emojis.ERROR} This command require a number' self.ERROR_NUMBER = f'{self.__emojis.ERROR} This command require a number'
self.ERROR_VOLUME_NUMBER = f'{self.__emojis.ERROR} This command require a number between 0 and 100'
self.ERROR_PLAYING = f'{self.__emojis.ERROR} Error while playing songs' self.ERROR_PLAYING = f'{self.__emojis.ERROR} Error while playing songs'
self.COMMAND_NOT_FOUND = f'{self.__emojis.ERROR} Command not found, type {configs.BOT_PREFIX}help to see all commands' self.COMMAND_NOT_FOUND = f'{self.__emojis.ERROR} Command not found, type {configs.BOT_PREFIX}help to see all commands'
self.UNKNOWN_ERROR = f'{self.__emojis.ERROR} Unknown Error, if needed, use {configs.BOT_PREFIX}reset to reset the player of your server' self.UNKNOWN_ERROR = f'{self.__emojis.ERROR} Unknown Error, if needed, use {configs.BOT_PREFIX}reset to reset the player of your server'

View File

@@ -21,7 +21,7 @@ class ControlCog(Cog):
'MUSIC': ['resume', 'pause', 'loop', 'stop', 'MUSIC': ['resume', 'pause', 'loop', 'stop',
'skip', 'play', 'queue', 'clear', 'skip', 'play', 'queue', 'clear',
'np', 'shuffle', 'move', 'remove', 'np', 'shuffle', 'move', 'remove',
'reset', 'prev', 'history'], 'reset', 'prev', 'history', 'volume'],
'RANDOM': ['choose', 'cara', 'random'] 'RANDOM': ['choose', 'cara', 'random']
} }
@@ -74,7 +74,8 @@ class ControlCog(Cog):
colour=self.__colors.BLUE colour=self.__colors.BLUE
) )
embedhelp.set_thumbnail(url=self.__bot.user.avatar) if self.__bot.user.avatar != None:
embedhelp.set_thumbnail(url=self.__bot.user.avatar)
await ctx.send(embed=embedhelp) await ctx.send(embed=embedhelp)
@command(name='invite', help=helper.HELP_INVITE, description=helper.HELP_INVITE_LONG, aliases=['convite', 'inv', 'convidar']) @command(name='invite', help=helper.HELP_INVITE, description=helper.HELP_INVITE_LONG, aliases=['convite', 'inv', 'convidar'])

View File

@@ -17,13 +17,15 @@ from Handlers.ResumeHandler import ResumeHandler
from Handlers.HistoryHandler import HistoryHandler from Handlers.HistoryHandler import HistoryHandler
from Handlers.QueueHandler import QueueHandler from Handlers.QueueHandler import QueueHandler
from Handlers.LoopHandler import LoopHandler from Handlers.LoopHandler import LoopHandler
from Handlers.VolumeHandler import VolumeHandler
from Messages.MessagesCategory import MessagesCategory from Messages.MessagesCategory import MessagesCategory
from Messages.Responses.EmoteCogResponse import EmoteCommandResponse from Messages.Responses.EmoteCogResponse import EmoteCommandResponse
from Messages.Responses.EmbedCogResponse import EmbedCommandResponse from Messages.Responses.EmbedCogResponse import EmbedCommandResponse
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
from Config.Configs import VConfigs from Config.Configs import VConfigs
from Config.Embeds import VEmbeds from Config.Embeds import VEmbeds
from Parallelism.ProcessManager import ProcessManager from Parallelism.ProcessPlayerManager import ProcessPlayerManager
from Parallelism.ThreadPlayerManager import ThreadPlayerManager
helper = Helper() helper = Helper()
@@ -38,7 +40,11 @@ class MusicCog(Cog):
def __init__(self, bot: VulkanBot) -> None: def __init__(self, bot: VulkanBot) -> None:
self.__bot: VulkanBot = bot self.__bot: VulkanBot = bot
self.__embeds = VEmbeds() self.__embeds = VEmbeds()
VConfigs().setProcessManager(ProcessManager(bot)) configs = VConfigs()
if configs.SONG_PLAYBACK_IN_SEPARATE_PROCESS:
configs.setPlayersManager(ProcessPlayerManager(bot))
else:
configs.setPlayersManager(ThreadPlayerManager(bot))
@command(name="play", help=helper.HELP_PLAY, description=helper.HELP_PLAY_LONG, aliases=['p', 'tocar']) @command(name="play", help=helper.HELP_PLAY, description=helper.HELP_PLAY_LONG, aliases=['p', 'tocar'])
async def play(self, ctx: Context, *args) -> None: async def play(self, ctx: Context, *args) -> None:
@@ -59,6 +65,25 @@ class MusicCog(Cog):
except Exception as e: except Exception as e:
print(f'[ERROR IN COG] -> {e}') print(f'[ERROR IN COG] -> {e}')
@command(name="volume", help=helper.CHANGE_VOLUME, description=helper.CHANGE_VOLUME_LONG, aliases=['v'])
async def volume(self, ctx: Context, *args) -> None:
try:
controller = VolumeHandler(ctx, self.__bot)
if len(args) > 1:
track = " ".join(args)
else:
track = args[0]
response = await controller.run(track)
if response is not None:
cogResponser1 = EmbedCommandResponse(response, MessagesCategory.PLAYER)
cogResponser2 = EmoteCommandResponse(response, MessagesCategory.PLAYER)
await cogResponser1.run()
await cogResponser2.run()
except Exception as e:
print(f'[ERROR IN COG] -> {e}')
@command(name="queue", help=helper.HELP_QUEUE, description=helper.HELP_QUEUE_LONG, aliases=['q', 'fila', 'musicas']) @command(name="queue", help=helper.HELP_QUEUE, description=helper.HELP_QUEUE_LONG, aliases=['q', 'fila', 'musicas'])
async def queue(self, ctx: Context, *args) -> None: async def queue(self, ctx: Context, *args) -> None:
try: try:

View File

@@ -15,6 +15,7 @@ from Handlers.ResumeHandler import ResumeHandler
from Handlers.HistoryHandler import HistoryHandler from Handlers.HistoryHandler import HistoryHandler
from Handlers.QueueHandler import QueueHandler from Handlers.QueueHandler import QueueHandler
from Handlers.LoopHandler import LoopHandler from Handlers.LoopHandler import LoopHandler
from Handlers.VolumeHandler import VolumeHandler
from Messages.MessagesCategory import MessagesCategory from Messages.MessagesCategory import MessagesCategory
from Messages.Responses.SlashEmbedResponse import SlashEmbedResponse from Messages.Responses.SlashEmbedResponse import SlashEmbedResponse
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
@@ -237,6 +238,22 @@ class SlashCommands(Cog):
except Exception: except Exception:
print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}') print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}')
@slash_command(name='volume', description=helper.CHANGE_VOLUME_LONG)
async def move(self, ctx: ApplicationContext,
volume: Option(float, "The new volume of the song", min_value=1, default= 100)) -> None:
if not self.__bot.listingSlash:
return
try:
await ctx.defer()
controller = VolumeHandler(ctx, self.__bot)
response = await controller.run(f'{volume}')
cogResponser = SlashEmbedResponse(response, ctx, MessagesCategory.PLAYER)
await cogResponser.run()
except Exception:
print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}')
@slash_command(name='remove', description=helper.HELP_REMOVE) @slash_command(name='remove', description=helper.HELP_REMOVE)
async def remove(self, ctx: ApplicationContext, async def remove(self, ctx: ApplicationContext,
position: Option(int, "The song position to remove", min_value=1)) -> None: position: Option(int, "The song position to remove", min_value=1)) -> None:

11
Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM python:3.10.0
ENV PYTHONUNBUFFERED=1
WORKDIR /app
RUN git clone https://github.com/RafaelSolVargas/Vulkan.git /app
RUN pip install -r requirements.txt
RUN apt-get update && apt-get install -y software-properties-common && apt-get install -y ffmpeg
RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser
CMD ["python", "main.py"]

View File

@@ -4,7 +4,7 @@ from discord.ext.commands import Context
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
from Handlers.AbstractHandler import AbstractHandler from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse from Handlers.HandlerResponse import HandlerResponse
from Parallelism.ProcessInfo import ProcessInfo from Parallelism.AbstractProcessManager import AbstractPlayersManager
class ClearHandler(AbstractHandler): class ClearHandler(AbstractHandler):
@@ -13,19 +13,18 @@ class ClearHandler(AbstractHandler):
async def run(self) -> HandlerResponse: async def run(self) -> HandlerResponse:
# Get the current process of the guild # Get the current process of the guild
processManager = self.config.getProcessManager() playersManager: AbstractPlayersManager = self.config.getPlayersManager()
processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild) if playersManager.verifyIfPlayerExists(self.guild):
if processInfo:
# Clear the playlist # Clear the playlist
playlist = processInfo.getPlaylist() playlist = playersManager.getPlayerPlaylist(self.guild)
processLock = processInfo.getLock() playerLock = playersManager.getPlayerLock(self.guild)
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) acquired = playerLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired: if acquired:
playlist.clear() playlist.clear()
processLock.release() playerLock.release()
embed = self.embeds.PLAYLIST_CLEAR() embed = self.embeds.PLAYLIST_CLEAR()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)
else: else:
processManager.resetProcess(self.guild, self.ctx) playersManager.resetPlayer(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED() embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)

View File

@@ -2,6 +2,7 @@ from discord.ext.commands import Context
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
from Handlers.AbstractHandler import AbstractHandler from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse from Handlers.HandlerResponse import HandlerResponse
from Parallelism.AbstractProcessManager import AbstractPlayersManager
from Utils.Utils import Utils from Utils.Utils import Utils
from typing import Union from typing import Union
from discord import Interaction from discord import Interaction
@@ -13,18 +14,16 @@ class HistoryHandler(AbstractHandler):
async def run(self) -> HandlerResponse: async def run(self) -> HandlerResponse:
# Get the current process of the guild # Get the current process of the guild
processManager = self.config.getProcessManager() playersManager: AbstractPlayersManager = self.config.getPlayersManager()
processInfo = processManager.getRunningPlayerInfo(self.guild) if playersManager.verifyIfPlayerExists(self.guild):
if processInfo: playerLock = playersManager.getPlayerLock(self.guild)
processLock = processInfo.getLock() acquired = playerLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired: if acquired:
playlist = processInfo.getPlaylist() history = playersManager.getPlayerPlaylist(self.guild).getSongsHistory()
history = playlist.getSongsHistory() playerLock.release()
processLock.release()
else: else:
# If the player doesn't respond in time we restart it # If the player doesn't respond in time we restart it
processManager.resetProcess(self.guild, self.ctx) playersManager.resetPlayer(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED() embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)
else: else:

View File

@@ -6,6 +6,7 @@ from discord import Interaction
from Handlers.HandlerResponse import HandlerResponse from Handlers.HandlerResponse import HandlerResponse
from Music.Playlist import Playlist from Music.Playlist import Playlist
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
from Parallelism.AbstractProcessManager import AbstractPlayersManager
from Parallelism.Commands import VCommands, VCommandsType from Parallelism.Commands import VCommands, VCommandsType
@@ -16,32 +17,31 @@ class JumpMusicHandler(AbstractHandler):
super().__init__(ctx, bot) super().__init__(ctx, bot)
async def run(self, musicPos: str) -> HandlerResponse: async def run(self, musicPos: str) -> HandlerResponse:
processManager = self.config.getProcessManager() playersManager: AbstractPlayersManager = self.config.getPlayersManager()
processInfo = processManager.getRunningPlayerInfo(self.guild) if not playersManager.verifyIfPlayerExists(self.guild):
if not processInfo:
embed = self.embeds.NOT_PLAYING() embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage() error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error) return HandlerResponse(self.ctx, embed, error)
processLock = processInfo.getLock() playerLock = playersManager.getPlayerLock(self.guild)
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) acquired = playerLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired: if acquired:
# Try to convert input to int # Try to convert input to int
error = self.__validateInput(musicPos) error = self.__validateInput(musicPos)
if error: if error:
embed = self.embeds.ERROR_EMBED(error.message) embed = self.embeds.ERROR_EMBED(error.message)
processLock.release() playerLock.release()
return HandlerResponse(self.ctx, embed, error) return HandlerResponse(self.ctx, embed, error)
# Sanitize the input # Sanitize the input
playlist: Playlist = processInfo.getPlaylist() playlist = playersManager.getPlayerPlaylist(self.guild)
musicPos = self.__sanitizeInput(playlist, musicPos) musicPos = self.__sanitizeInput(playlist, musicPos)
# Validate the position # Validate the position
if not playlist.validate_position(musicPos): if not playlist.validate_position(musicPos):
error = InvalidInput() error = InvalidInput()
embed = self.embeds.PLAYLIST_RANGE_ERROR() embed = self.embeds.PLAYLIST_RANGE_ERROR()
processLock.release() playerLock.release()
return HandlerResponse(self.ctx, embed, error) return HandlerResponse(self.ctx, embed, error)
try: try:
# Move the selected song # Move the selected song
@@ -49,19 +49,17 @@ class JumpMusicHandler(AbstractHandler):
# Send a command to the player to skip the music # Send a command to the player to skip the music
command = VCommands(VCommandsType.SKIP, None) command = VCommands(VCommandsType.SKIP, None)
queue = processInfo.getQueueToPlayer() await playersManager.sendCommandToPlayer(command, self.guild, self.ctx)
queue.put(command)
processLock.release()
return HandlerResponse(self.ctx) return HandlerResponse(self.ctx)
except: except:
# Release the acquired Lock
processLock.release()
embed = self.embeds.ERROR_MOVING() embed = self.embeds.ERROR_MOVING()
error = UnknownError() error = UnknownError()
return HandlerResponse(self.ctx, embed, error) return HandlerResponse(self.ctx, embed, error)
finally:
playerLock.release()
else: else:
processManager.resetProcess(self.guild, self.ctx) playersManager.resetPlayer(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED() embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)

View File

@@ -5,6 +5,7 @@ from Handlers.HandlerResponse import HandlerResponse
from Config.Exceptions import BadCommandUsage from Config.Exceptions import BadCommandUsage
from typing import Union from typing import Union
from discord import Interaction from discord import Interaction
from Parallelism.AbstractProcessManager import AbstractPlayersManager
class LoopHandler(AbstractHandler): class LoopHandler(AbstractHandler):
@@ -12,23 +13,20 @@ class LoopHandler(AbstractHandler):
super().__init__(ctx, bot) super().__init__(ctx, bot)
async def run(self, args: str) -> HandlerResponse: async def run(self, args: str) -> HandlerResponse:
# Get the current process of the guild playersManager: AbstractPlayersManager = self.config.getPlayersManager()
processManager = self.config.getProcessManager() if not playersManager.verifyIfPlayerExists(self.guild):
processInfo = processManager.getRunningPlayerInfo(self.guild)
if not processInfo:
embed = self.embeds.NOT_PLAYING() embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage() error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error) return HandlerResponse(self.ctx, embed, error)
playlist = processInfo.getPlaylist() playlist = playersManager.getPlayerPlaylist(self.guild)
playerLock = playersManager.getPlayerLock(self.guild)
processLock = processInfo.getLock() acquired = playerLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired: if acquired:
if args == '' or args is None: if args == '' or args is None:
playlist.loop_all() playlist.loop_all()
embed = self.embeds.LOOP_ALL_ACTIVATED() embed = self.embeds.LOOP_ALL_ACTIVATED()
processLock.release() playerLock.release()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)
args = args.lower() args = args.lower()
@@ -51,9 +49,9 @@ class LoopHandler(AbstractHandler):
error = BadCommandUsage() error = BadCommandUsage()
embed = self.embeds.BAD_LOOP_USE() embed = self.embeds.BAD_LOOP_USE()
processLock.release() playerLock.release()
return HandlerResponse(self.ctx, embed, error) return HandlerResponse(self.ctx, embed, error)
else: else:
processManager.resetProcess(self.guild, self.ctx) playersManager.resetPlayer(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED() embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)

View File

@@ -7,6 +7,7 @@ from Config.Exceptions import BadCommandUsage, VulkanError, InvalidInput, Number
from Music.Playlist import Playlist from Music.Playlist import Playlist
from typing import Union from typing import Union
from discord import Interaction from discord import Interaction
from Parallelism.AbstractProcessManager import AbstractPlayersManager
class MoveHandler(AbstractHandler): class MoveHandler(AbstractHandler):
@@ -14,45 +15,44 @@ class MoveHandler(AbstractHandler):
super().__init__(ctx, bot) super().__init__(ctx, bot)
async def run(self, pos1: str, pos2: str) -> HandlerResponse: async def run(self, pos1: str, pos2: str) -> HandlerResponse:
processManager = self.config.getProcessManager() playersManager: AbstractPlayersManager = self.config.getPlayersManager()
processInfo = processManager.getRunningPlayerInfo(self.guild) if not playersManager.verifyIfPlayerExists(self.guild):
if not processInfo:
embed = self.embeds.NOT_PLAYING() embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage() error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error) return HandlerResponse(self.ctx, embed, error)
processLock = processInfo.getLock() playerLock = playersManager.getPlayerLock(self.guild)
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) acquired = playerLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired: if acquired:
error = self.__validateInput(pos1, pos2) error = self.__validateInput(pos1, pos2)
if error: if error:
embed = self.embeds.ERROR_EMBED(error.message) embed = self.embeds.ERROR_EMBED(error.message)
processLock.release() playerLock.release()
return HandlerResponse(self.ctx, embed, error) return HandlerResponse(self.ctx, embed, error)
playlist = processInfo.getPlaylist() playlist = playersManager.getPlayerPlaylist(self.guild)
pos1, pos2 = self.__sanitizeInput(playlist, pos1, pos2) pos1, pos2 = self.__sanitizeInput(playlist, pos1, pos2)
if not playlist.validate_position(pos1) or not playlist.validate_position(pos2): if not playlist.validate_position(pos1) or not playlist.validate_position(pos2):
error = InvalidInput() error = InvalidInput()
embed = self.embeds.PLAYLIST_RANGE_ERROR() embed = self.embeds.PLAYLIST_RANGE_ERROR()
processLock.release() playerLock.release()
return HandlerResponse(self.ctx, embed, error) return HandlerResponse(self.ctx, embed, error)
try: try:
song = playlist.move_songs(pos1, pos2) song = playlist.move_songs(pos1, pos2)
song_name = song.title if song.title else song.identifier song_name = song.title if song.title else song.identifier
embed = self.embeds.SONG_MOVED(song_name, pos1, pos2) embed = self.embeds.SONG_MOVED(song_name, pos1, pos2)
processLock.release() playerLock.release()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)
except: except:
# Release the acquired Lock # Release the acquired Lock
processLock.release() playerLock.release()
embed = self.embeds.ERROR_MOVING() embed = self.embeds.ERROR_MOVING()
error = UnknownError() error = UnknownError()
return HandlerResponse(self.ctx, embed, error) return HandlerResponse(self.ctx, embed, error)
else: else:
processManager.resetProcess(self.guild, self.ctx) playersManager.resetPlayer(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED() embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)

View File

@@ -2,6 +2,7 @@ from discord.ext.commands import Context
from Handlers.AbstractHandler import AbstractHandler from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse from Handlers.HandlerResponse import HandlerResponse
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
from Parallelism.AbstractProcessManager import AbstractPlayersManager
from Utils.Cleaner import Cleaner from Utils.Cleaner import Cleaner
from typing import Union from typing import Union
from discord import Interaction from discord import Interaction
@@ -13,14 +14,12 @@ class NowPlayingHandler(AbstractHandler):
self.__cleaner = Cleaner() self.__cleaner = Cleaner()
async def run(self) -> HandlerResponse: async def run(self) -> HandlerResponse:
# Get the current process of the guild playersManager: AbstractPlayersManager = self.config.getPlayersManager()
processManager = self.config.getProcessManager() if not playersManager.verifyIfPlayerExists(self.guild):
processInfo = processManager.getRunningPlayerInfo(self.guild)
if not processInfo:
embed = self.embeds.NOT_PLAYING() embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)
playlist = processInfo.getPlaylist() playlist = playersManager.getPlayerPlaylist(self.guild)
if playlist.getCurrentSong() is None: if playlist.getCurrentSong() is None:
embed = self.embeds.NOT_PLAYING() embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)

View File

@@ -1,8 +1,8 @@
from discord.ext.commands import Context from discord.ext.commands import Context
from Handlers.AbstractHandler import AbstractHandler from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse from Handlers.HandlerResponse import HandlerResponse
from Parallelism.AbstractProcessManager import AbstractPlayersManager
from Parallelism.Commands import VCommands, VCommandsType from Parallelism.Commands import VCommands, VCommandsType
from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
from typing import Union from typing import Union
from discord import Interaction from discord import Interaction
@@ -13,17 +13,10 @@ class PauseHandler(AbstractHandler):
super().__init__(ctx, bot) super().__init__(ctx, bot)
async def run(self) -> HandlerResponse: async def run(self) -> HandlerResponse:
processManager = self.config.getProcessManager() playersManager: AbstractPlayersManager = self.config.getPlayersManager()
processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild) if playersManager.verifyIfPlayerExists(self.guild):
if processInfo:
if processInfo.getStatus() == ProcessStatus.SLEEPING:
embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed)
# Send Pause command to be execute by player process
command = VCommands(VCommandsType.PAUSE, None) command = VCommands(VCommandsType.PAUSE, None)
queue = processInfo.getQueueToPlayer() await playersManager.sendCommandToPlayer(command, self.guild, self.ctx)
queue.put(command)
embed = self.embeds.PLAYER_PAUSED() embed = self.embeds.PLAYER_PAUSED()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)

View File

@@ -1,6 +1,6 @@
import asyncio import asyncio
import traceback import traceback
from typing import List from typing import List, Union
from Config.Exceptions import DownloadingError, InvalidInput, VulkanError from Config.Exceptions import DownloadingError, InvalidInput, VulkanError
from discord.ext.commands import Context from discord.ext.commands import Context
from Handlers.AbstractHandler import AbstractHandler from Handlers.AbstractHandler import AbstractHandler
@@ -9,10 +9,9 @@ from Handlers.HandlerResponse import HandlerResponse
from Music.Downloader import Downloader from Music.Downloader import Downloader
from Music.Searcher import Searcher from Music.Searcher import Searcher
from Music.Song import Song from Music.Song import Song
from Parallelism.ProcessInfo import ProcessInfo from Parallelism.AbstractProcessManager import AbstractPlayersManager
from Parallelism.Commands import VCommands, VCommandsType from Parallelism.Commands import VCommands, VCommandsType
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
from typing import Union
from discord import Interaction from discord import Interaction
@@ -35,13 +34,12 @@ class PlayHandler(AbstractHandler):
if musicsInfo is None or len(musicsInfo) == 0: if musicsInfo is None or len(musicsInfo) == 0:
raise InvalidInput(self.messages.INVALID_INPUT, self.messages.ERROR_TITLE) raise InvalidInput(self.messages.INVALID_INPUT, self.messages.ERROR_TITLE)
# Get the process context for the current guild # If there is no executing player for the guild then we create the player
processManager = self.config.getProcessManager() playersManager: AbstractPlayersManager = self.config.getPlayersManager()
processInfo = processManager.getOrCreatePlayerInfo(self.guild, self.ctx) if not playersManager.verifyIfPlayerExists(self.guild):
playlist = processInfo.getPlaylist() playersManager.createPlayerForGuild(self.guild, self.ctx)
process = processInfo.getProcess()
if not process.is_alive(): # If process has not yet started, start playlist = playersManager.getPlayerPlaylist(self.guild)
process.start()
# Create the Songs objects # Create the Songs objects
songs: List[Song] = [] songs: List[Song] = []
@@ -65,25 +63,24 @@ class PlayHandler(AbstractHandler):
embed = self.embeds.SONG_ADDED_TWO(song.info, pos) embed = self.embeds.SONG_ADDED_TWO(song.info, pos)
response = HandlerResponse(self.ctx, embed) response = HandlerResponse(self.ctx, embed)
# Add the unique song to the playlist and send a command to player process # Add the unique song to the playlist and send a command to player
processLock = processInfo.getLock() playerLock = playersManager.getPlayerLock(self.guild)
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) acquired = playerLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired: if acquired:
playlist.add_song(song) playlist.add_song(song)
# Release the acquired Lock # Release the acquired Lock
processLock.release() playerLock.release()
queue = processInfo.getQueueToPlayer()
playCommand = VCommands(VCommandsType.PLAY, None) playCommand = VCommands(VCommandsType.PLAY, None)
queue.put(playCommand) await playersManager.sendCommandToPlayer(playCommand, self.guild, self.ctx)
else: else:
processManager.resetProcess(self.guild, self.ctx) playersManager.resetPlayer(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED() embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)
return response return response
else: # If multiple songs added else: # If multiple songs added
# Trigger a task to download all songs and then store them in the process playlist # Trigger a task to download all songs and then store them in the playlist
asyncio.create_task(self.__downloadSongsAndStore(songs, processInfo)) asyncio.create_task(self.__downloadSongsInLots(songs, playersManager))
embed = self.embeds.SONGS_ADDED(len(songs)) embed = self.embeds.SONGS_ADDED(len(songs))
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)
@@ -92,43 +89,49 @@ class PlayHandler(AbstractHandler):
embed = self.embeds.DOWNLOADING_ERROR() embed = self.embeds.DOWNLOADING_ERROR()
return HandlerResponse(self.ctx, embed, error) return HandlerResponse(self.ctx, embed, error)
except Exception as error: except Exception as error:
if isinstance(error, VulkanError): # If error was already processed print(f'[ERROR IN PLAYHANDLER] -> {traceback.format_exc()}', {type(error)})
print( if isinstance(error, VulkanError):
f'DEVELOPER NOTE -s> PlayController Error: {traceback.format_exc()}', {type(error)})
embed = self.embeds.CUSTOM_ERROR(error) embed = self.embeds.CUSTOM_ERROR(error)
else: else:
print(
f'DEVELOPER NOTE -> PlayController Error: {traceback.format_exc()}, {type(error)}')
error = UnknownError() error = UnknownError()
embed = self.embeds.UNKNOWN_ERROR() embed = self.embeds.UNKNOWN_ERROR()
return HandlerResponse(self.ctx, embed, error) return HandlerResponse(self.ctx, embed, error)
async def __downloadSongsAndStore(self, songs: List[Song], processInfo: ProcessInfo) -> None: async def __downloadSongsInLots(self, songs: List[Song], playersManager: AbstractPlayersManager) -> None:
playlist = processInfo.getPlaylist() """
queue = processInfo.getQueueToPlayer() 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) playCommand = VCommands(VCommandsType.PLAY, None)
# Trigger a task for each song to be downloaded maxDownloads = self.config.MAX_DOWNLOAD_SONGS_AT_A_TIME
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 while len(songs) > 0:
processManager = self.config.getProcessManager() # Verify how many songs will be downloaded in this lot and extract from the songs list
for index, task in enumerate(tasks): songsQuant = min(maxDownloads, len(songs))
await task # Get the first quantInLot songs
song = songs[index] songsInLot = songs[:songsQuant]
if not song.problematic: # If downloaded add to the playlist and send play command # Remove the first quantInLot songs from the songs
processInfo = processManager.getOrCreatePlayerInfo(self.guild, self.ctx) songs = songs[songsQuant:]
processLock = processInfo.getLock()
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) # Create task to download the songs in the lot
if acquired: tasks: List[asyncio.Task] = []
playlist.add_song(song) for index, song in enumerate(songsInLot):
queue.put(playCommand) task = asyncio.create_task(self.__down.download_song(song))
processLock.release() tasks.append(task)
else:
processManager.resetProcess(self.guild, self.ctx) 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, self.ctx)
playerLock.release()
else:
playersManager.resetPlayer(self.guild, self.ctx)
def __isUserConnected(self) -> bool: def __isUserConnected(self) -> bool:
if self.ctx.author.voice: if self.ctx.author.voice:

View File

@@ -2,6 +2,7 @@ from discord.ext.commands import Context
from Handlers.AbstractHandler import AbstractHandler from Handlers.AbstractHandler import AbstractHandler
from Config.Exceptions import BadCommandUsage, ImpossibleMove from Config.Exceptions import BadCommandUsage, ImpossibleMove
from Handlers.HandlerResponse import HandlerResponse from Handlers.HandlerResponse import HandlerResponse
from Parallelism.AbstractProcessManager import AbstractPlayersManager
from Parallelism.Commands import VCommands, VCommandsType from Parallelism.Commands import VCommands, VCommandsType
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
from typing import Union from typing import Union
@@ -18,14 +19,13 @@ class PrevHandler(AbstractHandler):
embed = self.embeds.NO_CHANNEL() embed = self.embeds.NO_CHANNEL()
return HandlerResponse(self.ctx, embed, error) return HandlerResponse(self.ctx, embed, error)
processManager = self.config.getProcessManager() playersManager: AbstractPlayersManager = self.config.getPlayersManager()
processInfo = processManager.getOrCreatePlayerInfo(self.guild, self.ctx) if not playersManager.verifyIfPlayerExists(self.guild):
if not processInfo:
embed = self.embeds.NOT_PLAYING() embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage() error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error) return HandlerResponse(self.ctx, embed, error)
playlist = processInfo.getPlaylist() playlist = playersManager.getPlayerPlaylist(self.guild)
if len(playlist.getHistory()) == 0: if len(playlist.getHistory()) == 0:
error = ImpossibleMove() error = ImpossibleMove()
embed = self.embeds.NOT_PREVIOUS_SONG() embed = self.embeds.NOT_PREVIOUS_SONG()
@@ -36,15 +36,9 @@ class PrevHandler(AbstractHandler):
embed = self.embeds.FAIL_DUE_TO_LOOP_ON() embed = self.embeds.FAIL_DUE_TO_LOOP_ON()
return HandlerResponse(self.ctx, embed, error) return HandlerResponse(self.ctx, embed, error)
# If not started, start the player process
process = processInfo.getProcess()
if not process.is_alive():
process.start()
# Send a prev command, together with the user voice channel # Send a prev command, together with the user voice channel
prevCommand = VCommands(VCommandsType.PREV, self.author.voice.channel.id) prevCommand = VCommands(VCommandsType.PREV, self.author.voice.channel.id)
queue = processInfo.getQueueToPlayer() await playersManager.sendCommandToPlayer(prevCommand, self.guild, self.ctx)
queue.put(prevCommand)
embed = self.embeds.RETURNING_SONG() embed = self.embeds.RETURNING_SONG()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)

View File

@@ -4,6 +4,7 @@ from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse from Handlers.HandlerResponse import HandlerResponse
from Handlers.JumpMusicHandler import JumpMusicHandler from Handlers.JumpMusicHandler import JumpMusicHandler
from Messages.MessagesCategory import MessagesCategory from Messages.MessagesCategory import MessagesCategory
from Parallelism.AbstractProcessManager import AbstractPlayersManager
from UI.Views.BasicView import BasicView from UI.Views.BasicView import BasicView
from Utils.Utils import Utils from Utils.Utils import Utils
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
@@ -21,29 +22,27 @@ class QueueHandler(AbstractHandler):
super().__init__(ctx, bot) super().__init__(ctx, bot)
async def run(self, pageNumber=0) -> HandlerResponse: async def run(self, pageNumber=0) -> HandlerResponse:
# Retrieve the process of the guild playersManager: AbstractPlayersManager = self.config.getPlayersManager()
processManager = self.config.getProcessManager() if not playersManager.verifyIfPlayerExists(self.guild):
processInfo = processManager.getRunningPlayerInfo(self.guild)
if not processInfo: # If no process return empty list
embed = self.embeds.EMPTY_QUEUE() embed = self.embeds.EMPTY_QUEUE()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)
# Acquire the Lock to manipulate the playlist # Acquire the Lock to manipulate the playlist
processLock = processInfo.getLock() playerLock = playersManager.getPlayerLock(self.guild)
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) acquired = playerLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired: if acquired:
playlist: Playlist = processInfo.getPlaylist() playlist: Playlist = playersManager.getPlayerPlaylist(self.guild)
if playlist.isLoopingOne(): if playlist.isLoopingOne():
song = playlist.getCurrentSong() song = playlist.getCurrentSong()
embed = self.embeds.ONE_SONG_LOOPING(song.info) embed = self.embeds.ONE_SONG_LOOPING(song.info)
processLock.release() # Release the Lock playerLock.release() # Release the Lock
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)
allSongs = playlist.getSongs() allSongs = playlist.getSongs()
if len(allSongs) == 0: if len(allSongs) == 0:
embed = self.embeds.EMPTY_QUEUE() embed = self.embeds.EMPTY_QUEUE()
processLock.release() # Release the Lock playerLock.release() # Release the Lock
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)
songsPages = playlist.getSongsPages() songsPages = playlist.getSongsPages()
@@ -92,10 +91,10 @@ class QueueHandler(AbstractHandler):
embed = self.embeds.QUEUE(title, text) embed = self.embeds.QUEUE(title, text)
# Release the acquired Lock # Release the acquired Lock
processLock.release() playerLock.release()
return HandlerResponse(self.ctx, embed, view=queueView) return HandlerResponse(self.ctx, embed, view=queueView)
else: else:
processManager.resetProcess(self.guild, self.ctx) playersManager.resetPlayer(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED() embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)

View File

@@ -4,7 +4,7 @@ from Handlers.HandlerResponse import HandlerResponse
from Config.Exceptions import BadCommandUsage, VulkanError, ErrorRemoving, InvalidInput, NumberRequired from Config.Exceptions import BadCommandUsage, VulkanError, ErrorRemoving, InvalidInput, NumberRequired
from Music.Playlist import Playlist from Music.Playlist import Playlist
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
from Parallelism.ProcessInfo import ProcessInfo from Parallelism.AbstractProcessManager import AbstractPlayersManager
from typing import Union from typing import Union
from discord import Interaction from discord import Interaction
@@ -14,15 +14,13 @@ class RemoveHandler(AbstractHandler):
super().__init__(ctx, bot) super().__init__(ctx, bot)
async def run(self, position: str) -> HandlerResponse: async def run(self, position: str) -> HandlerResponse:
# Get the current process of the guild playersManager: AbstractPlayersManager = self.config.getPlayersManager()
processManager = self.config.getProcessManager() if not playersManager.verifyIfPlayerExists(self.guild):
processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild)
if not processInfo:
embed = self.embeds.NOT_PLAYING() embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage() error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error) return HandlerResponse(self.ctx, embed, error)
playlist = processInfo.getPlaylist() playlist = playersManager.getPlayerPlaylist(self.guild)
if playlist is None: if playlist is None:
embed = self.embeds.NOT_PLAYING() embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage() error = BadCommandUsage()

View File

@@ -1,7 +1,7 @@
from discord.ext.commands import Context from discord.ext.commands import Context
from Handlers.AbstractHandler import AbstractHandler from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse from Handlers.HandlerResponse import HandlerResponse
from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus from Parallelism.AbstractProcessManager import AbstractPlayersManager
from Parallelism.Commands import VCommands, VCommandsType from Parallelism.Commands import VCommands, VCommandsType
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
from typing import Union from typing import Union
@@ -13,18 +13,10 @@ class ResetHandler(AbstractHandler):
super().__init__(ctx, bot) super().__init__(ctx, bot)
async def run(self) -> HandlerResponse: async def run(self) -> HandlerResponse:
# Get the current process of the guild playersManager: AbstractPlayersManager = self.config.getPlayersManager()
processManager = self.config.getProcessManager() if playersManager.verifyIfPlayerExists(self.guild):
processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild)
if processInfo:
if processInfo.getStatus() == ProcessStatus.SLEEPING:
embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed)
command = VCommands(VCommandsType.RESET, None) command = VCommands(VCommandsType.RESET, None)
queue = processInfo.getQueueToPlayer() await playersManager.sendCommandToPlayer(command, self.guild, self.ctx)
queue.put(command)
return HandlerResponse(self.ctx) return HandlerResponse(self.ctx)
else: else:
embed = self.embeds.NOT_PLAYING() embed = self.embeds.NOT_PLAYING()

View File

@@ -1,7 +1,7 @@
from discord.ext.commands import Context from discord.ext.commands import Context
from Handlers.AbstractHandler import AbstractHandler from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse from Handlers.HandlerResponse import HandlerResponse
from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus from Parallelism.AbstractProcessManager import AbstractPlayersManager
from Parallelism.Commands import VCommands, VCommandsType from Parallelism.Commands import VCommands, VCommandsType
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
from typing import Union from typing import Union
@@ -13,18 +13,10 @@ class ResumeHandler(AbstractHandler):
super().__init__(ctx, bot) super().__init__(ctx, bot)
async def run(self) -> HandlerResponse: async def run(self) -> HandlerResponse:
processManager = self.config.getProcessManager() playersManager: AbstractPlayersManager = self.config.getPlayersManager()
processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild) if playersManager.verifyIfPlayerExists(self.guild):
if processInfo:
if processInfo.getStatus() == ProcessStatus.SLEEPING:
embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed)
# Send Resume command to be execute by player process
command = VCommands(VCommandsType.RESUME, None) command = VCommands(VCommandsType.RESUME, None)
queue = processInfo.getQueueToPlayer() await playersManager.sendCommandToPlayer(command, self.guild, self.ctx)
queue.put(command)
embed = self.embeds.PLAYER_RESUMED() embed = self.embeds.PLAYER_RESUMED()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)
else: else:

View File

@@ -6,25 +6,26 @@ from Music.VulkanBot import VulkanBot
from typing import Union from typing import Union
from discord import Interaction from discord import Interaction
from Parallelism.AbstractProcessManager import AbstractPlayersManager
class ShuffleHandler(AbstractHandler): class ShuffleHandler(AbstractHandler):
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None: def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
super().__init__(ctx, bot) super().__init__(ctx, bot)
async def run(self) -> HandlerResponse: async def run(self) -> HandlerResponse:
processManager = self.config.getProcessManager() playersManager: AbstractPlayersManager = self.config.getPlayersManager()
processInfo = processManager.getRunningPlayerInfo(self.guild) if playersManager.verifyIfPlayerExists(self.guild):
if processInfo:
try: try:
processLock = processInfo.getLock() playerLock = playersManager.getPlayerLock(self.guild)
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) acquired = playerLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired: if acquired:
playlist = processInfo.getPlaylist() playlist = playersManager.getPlayerPlaylist(self.guild)
playlist.shuffle() playlist.shuffle()
# Release the acquired Lock # Release the acquired Lock
processLock.release() playerLock.release()
else: else:
processManager.resetProcess(self.guild, self.ctx) playersManager.resetPlayer(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED() embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)

View File

@@ -1,9 +1,8 @@
from discord.ext.commands import Context from discord.ext.commands import Context
from Handlers.AbstractHandler import AbstractHandler from Handlers.AbstractHandler import AbstractHandler
from Config.Exceptions import BadCommandUsage, ImpossibleMove
from Handlers.HandlerResponse import HandlerResponse from Handlers.HandlerResponse import HandlerResponse
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus from Parallelism.AbstractProcessManager import AbstractPlayersManager
from Parallelism.Commands import VCommands, VCommandsType from Parallelism.Commands import VCommands, VCommandsType
from typing import Union from typing import Union
from discord import Interaction from discord import Interaction
@@ -14,31 +13,12 @@ class SkipHandler(AbstractHandler):
super().__init__(ctx, bot) super().__init__(ctx, bot)
async def run(self) -> HandlerResponse: async def run(self) -> HandlerResponse:
if not self.__user_connected(): playersManager: AbstractPlayersManager = self.config.getPlayersManager()
error = ImpossibleMove() if playersManager.verifyIfPlayerExists(self.guild):
embed = self.embeds.NO_CHANNEL()
return HandlerResponse(self.ctx, embed, error)
processManager = self.config.getProcessManager()
processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild)
if processInfo: # Verify if there is a running process
if processInfo.getStatus() == ProcessStatus.SLEEPING:
embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed)
# Send a command to the player process to skip the music
command = VCommands(VCommandsType.SKIP, None) command = VCommands(VCommandsType.SKIP, None)
queue = processInfo.getQueueToPlayer() await playersManager.sendCommandToPlayer(command, self.guild, self.ctx)
queue.put(command)
embed = self.embeds.SKIPPING_SONG() embed = self.embeds.SKIPPING_SONG()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)
else: else:
embed = self.embeds.NOT_PLAYING() embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)
def __user_connected(self) -> bool:
if self.author.voice:
return True
else:
return False

View File

@@ -2,7 +2,7 @@ from discord.ext.commands import Context
from Handlers.AbstractHandler import AbstractHandler from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse from Handlers.HandlerResponse import HandlerResponse
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus from Parallelism.AbstractProcessManager import AbstractPlayersManager
from Parallelism.Commands import VCommands, VCommandsType from Parallelism.Commands import VCommands, VCommandsType
from typing import Union from typing import Union
from discord import Interaction from discord import Interaction
@@ -13,18 +13,10 @@ class StopHandler(AbstractHandler):
super().__init__(ctx, bot) super().__init__(ctx, bot)
async def run(self) -> HandlerResponse: async def run(self) -> HandlerResponse:
processManager = self.config.getProcessManager() playersManager: AbstractPlayersManager = self.config.getPlayersManager()
processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild) if playersManager.verifyIfPlayerExists(self.guild):
if processInfo:
if processInfo.getStatus() == ProcessStatus.SLEEPING:
embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed)
# Send command to player process stop
command = VCommands(VCommandsType.STOP, None) command = VCommands(VCommandsType.STOP, None)
queue = processInfo.getQueueToPlayer() await playersManager.sendCommandToPlayer(command, self.guild, self.ctx)
queue.put(command)
embed = self.embeds.STOPPING_PLAYER() embed = self.embeds.STOPPING_PLAYER()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)
else: else:

62
Handlers/VolumeHandler.py Normal file
View File

@@ -0,0 +1,62 @@
from Config.Exceptions import BadCommandUsage, NumberRequired, VulkanError
from Parallelism.AbstractProcessManager import AbstractPlayersManager
from Parallelism.Commands import VCommands, VCommandsType
from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse
from discord.ext.commands import Context
from Music.VulkanBot import VulkanBot
from discord import Interaction
from typing import Union
class VolumeHandler(AbstractHandler):
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
super().__init__(ctx, bot)
async def run(self, args: str) -> HandlerResponse:
if args is None or args.strip() == '':
error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error)
error = self.__validateInput(args)
if error:
embed = self.embeds.ERROR_EMBED(error.message)
return HandlerResponse(self.ctx, embed, error)
playersManager: AbstractPlayersManager = self.config.getPlayersManager()
if not playersManager.verifyIfPlayerExists(self.guild):
embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error)
playerLock = playersManager.getPlayerLock(self.guild)
acquired = playerLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
volume = self.__convert_input_to_volume(args)
if acquired:
volumeCommand = VCommands(VCommandsType.VOLUME, volume)
await playersManager.sendCommandToPlayer(volumeCommand, self.guild, self.ctx)
playerLock.release()
embed = self.embeds.VOLUME_CHANGED(volume)
return HandlerResponse(self.ctx, embed)
else:
playersManager.resetPlayer(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed)
def __convert_input_to_volume(self, input_volume: str) -> float:
volume = float(input_volume)
if volume < 0:
volume = 0
if volume > 100:
volume = 100
return volume
def __validateInput(self, volume: str) -> Union[VulkanError, None]:
try:
_ = float(volume)
except:
return NumberRequired(self.messages.ERROR_VOLUME_NUMBER)

View File

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

View File

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

View File

@@ -1,14 +1,14 @@
from Config.Exceptions import DeezerError, InvalidInput, SpotifyError, VulkanError, YoutubeError from Config.Exceptions import DeezerError, InvalidInput, SpotifyError, VulkanError, YoutubeError
from Music.Downloader import Downloader
from Music.Types import Provider
from Music.SpotifySearcher import SpotifySearch from Music.SpotifySearcher import SpotifySearch
from Music.DeezerSearcher import DeezerSearcher from Music.DeezerSearcher import DeezerSearcher
from Utils.Utils import Utils
from Utils.UrlAnalyzer import URLAnalyzer from Utils.UrlAnalyzer import URLAnalyzer
from Config.Messages import SearchMessages from Config.Messages import SearchMessages
from Music.Downloader import Downloader
from Music.Types import Provider
from Utils.Utils import Utils
class Searcher(): class Searcher:
def __init__(self) -> None: def __init__(self) -> None:
self.__spotify = SpotifySearch() self.__spotify = SpotifySearch()
self.__deezer = DeezerSearcher() self.__deezer = DeezerSearcher()
@@ -35,8 +35,7 @@ class Searcher():
try: try:
musics = self.__spotify.search(track) musics = self.__spotify.search(track)
if musics == None or len(musics) == 0: if musics == None or len(musics) == 0:
raise SpotifyError(self.__messages.SPOTIFY_NOT_FOUND, raise SpotifyError(self.__messages.SPOTIFY_NOT_FOUND, self.__messages.GENERIC_TITLE)
self.__messages.GENERIC_TITLE)
return musics return musics
except SpotifyError as error: except SpotifyError as error:
@@ -79,7 +78,7 @@ class Searcher():
if not Utils.is_url(track): if not Utils.is_url(track):
return Provider.Name return Provider.Name
if "https://www.youtu" in track or "https://youtu.be" in track or "https://music.youtube" in track: if "https://www.youtu" in track or "https://youtu.be" in track or "https://music.youtube" in track or "m.youtube" in track:
return Provider.YouTube return Provider.YouTube
if "https://open.spotify.com" in track: if "https://open.spotify.com" in track:

View File

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

View File

@@ -9,6 +9,7 @@ from Config.Embeds import VEmbeds
class VulkanBot(Bot): class VulkanBot(Bot):
def __init__(self, listingSlash: bool = False, *args, **kwargs): 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) super().__init__(*args, **kwargs)
self.__listingSlash = listingSlash self.__listingSlash = listingSlash
self.__configs = VConfigs() self.__configs = VConfigs()
@@ -43,9 +44,11 @@ class VulkanBot(Bot):
await self.connect(reconnect=True) await self.connect(reconnect=True)
async def on_ready(self): 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")) 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): async def on_command_error(self, ctx, error):
if isinstance(error, MissingRequiredArgument): if isinstance(error, MissingRequiredArgument):

View File

@@ -50,7 +50,8 @@ class VulkanInitializer:
raise VulkanError(message='Failed to load some Cog') raise VulkanError(message='Failed to load some Cog')
except VulkanError as e: except VulkanError as e:
print(f'[Error Loading Vulkan] -> {e.message}') print(f'[Error Loading Vulkan]')
print(e)
def __getTotalCogs(self) -> int: def __getTotalCogs(self) -> int:
quant = 0 quant = 0

View File

@@ -0,0 +1,47 @@
from abc import ABC, abstractmethod
from threading import Lock
from typing import Union
from discord.ext.commands import Context
from discord import Guild, Interaction
from Music.Playlist import Playlist
from Music.Song import Song
from Parallelism.Commands import VCommands
class AbstractPlayersManager(ABC):
def __init__(self, bot) -> None:
pass
@abstractmethod
async def sendCommandToPlayer(self, command: VCommands, guild: Guild, context: Union[Context, Interaction], forceCreation: bool = False):
"""If the forceCreation boolean is True, then the context must be provided for the Player to be created"""
pass
@abstractmethod
def getPlayerPlaylist(self, guild: Guild) -> Playlist:
"""If there is a player process for the guild, then return the playlist of the guild"""
pass
@abstractmethod
def getPlayerLock(self, guild: Guild) -> Lock:
"""If there is a player process for the guild, then return the lock of the guild"""
pass
@abstractmethod
def verifyIfPlayerExists(self, guild: Guild) -> bool:
"""Returns if a player for the guild exists"""
pass
@abstractmethod
def createPlayerForGuild(self, guild: Guild, context: Union[Context, Interaction]) -> None:
"""With the context information of a guild create a internal player for the guild"""
pass
@abstractmethod
def resetPlayer(self, guild: Guild, context: Context) -> None:
"""Tries to reset the player of the guild"""
pass
@abstractmethod
async def showNowPlaying(self, guildID: int, song: Song) -> None:
pass

View File

@@ -13,6 +13,7 @@ class VCommandsType(Enum):
RESET = 'Reset' RESET = 'Reset'
NOW_PLAYING = 'Now Playing' NOW_PLAYING = 'Now Playing'
TERMINATE = 'Terminate' TERMINATE = 'Terminate'
VOLUME = 'Volume'
SLEEPING = 'Sleeping' SLEEPING = 'Sleeping'

View File

@@ -1,349 +0,0 @@
import asyncio
from Music.VulkanInitializer import VulkanInitializer
from discord import User, Member, Message
from asyncio import AbstractEventLoop, Semaphore, Queue
from multiprocessing import Process, RLock, Lock, Queue
from threading import Thread
from typing import Callable, List
from discord import Guild, FFmpegPCMAudio, VoiceChannel, TextChannel
from Music.Playlist import Playlist
from Music.Song import Song
from Config.Configs import VConfigs
from Config.Messages import Messages
from Music.VulkanBot import VulkanBot
from Config.Embeds import VEmbeds
from Parallelism.Commands import VCommands, VCommandsType
class TimeoutClock:
def __init__(self, callback: Callable, loop: asyncio.AbstractEventLoop):
self.__callback = callback
self.__task = loop.create_task(self.__executor())
async def __executor(self):
await asyncio.sleep(VConfigs().VC_TIMEOUT)
await self.__callback()
def cancel(self):
self.__task.cancel()
class PlayerProcess(Process):
"""Process that will play songs, receive commands from the main process by a Queue"""
def __init__(self, name: str, playlist: Playlist, lock: Lock, queueToReceive: Queue, queueToSend: 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, name=name, group=None, target=None, args=(), kwargs={})
# Synchronization objects
self.__playlist: Playlist = playlist
self.__playlistLock: Lock = lock
self.__queueReceive: Queue = queueToReceive
self.__queueSend: Queue = queueToSend
self.__semStopPlaying: Semaphore = None
self.__loop: AbstractEventLoop = None
# Discord context ID
self.__textChannelID = textID
self.__guildID = guildID
self.__voiceChannelID = voiceID
self.__authorID = authorID
# All information of discord context will be retrieved directly with discord API
self.__guild: Guild = None
self.__bot: VulkanBot = None
self.__voiceChannel: VoiceChannel = None
self.__textChannel: TextChannel = None
self.__author: User = None
self.__botMember: Member = None
self.__configs: VConfigs = None
self.__embeds: VEmbeds = None
self.__messages: Messages = None
self.__messagesToDelete: List[Message] = []
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:
"""Method called by process.start(), this will exec the actually _run method in a event loop"""
try:
print(f'Starting Process {self.name}')
self.__playerLock = RLock()
self.__loop = asyncio.get_event_loop_policy().new_event_loop()
asyncio.set_event_loop(self.__loop)
self.__configs = VConfigs()
self.__messages = Messages()
self.__embeds = VEmbeds()
self.__semStopPlaying = Semaphore(0)
self.__loop.run_until_complete(self._run())
except Exception as e:
print(f'[Error in Process {self.name}] -> {e}')
async def _run(self) -> None:
# Recreate the bot instance and objects using discord API
self.__bot = await self.__createBotInstance()
self.__guild = self.__bot.get_guild(self.__guildID)
self.__voiceChannel = self.__bot.get_channel(self.__voiceChannelID)
self.__textChannel = self.__bot.get_channel(self.__textChannelID)
self.__author = self.__bot.get_channel(self.__authorID)
self.__botMember = self.__getBotMember()
# Connect to voice Channel
await self.__connectToVoiceChannel()
# Start the timeout function
self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop)
# Thread that will receive commands to be executed in this Process
self.__commandsReceiver = Thread(target=self.__commandsReceiver, daemon=True)
self.__commandsReceiver.start()
# Start a Task to play songs
self.__loop.create_task(self.__playPlaylistSongs())
# Try to acquire a semaphore, it'll be release when timeout function trigger, we use the Semaphore
# from the asyncio lib to not block the event loop
await self.__semStopPlaying.acquire()
# In this point the process should finalize
self.__timer.cancel()
async def __playPlaylistSongs(self) -> None:
"""If the player is not running trigger to play a new song"""
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()
if song is not None:
self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}')
async def __playSong(self, song: Song) -> None:
"""Function that will trigger the player to play the song"""
try:
self.__playerLock.acquire()
if song is None:
return
if song.source is None:
return self.__playNext(None)
# If not connected, connect to bind channel
if self.__guild.voice_client is None:
await self.__connectToVoiceChannel()
# If the player is already playing return
if self.__guild.voice_client.is_playing():
return
self.__playing = True
self.__playingSong = song
player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS)
self.__guild.voice_client.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)}')
self.__playNext(None)
finally:
self.__playerLock.release()
def __playNext(self, error) -> None:
with self.__playlistLock:
with self.__playerLock:
if self.__forceStop: # If it's forced to stop player
self.__forceStop = False
return None
song = self.__playlist.next_song()
if song is not None:
self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}')
else:
self.__playlist.loop_off()
self.__playingSong = None
self.__playing = False
# Send a command to the main process put this one to sleep
sleepCommand = VCommands(VCommandsType.SLEEPING)
self.__queueSend.put(sleepCommand)
# Release the semaphore to finish the process
self.__semStopPlaying.release()
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
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():
# Will forbidden next_song to execute after stopping current player
self.__forceStop = True
self.__guild.voice_client.stop()
self.__playing = False
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()
try:
self.__playerLock.acquire()
if type == VCommandsType.PAUSE:
self.__pause()
elif type == VCommandsType.RESUME:
self.__resume()
elif type == VCommandsType.SKIP:
self.__skip()
elif type == VCommandsType.PLAY:
asyncio.run_coroutine_threadsafe(self.__playPlaylistSongs(), self.__loop)
elif type == VCommandsType.PREV:
asyncio.run_coroutine_threadsafe(self.__playPrev(args), self.__loop)
elif type == VCommandsType.RESET:
asyncio.run_coroutine_threadsafe(self.__reset(), self.__loop)
elif type == VCommandsType.STOP:
asyncio.run_coroutine_threadsafe(self.__stop(), self.__loop)
else:
print(f'[ERROR] -> Unknown Command Received: {command}')
except Exception as e:
print(f'[ERROR IN COMMAND RECEIVER] -> {type} - {e}')
finally:
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()
async def __reset(self) -> None:
if self.__guild.voice_client 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()
async def __stop(self) -> None:
if self.__guild.voice_client is not None:
if self.__guild.voice_client.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.__playingSong = None
await self.__guild.voice_client.disconnect()
self.__semStopPlaying.release()
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.__playing = False
self.__guild.voice_client.stop()
async def __forceStop(self) -> None:
# Lock to work with Player
with self.__playerLock:
if self.__guild.voice_client is None:
return
self.__guild.voice_client.stop()
await self.__guild.voice_client.disconnect()
with self.__playlistLock:
self.__playlist.clear()
self.__playlist.loop_off()
async def __createBotInstance(self) -> VulkanBot:
"""Load a new bot instance that should not be directly called."""
initializer = VulkanInitializer(willListen=False)
bot = initializer.getBot()
await bot.startBotCoro(self.__loop)
await self.__ensureDiscordConnection(bot)
return bot
async def __timeoutHandler(self) -> None:
try:
if self.__guild.voice_client 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
# 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()
except Exception as e:
print(f'[Error in Timeout] -> {e}')
def __isBotAloneInChannel(self) -> bool:
try:
if len(self.__guild.voice_client.channel.members) <= 1:
return True
else:
return False
except Exception as e:
print(f'[ERROR IN CHECK BOT ALONE] -> {e}')
return False
async def __ensureDiscordConnection(self, bot: VulkanBot) -> 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 Exception as e:
print(f'[ERROR CONNECTING TO VC] -> {e}')
return False
def __getBotMember(self) -> Member:
guild_members: List[Member] = self.__guild.members
for member in guild_members:
if member.id == self.__bot.user.id:
return member

View File

@@ -1,10 +1,10 @@
from typing import List from typing import List
from discord import Button, TextChannel from discord import Button, Guild, TextChannel
from discord.ui import View from discord.ui import View
from Config.Emojis import VEmojis from Config.Emojis import VEmojis
from Messages.MessagesCategory import MessagesCategory from Messages.MessagesCategory import MessagesCategory
from Music.Playlist import Playlist
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
from Parallelism.ProcessInfo import ProcessInfo
from Config.Messages import Messages from Config.Messages import Messages
from Music.Song import Song from Music.Song import Song
from Config.Embeds import VEmbeds from Config.Embeds import VEmbeds
@@ -21,6 +21,11 @@ from Handlers.QueueHandler import QueueHandler
class ProcessCommandsExecutor: class ProcessCommandsExecutor:
MESSAGES = Messages()
EMBEDS = VEmbeds()
EMOJIS = VEmojis()
MSG_MANAGER = MessagesManager()
def __init__(self, bot: VulkanBot, guildID: int) -> None: def __init__(self, bot: VulkanBot, guildID: int) -> None:
self.__bot = bot self.__bot = bot
self.__guildID = guildID self.__guildID = guildID
@@ -29,9 +34,58 @@ class ProcessCommandsExecutor:
self.__embeds = VEmbeds() self.__embeds = VEmbeds()
self.__emojis = VEmojis() self.__emojis = VEmojis()
async def sendNowPlaying(self, processInfo: ProcessInfo, song: Song) -> None: @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 # Get the lock of the playlist
playlist = processInfo.getPlaylist()
if playlist.isLoopingOne(): if playlist.isLoopingOne():
title = self.__messages.ONE_SONG_LOOPING title = self.__messages.ONE_SONG_LOOPING
else: else:
@@ -39,7 +93,6 @@ class ProcessCommandsExecutor:
# Create View and Embed # Create View and Embed
embed = self.__embeds.SONG_INFO(song.info, title) embed = self.__embeds.SONG_INFO(song.info, title)
channel = processInfo.getTextChannel()
view = self.__getPlayerView(channel) view = self.__getPlayerView(channel)
# Send Message and add to the MessagesManager # Send Message and add to the MessagesManager
message = await channel.send(embed=embed, view=view) message = await channel.send(embed=embed, view=view)

View File

@@ -1,51 +0,0 @@
from enum import Enum
from multiprocessing import Process, Queue, Lock
from discord import TextChannel
from Music.Playlist import Playlist
class ProcessStatus(Enum):
RUNNING = 'Running'
SLEEPING = 'Sleeping'
class ProcessInfo:
"""
Class to store the reference to all structures to maintain a player process
"""
def __init__(self, process: Process, queueToPlayer: Queue, queueToMain: Queue, playlist: Playlist, lock: Lock, textChannel: TextChannel) -> None:
self.__process = process
self.__queueToPlayer = queueToPlayer
self.__queueToMain = queueToMain
self.__playlist = playlist
self.__lock = lock
self.__textChannel = textChannel
self.__status = ProcessStatus.RUNNING
def setProcess(self, newProcess: Process) -> None:
self.__process = newProcess
def getStatus(self) -> ProcessStatus:
return self.__status
def setStatus(self, status: ProcessStatus) -> None:
self.__status = status
def getProcess(self) -> Process:
return self.__process
def getQueueToPlayer(self) -> Queue:
return self.__queueToPlayer
def getQueueToMain(self) -> Queue:
return self.__queueToMain
def getPlaylist(self) -> Playlist:
return self.__playlist
def getLock(self) -> Lock:
return self.__lock
def getTextChannel(self) -> TextChannel:
return self.__textChannel

View File

@@ -0,0 +1,471 @@
import asyncio
from time import sleep, time
from urllib.parse import parse_qs, urlparse
from Music.VulkanInitializer import VulkanInitializer
from discord import PCMVolumeTransformer, VoiceClient
from asyncio import AbstractEventLoop, Semaphore, Queue
from multiprocessing import Process, RLock, Lock, Queue
from threading import Thread
from typing import Callable
from discord import Guild, FFmpegPCMAudio, VoiceChannel
from Music.Playlist import Playlist
from Music.Song import Song
from Config.Configs import VConfigs
from Music.VulkanBot import VulkanBot
from Music.Downloader import Downloader
from Parallelism.Commands import VCommands, VCommandsType
class TimeoutClock:
def __init__(self, callback: Callable, loop: asyncio.AbstractEventLoop):
self.__callback = callback
self.__task = loop.create_task(self.__executor())
async def __executor(self):
await asyncio.sleep(VConfigs().VC_TIMEOUT)
await self.__callback()
def cancel(self):
self.__task.cancel()
class ProcessPlayer(Process):
"""Process that will play songs, receive commands from the main process by a Queue"""
def __init__(self, name: str, playlist: Playlist, lock: Lock, queueToReceive: Queue, queueToSend: Queue, guildID: int, voiceID: 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, name=name, group=None, target=None, args=(), kwargs={})
# Synchronization objects
self.__playlist: Playlist = playlist
self.__playlistLock: Lock = lock
self.__queueReceive: Queue = queueToReceive
self.__queueSend: Queue = queueToSend
self.__semStopPlaying: Semaphore = None
self.__loop: AbstractEventLoop = None
# Discord context ID
self.__guildID = guildID
self.__voiceChannelID = voiceID
# All information of discord context will be retrieved directly with discord API
self.__guild: Guild = None
self.__bot: VulkanBot = None
self.__voiceChannel: VoiceChannel = None
self.__voiceClient: VoiceClient = None
self.__songVolumeUsing = 1
self.__currentSongChangeVolume = False
self.__playing = False
self.__forceStop = False
self.__botCompletedLoad = False
self.FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
'options': '-vn'}
def run(self) -> None:
"""Method called by process.start(), this will exec the actually _run method in a event loop"""
try:
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)
self.__downloader = Downloader()
self.__semStopPlaying = Semaphore(0)
self.__loop.run_until_complete(self._run())
except Exception as e:
print(f'[ERROR IN PROCESS {self.name}] -> {e}')
async def _run(self) -> None:
# Recreate the bot instance and objects using discord API
self.__bot = await self.__createBotInstance()
self.__botCompletedLoad = True
self.__guild = self.__bot.get_guild(self.__guildID)
self.__voiceChannel = self.__bot.get_channel(self.__voiceChannelID)
# Connect to voice Channel
await self.__connectToVoiceChannel()
# Start the timeout function
self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop)
# Thread that will receive commands to be executed in this Process
self.__commandsReceiver = Thread(target=self.__commandsReceiver, daemon=True)
self.__commandsReceiver.start()
# Start a Task to play songs
self.__loop.create_task(self.__playPlaylistSongs())
# Try to acquire a semaphore, it'll be release when timeout function trigger, we use the Semaphore
# from the asyncio lib to not block the event loop
await self.__semStopPlaying.acquire()
# In this point the process should finalize
self.__timer.cancel()
def __set_volume(self, volume: float) -> None:
"""Set the volume of the player, must be values between 0 and 100"""
try:
if self.__voiceClient is None:
return
if not isinstance(volume, float):
print('[PROCESS ERROR] -> Volume instance must be float')
return
if volume < 0:
volume = 0
if volume > 100:
volume = 100
volume = volume / 100
if not self.__currentSongChangeVolume:
print('[PROCESS ERROR] -> Cannot change the volume of this song')
return
self.__songVolumeUsing = volume
self.__voiceClient.source.volume = volume
except Exception as e:
print(e)
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:
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"""
try:
self.__playerLock.acquire()
if song is None:
return
if song.source is None:
return self.__playNext(None)
# If not connected, connect to bind channel
if self.__voiceClient is None:
await self.__connectToVoiceChannel()
# If the voice channel disconnect for some reason
if not self.__voiceClient.is_connected():
print('[PROCESS PLAYER -> 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('[PROCESS PLAYER -> SONG ALREADY PLAYING, RETURNING]')
self.__playlist.add_song_start(song)
return
songStillAvailable = self.__verifyIfSongAvailable(song)
if not songStillAvailable:
print('[PROCESS PLAYER -> SONG NOT AVAILABLE ANYMORE, DOWNLOADING AGAIN]')
song = self.__downloadSongAgain(song)
self.__playing = True
self.__songPlaying = song
player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS)
if not player.is_opus():
player = PCMVolumeTransformer(player, self.__songVolumeUsing)
self.__currentSongChangeVolume = True
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'[PROCESS PLAYER -> 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'[PROCESS PLAYER -> ERROR PLAYING SONG] -> {error}')
with self.__playlistLock:
with self.__playerLock:
self.__currentSongChangeVolume = False
if self.__forceStop: # If it's forced to stop player
self.__forceStop = False
return None
song = self.__playlist.next_song()
if song is not None:
self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}')
else:
self.__playlist.loop_off()
self.__songPlaying = None
self.__playing = False
# Send a command to the main process put this one to sleep
sleepCommand = VCommands(VCommandsType.SLEEPING)
self.__queueSend.put(sleepCommand)
# 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'[PROCESS PLAYER -> 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 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.__verifyIfIsPlaying():
# Will forbidden next_song to execute after stopping current player
self.__forceStop = True
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:
# Forces the Thread that listen to the commands to await this bot instance
# to stablish the connection with discord, may delay when running bots in several servers
while True:
if self.__botCompletedLoad:
break
sleep(0.1)
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:
asyncio.run_coroutine_threadsafe(self.__resume(), self.__loop)
elif type == VCommandsType.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:
asyncio.run_coroutine_threadsafe(self.__playPrev(args), self.__loop)
elif type == VCommandsType.RESET:
asyncio.run_coroutine_threadsafe(self.__reset(), self.__loop)
elif type == VCommandsType.STOP:
asyncio.run_coroutine_threadsafe(self.__stop(), self.__loop)
elif type == VCommandsType.VOLUME:
self.__set_volume(args)
else:
print(f'[PROCESS PLAYER ERROR] -> Unknown Command Received: {command}')
except Exception as e:
print(f'[PROCESS PLAYER -> ERROR IN COMMAND RECEIVER] -> {type} - {e}')
finally:
self.__playerLock.release()
def __pause(self) -> None:
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.__voiceClient is None:
return
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.__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.__voiceClient.stop()
await self.__voiceClient.disconnect()
self.__songPlaying = None
self.__playing = False
self.__voiceClient = None
self.__semStopPlaying.release()
# If the voiceClient is not None we finish things
else:
await self.__forceBotDisconnectAndStop()
async def __resume(self) -> None:
# Lock to work with Player
with self.__playerLock:
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.__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('[PROCESS PLAYER -> 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'[PROCESS PLAYER -> ERROR FORCING BOT TO STOP] -> {e}')
finally:
self.__voiceClient = None
with self.__playlistLock:
self.__playlist.clear()
self.__playlist.loop_off()
async def __createBotInstance(self) -> VulkanBot:
"""Load a new bot instance that should not be directly called."""
initializer = VulkanInitializer(willListen=False)
bot = initializer.getBot()
await bot.startBotCoro(self.__loop)
await self.__ensureDiscordConnection(bot)
return bot
async def __timeoutHandler(self) -> None:
try:
# 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.__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
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'[PROCESS PLAYER -> ERROR IN TIMEOUT] -> {e}')
def __isBotAloneInChannel(self) -> bool:
try:
if len(self.__voiceClient.channel.members) <= 1:
return True
else:
return False
except Exception as e:
print(f'[PROCESS PLAYER -> ERROR IN CHECK BOT ALONE] -> {e}')
return False
async def __ensureDiscordConnection(self, bot: VulkanBot) -> 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:
print('[PROCESS PLAYER -> CONNECTING TO VOICE CHANNEL]')
# If the voiceChannel is not defined yet, like if the Bot is still loading, wait until we get the voiceChannel
if self.__voiceChannel is None:
while True:
self.__voiceChannel = self.__bot.get_channel(self.__voiceChannelID)
if self.__voiceChannel is None:
await asyncio.sleep(0.2)
else:
break
if self.__voiceClient is not None:
try:
await self.__voiceClient.disconnect(force=True)
except Exception as e:
print(f'[PROCESS PLAYER -> ERROR FORCING DISCONNECT] -> {e}')
self.__voiceClient = await self.__voiceChannel.connect(reconnect=True, timeout=None)
return True
except Exception as e:
print(f'[PROCESS PLAYER -> ERROR CONNECTING TO VC] -> {e}')
return False

View File

@@ -1,55 +1,130 @@
import asyncio import asyncio
from multiprocessing import Lock, Queue from enum import Enum
from multiprocessing import Lock, Process, Queue
from multiprocessing.managers import BaseManager, NamespaceProxy from multiprocessing.managers import BaseManager, NamespaceProxy
from queue import Empty from queue import Empty
from threading import Thread from threading import Thread
from typing import Dict, Tuple, Union from typing import Dict, Tuple, Union
from Config.Singleton import Singleton from Config.Singleton import Singleton
from discord import Guild, Interaction from discord import Guild, Interaction, TextChannel, VoiceChannel
from discord.ext.commands import Context from discord.ext.commands import Context
from Parallelism.AbstractProcessManager import AbstractPlayersManager
from Parallelism.ProcessExecutor import ProcessCommandsExecutor from Parallelism.ProcessExecutor import ProcessCommandsExecutor
from Music.Song import Song from Music.Song import Song
from Parallelism.PlayerProcess import PlayerProcess from Parallelism.ProcessPlayer import ProcessPlayer
from Music.Playlist import Playlist from Music.Playlist import Playlist
from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus
from Parallelism.Commands import VCommands, VCommandsType from Parallelism.Commands import VCommands, VCommandsType
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
class ProcessManager(Singleton): class ProcessStatus(Enum):
RUNNING = 'Running'
SLEEPING = 'Sleeping'
class PlayerProcessInfo:
"""
Class to store the reference to all structures to maintain a process player
"""
def __init__(self, process: Process, queueToPlayer: Queue, queueToMain: Queue, playlist: Playlist, lock: Lock, textChannel: TextChannel) -> None:
self.__process = process
self.__queueToPlayer = queueToPlayer
self.__queueToMain = queueToMain
self.__playlist = playlist
self.__lock = lock
self.__textChannel = textChannel
self.__status = ProcessStatus.RUNNING
def setProcess(self, newProcess: Process) -> None:
self.__process = newProcess
def getStatus(self) -> ProcessStatus:
return self.__status
def setStatus(self, status: ProcessStatus) -> None:
self.__status = status
def getProcess(self) -> Process:
return self.__process
def getQueueToPlayer(self) -> Queue:
return self.__queueToPlayer
def getQueueToMain(self) -> Queue:
return self.__queueToMain
def getPlaylist(self) -> Playlist:
return self.__playlist
def getLock(self) -> Lock:
return self.__lock
def getTextChannel(self) -> TextChannel:
return self.__textChannel
class ProcessPlayerManager(Singleton, AbstractPlayersManager):
""" """
Manage all running player process, creating and storing them for future calls Manage all running player process, creating and storing them for future calls
Deal with the creation of shared memory Deals with the creation of shared memory
""" """
def __init__(self, bot: VulkanBot = None) -> None: def __init__(self, bot: VulkanBot = None) -> None:
if not super().created: if not super().created:
self.__bot = bot self.__bot = bot
VManager.register('Playlist', Playlist) VManager.register('Playlist', Playlist)
VManager.register('VoiceChannel', VoiceChannel)
self.__manager = VManager() self.__manager = VManager()
self.__manager.start() self.__manager.start()
self.__playersProcess: Dict[Guild, ProcessInfo] = {} self.__playersProcess: Dict[int, PlayerProcessInfo] = {}
self.__playersListeners: Dict[Guild, Tuple[Thread, bool]] = {} self.__playersListeners: Dict[int, Tuple[Thread, bool]] = {}
self.__playersCommandsExecutor: Dict[Guild, ProcessCommandsExecutor] = {} self.__playersCommandsExecutor: Dict[int, ProcessCommandsExecutor] = {}
def setPlayerInfo(self, guild: Guild, info: ProcessInfo): async def sendCommandToPlayer(self, command: VCommands, guild: Guild, context: Union[Context, Interaction], forceCreation: bool = False):
self.__playersProcess[guild.id] = info if forceCreation:
processInfo = self.createPlayerForGuild(guild, context)
else:
processInfo = self.__getRunningPlayerInfo(guild)
if processInfo == None:
return
def getOrCreatePlayerInfo(self, guild: Guild, context: Union[Context, Interaction]) -> ProcessInfo: if processInfo.getStatus() == ProcessStatus.SLEEPING:
"""Return the process info for the guild, the user in context must be connected to a voice_channel""" self.resetPlayer(guild, context)
processInfo = self.__getRunningPlayerInfo(guild)
queue = processInfo.getQueueToPlayer()
self.__putCommandInQueue(queue, command)
def getPlayerPlaylist(self, guild: Guild) -> Playlist:
playerInfo = self.__getRunningPlayerInfo(guild)
if playerInfo:
return playerInfo.getPlaylist()
def getPlayerLock(self, guild: Guild) -> Lock:
playerInfo = self.__getRunningPlayerInfo(guild)
if playerInfo:
return playerInfo.getLock()
def verifyIfPlayerExists(self, guild: Guild) -> bool:
return guild.id in self.__playersProcess.keys()
def createPlayerForGuild(self, guild: Guild, context: Union[Context, Interaction]) -> None:
try: try:
if guild.id not in self.__playersProcess.keys(): if guild.id not in self.__playersProcess.keys():
self.__playersProcess[guild.id] = self.__createProcessInfo(guild, context) self.__playersProcess[guild.id] = self.__createProcessPlayerInfo(guild, context)
else: else:
# If the process has ended create a new one # If the process has ended create a new one
if not self.__playersProcess[guild.id].getProcess().is_alive(): if not self.__playersProcess[guild.id].getProcess().is_alive():
self.__playersProcess[guild.id] = self.__recreateProcess(guild, context) self.__playersProcess[guild.id] = self.__recreateProcess(guild, context)
# Start the process
self.__playersProcess[guild.id].getProcess().start()
return self.__playersProcess[guild.id] return self.__playersProcess[guild.id]
except Exception as e: except Exception as e:
print(f'[Error In GetPlayerContext] -> {e}') print(f'[Error In GetPlayerContext] -> {e}')
def resetProcess(self, guild: Guild, context: Context) -> None: def resetPlayer(self, guild: Guild, context: Context) -> None:
"""Restart a running process, already start it to return to play""" """Restart a running process, already start it to return to play"""
if guild.id not in self.__playersProcess.keys(): if guild.id not in self.__playersProcess.keys():
return None return None
@@ -59,30 +134,29 @@ class ProcessManager(Singleton):
newProcessInfo.getProcess().start() # Start the process newProcessInfo.getProcess().start() # Start the process
# Send a command to start the play again # Send a command to start the play again
playCommand = VCommands(VCommandsType.PLAY) playCommand = VCommands(VCommandsType.PLAY)
newProcessInfo.getQueueToPlayer().put(playCommand) self.__putCommandInQueue(newProcessInfo.getQueueToPlayer(), playCommand)
self.__playersProcess[guild.id] = newProcessInfo self.__playersProcess[guild.id] = newProcessInfo
def getRunningPlayerInfo(self, guild: Guild) -> ProcessInfo: def __getRunningPlayerInfo(self, guild: Guild) -> PlayerProcessInfo:
"""Return the process info for the guild, if not, return None""" """Return the process info for the guild, if not, return None"""
if guild.id not in self.__playersProcess.keys(): if guild.id not in self.__playersProcess.keys():
print('Process Info not found')
return None return None
return self.__playersProcess[guild.id] return self.__playersProcess[guild.id]
def __createProcessInfo(self, guild: Guild, context: Context) -> ProcessInfo: def __createProcessPlayerInfo(self, guild: Guild, context: Context) -> PlayerProcessInfo:
guildID: int = context.guild.id guildID: int = context.guild.id
textID: int = context.channel.id
voiceID: int = context.author.voice.channel.id voiceID: int = context.author.voice.channel.id
authorID: int = context.author.id
playlist: Playlist = self.__manager.Playlist() playlist: Playlist = self.__manager.Playlist()
lock = Lock() lock = Lock()
queueToListen = Queue() queueToListen = Queue()
queueToSend = Queue() queueToSend = Queue()
process = PlayerProcess(context.guild.name, playlist, lock, queueToSend, process = ProcessPlayer(context.guild.name, playlist, lock, queueToSend,
queueToListen, guildID, textID, voiceID, authorID) queueToListen, guildID, voiceID)
processInfo = ProcessInfo(process, queueToSend, queueToListen, processInfo = PlayerProcessInfo(process, queueToSend, queueToListen,
playlist, lock, context.channel) playlist, lock, context.channel)
# Create a Thread to listen for the queue coming from the Player Process, this will redirect the Queue to a async # Create a Thread to listen for the queue coming from the Player Process, this will redirect the Queue to a async
thread = Thread(target=self.__listenToCommands, thread = Thread(target=self.__listenToCommands,
@@ -95,25 +169,36 @@ class ProcessManager(Singleton):
return processInfo return processInfo
def __recreateProcess(self, guild: Guild, context: Union[Context, Interaction]) -> 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()
except ValueError:
pass
except Exception as e:
print(f'[WARNINGS] -> {e}')
def __recreateProcess(self, guild: Guild, context: Union[Context, Interaction]) -> PlayerProcessInfo:
"""Create a new process info using previous playlist""" """Create a new process info using previous playlist"""
self.__stopPossiblyRunningProcess(guild)
guildID: int = context.guild.id guildID: int = context.guild.id
textID: int = context.channel.id
if isinstance(context, Interaction): if isinstance(context, Interaction):
authorID: int = context.user.id
voiceID: int = context.user.voice.channel.id voiceID: int = context.user.voice.channel.id
else: else:
authorID: int = context.author.id
voiceID: int = context.author.voice.channel.id voiceID: int = context.author.voice.channel.id
playlist: Playlist = self.__playersProcess[guildID].getPlaylist() playlist: Playlist = self.__playersProcess[guildID].getPlaylist()
lock = Lock() lock = Lock()
queueToListen = Queue() queueToListen = Queue()
queueToSend = Queue() queueToSend = Queue()
process = PlayerProcess(context.guild.name, playlist, lock, queueToSend, process = ProcessPlayer(context.guild.name, playlist, lock, queueToSend,
queueToListen, guildID, textID, voiceID, authorID) queueToListen, guildID, voiceID)
processInfo = ProcessInfo(process, queueToSend, queueToListen, processInfo = PlayerProcessInfo(process, queueToSend, queueToListen,
playlist, lock, context.channel) playlist, lock, context.channel)
# Create a Thread to listen for the queue coming from the Player Process, this will redirect the Queue to a async # Create a Thread to listen for the queue coming from the Player Process, this will redirect the Queue to a async
thread = Thread(target=self.__listenToCommands, thread = Thread(target=self.__listenToCommands,
@@ -173,10 +258,18 @@ class ProcessManager(Singleton):
# Set the status of this process as sleeping, only the playlist object remains # Set the status of this process as sleeping, only the playlist object remains
self.__playersProcess[guildID].setStatus(ProcessStatus.SLEEPING) self.__playersProcess[guildID].setStatus(ProcessStatus.SLEEPING)
def __putCommandInQueue(self, queue: Queue, command: VCommands) -> None:
try:
queue.put(command)
except Exception as e:
print(f'[ERROR PUTTING COMMAND IN QUEUE] -> {e}')
async def showNowPlaying(self, guildID: int, song: Song) -> None: async def showNowPlaying(self, guildID: int, song: Song) -> None:
commandExecutor = self.__playersCommandsExecutor[guildID] commandExecutor = self.__playersCommandsExecutor[guildID]
processInfo = self.__playersProcess[guildID] processInfo = self.__playersProcess[guildID]
await commandExecutor.sendNowPlaying(processInfo, song) playlist = processInfo.getPlaylist()
channel = processInfo.getTextChannel()
await commandExecutor.sendNowPlaying(playlist, channel, song)
class VManager(BaseManager): class VManager(BaseManager):

391
Parallelism/ThreadPlayer.py Normal file
View File

@@ -0,0 +1,391 @@
import asyncio
from time import time
from urllib.parse import parse_qs, urlparse
from discord import PCMVolumeTransformer, VoiceClient
from asyncio import AbstractEventLoop
from threading import RLock, Thread
from multiprocessing import Lock
from typing import Callable
from discord import Guild, FFmpegPCMAudio, VoiceChannel
from Music.Playlist import Playlist
from Music.Song import Song
from Config.Configs import VConfigs
from Music.VulkanBot import VulkanBot
from Music.Downloader import Downloader
from Parallelism.Commands import VCommands, VCommandsType
class TimeoutClock:
def __init__(self, callback: Callable, loop: asyncio.AbstractEventLoop):
self.__callback = callback
self.__task = loop.create_task(self.__executor())
async def __executor(self):
await asyncio.sleep(VConfigs().VC_TIMEOUT)
await self.__callback()
def cancel(self):
self.__task.cancel()
class ThreadPlayer(Thread):
"""Player Thread to control the song playback in the same Process of the Main Process"""
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 = bot.loop
self.__playerLock: RLock = RLock()
# Discord context ID
self.__voiceChannelID = voiceID
self.__guild: Guild = guild
self.__voiceChannel: VoiceChannel = voiceChannel
self.__voiceClient: VoiceClient = None
self.__currentSongChangeVolume = False
self.__songVolumeUsing = 1
self.__downloader = Downloader()
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 __set_volume(self, volume: float) -> None:
"""Set the volume of the player, must be values between 0 and 100"""
try:
if self.__voiceClient is None:
return
if not isinstance(volume, float):
print('[THREAD ERROR] -> Volume instance must be float')
return
if volume < 0:
volume = 0
if volume > 100:
volume = 100
volume = volume / 100
if not self.__currentSongChangeVolume:
print('[THREAD ERROR] -> Cannot change the volume of this song')
return
self.__songVolumeUsing = volume
self.__voiceClient.source.volume = volume
except Exception as e:
print(e)
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:
song = self.__playlist.next_song()
if song is not None:
await self.__playSong(song)
self.__playing = True
async def __playSong(self, song: Song) -> None:
"""Function that will trigger the player to play the song"""
try:
self.__playerLock.acquire()
if song is None:
return
if song.source is None:
return self.__playNext(None)
# If not connected, connect to bind channel
if self.__voiceClient is None:
await self.__connectToVoiceChannel()
# If the voice channel disconnect for some reason
if not self.__voiceClient.is_connected():
print('[THREAD PLAYER -> 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('[THREAD PLAYER -> SONG ALREADY PLAYING, RETURNING]')
self.__playlist.add_song_start(song)
return
songStillAvailable = self.__verifyIfSongAvailable(song)
if not songStillAvailable:
print('[THREAD PLAYER -> SONG NOT AVAILABLE ANYMORE, DOWNLOADING AGAIN]')
song = self.__downloadSongAgain(song)
self.__playing = True
self.__songPlaying = song
player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS)
if not player.is_opus():
player = PCMVolumeTransformer(player, self.__songVolumeUsing)
self.__currentSongChangeVolume = True
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)
await self.__callback(nowPlayingCommand, self.__guild, song)
except Exception as e:
print(f'[THREAD PLAYER -> 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'[THREAD PLAYER -> ERROR PLAYING SONG] -> {error}')
with self.__playlistLock:
with self.__playerLock:
self.__currentSongChangeVolume = False
if self.__forceStop: # If it's forced to stop player
self.__forceStop = False
return None
song = self.__playlist.next_song()
if song is not None:
self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}')
else:
self.__playlist.loop_off()
self.__songPlaying = None
self.__playing = False
# Send a command to the main process to kill this thread
self.__exitCB(self.__guild)
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'[THREAD PLAYER -> 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 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.__verifyIfIsPlaying():
# Will forbidden next_song to execute after stopping current player
self.__forceStop = True
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}')
async def receiveCommand(self, command: VCommands) -> None:
try:
self.__playerLock.acquire()
type = command.getType()
args = command.getArgs()
if type == VCommandsType.PAUSE:
self.__pause()
elif type == VCommandsType.RESUME:
await self.__resume()
elif type == VCommandsType.SKIP:
await self.__skip()
elif type == VCommandsType.PLAY:
await self.__playPlaylistSongs()
elif type == VCommandsType.PREV:
await self.__playPrev(args)
elif type == VCommandsType.RESET:
await self.__reset()
elif type == VCommandsType.STOP:
await self.__stop()
elif type == VCommandsType.VOLUME:
self.__set_volume(args)
else:
print(f'[THREAD PLAYER ERROR] -> Unknown Command Received: {command}')
except Exception as e:
print(f'[THREAD PLAYER -> ERROR IN COMMAND RECEIVER] -> {type} - {e}')
finally:
self.__playerLock.release()
def __pause(self) -> None:
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.__voiceClient is None:
return
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.__voiceClient is not None:
if self.__voiceClient.is_connected():
with self.__playlistLock:
self.__playlist.loop_off()
self.__playlist.clear()
self.__voiceClient.stop()
await self.__voiceClient.disconnect()
self.__songPlaying = None
self.__playing = False
self.__voiceClient = None
# If the voiceClient is not None we finish things
else:
await self.__forceBotDisconnectAndStop()
async def __resume(self) -> None:
# Lock to work with Player
with self.__playerLock:
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.__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('[THREAD PLAYER -> 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'[THREAD PLAYER -> ERROR FORCING BOT TO STOP] -> {e}')
finally:
self.__voiceClient = None
with self.__playlistLock:
self.__playlist.clear()
self.__playlist.loop_off()
async def __timeoutHandler(self) -> None:
try:
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.__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
with self.__playerLock:
with self.__playlistLock:
self.__playlist.loop_off()
await self.__forceBotDisconnectAndStop()
except Exception as e:
print(f'[THREAD PLAYER -> ERROR IN TIMEOUT] -> {e}')
def __isBotAloneInChannel(self) -> bool:
try:
if len(self.__voiceClient.channel.members) <= 1:
return True
else:
return False
except Exception as e:
print(f'[THREAD PLAYER -> ERROR IN CHECK BOT ALONE] -> {e}')
return False
async def __connectToVoiceChannel(self) -> bool:
try:
print('[THREAD PLAYER -> CONNECTING TO VOICE CHANNEL]')
# If the voiceChannel is not defined yet, like if the Bot is still loading, wait until we get the voiceChannel
if self.__voiceChannel is None:
while True:
self.__voiceChannel = self.__bot.get_channel(self.__voiceChannelID)
if self.__voiceChannel is None:
await asyncio.sleep(0.2)
else:
break
try:
voiceClient = self.__guild.voice_client
if voiceClient is not None:
await voiceClient.disconnect(force=True)
except Exception as e:
print(f'[THREAD PLAYER -> ERROR FORCING DISCONNECT] -> {e}')
self.__voiceClient = await self.__voiceChannel.connect(reconnect=True, timeout=None)
return True
except Exception as e:
print(f'[THREAD PLAYER -> ERROR CONNECTING TO VC] -> {e}')
return False

View File

@@ -0,0 +1,163 @@
from threading import RLock
from typing import Any, Dict, Union
from Config.Singleton import Singleton
from discord import Guild, Interaction, TextChannel
from discord.ext.commands import Context
from Parallelism.AbstractProcessManager import AbstractPlayersManager
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
class ThreadPlayerInfo:
"""
Class to store the reference to all structures to maintain a player thread
"""
def __init__(self, thread: ThreadPlayer, playlist: Playlist, lock: RLock, textChannel: TextChannel) -> None:
self.__thread = thread
self.__playlist = playlist
self.__lock = lock
self.__textChannel = textChannel
def getPlayer(self) -> ThreadPlayer:
return self.__thread
def getPlaylist(self) -> Playlist:
return self.__playlist
def getLock(self) -> RLock:
return self.__lock
def getTextChannel(self) -> TextChannel:
return self.__textChannel
class ThreadPlayerManager(Singleton, AbstractPlayersManager):
"""
Manage all running player threads, creating and storing them for future calls
"""
def __init__(self, bot: VulkanBot = None) -> None:
if not super().created:
self.__bot = bot
self.__playersThreads: Dict[int, ThreadPlayerInfo] = {}
async def sendCommandToPlayer(self, command: VCommands, guild: Guild, context: Union[Context, Interaction], forceCreation: bool = False):
playerInfo = self.__playersThreads[guild.id]
player = playerInfo.getPlayer()
if player is None and forceCreation:
self.__createPlayerThreadInfo(context)
if player is None:
return
await player.receiveCommand(command)
async def __receiveCommand(self, command: VCommands, guild: Guild, args: Any) -> None:
commandType = command.getType()
if commandType == VCommandsType.NOW_PLAYING:
await self.showNowPlaying(guild, args)
else:
print(
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) -> RLock:
playerInfo = self.__getRunningPlayerInfo(guild)
if playerInfo:
return playerInfo.getLock()
def verifyIfPlayerExists(self, guild: Guild) -> bool:
return guild.id in self.__playersThreads.keys()
def createPlayerForGuild(self, guild: Guild, context: Union[Context, Interaction]):
try:
if guild.id not in self.__playersThreads.keys():
self.__playersThreads[guild.id] = self.__createPlayerThreadInfo(context)
else:
# If the thread has ended create a new one
if not self.__playersThreads[guild.id].getPlayer().is_alive():
self.__playersThreads[guild.id] = self.__recreateThread(guild, context)
return self.__playersThreads[guild.id]
except Exception as e:
print(f'[Error In GetPlayerContext] -> {e}')
def resetPlayer(self, guild: Guild, context: Context) -> None:
if guild.id not in self.__playersThreads.keys():
return None
# Recreate the thread keeping the playlist
newPlayerInfo = self.__recreateThread(guild, context)
newPlayerInfo.getPlayer().start()
# Send a command to start the play again
playCommand = VCommands(VCommandsType.PLAY)
newPlayerInfo.getQueueToPlayer().put(playCommand)
self.__playersThreads[guild.id] = newPlayerInfo
def __getRunningPlayerInfo(self, guild: Guild) -> ThreadPlayerInfo:
if guild.id not in self.__playersThreads.keys():
print('Process Info not found')
return None
return self.__playersThreads[guild.id]
def __createPlayerThreadInfo(self, context: Union[Context, Interaction]) -> ThreadPlayerInfo:
guildID: int = context.guild.id
if isinstance(context, Interaction):
voiceID: int = context.user.voice.channel.id
else:
voiceID: int = context.author.voice.channel.id
voiceChannel = context.author.voice.channel
playlist = Playlist()
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)
player.start()
return playerInfo
def __deleteThread(self, guild: Guild) -> None:
"""Tries to delete the thread and removes all the references to it"""
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
def __recreateThread(self, guild: Guild, context: Union[Context, Interaction]) -> ThreadPlayerInfo:
self.__stopPossiblyRunningProcess(guild)
guildID: int = context.guild.id
if isinstance(context, Interaction):
voiceID: int = context.user.voice.channel.id
else:
voiceID: int = context.author.voice.channel.id
voiceChannel = context.author.voice.channel
playlist = self.__playersThreads[guildID].getPlaylist()
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)
player.start()
return playerInfo
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)

View File

@@ -18,6 +18,7 @@ Vulkan uses multiprocessing and asynchronous Python modules to maximize Music Pl
- Support for the new Discord Slash commands. - Support for the new Discord Slash commands.
- Search for all musics in Queue using buttons. - Search for all musics in Queue using buttons.
- Shortcut the playing of one song using dropdown menu. - Shortcut the playing of one song using dropdown menu.
- Manage the volume of the songs.
- Manage the loop of one or all playing musics. - Manage the loop of one or all playing musics.
- Manage the order and remove musics from the queue. - Manage the order and remove musics from the queue.
- Shuffle the musics queue order. - Shuffle the musics queue order.
@@ -34,8 +35,10 @@ Vulkan uses multiprocessing and asynchronous Python modules to maximize Music Pl
</p> </p>
<hr>
<br>
# **How to use it** ## **Setting up for yourself**
### **Requirements** ### **Requirements**
@@ -60,29 +63,38 @@ You can download the executables in this link `https://www.ffmpeg.org/download.h
### **.Env File Example** ### **.Env File Example**
This is an example of how your .env file (located in root) should look like. This is an example of how your .env file (located in root) should look like.
``` ```env
BOT_TOKEN=Your_Own_Bot_Token BOT_TOKEN=Your_Own_Bot_Token
SPOTIFY_ID=Your_Own_Spotify_ID SPOTIFY_ID=Your_Own_Spotify_ID
SPOTIFY_SECRET=Your_Own_Spotify_Secret SPOTIFY_SECRET=Your_Own_Spotify_Secret
BOT_PREFIX=Your_Wanted_Prefix_For_Vulkan BOT_PREFIX=Your_Wanted_Prefix_For_Vulkan
SHOULD_AUTO_DISCONNECT_WHEN_ALONE=True #all settings can be set like this
#etc... All settings can be set this way
``` ```
### **⚙️ Configs** ### **⚙️ 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 bot's configuration is stored in the [.env](.env) file, you can change the prefix and the bot token there, as well as all the other configurations.
Take a look in the [Settings page](.github/Docs/SETTINGS.md) to personalize the Bot for you.
### **Initialization** ### **Initialization**
- Go to [Discord](https://discord.com/developers) and invite your Bot to your own server - Go to [Discord](https://discord.com/developers) and invite your Bot to your own server
- Run ```python main.py``` in console to start - Run ```python main.py``` in console to start
- Give this project a nice 🌟
### **🐳 Docker**
You can also run this project in a Docker container. You can find the instructions to run this project in a Docker container in the [Docker Instructions](.github/Docs/DOCKER.md) page.
<br> <br>
<hr> <hr>
<br> <br>
## **🚀 Heroku**
To deploy and run your Bot in Heroku 24/7, follow the instructions in the [Heroku Instructions](HEROKU.md) page. ## **🚀 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](.github/Docs/HEROKU.md) page.
## 🧪 Tests ## 🧪 Tests
The tests were written manually with no package due to problems with async function in other packages, to execute them type in root: <br> The tests were written manually with no package due to problems with async function in other packages, to execute them type in root: <br>

8
docker-compose.yaml Normal file
View File

@@ -0,0 +1,8 @@
version: '3'
services:
vulkan:
build:
context: .
dockerfile: Dockerfile
env_file:
- .env

Binary file not shown.