60 Commits
v1.4 ... master

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
Rafael Vargas
10e38a8809 Fixing error 2022-10-03 21:40:54 -03:00
Rafael Vargas
3b198cf78a Adding support to slash commands 2022-09-22 16:20:05 -03:00
Rafael Vargas
ef66bf8bcb Fixing error in getting song original url in queue handler 2022-09-16 22:01:45 -03:00
Rafael Vargas
d10264b97c Updating requirements and fixing small bug 2022-09-16 21:33:09 -03:00
Rafael Vargas
ba57a3e18d Now using hyperlinks in Queue and adding support message as random string 2022-09-07 21:47:09 -03:00
Rafael Vargas
5f60c12179 Merge pull request #26 from RafaelSolVargas/jumpMusic
Upgrading Song Queue message
2022-08-16 19:36:17 -03:00
Rafael Vargas
de5aed380b Updating README 2022-08-16 18:32:11 -04:00
Rafael Vargas
2794f1a6d0 Upgrading views manager in messages timeout 2022-08-16 18:19:17 -04:00
Rafael Vargas
2d27a2f080 Changing View in Queue message and creating new handler to jump to music 2022-08-07 20:03:13 -04:00
Rafael Vargas
15f8ea7cb2 Deleting not used files 2022-08-04 18:29:58 -04:00
Rafael Vargas
0c20f68c2b Upgrading messages sended mananger and refactoring Buttons logic 2022-08-04 18:25:06 -04:00
Rafael Vargas
2627f95a6d Adding pages to songs queue to move between all queue, adding buttons to queue embed to better user experience 2022-08-02 21:52:54 -04:00
Rafael Vargas
6ba7734a36 Updating README.md and creating HEROKU.md 2022-07-31 22:15:18 -04:00
Rafael Vargas
4fd23c56b6 Fixing error in sending commands to closed process queue 2022-07-31 19:50:59 -04:00
Rafael Vargas
5b61947904 Fixing error in modules import when bot runned out of the root 2022-07-31 19:03:41 -04:00
Rafael Vargas
a9cfaf62a4 Updating README 2022-07-29 18:12:04 -03:00
Rafael Vargas
a5cecd85d4 Merge pull request #22 from RafaelSolVargas/upgradingUI
Creating Buttons for Commands
2022-07-29 00:46:31 -03:00
Rafael Vargas
7f1ffb6b23 Fixing erros with buttons handlers and updating README 2022-07-29 00:41:03 -03:00
84 changed files with 3251 additions and 1298 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
```

25
.github/Docs/HEROKU.md vendored Normal file
View File

@@ -0,0 +1,25 @@
<h1 align="center">Configuring Heroku</h1>
> Heroku doesn't offer free services anymore
Nobody wants to run the Vulkan process on their machine, so we host the process on Heroku, <s>a cloud platform that contains free</s>.<br>
To configure the Vulkan to run in your Heroku account you will need to:
- Create an application project in Heroku.
- Configure the environment variables in your application.
- Add these buildpacks to your application:
```
- heroku/python
- https://github.com/jonathanong/heroku-buildpack-ffmpeg-latest.git
- https://github.com/xrisk/heroku-opus.git
```
The order shown above is exactly the upside-down order that Buildpacks should appear in Heroku.
- Set the heroku stack to be `heroku-20`. <br>
As shown in this issue: [Issue](https://github.com/RafaelSolVargas/Vulkan/issues/25) the heroku-buildpack doesn't work properly with the heroku application Stack set as above `heroku-20`.
<br>
This [Youtube Video](https://www.youtube.com/watch?v=BPvg9bndP1U&ab_channel=TechWithTim) shows the process of hosting a Bot in Heroku.
You can also fork this project and set in Heroku to your application automatically deploy when your project receive a new commit, and then control when new versions of Vulkan become available to your Bot.

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.

1
.gitignore vendored
View File

@@ -1,5 +1,4 @@
.vscode
assets/
__pycache__
.env
.cache

BIN
Assets/playermenu.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
Assets/queuemessage.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

BIN
Assets/vulkan-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
Assets/vulkancommands.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

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

View File

@@ -1,3 +1,4 @@
from random import random
from Config.Messages import Messages
from Config.Exceptions import VulkanError
from discord import Embed
@@ -12,6 +13,13 @@ class VEmbeds:
self.__messages = Messages()
self.__colors = VColors()
def __willShowProject(self) -> bool:
return (random() * 100 < self.__config.CHANCE_SHOW_PROJECT)
def __addFooterContent(self, embed: Embed) -> Embed:
footerText = f'\u200b Please support this project by leaving a star: {self.__config.PROJECT_URL}'
return embed.set_footer(text=footerText, icon_url=self.__config.SUPPORTING_ICON)
def ONE_SONG_LOOPING(self, info: dict) -> Embed:
title = self.__messages.ONE_SONG_LOOPING
return self.SONG_INFO(info, title)
@@ -34,6 +42,14 @@ class VEmbeds:
)
return embed
def INVALID_INDEX(self) -> Embed:
embed = Embed(
title=self.__messages.BAD_COMMAND_TITLE,
description=self.__messages.INVALID_INDEX,
colour=self.__colors.BLACK
)
return embed
def SONG_ADDED_TWO(self, info: dict, pos: int) -> Embed:
embed = self.SONG_INFO(info, self.__messages.SONG_ADDED_TWO, pos)
return embed
@@ -105,6 +121,8 @@ class VEmbeds:
value=position,
inline=True)
if self.__willShowProject():
embedvc = self.__addFooterContent(embedvc)
return embedvc
def SONG_MOVED(self, song_name: str, pos1: int, pos2: int) -> Embed:
@@ -162,6 +180,14 @@ class VEmbeds:
)
return embed
def INVALID_ARGUMENTS(self):
embed = Embed(
title=self.__messages.BAD_COMMAND_TITLE,
description=self.__messages.INVALID_ARGUMENTS,
colour=self.__colors.BLACK
)
return embed
def COMMAND_NOT_FOUND(self) -> Embed:
embed = Embed(
title=self.__messages.COMMAND_NOT_FOUND_TITLE,
@@ -261,6 +287,41 @@ class VEmbeds:
)
return embed
def PLAYER_RESUMED(self) -> Embed:
embed = Embed(
title=self.__messages.SONG_RESUMED,
colour=self.__colors.BLUE
)
return embed
def SKIPPING_SONG(self) -> Embed:
embed = Embed(
title=self.__messages.SONG_SKIPPED,
colour=self.__colors.BLUE
)
return embed
def STOPPING_PLAYER(self) -> Embed:
embed = Embed(
title=self.__messages.STOPPING,
colour=self.__colors.BLUE
)
return embed
def RETURNING_SONG(self) -> Embed:
embed = Embed(
title=self.__messages.RETURNING_SONG,
colour=self.__colors.BLUE
)
return embed
def PLAYER_PAUSED(self) -> Embed:
embed = Embed(
title=self.__messages.SONG_PAUSED,
colour=self.__colors.BLUE
)
return embed
def NOT_PREVIOUS_SONG(self) -> Embed:
embed = Embed(
title=self.__messages.SONG_PLAYER,
@@ -282,6 +343,13 @@ class VEmbeds:
description=self.__messages.PLAYER_NOT_PLAYING,
colour=self.__colors.BLUE)
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:
embed = Embed(
@@ -289,6 +357,9 @@ class VEmbeds:
description=description,
colour=self.__colors.BLUE
)
if self.__willShowProject():
embed = self.__addFooterContent(embed)
return embed
def INVITE(self, bot_id: str) -> Embed:
@@ -332,6 +403,11 @@ class VEmbeds:
)
return embed
def PLAYLIST_CLEAR(self) -> Embed:
return Embed(
description=self.__messages.PLAYLIST_CLEAR
)
def CARA_COROA(self, result: str) -> Embed:
embed = Embed(
title='Cara Coroa',

View File

@@ -79,6 +79,11 @@ class ErrorRemoving(VulkanError):
super().__init__(message, title, *args)
class InvalidIndex(VulkanError):
def __init__(self, message='', title='', *args: object) -> None:
super().__init__(message, title, *args)
class NumberRequired(VulkanError):
def __init__(self, message='', title='', *args: object) -> None:
super().__init__(message, title, *args)

19
Config/Folder.py Normal file
View File

@@ -0,0 +1,19 @@
import os
from Config.Singleton import Singleton
class Folder(Singleton):
def __init__(self) -> None:
if not self.created:
filePath = os.path.dirname(__file__)
self.rootFolder = self.__getRootFolder(filePath)
def __getRootFolder(self, current: str) -> str:
last_sep_index = -1
for x in range(len(current) - 1, -1, -1):
if current[x] == os.sep:
last_sep_index = x
break
path = current[:last_sep_index] + os.sep
return path

View File

@@ -21,19 +21,21 @@ class Helper(Singleton):
Off - Disable loop."""
self.HELP_NP = 'Show the info of the current song.'
self.HELP_NP_LONG = 'Show the information of the song being played.\n\nRequire: A song being played.\nArguments: None.'
self.HELP_QUEUE = f'Show the first {config.MAX_PRELOAD_SONGS} songs in queue.'
self.HELP_QUEUE_LONG = f'Show the first {config.MAX_PRELOAD_SONGS} song in the queue.\n\nArguments: None.'
self.HELP_QUEUE = f'Show the first {config.MAX_SONGS_IN_PAGE} songs in queue.'
self.HELP_QUEUE_LONG = f'Show the first {config.MAX_SONGS_IN_PAGE} song in the queue.\n\nArguments: None.'
self.HELP_PAUSE = 'Pauses the song player.'
self.HELP_PAUSE_LONG = 'If playing, pauses the song player.\n\nArguments: None'
self.HELP_PREV = 'Play the previous song.'
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_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_HISTORY = f'Show the history of played songs.'
self.HELP_HISTORY_LONG = f'Show the last {config.MAX_SONGS_HISTORY} played songs'
self.HELP_MOVE = 'Moves a song from position x to y in queue.'
self.HELP_MOVE = 'Moves a song from position pos1 to pos2 in queue.'
self.HELP_MOVE_LONG = 'Moves a song from position x to position y in queue.\n\nRequire: Positions to be both valid numbers.\nArguments: 1º Number => Initial position, 2º Number => Destination position. Both numbers could be -1 to refer to the last song in queue.\nDefault: By default, if the second number is not passed, it will be 1, moving the selected song to 1º position.'
self.HELP_REMOVE = 'Remove a song in position x.'
self.HELP_REMOVE_LONG = 'Remove a song from queue in the position passed.\n\nRequire: Position to be a valid number.\nArguments: 1º self.Number => Position in queue of the song.'
@@ -49,3 +51,6 @@ class Helper(Singleton):
self.HELP_CHOOSE_LONG = 'Choose randomly one item passed in this command.\n\nRequire: Itens to be separated by comma.\nArguments: As much as you want.'
self.HELP_CARA = 'Return cara or coroa.'
self.HELP_CARA_LONG = 'Return cara or coroa.'
self.SLASH_QUEUE_DESCRIPTION = f'Number of queue page, there is only {config.MAX_SONGS_IN_PAGE} musics by page'
self.SLASH_MOVE_HELP = 'Moves a song from position pos1 to pos2 in queue.'

View File

@@ -16,6 +16,7 @@ class Messages(Singleton):
self.SONGINFO_REQUESTER = 'Requester: '
self.SONGINFO_POSITION = 'Position: '
self.VOLUME_CHANGED = 'Song volume changed to `{}`%'
self.SONGS_ADDED = 'Downloading `{}` songs 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'
@@ -26,8 +27,12 @@ class Messages(Singleton):
self.ALL_SONGS_LOOPING = f'{self.__emojis.MUSIC} Looping All Songs'
self.SONG_PAUSED = f'{self.__emojis.PAUSE} Song paused'
self.SONG_RESUMED = f'{self.__emojis.PLAY} Song playing'
self.SONG_SKIPPED = f'{self.__emojis.SKIP} Song skipped'
self.RETURNING_SONG = f'{self.__emojis.BACK} Playing previous song'
self.STOPPING = f'{self.__emojis.STOP} Player Stopped'
self.EMPTY_QUEUE = f'{self.__emojis.QUEUE} Song queue is empty, use {configs.BOT_PREFIX}play to add new songs'
self.SONG_DOWNLOADING = f'{self.__emojis.DOWNLOADING} Downloading...'
self.PLAYLIST_CLEAR = f'{self.__emojis.MUSIC} Playlist is now empty'
self.HISTORY_TITLE = f'{self.__emojis.MUSIC} Played Songs'
self.HISTORY_EMPTY = f'{self.__emojis.QUEUE} There is no musics in history'
@@ -52,6 +57,7 @@ class Messages(Singleton):
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.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.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'
@@ -64,6 +70,8 @@ class Messages(Singleton):
self.NO_CHANNEL = 'To play some music, connect to any voice channel first.'
self.NO_GUILD = f'This server does not has a Player, try {configs.BOT_PREFIX}reset'
self.INVALID_INPUT = f'This URL was too strange, try something better or type {configs.BOT_PREFIX}help play'
self.INVALID_INDEX = f'Invalid index passed as argument.'
self.INVALID_ARGUMENTS = f'Invalid arguments passed to command.'
self.DOWNLOADING_ERROR = f"{self.__emojis.ERROR} It's impossible to download and play this video"
self.EXTRACTING_ERROR = f'{self.__emojis.ERROR} An error ocurred while searching for the songs'

View File

@@ -21,7 +21,7 @@ class ControlCog(Cog):
'MUSIC': ['resume', 'pause', 'loop', 'stop',
'skip', 'play', 'queue', 'clear',
'np', 'shuffle', 'move', 'remove',
'reset', 'prev', 'history'],
'reset', 'prev', 'history', 'volume'],
'RANDOM': ['choose', 'cara', 'random']
}
@@ -74,7 +74,8 @@ class ControlCog(Cog):
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)
@command(name='invite', help=helper.HELP_INVITE, description=helper.HELP_INVITE_LONG, aliases=['convite', 'inv', 'convidar'])

View File

@@ -1,6 +1,8 @@
from discord.ext.commands import Context, command, Cog
from Config.Exceptions import InvalidInput
from Config.Helper import Helper
from Handlers.ClearHandler import ClearHandler
from Handlers.HandlerResponse import HandlerResponse
from Handlers.MoveHandler import MoveHandler
from Handlers.NowPlayingHandler import NowPlayingHandler
from Handlers.PlayHandler import PlayHandler
@@ -15,12 +17,15 @@ from Handlers.ResumeHandler import ResumeHandler
from Handlers.HistoryHandler import HistoryHandler
from Handlers.QueueHandler import QueueHandler
from Handlers.LoopHandler import LoopHandler
from UI.Responses.EmoteCogResponse import EmoteCommandResponse
from UI.Responses.EmbedCogResponse import EmbedCommandResponse
from UI.Views.PlayerView import PlayerView
from Handlers.VolumeHandler import VolumeHandler
from Messages.MessagesCategory import MessagesCategory
from Messages.Responses.EmoteCogResponse import EmoteCommandResponse
from Messages.Responses.EmbedCogResponse import EmbedCommandResponse
from Music.VulkanBot import VulkanBot
from Config.Configs import VConfigs
from Parallelism.ProcessManager import ProcessManager
from Config.Embeds import VEmbeds
from Parallelism.ProcessPlayerManager import ProcessPlayerManager
from Parallelism.ThreadPlayerManager import ThreadPlayerManager
helper = Helper()
@@ -34,30 +39,75 @@ class MusicCog(Cog):
def __init__(self, bot: VulkanBot) -> None:
self.__bot: VulkanBot = bot
VConfigs().setProcessManager(ProcessManager(bot))
self.__embeds = VEmbeds()
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'])
async def play(self, ctx: Context, *args) -> None:
try:
controller = PlayHandler(ctx, self.__bot)
response = await controller.run(args)
if len(args) > 1:
track = " ".join(args)
else:
track = args[0]
response = await controller.run(track)
if response is not None:
view1 = EmbedCommandResponse(response)
view2 = EmoteCommandResponse(response)
await view1.run()
await view2.run()
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="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'])
async def queue(self, ctx: Context) -> None:
async def queue(self, ctx: Context, *args) -> None:
try:
pageNumber = " ".join(args)
controller = QueueHandler(ctx, self.__bot)
response = await controller.run()
view2 = EmbedCommandResponse(response)
await view2.run()
if pageNumber == "":
response = await controller.run()
else:
pageNumber = int(pageNumber)
pageNumber -= 1 # Change index 1 to 0
response = await controller.run(pageNumber)
cogResponser = EmbedCommandResponse(response, MessagesCategory.QUEUE)
await cogResponser.run()
except ValueError as e:
# Draft a Handler Response to pass to cogResponser
error = InvalidInput()
embed = self.__embeds.INVALID_ARGUMENTS()
response = HandlerResponse(ctx, embed, error)
cogResponser = EmbedCommandResponse(response, MessagesCategory.QUEUE)
await cogResponser.run(deleteLast=False)
except Exception as e:
print(f'[ERROR IN COG] -> {e}')
@@ -67,12 +117,10 @@ class MusicCog(Cog):
controller = SkipHandler(ctx, self.__bot)
response = await controller.run()
if response.success:
view = EmoteCommandResponse(response)
else:
view = EmbedCommandResponse(response)
await view.run()
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}')
@@ -82,12 +130,10 @@ class MusicCog(Cog):
controller = StopHandler(ctx, self.__bot)
response = await controller.run()
if response.success:
view = EmoteCommandResponse(response)
else:
view = EmbedCommandResponse(response)
await view.run()
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}')
@@ -97,10 +143,10 @@ class MusicCog(Cog):
controller = PauseHandler(ctx, self.__bot)
response = await controller.run()
view1 = EmoteCommandResponse(response)
view2 = EmbedCommandResponse(response)
await view1.run()
await view2.run()
cogResponser1 = EmoteCommandResponse(response, MessagesCategory.PLAYER)
cogResponser2 = EmbedCommandResponse(response, MessagesCategory.PLAYER)
await cogResponser1.run()
await cogResponser2.run()
except Exception as e:
print(f'[ERROR IN COG] -> {e}')
@@ -110,10 +156,10 @@ class MusicCog(Cog):
controller = ResumeHandler(ctx, self.__bot)
response = await controller.run()
view1 = EmoteCommandResponse(response)
view2 = EmbedCommandResponse(response)
await view1.run()
await view2.run()
cogResponser1 = EmoteCommandResponse(response, MessagesCategory.PLAYER)
cogResponser2 = EmbedCommandResponse(response, MessagesCategory.PLAYER)
await cogResponser1.run()
await cogResponser2.run()
except Exception as e:
print(f'[ERROR IN COG] -> {e}')
@@ -124,10 +170,10 @@ class MusicCog(Cog):
response = await controller.run()
if response is not None:
view1 = EmbedCommandResponse(response)
view2 = EmoteCommandResponse(response)
await view1.run()
await view2.run()
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}')
@@ -137,10 +183,10 @@ class MusicCog(Cog):
controller = HistoryHandler(ctx, self.__bot)
response = await controller.run()
view1 = EmbedCommandResponse(response)
view2 = EmoteCommandResponse(response)
await view1.run()
await view2.run()
cogResponser1 = EmbedCommandResponse(response, MessagesCategory.HISTORY)
cogResponser2 = EmoteCommandResponse(response, MessagesCategory.HISTORY)
await cogResponser1.run()
await cogResponser2.run()
except Exception as e:
print(f'[ERROR IN COG] -> {e}')
@@ -150,10 +196,10 @@ class MusicCog(Cog):
controller = LoopHandler(ctx, self.__bot)
response = await controller.run(args)
view1 = EmoteCommandResponse(response)
view2 = EmbedCommandResponse(response)
await view1.run()
await view2.run()
cogResponser1 = EmoteCommandResponse(response, MessagesCategory.LOOP)
cogResponser2 = EmbedCommandResponse(response, MessagesCategory.LOOP)
await cogResponser1.run()
await cogResponser2.run()
except Exception as e:
print(f'[ERROR IN COG] -> {e}')
@@ -163,8 +209,10 @@ class MusicCog(Cog):
controller = ClearHandler(ctx, self.__bot)
response = await controller.run()
view = EmoteCommandResponse(response)
await view.run()
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}')
@@ -174,10 +222,10 @@ class MusicCog(Cog):
controller = NowPlayingHandler(ctx, self.__bot)
response = await controller.run()
view1 = EmbedCommandResponse(response)
view2 = EmoteCommandResponse(response)
await view1.run()
await view2.run()
cogResponser1 = EmbedCommandResponse(response, MessagesCategory.NOW_PLAYING)
cogResponser2 = EmoteCommandResponse(response, MessagesCategory.NOW_PLAYING)
await cogResponser1.run()
await cogResponser2.run()
except Exception as e:
print(f'[ERROR IN COG] -> {e}')
@@ -187,10 +235,10 @@ class MusicCog(Cog):
controller = ShuffleHandler(ctx, self.__bot)
response = await controller.run()
view1 = EmbedCommandResponse(response)
view2 = EmoteCommandResponse(response)
await view1.run()
await view2.run()
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}')
@@ -200,10 +248,10 @@ class MusicCog(Cog):
controller = MoveHandler(ctx, self.__bot)
response = await controller.run(pos1, pos2)
view1 = EmbedCommandResponse(response)
view2 = EmoteCommandResponse(response)
await view1.run()
await view2.run()
cogResponser1 = EmbedCommandResponse(response, MessagesCategory.MANAGING_QUEUE)
cogResponser2 = EmoteCommandResponse(response, MessagesCategory.MANAGING_QUEUE)
await cogResponser1.run()
await cogResponser2.run()
except Exception as e:
print(f'[ERROR IN COG] -> {e}')
@@ -213,10 +261,10 @@ class MusicCog(Cog):
controller = RemoveHandler(ctx, self.__bot)
response = await controller.run(position)
view1 = EmbedCommandResponse(response)
view2 = EmoteCommandResponse(response)
await view1.run()
await view2.run()
cogResponser1 = EmbedCommandResponse(response, MessagesCategory.MANAGING_QUEUE)
cogResponser2 = EmoteCommandResponse(response, MessagesCategory.MANAGING_QUEUE)
await cogResponser1.run()
await cogResponser2.run()
except Exception as e:
print(f'[ERROR IN COG] -> {e}')
@@ -226,18 +274,13 @@ class MusicCog(Cog):
controller = ResetHandler(ctx, self.__bot)
response = await controller.run()
view1 = EmbedCommandResponse(response)
view2 = EmoteCommandResponse(response)
await view1.run()
await view2.run()
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='rafael')
async def rafael(self, ctx: Context) -> None:
view = PlayerView(self.__bot)
await ctx.send(view=view)
def setup(bot):
bot.add_cog(MusicCog(bot))

288
DiscordCogs/SlashCog.py Normal file
View File

@@ -0,0 +1,288 @@
from discord.ext.commands import slash_command, Cog
from discord import Option, ApplicationContext, OptionChoice
from Handlers.ClearHandler import ClearHandler
from Handlers.MoveHandler import MoveHandler
from Handlers.NowPlayingHandler import NowPlayingHandler
from Handlers.PlayHandler import PlayHandler
from Handlers.PrevHandler import PrevHandler
from Handlers.RemoveHandler import RemoveHandler
from Handlers.ResetHandler import ResetHandler
from Handlers.ShuffleHandler import ShuffleHandler
from Handlers.SkipHandler import SkipHandler
from Handlers.PauseHandler import PauseHandler
from Handlers.StopHandler import StopHandler
from Handlers.ResumeHandler import ResumeHandler
from Handlers.HistoryHandler import HistoryHandler
from Handlers.QueueHandler import QueueHandler
from Handlers.LoopHandler import LoopHandler
from Handlers.VolumeHandler import VolumeHandler
from Messages.MessagesCategory import MessagesCategory
from Messages.Responses.SlashEmbedResponse import SlashEmbedResponse
from Music.VulkanBot import VulkanBot
from Config.Embeds import VEmbeds
from Config.Helper import Helper
import traceback
helper = Helper()
class SlashCommands(Cog):
"""
Class to listen to Music commands
It'll listen for commands from discord, when triggered will create a specific Handler for the command
Execute the handler and then create a specific View to be showed in Discord
"""
def __init__(self, bot: VulkanBot) -> None:
self.__bot: VulkanBot = bot
self.__embeds = VEmbeds()
@slash_command(name="play", description=helper.HELP_PLAY)
async def play(self, ctx: ApplicationContext,
music: Option(str, "The music name or URL", required=True)) -> None:
# Due to the utilization of multiprocessing module in this Project, we have multiple instances of the Bot, and by using this flag
# we can control witch bot instance will listen to the commands that Discord send to our application
if not self.__bot.listingSlash:
return
try:
await ctx.defer()
controller = PlayHandler(ctx, self.__bot)
response = await controller.run(music)
if response is not None:
cogResponser1 = SlashEmbedResponse(response, ctx, MessagesCategory.PLAYER)
await cogResponser1.run()
except Exception:
print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}')
@slash_command(name="queue", description=helper.HELP_QUEUE)
async def queue(self, ctx: ApplicationContext,
page_number: Option(int, helper.SLASH_QUEUE_DESCRIPTION, min_value=1, default=1)) -> None:
if not self.__bot.listingSlash:
return
try:
await ctx.defer()
controller = QueueHandler(ctx, self.__bot)
# Change index 1 to 0
page_number -= 1
response = await controller.run(page_number)
cogResponser = SlashEmbedResponse(response, ctx, MessagesCategory.QUEUE)
await cogResponser.run()
except Exception:
print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}')
@slash_command(name="skip", description=helper.HELP_SKIP)
async def skip(self, ctx: ApplicationContext) -> None:
if not self.__bot.listingSlash:
return
try:
await ctx.defer()
controller = SkipHandler(ctx, self.__bot)
response = await controller.run()
cogResponser = SlashEmbedResponse(response, ctx, MessagesCategory.PLAYER)
await cogResponser.run()
except Exception:
print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}')
@slash_command(name='stop', description=helper.HELP_STOP)
async def stop(self, ctx: ApplicationContext) -> None:
if not self.__bot.listingSlash:
return
try:
await ctx.defer()
controller = StopHandler(ctx, self.__bot)
response = await controller.run()
cogResponser = SlashEmbedResponse(response, ctx, MessagesCategory.PLAYER)
await cogResponser.run()
except Exception:
print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}')
@slash_command(name='pause', description=helper.HELP_PAUSE)
async def pause(self, ctx: ApplicationContext) -> None:
if not self.__bot.listingSlash:
return
try:
await ctx.defer()
controller = PauseHandler(ctx, self.__bot)
response = await controller.run()
cogResponser = SlashEmbedResponse(response, ctx, MessagesCategory.PLAYER)
await cogResponser.run()
except Exception:
print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}')
@slash_command(name='resume', description=helper.HELP_RESUME)
async def resume(self, ctx: ApplicationContext) -> None:
if not self.__bot.listingSlash:
return
try:
await ctx.defer()
controller = ResumeHandler(ctx, self.__bot)
response = await controller.run()
cogResponser = SlashEmbedResponse(response, ctx, MessagesCategory.PLAYER)
await cogResponser.run()
except Exception:
print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}')
@slash_command(name='previous', description=helper.HELP_PREV)
async def previous(self, ctx: ApplicationContext) -> None:
if not self.__bot.listingSlash:
return
try:
await ctx.defer()
controller = PrevHandler(ctx, self.__bot)
response = await controller.run()
if response is not None:
cogResponser = SlashEmbedResponse(response, ctx, MessagesCategory.PLAYER)
await cogResponser.run()
except Exception:
print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}')
@slash_command(name='history', description=helper.HELP_HISTORY)
async def history(self, ctx: ApplicationContext) -> None:
if not self.__bot.listingSlash:
return
try:
await ctx.defer()
controller = HistoryHandler(ctx, self.__bot)
response = await controller.run()
cogResponser = SlashEmbedResponse(response, ctx, MessagesCategory.HISTORY)
await cogResponser.run()
except Exception:
print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}')
@slash_command(name='loop', description=helper.HELP_LOOP)
async def loop(self, ctx: ApplicationContext,
loop_type: Option(str, choices=[
OptionChoice(name='off', value='off'),
OptionChoice(name='one', value='one'),
OptionChoice(name='all', value='all')
])) -> None:
if not self.__bot.listingSlash:
return
try:
await ctx.defer()
controller = LoopHandler(ctx, self.__bot)
response = await controller.run(loop_type)
cogResponser = SlashEmbedResponse(response, ctx, MessagesCategory.LOOP)
await cogResponser.run()
except Exception:
print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}')
@slash_command(name='clear', description=helper.HELP_CLEAR)
async def clear(self, ctx: ApplicationContext) -> None:
if not self.__bot.listingSlash:
return
try:
await ctx.defer()
controller = ClearHandler(ctx, self.__bot)
response = await controller.run()
cogResponser = SlashEmbedResponse(response, ctx, MessagesCategory.PLAYER)
await cogResponser.run()
except Exception:
print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}')
@slash_command(name='now_playing', description=helper.HELP_NP)
async def now_playing(self, ctx: ApplicationContext) -> None:
if not self.__bot.listingSlash:
return
try:
await ctx.defer()
controller = NowPlayingHandler(ctx, self.__bot)
response = await controller.run()
cogResponser = SlashEmbedResponse(response, ctx, MessagesCategory.NOW_PLAYING)
await cogResponser.run()
except Exception:
print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}')
@slash_command(name='shuffle_songs', description=helper.HELP_SHUFFLE)
async def shuffle(self, ctx: ApplicationContext) -> None:
if not self.__bot.listingSlash:
return
try:
await ctx.defer()
controller = ShuffleHandler(ctx, self.__bot)
response = await controller.run()
cogResponser = SlashEmbedResponse(response, ctx, MessagesCategory.PLAYER)
await cogResponser.run()
except Exception:
print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}')
@slash_command(name='move_song', description=helper.SLASH_MOVE_HELP)
async def move(self, ctx: ApplicationContext,
from_pos: Option(int, "The position of song to move", min_value=1),
to_pos: Option(int, "The position to put the song, default 1", min_value=1, default=1)) -> None:
if not self.__bot.listingSlash:
return
try:
await ctx.defer()
if from_pos == 0:
from_pos = 1
controller = MoveHandler(ctx, self.__bot)
response = await controller.run(from_pos, to_pos)
cogResponser = SlashEmbedResponse(response, ctx, MessagesCategory.MANAGING_QUEUE)
await cogResponser.run()
except Exception:
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)
async def remove(self, ctx: ApplicationContext,
position: Option(int, "The song position to remove", min_value=1)) -> None:
if not self.__bot.listingSlash:
return
try:
await ctx.defer()
controller = RemoveHandler(ctx, self.__bot)
response = await controller.run(position)
cogResponser = SlashEmbedResponse(response, ctx, MessagesCategory.MANAGING_QUEUE)
await cogResponser.run()
except Exception:
print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}')
@slash_command(name='reset', description=helper.HELP_RESET)
async def reset(self, ctx: ApplicationContext) -> None:
if not self.__bot.listingSlash:
return
try:
await ctx.defer()
controller = ResetHandler(ctx, self.__bot)
response = await controller.run()
cogResponser = SlashEmbedResponse(response, ctx, MessagesCategory.PLAYER)
await cogResponser.run()
except Exception:
print(f'[ERROR IN SLASH COMMAND] -> {traceback.format_exc()}')
def setup(bot):
bot.add_cog(SlashCommands(bot))

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

View File

@@ -2,14 +2,16 @@ from typing import Union
from discord.ext.commands import Context
from Config.Exceptions import VulkanError
from discord import Embed, Interaction
from UI.Views.AbstractView import AbstractView
class HandlerResponse:
def __init__(self, ctx: Union[Context, Interaction], embed: Embed = None, error: VulkanError = None) -> None:
def __init__(self, ctx: Union[Context, Interaction], embed: Embed = None, error: VulkanError = None, view=None) -> None:
self.__ctx: Context = ctx
self.__error: VulkanError = error
self.__embed: Embed = embed
self.__success = False if error else True
self.__view = view
@property
def ctx(self) -> Union[Context, Interaction]:
@@ -19,6 +21,10 @@ class HandlerResponse:
def embed(self) -> Union[Embed, None]:
return self.__embed
@property
def view(self) -> AbstractView:
return self.__view
def error(self) -> Union[VulkanError, None]:
return self.__error

View File

@@ -2,6 +2,7 @@ from discord.ext.commands import Context
from Music.VulkanBot import VulkanBot
from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse
from Parallelism.AbstractProcessManager import AbstractPlayersManager
from Utils.Utils import Utils
from typing import Union
from discord import Interaction
@@ -13,18 +14,16 @@ class HistoryHandler(AbstractHandler):
async def run(self) -> HandlerResponse:
# Get the current process of the guild
processManager = self.config.getProcessManager()
processInfo = processManager.getRunningPlayerInfo(self.guild)
if processInfo:
processLock = processInfo.getLock()
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
playersManager: AbstractPlayersManager = self.config.getPlayersManager()
if playersManager.verifyIfPlayerExists(self.guild):
playerLock = playersManager.getPlayerLock(self.guild)
acquired = playerLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired:
playlist = processInfo.getPlaylist()
history = playlist.getSongsHistory()
processLock.release()
history = playersManager.getPlayerPlaylist(self.guild).getSongsHistory()
playerLock.release()
else:
# 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()
return HandlerResponse(self.ctx, embed)
else:

View File

@@ -0,0 +1,78 @@
from typing import Union
from Config.Exceptions import BadCommandUsage, InvalidInput, NumberRequired, UnknownError, VulkanError
from Handlers.AbstractHandler import AbstractHandler
from discord.ext.commands import Context
from discord import Interaction
from Handlers.HandlerResponse import HandlerResponse
from Music.Playlist import Playlist
from Music.VulkanBot import VulkanBot
from Parallelism.AbstractProcessManager import AbstractPlayersManager
from Parallelism.Commands import VCommands, VCommandsType
class JumpMusicHandler(AbstractHandler):
"""Move a music from a specific position and play it directly"""
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
super().__init__(ctx, bot)
async def run(self, musicPos: str) -> HandlerResponse:
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)
if acquired:
# Try to convert input to int
error = self.__validateInput(musicPos)
if error:
embed = self.embeds.ERROR_EMBED(error.message)
playerLock.release()
return HandlerResponse(self.ctx, embed, error)
# Sanitize the input
playlist = playersManager.getPlayerPlaylist(self.guild)
musicPos = self.__sanitizeInput(playlist, musicPos)
# Validate the position
if not playlist.validate_position(musicPos):
error = InvalidInput()
embed = self.embeds.PLAYLIST_RANGE_ERROR()
playerLock.release()
return HandlerResponse(self.ctx, embed, error)
try:
# Move the selected song
playlist.move_songs(musicPos, 1)
# Send a command to the player to skip the music
command = VCommands(VCommandsType.SKIP, None)
await playersManager.sendCommandToPlayer(command, self.guild, self.ctx)
return HandlerResponse(self.ctx)
except:
embed = self.embeds.ERROR_MOVING()
error = UnknownError()
return HandlerResponse(self.ctx, embed, error)
finally:
playerLock.release()
else:
playersManager.resetPlayer(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed)
def __validateInput(self, position: str) -> Union[VulkanError, None]:
try:
position = int(position)
except:
return NumberRequired(self.messages.ERROR_NUMBER)
def __sanitizeInput(self, playlist: Playlist, position: int) -> int:
position = int(position)
if position == -1:
position = len(playlist.getSongs())
return position

View File

@@ -5,6 +5,7 @@ from Handlers.HandlerResponse import HandlerResponse
from Config.Exceptions import BadCommandUsage
from typing import Union
from discord import Interaction
from Parallelism.AbstractProcessManager import AbstractPlayersManager
class LoopHandler(AbstractHandler):
@@ -12,23 +13,20 @@ class LoopHandler(AbstractHandler):
super().__init__(ctx, bot)
async def run(self, args: str) -> HandlerResponse:
# Get the current process of the guild
processManager = self.config.getProcessManager()
processInfo = processManager.getRunningPlayerInfo(self.guild)
if not processInfo:
playersManager: AbstractPlayersManager = self.config.getPlayersManager()
if not playersManager.verifyIfPlayerExists(self.guild):
embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error)
playlist = processInfo.getPlaylist()
processLock = processInfo.getLock()
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
playlist = playersManager.getPlayerPlaylist(self.guild)
playerLock = playersManager.getPlayerLock(self.guild)
acquired = playerLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired:
if args == '' or args is None:
playlist.loop_all()
embed = self.embeds.LOOP_ALL_ACTIVATED()
processLock.release()
playerLock.release()
return HandlerResponse(self.ctx, embed)
args = args.lower()
@@ -51,9 +49,9 @@ class LoopHandler(AbstractHandler):
error = BadCommandUsage()
embed = self.embeds.BAD_LOOP_USE()
processLock.release()
playerLock.release()
return HandlerResponse(self.ctx, embed, error)
else:
processManager.resetProcess(self.guild, self.ctx)
playersManager.resetPlayer(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED()
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 typing import Union
from discord import Interaction
from Parallelism.AbstractProcessManager import AbstractPlayersManager
class MoveHandler(AbstractHandler):
@@ -14,45 +15,44 @@ class MoveHandler(AbstractHandler):
super().__init__(ctx, bot)
async def run(self, pos1: str, pos2: str) -> HandlerResponse:
processManager = self.config.getProcessManager()
processInfo = processManager.getRunningPlayerInfo(self.guild)
if not processInfo:
playersManager: AbstractPlayersManager = self.config.getPlayersManager()
if not playersManager.verifyIfPlayerExists(self.guild):
embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error)
processLock = processInfo.getLock()
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
playerLock = playersManager.getPlayerLock(self.guild)
acquired = playerLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired:
error = self.__validateInput(pos1, pos2)
if error:
embed = self.embeds.ERROR_EMBED(error.message)
processLock.release()
playerLock.release()
return HandlerResponse(self.ctx, embed, error)
playlist = processInfo.getPlaylist()
playlist = playersManager.getPlayerPlaylist(self.guild)
pos1, pos2 = self.__sanitizeInput(playlist, pos1, pos2)
if not playlist.validate_position(pos1) or not playlist.validate_position(pos2):
error = InvalidInput()
embed = self.embeds.PLAYLIST_RANGE_ERROR()
processLock.release()
playerLock.release()
return HandlerResponse(self.ctx, embed, error)
try:
song = playlist.move_songs(pos1, pos2)
song_name = song.title if song.title else song.identifier
embed = self.embeds.SONG_MOVED(song_name, pos1, pos2)
processLock.release()
playerLock.release()
return HandlerResponse(self.ctx, embed)
except:
# Release the acquired Lock
processLock.release()
playerLock.release()
embed = self.embeds.ERROR_MOVING()
error = UnknownError()
return HandlerResponse(self.ctx, embed, error)
else:
processManager.resetProcess(self.guild, self.ctx)
playersManager.resetPlayer(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed)

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ from discord.ext.commands import Context
from Handlers.AbstractHandler import AbstractHandler
from Config.Exceptions import BadCommandUsage, ImpossibleMove
from Handlers.HandlerResponse import HandlerResponse
from Parallelism.AbstractProcessManager import AbstractPlayersManager
from Parallelism.Commands import VCommands, VCommandsType
from Music.VulkanBot import VulkanBot
from typing import Union
@@ -13,39 +14,34 @@ class PrevHandler(AbstractHandler):
super().__init__(ctx, bot)
async def run(self) -> HandlerResponse:
processManager = self.config.getProcessManager()
processInfo = processManager.getPlayerInfo(self.guild, self.ctx)
if not processInfo:
if not self.__user_connected():
error = ImpossibleMove()
embed = self.embeds.NO_CHANNEL()
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)
playlist = processInfo.getPlaylist()
playlist = playersManager.getPlayerPlaylist(self.guild)
if len(playlist.getHistory()) == 0:
error = ImpossibleMove()
embed = self.embeds.NOT_PREVIOUS_SONG()
return HandlerResponse(self.ctx, embed, error)
if not self.__user_connected():
error = ImpossibleMove()
embed = self.embeds.NO_CHANNEL()
return HandlerResponse(self.ctx, embed, error)
if playlist.isLoopingAll() or playlist.isLoopingOne():
error = BadCommandUsage()
embed = self.embeds.FAIL_DUE_TO_LOOP_ON()
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
prevCommand = VCommands(VCommandsType.PREV, self.author.voice.channel.id)
queue = processInfo.getQueueToPlayer()
queue.put(prevCommand)
return HandlerResponse(self.ctx)
await playersManager.sendCommandToPlayer(prevCommand, self.guild, self.ctx)
embed = self.embeds.RETURNING_SONG()
return HandlerResponse(self.ctx, embed)
def __user_connected(self) -> bool:
if self.author.voice:

View File

@@ -1,45 +1,64 @@
from discord.ext.commands import Context
from Config.Exceptions import InvalidIndex
from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse
from Music.Downloader import Downloader
from Handlers.JumpMusicHandler import JumpMusicHandler
from Messages.MessagesCategory import MessagesCategory
from Parallelism.AbstractProcessManager import AbstractPlayersManager
from UI.Views.BasicView import BasicView
from Utils.Utils import Utils
from Music.VulkanBot import VulkanBot
from typing import Union
from discord import Interaction
from Music.Song import Song
from Music.Playlist import Playlist
from typing import List, Union
from discord import Button, Interaction
from UI.Buttons.CallbackButton import CallbackButton
from UI.Buttons.PlaylistDropdown import PlaylistDropdown
from Config.Emojis import VEmojis
class QueueHandler(AbstractHandler):
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
super().__init__(ctx, bot)
self.__down = Downloader()
async def run(self) -> HandlerResponse:
# Retrieve the process of the guild
processManager = self.config.getProcessManager()
processInfo = processManager.getRunningPlayerInfo(self.guild)
if not processInfo: # If no process return empty list
async def run(self, pageNumber=0) -> HandlerResponse:
playersManager: AbstractPlayersManager = self.config.getPlayersManager()
if not playersManager.verifyIfPlayerExists(self.guild):
embed = self.embeds.EMPTY_QUEUE()
return HandlerResponse(self.ctx, embed)
# Acquire the Lock to manipulate the playlist
processLock = processInfo.getLock()
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
playerLock = playersManager.getPlayerLock(self.guild)
acquired = playerLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired:
playlist = processInfo.getPlaylist()
playlist: Playlist = playersManager.getPlayerPlaylist(self.guild)
if playlist.isLoopingOne():
song = playlist.getCurrentSong()
embed = self.embeds.ONE_SONG_LOOPING(song.info)
processLock.release() # Release the Lock
playerLock.release() # Release the Lock
return HandlerResponse(self.ctx, embed)
songs_preload = playlist.getSongsToPreload()
allSongs = playlist.getSongs()
if len(songs_preload) == 0:
if len(allSongs) == 0:
embed = self.embeds.EMPTY_QUEUE()
processLock.release() # Release the Lock
playerLock.release() # Release the Lock
return HandlerResponse(self.ctx, embed)
songsPages = playlist.getSongsPages()
# Truncate the pageNumber to the closest value
if pageNumber < 0:
pageNumber = 0
elif pageNumber >= len(songsPages):
pageNumber = len(songsPages) - 1
# Select the page in queue to be printed
songs = songsPages[pageNumber]
# Create view for this embed
buttons = self.__createViewButtons(songsPages, pageNumber)
buttons.extend(self.__createViewJumpButtons(playlist))
queueView = BasicView(self.bot, buttons, self.config.QUEUE_VIEW_TIMEOUT)
if playlist.isLoopingAll():
title = self.messages.ALL_SONGS_LOOPING
else:
@@ -49,17 +68,49 @@ class QueueHandler(AbstractHandler):
for song in allSongs]))
total_songs = len(playlist.getSongs())
text = f'📜 Queue length: {total_songs} | ⌛ Duration: `{total_time}` downloaded \n\n'
text = f'📜 Queue length: {total_songs} | Page Number: {pageNumber+1}/{len(songsPages)} | ⌛ Duration: `{total_time}` downloaded \n\n'
for pos, song in enumerate(songs_preload, start=1):
song_name = song.title if song.title else self.messages.SONG_DOWNLOADING
text += f"**`{pos}` - ** {song_name} - `{Utils.format_time(song.duration)}`\n"
# To work get the correct index of all songs
startIndex = (pageNumber * self.config.MAX_SONGS_IN_PAGE) + 1
for pos, song in enumerate(songs, start=startIndex):
song_name = song.title[:50] if song.title else self.messages.SONG_DOWNLOADING
songURL = ''
hasURL = False
if 'original_url' in song.info.keys():
hasURL = True
songURL = song.info['original_url']
elif 'webpage_url' in song.info.keys():
hasURL = True
songURL = song.info['webpage_url']
if hasURL:
text += f"**`{pos}` - ** [{song_name}]({songURL}) - `{Utils.format_time(song.duration)}`\n"
else:
text += f"**`{pos}` - ** {song_name} - `{Utils.format_time(song.duration)}`\n"
embed = self.embeds.QUEUE(title, text)
# Release the acquired Lock
processLock.release()
return HandlerResponse(self.ctx, embed)
playerLock.release()
return HandlerResponse(self.ctx, embed, view=queueView)
else:
processManager.resetProcess(self.guild, self.ctx)
playersManager.resetPlayer(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed)
def __createViewButtons(self, songsPages: List[List[Song]], pageNumber: int) -> List[Button]:
buttons = []
if pageNumber > 0:
prevPageNumber = pageNumber - 1
buttons.append(CallbackButton(self.bot, self.run, VEmojis().BACK, self.ctx.channel,
self.guild.id, MessagesCategory.QUEUE, "Prev Page", pageNumber=prevPageNumber))
if pageNumber < len(songsPages) - 1:
nextPageNumber = pageNumber + 1
buttons.append(CallbackButton(self.bot, self.run, VEmojis().SKIP, self.ctx.channel,
self.guild.id, MessagesCategory.QUEUE, "Next Page", pageNumber=nextPageNumber))
return buttons
def __createViewJumpButtons(self, playlist: Playlist) -> List[Button]:
return [PlaylistDropdown(self.bot, JumpMusicHandler, playlist, self.ctx.channel, self.guild.id, MessagesCategory.PLAYER)]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,39 @@
from discord import Message, WebhookMessage
from abc import ABC, abstractmethod
class VAbstractMessage(ABC):
"""
Abstract class to allow create a pattern when dealing with multiple Discord
messages types, such as Interaction Messages and the standard discord messages
that contains two different ways of deletion
"""
@abstractmethod
async def delete(self):
pass
class VWebHookMessage(VAbstractMessage):
"""
Holds a WebhookMessage instance
"""
def __init__(self, message: WebhookMessage) -> None:
self.__message = message
super().__init__()
async def delete(self):
await self.__message.delete()
class VDefaultMessage(VAbstractMessage):
"""
Holds a Message instance, the basic Discord message type
"""
def __init__(self, message: Message) -> None:
self.__message = message
super().__init__()
async def delete(self):
await self.__message.delete()

View File

@@ -0,0 +1,11 @@
from enum import Enum
class MessagesCategory(Enum):
QUEUE = 1
HISTORY = 2
LOOP = 3
NOW_PLAYING = 4
PLAYER = 5
MANAGING_QUEUE = 6
OTHERS = 7

View File

@@ -0,0 +1,80 @@
from typing import Dict, List
from Config.Singleton import Singleton
from UI.Views.AbstractView import AbstractView
from Messages.MessagesCategory import MessagesCategory
from Messages.DiscordMessages import VAbstractMessage
import traceback
class MessagesManager(Singleton):
def __init__(self) -> None:
if not super().created:
# For each guild, and for each category, there will be a list of messages
self.__guildsMessages: Dict[int, Dict[MessagesCategory, List[VAbstractMessage]]] = {}
# Will, for each message, store the AbstractView that controls it
self.__messagesViews: Dict[VAbstractMessage, AbstractView] = {}
def addMessage(self, guildID: int, category: MessagesCategory, message: VAbstractMessage, view: AbstractView = None) -> None:
if message is None:
return
# If guild not exists create Dict
if guildID not in self.__guildsMessages.keys():
self.__guildsMessages[guildID] = {}
# If category not in guild yet, add
if category not in self.__guildsMessages[guildID].keys():
self.__guildsMessages[guildID][category] = []
sendedMessages = self.__guildsMessages[guildID][category]
if view is not None and isinstance(view, AbstractView):
self.__messagesViews[message] = view
sendedMessages.append(message)
async def addMessageAndClearPrevious(self, guildID: int, category: MessagesCategory, message: VAbstractMessage, view: AbstractView = None) -> None:
if message is None:
return
# If guild not exists create Dict
if guildID not in self.__guildsMessages.keys():
self.__guildsMessages[guildID] = {}
# If category not in guild yet, add
if category not in self.__guildsMessages[guildID].keys():
self.__guildsMessages[guildID][category] = []
sendedMessages = self.__guildsMessages[guildID][category]
# Delete sended all messages of this category
for previousMessage in sendedMessages:
await self.__deleteMessage(previousMessage)
# Create a new list with only the new message
self.__guildsMessages[guildID][category] = [message]
# Store the view of this message
if view is not None and isinstance(view, AbstractView):
self.__messagesViews[message] = view
async def clearMessagesOfCategory(self, guildID: int, category: MessagesCategory) -> None:
sendedMessages = self.__guildsMessages[guildID][category]
for message in sendedMessages:
self.__deleteMessage(message)
async def clearMessagesOfGuild(self, guildID: int) -> None:
categoriesMessages = self.__guildsMessages[guildID]
for category in categoriesMessages.keys():
for message in categoriesMessages[category]:
self.__deleteMessage(message)
async def __deleteMessage(self, message: VAbstractMessage) -> None:
try:
# If there is a view for this message delete the key
if message in self.__messagesViews.keys():
messageView = self.__messagesViews.pop(message)
messageView.stopView()
del messageView
await message.delete()
except Exception:
print(f'[ERROR DELETING MESSAGE] -> {traceback.format_exc()}')

View File

@@ -2,12 +2,16 @@ from abc import ABC, abstractmethod
from Handlers.HandlerResponse import HandlerResponse
from discord.ext.commands import Context
from discord import Message
from Messages.MessagesCategory import MessagesCategory
from Messages.MessagesManager import MessagesManager
from Music.VulkanBot import VulkanBot
class AbstractCommandResponse(ABC):
def __init__(self, response: HandlerResponse) -> None:
def __init__(self, response: HandlerResponse, category: MessagesCategory) -> None:
self.__messagesManager = MessagesManager()
self.__response: HandlerResponse = response
self.__category: MessagesCategory = category
self.__context: Context = response.ctx
self.__message: Message = response.ctx.message
self.__bot: VulkanBot = response.ctx.bot
@@ -16,6 +20,10 @@ class AbstractCommandResponse(ABC):
def response(self) -> HandlerResponse:
return self.__response
@property
def category(self) -> MessagesCategory:
return self.__category
@property
def bot(self) -> VulkanBot:
return self.__bot
@@ -28,6 +36,10 @@ class AbstractCommandResponse(ABC):
def context(self) -> Context:
return self.__context
@property
def manager(self) -> MessagesManager:
return self.__messagesManager
@abstractmethod
async def run(self) -> None:
async def run(self, deleteLast: bool = True) -> None:
pass

View File

@@ -0,0 +1,29 @@
from Messages.Responses.AbstractCogResponse import AbstractCommandResponse
from Handlers.HandlerResponse import HandlerResponse
from Messages.MessagesCategory import MessagesCategory
from Messages.DiscordMessages import VAbstractMessage, VDefaultMessage
class EmbedCommandResponse(AbstractCommandResponse):
def __init__(self, response: HandlerResponse, category: MessagesCategory) -> None:
super().__init__(response, category)
async def run(self, deleteLast: bool = True) -> None:
message = None
# If the response has both embed and view to be sended
if self.response.embed and self.response.view:
message = await self.context.send(embed=self.response.embed, view=self.response.view)
# Set the view to contain the sended message
self.response.view.set_message(message)
# Or just a embed
elif self.response.embed:
message = await self.context.send(embed=self.response.embed)
if message:
vMessage: VAbstractMessage = VDefaultMessage(message)
# Only delete the previous message if this is not error and not forbidden by method caller
if deleteLast and self.response.success:
await self.manager.addMessageAndClearPrevious(self.context.guild.id, self.category, vMessage, self.response.view)
else:
self.manager.addMessage(self.context.guild.id, self.category, vMessage)

View File

@@ -0,0 +1,21 @@
from Config.Emojis import VEmojis
from Messages.Responses.AbstractCogResponse import AbstractCommandResponse
from Handlers.HandlerResponse import HandlerResponse
from Messages.MessagesCategory import MessagesCategory
class EmoteCommandResponse(AbstractCommandResponse):
def __init__(self, response: HandlerResponse, category: MessagesCategory) -> None:
super().__init__(response, category)
self.__emojis = VEmojis()
async def run(self, deleteLast: bool = True) -> None:
# Now with Discord Interactions some commands are triggered without message
if (self.message is None):
return None
if self.response.success:
await self.message.add_reaction(self.__emojis.SUCCESS)
else:
await self.message.add_reaction(self.__emojis.ERROR)

View File

@@ -0,0 +1,35 @@
from Messages.Responses.AbstractCogResponse import AbstractCommandResponse
from Handlers.HandlerResponse import HandlerResponse
from Messages.MessagesCategory import MessagesCategory
from Messages.DiscordMessages import VAbstractMessage, VWebHookMessage
from discord import ApplicationContext
class SlashEmbedResponse(AbstractCommandResponse):
def __init__(self, response: HandlerResponse, ctx: ApplicationContext, category: MessagesCategory) -> None:
self.__ctx = ctx
super().__init__(response, category)
async def run(self, deleteLast: bool = True) -> None:
message = None
# If the response has both embed and view to send
if self.response.embed and self.response.view:
# Respond the Slash command and set the view to contain the sended message
message = await self.__ctx.send_followup(embed=self.response.embed, view=self.response.view)
self.response.view.set_message(message)
# If the response only has the embed then send the embed
elif self.response.embed:
message = await self.__ctx.send_followup(embed=self.response.embed)
else:
message = await self.__ctx.send_followup('Ok!')
# If any message was sended
if message:
# Convert the Discord message type to an Vulkan type
vMessage: VAbstractMessage = VWebHookMessage(message)
# Only delete the previous message if this is not error and not forbidden by method caller
if deleteLast and self.response.success:
await self.manager.addMessageAndClearPrevious(self.context.guild.id, self.category, vMessage, self.response.view)
else:
self.manager.addMessage(self.context.guild.id, self.category, vMessage)

View File

@@ -15,21 +15,24 @@ class Downloader:
'playliststart': 0,
'extract_flat': False,
'playlistend': config.MAX_PLAYLIST_LENGTH,
'quiet': True
'quiet': True,
'ignore_no_formats_error': True
}
__YDL_OPTIONS_EXTRACT = {'format': 'bestaudio/best',
'default_search': 'auto',
'playliststart': 0,
'extract_flat': True,
'playlistend': config.MAX_PLAYLIST_LENGTH,
'quiet': True
'quiet': True,
'ignore_no_formats_error': True
}
__YDL_OPTIONS_FORCE_EXTRACT = {'format': 'bestaudio/best',
'default_search': 'auto',
'playliststart': 0,
'extract_flat': False,
'playlistend': config.MAX_PLAYLIST_LENGTH,
'quiet': True
'quiet': True,
'ignore_no_formats_error': True
}
__BASE_URL = 'https://www.youtube.com/watch?v={}'
@@ -53,8 +56,8 @@ class Downloader:
song.finish_down(song_info)
return song
# Convert yt_dlp error to my own error
except DownloadError:
raise DownloadingError()
except DownloadError as e:
raise DownloadingError(e.msg)
@run_async
def extract_info(self, url: str) -> List[dict]:
@@ -110,7 +113,7 @@ class Downloader:
return result
except Exception as e: # Any type of error in download
print(f'DEVELOPER NOTE -> Error Downloading URL {e}')
print(f'DEVELOPER NOTE -> Error Downloading {url} -> {e}')
return None
async def download_song(self, song: Song) -> None:
@@ -118,12 +121,15 @@ class Downloader:
return None
def __download_func(song: Song) -> None:
if Utils.is_url(song.identifier):
song_info = self.__download_url(song.identifier)
else:
song_info = self.__download_title(song.identifier)
try:
if Utils.is_url(song.identifier):
song_info = self.__download_url(song.identifier)
else:
song_info = self.__download_title(song.identifier)
song.finish_down(song_info)
song.finish_down(song_info)
except Exception as e:
print(f'DEVELOPER NOTE -> Error Downloading {song.identifier} -> {e}')
# Creating a loop task to download each song
loop = asyncio.get_event_loop()
@@ -145,6 +151,8 @@ class Downloader:
return {}
if self.__is_multiple_musics(extracted_info):
if len(extracted_info['entries']) == 0:
return {}
return extracted_info['entries'][0]
else:
print(f'DEVELOPER NOTE -> Failed to extract title {title}')

View File

@@ -1,65 +0,0 @@
from typing import List
from discord import Embed, Message, TextChannel
from Music.VulkanBot import VulkanBot
from Parallelism.ProcessInfo import ProcessInfo
from Config.Configs import VConfigs
from Config.Messages import Messages
from Music.Song import Song
from Config.Embeds import VEmbeds
from UI.Views.PlayerView import PlayerView
class MessagesController:
def __init__(self, bot: VulkanBot) -> None:
self.__bot = bot
self.__previousMessages = []
self.__configs = VConfigs()
self.__messages = Messages()
self.__embeds = VEmbeds()
async def sendNowPlaying(self, processInfo: ProcessInfo, song: Song) -> None:
# Get the lock of the playlist
playlist = processInfo.getPlaylist()
if playlist.isLoopingOne():
title = self.__messages.ONE_SONG_LOOPING
else:
title = self.__messages.SONG_PLAYING
# Create View and Embed
embed = self.__embeds.SONG_INFO(song.info, title)
view = PlayerView(self.__bot)
channel = processInfo.getTextChannel()
# Delete the previous and send the message
await self.__deletePreviousNPMessages()
await channel.send(embed=embed, view=view)
# Get the sended message
sendedMessage = await self.__getSendedMessage(channel)
# Set the message witch contains the view
view.set_message(message=sendedMessage)
self.__previousMessages.append(sendedMessage)
async def __deletePreviousNPMessages(self) -> None:
for message in self.__previousMessages:
try:
await message.delete()
except:
pass
self.__previousMessages.clear()
async def __getSendedMessage(self, channel: TextChannel) -> Message:
stringToIdentify = 'Uploader:'
last_messages: List[Message] = await channel.history(limit=5).flatten()
for message in last_messages:
try:
if message.author == self.__bot.user:
if len(message.embeds) > 0:
embed: Embed = message.embeds[0]
if len(embed.fields) > 0:
if embed.fields[0].name == stringToIdentify:
return message
except Exception as e:
print(f'DEVELOPER NOTE -> Error cleaning messages {e}')
continue

View File

@@ -50,6 +50,15 @@ class Playlist:
def getSongsToPreload(self) -> List[Song]:
return list(self.__queue)[:self.__configs.MAX_PRELOAD_SONGS]
def getSongsPages(self) -> List[List[Song]]:
songsPages = []
for x in range(0, len(self.__queue), self.__configs.MAX_SONGS_IN_PAGE):
endIndex = x + self.__configs.MAX_SONGS_IN_PAGE
startIndex = x
songsPages.append(list(self.__queue)[startIndex:endIndex])
return songsPages
def __len__(self) -> int:
return len(self.__queue)
@@ -79,7 +88,7 @@ class Playlist:
self.__current = None
return None
self.__current = self.__queue.popleft()
self.__current: Song = self.__queue.popleft()
return self.__current
def prev_song(self) -> Song:
@@ -97,6 +106,10 @@ class Playlist:
self.__queue.append(song)
return song
def add_song_start(self, song: Song) -> Song:
self.__queue.insert(0, song)
return song
def shuffle(self) -> None:
random.shuffle(self.__queue)

View File

@@ -1,14 +1,14 @@
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.DeezerSearcher import DeezerSearcher
from Utils.Utils import Utils
from Utils.UrlAnalyzer import URLAnalyzer
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:
self.__spotify = SpotifySearch()
self.__deezer = DeezerSearcher()
@@ -35,8 +35,7 @@ class Searcher():
try:
musics = self.__spotify.search(track)
if musics == None or len(musics) == 0:
raise SpotifyError(self.__messages.SPOTIFY_NOT_FOUND,
self.__messages.GENERIC_TITLE)
raise SpotifyError(self.__messages.SPOTIFY_NOT_FOUND, self.__messages.GENERIC_TITLE)
return musics
except SpotifyError as error:
@@ -79,7 +78,7 @@ class Searcher():
if not Utils.is_url(track):
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
if "https://open.spotify.com" in track:

View File

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

View File

@@ -8,13 +8,19 @@ from Config.Embeds import VEmbeds
class VulkanBot(Bot):
def __init__(self, *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)
self.__listingSlash = listingSlash
self.__configs = VConfigs()
self.__messages = Messages()
self.__embeds = VEmbeds()
self.remove_command("help")
@property
def listingSlash(self) -> bool:
return self.__listingSlash
def startBot(self) -> None:
"""Blocking function that will start the bot"""
if self.__configs.BOT_TOKEN == '':
@@ -38,9 +44,11 @@ class VulkanBot(Bot):
await self.connect(reconnect=True)
async def on_ready(self):
print(self.__messages.STARTUP_MESSAGE)
if self.__listingSlash:
print(self.__messages.STARTUP_MESSAGE)
await self.change_presence(status=Status.online, activity=Game(name=f"Vulkan | {self.__configs.BOT_PREFIX}help"))
print(self.__messages.STARTUP_COMPLETE_MESSAGE)
if self.__listingSlash:
print(self.__messages.STARTUP_COMPLETE_MESSAGE)
async def on_command_error(self, ctx, error):
if isinstance(error, MissingRequiredArgument):

View File

@@ -23,21 +23,26 @@ class VulkanInitializer:
def __create_bot(self, willListen: bool) -> VulkanBot:
if willListen:
prefix = self.__config.BOT_PREFIX
bot = VulkanBot(listingSlash=True,
command_prefix=prefix,
pm_help=True,
case_insensitive=True,
intents=self.__intents)
else:
prefix = ''.join(choices(string.ascii_uppercase + string.digits, k=4))
bot = VulkanBot(command_prefix=prefix,
pm_help=True,
case_insensitive=True,
intents=self.__intents)
bot = VulkanBot(listingSlash=False,
command_prefix=prefix,
pm_help=True,
case_insensitive=True,
intents=self.__intents)
return bot
def __add_cogs(self, bot: Bot) -> None:
try:
cogsStatus = []
for filename in listdir(f'./{self.__config.COMMANDS_PATH}'):
for filename in listdir(self.__config.COMMANDS_PATH):
if filename.endswith('.py'):
cogPath = f'{self.__config.COMMANDS_PATH}.{filename[:-3]}'
cogPath = f'{self.__config.COMMANDS_FOLDER_NAME}.{filename[:-3]}'
cogsStatus.append(bot.load_extension(cogPath, store=True))
if len(bot.cogs.keys()) != self.__getTotalCogs():
@@ -45,11 +50,12 @@ class VulkanInitializer:
raise VulkanError(message='Failed to load some Cog')
except VulkanError as e:
print(f'[Error Loading Vulkan] -> {e.message}')
print(f'[Error Loading Vulkan]')
print(e)
def __getTotalCogs(self) -> int:
quant = 0
for filename in listdir(f'./{self.__config.COMMANDS_PATH}'):
for filename in listdir(self.__config.COMMANDS_PATH):
if filename.endswith('.py'):
quant += 1
return quant

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'
NOW_PLAYING = 'Now Playing'
TERMINATE = 'Terminate'
VOLUME = 'Volume'
SLEEPING = 'Sleeping'

View File

@@ -1,348 +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()
# 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

@@ -0,0 +1,132 @@
from typing import List
from discord import Button, Guild, TextChannel
from discord.ui import View
from Config.Emojis import VEmojis
from Messages.MessagesCategory import MessagesCategory
from Music.Playlist import Playlist
from Music.VulkanBot import VulkanBot
from Config.Messages import Messages
from Music.Song import Song
from Config.Embeds import VEmbeds
from UI.Buttons.HandlerButton import HandlerButton
from UI.Views.BasicView import BasicView
from Messages.MessagesManager import MessagesManager
from Handlers.PrevHandler import PrevHandler
from Handlers.PauseHandler import PauseHandler
from Handlers.SkipHandler import SkipHandler
from Handlers.StopHandler import StopHandler
from Handlers.ResumeHandler import ResumeHandler
from Handlers.LoopHandler import LoopHandler
from Handlers.QueueHandler import QueueHandler
class ProcessCommandsExecutor:
MESSAGES = Messages()
EMBEDS = VEmbeds()
EMOJIS = VEmojis()
MSG_MANAGER = MessagesManager()
def __init__(self, bot: VulkanBot, guildID: int) -> None:
self.__bot = bot
self.__guildID = guildID
self.__messagesManager = MessagesManager()
self.__messages = Messages()
self.__embeds = VEmbeds()
self.__emojis = VEmojis()
@classmethod
async def sendNowPlayingToGuild(cls, bot: VulkanBot, playlist: Playlist, channel: TextChannel, song: Song, guild: Guild) -> None:
# Get the lock of the playlist
if playlist.isLoopingOne():
title = cls.MESSAGES.ONE_SONG_LOOPING
else:
title = cls.MESSAGES.SONG_PLAYING
# Create View and Embed
embed = cls.EMBEDS.SONG_INFO(song.info, title)
view = cls.__getPlayerViewForGuild(channel, guild.id, bot)
# Send Message and add to the MessagesManager
message = await channel.send(embed=embed, view=view)
await cls.MSG_MANAGER.addMessageAndClearPrevious(guild.id, MessagesCategory.NOW_PLAYING, message, view)
# Set in the view the message witch contains the view
view.set_message(message=message)
@classmethod
def __getPlayerViewForGuild(cls, channel: TextChannel, guildID: int, bot: VulkanBot) -> View:
buttons = cls.__getPlayerButtonsForGuild(channel, guildID, bot)
view = BasicView(bot, buttons)
return view
@classmethod
def __getPlayerButtonsForGuild(cls, textChannel: TextChannel, guildID: int, bot: VulkanBot) -> List[Button]:
"""Create the Buttons to be inserted in the Player View"""
buttons: List[Button] = []
buttons.append(HandlerButton(bot, PrevHandler, cls.EMOJIS.BACK,
textChannel, guildID, MessagesCategory.PLAYER, "Back"))
buttons.append(HandlerButton(bot, PauseHandler, cls.EMOJIS.PAUSE,
textChannel, guildID, MessagesCategory.PLAYER, "Pause"))
buttons.append(HandlerButton(bot, ResumeHandler, cls.EMOJIS.PLAY,
textChannel, guildID, MessagesCategory.PLAYER, "Play"))
buttons.append(HandlerButton(bot, StopHandler, cls.EMOJIS.STOP,
textChannel, guildID, MessagesCategory.PLAYER, "Stop"))
buttons.append(HandlerButton(bot, SkipHandler, cls.EMOJIS.SKIP,
textChannel, guildID, MessagesCategory.PLAYER, "Skip"))
buttons.append(HandlerButton(bot, QueueHandler, cls.EMOJIS.QUEUE,
textChannel, guildID, MessagesCategory.QUEUE, "Songs"))
buttons.append(HandlerButton(bot, LoopHandler, cls.EMOJIS.LOOP_ONE,
textChannel, guildID, MessagesCategory.LOOP, "Loop One", 'One'))
buttons.append(HandlerButton(bot, LoopHandler, cls.EMOJIS.LOOP_OFF,
textChannel, guildID, MessagesCategory.LOOP, "Loop Off", 'Off'))
buttons.append(HandlerButton(bot, LoopHandler, cls.EMOJIS.LOOP_ALL,
textChannel, guildID, MessagesCategory.LOOP, "Loop All", 'All'))
return buttons
async def sendNowPlaying(self, playlist: Playlist, channel: TextChannel, song: Song) -> None:
# Get the lock of the playlist
if playlist.isLoopingOne():
title = self.__messages.ONE_SONG_LOOPING
else:
title = self.__messages.SONG_PLAYING
# Create View and Embed
embed = self.__embeds.SONG_INFO(song.info, title)
view = self.__getPlayerView(channel)
# Send Message and add to the MessagesManager
message = await channel.send(embed=embed, view=view)
await self.__messagesManager.addMessageAndClearPrevious(self.__guildID, MessagesCategory.NOW_PLAYING, message, view)
# Set in the view the message witch contains the view
view.set_message(message=message)
def __getPlayerView(self, channel: TextChannel) -> View:
buttons = self.__getPlayerButtons(channel)
view = BasicView(self.__bot, buttons)
return view
def __getPlayerButtons(self, textChannel: TextChannel) -> List[Button]:
"""Create the Buttons to be inserted in the Player View"""
buttons: List[Button] = []
buttons.append(HandlerButton(self.__bot, PrevHandler, self.__emojis.BACK,
textChannel, self.__guildID, MessagesCategory.PLAYER, "Back"))
buttons.append(HandlerButton(self.__bot, PauseHandler, self.__emojis.PAUSE,
textChannel, self.__guildID, MessagesCategory.PLAYER, "Pause"))
buttons.append(HandlerButton(self.__bot, ResumeHandler, self.__emojis.PLAY,
textChannel, self.__guildID, MessagesCategory.PLAYER, "Play"))
buttons.append(HandlerButton(self.__bot, StopHandler, self.__emojis.STOP,
textChannel, self.__guildID, MessagesCategory.PLAYER, "Stop"))
buttons.append(HandlerButton(self.__bot, SkipHandler, self.__emojis.SKIP,
textChannel, self.__guildID, MessagesCategory.PLAYER, "Skip"))
buttons.append(HandlerButton(self.__bot, QueueHandler, self.__emojis.QUEUE,
textChannel, self.__guildID, MessagesCategory.QUEUE, "Songs"))
buttons.append(HandlerButton(self.__bot, LoopHandler, self.__emojis.LOOP_ONE,
textChannel, self.__guildID, MessagesCategory.LOOP, "Loop One", 'One'))
buttons.append(HandlerButton(self.__bot, LoopHandler, self.__emojis.LOOP_OFF,
textChannel, self.__guildID, MessagesCategory.LOOP, "Loop Off", 'Off'))
buttons.append(HandlerButton(self.__bot, LoopHandler, self.__emojis.LOOP_ALL,
textChannel, self.__guildID, MessagesCategory.LOOP, "Loop All", 'All'))
return buttons

View File

@@ -1,38 +0,0 @@
from multiprocessing import Process, Queue, Lock
from discord import TextChannel
from Music.Playlist import Playlist
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
def setProcess(self, newProcess: Process) -> None:
self.__process = newProcess
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

@@ -1,188 +0,0 @@
import asyncio
from multiprocessing import Lock, Queue
from multiprocessing.managers import BaseManager, NamespaceProxy
from queue import Empty
from threading import Thread
from typing import Dict, Tuple, Union
from Config.Singleton import Singleton
from discord import Guild, Interaction
from discord.ext.commands import Context
from Music.MessagesController import MessagesController
from Music.Song import Song
from Parallelism.PlayerProcess import PlayerProcess
from Music.Playlist import Playlist
from Parallelism.ProcessInfo import ProcessInfo
from Parallelism.Commands import VCommands, VCommandsType
from Music.VulkanBot import VulkanBot
class ProcessManager(Singleton):
"""
Manage all running player process, creating and storing them for future calls
Deal with the creation of shared memory
"""
def __init__(self, bot: VulkanBot = None) -> None:
if not super().created:
self.__bot = bot
VManager.register('Playlist', Playlist)
self.__manager = VManager()
self.__manager.start()
self.__playersProcess: Dict[Guild, ProcessInfo] = {}
self.__playersListeners: Dict[Guild, Tuple[Thread, bool]] = {}
self.__playersMessages: Dict[Guild, MessagesController] = {}
def setPlayerInfo(self, guild: Guild, info: ProcessInfo):
self.__playersProcess[guild.id] = info
def getPlayerInfo(self, guild: Guild, context: Union[Context, Interaction]) -> ProcessInfo:
"""Return the process info for the guild, if not and context is a instance
of discord.Context then create one, else return None"""
try:
if isinstance(context, Interaction):
if guild.id not in self.__playersProcess.keys():
return None
else:
return self.__playersProcess[guild.id]
if guild.id not in self.__playersProcess.keys():
self.__playersProcess[guild.id] = self.__createProcessInfo(guild, context)
else:
# If the process has ended create a new one
if not self.__playersProcess[guild.id].getProcess().is_alive():
self.__playersProcess[guild.id] = self.__recreateProcess(guild, context)
return self.__playersProcess[guild.id]
except Exception as e:
print(f'[Error In GetPlayerContext] -> {e}')
def resetProcess(self, guild: Guild, context: Context) -> None:
"""Restart a running process, already start it to return to play"""
if guild.id not in self.__playersProcess.keys():
return None
# Recreate the process keeping the playlist
newProcessInfo = self.__recreateProcess(guild, context)
newProcessInfo.getProcess().start() # Start the process
# Send a command to start the play again
playCommand = VCommands(VCommandsType.PLAY)
newProcessInfo.getQueueToPlayer().put(playCommand)
self.__playersProcess[guild.id] = newProcessInfo
def getRunningPlayerInfo(self, guild: Guild) -> ProcessInfo:
"""Return the process info for the guild, if not, return None"""
if guild.id not in self.__playersProcess.keys():
return None
return self.__playersProcess[guild.id]
def __createProcessInfo(self, guild: Guild, context: Context) -> ProcessInfo:
guildID: int = context.guild.id
textID: int = context.channel.id
voiceID: int = context.author.voice.channel.id
authorID: int = context.author.id
playlist: Playlist = self.__manager.Playlist()
lock = Lock()
queueToListen = Queue()
queueToSend = Queue()
process = PlayerProcess(context.guild.name, playlist, lock, queueToSend,
queueToListen, guildID, textID, voiceID, authorID)
processInfo = ProcessInfo(process, queueToSend, queueToListen,
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
thread = Thread(target=self.__listenToCommands,
args=(queueToListen, guild), daemon=True)
self.__playersListeners[guildID] = (thread, False)
thread.start()
# Create a Message Controller for this player
self.__playersMessages[guildID] = MessagesController(self.__bot)
return processInfo
def __recreateProcess(self, guild: Guild, context: Context) -> ProcessInfo:
"""Create a new process info using previous playlist"""
guildID: int = context.guild.id
textID: int = context.channel.id
voiceID: int = context.author.voice.channel.id
authorID: int = context.author.id
playlist: Playlist = self.__playersProcess[guildID].getPlaylist()
lock = Lock()
queueToListen = Queue()
queueToSend = Queue()
process = PlayerProcess(context.guild.name, playlist, lock, queueToSend,
queueToListen, guildID, textID, voiceID, authorID)
processInfo = ProcessInfo(process, queueToSend, queueToListen,
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
thread = Thread(target=self.__listenToCommands,
args=(queueToListen, guild), daemon=True)
self.__playersListeners[guildID] = (thread, False)
thread.start()
return processInfo
def __listenToCommands(self, queue: Queue, guild: Guild) -> None:
guildID = guild.id
while True:
shouldEnd = self.__playersListeners[guildID][1]
if shouldEnd:
break
try:
command: VCommands = queue.get(timeout=5)
commandType = command.getType()
args = command.getArgs()
print(f'Process {guild.name} sended command {commandType}')
if commandType == VCommandsType.NOW_PLAYING:
asyncio.run_coroutine_threadsafe(self.showNowPlaying(
guild.id, args), self.__bot.loop)
elif commandType == VCommandsType.TERMINATE:
# Delete the process elements and return, to finish task
self.__terminateProcess(guildID)
return
elif commandType == VCommandsType.SLEEPING:
# The process might be used again
self.__sleepingProcess(guildID)
return
else:
print(f'[ERROR] -> Unknown Command Received from Process: {commandType}')
except Empty:
continue
except Exception as e:
print(f'[ERROR IN LISTENING PROCESS] -> {guild.name} - {e}')
def __terminateProcess(self, guildID: int) -> None:
# Delete all structures associated with the Player
del self.__playersProcess[guildID]
del self.__playersMessages[guildID]
threadListening = self.__playersListeners[guildID]
threadListening._stop()
del self.__playersListeners[guildID]
def __sleepingProcess(self, guildID: int) -> None:
# Disable all process structures, except Playlist
queue1 = self.__playersProcess[guildID].getQueueToMain()
queue2 = self.__playersProcess[guildID].getQueueToPlayer()
queue1.close()
queue1.join_thread()
queue2.close()
queue2.join_thread()
async def showNowPlaying(self, guildID: int, song: Song) -> None:
messagesController = self.__playersMessages[guildID]
processInfo = self.__playersProcess[guildID]
await messagesController.sendNowPlaying(processInfo, song)
class VManager(BaseManager):
pass
class VProxy(NamespaceProxy):
_exposed_ = ('__getattribute__', '__setattr__', '__delattr__')

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

@@ -0,0 +1,280 @@
import asyncio
from enum import Enum
from multiprocessing import Lock, Process, Queue
from multiprocessing.managers import BaseManager, NamespaceProxy
from queue import Empty
from threading import Thread
from typing import Dict, Tuple, Union
from Config.Singleton import Singleton
from discord import Guild, Interaction, TextChannel, VoiceChannel
from discord.ext.commands import Context
from Parallelism.AbstractProcessManager import AbstractPlayersManager
from Parallelism.ProcessExecutor import ProcessCommandsExecutor
from Music.Song import Song
from Parallelism.ProcessPlayer import ProcessPlayer
from Music.Playlist import Playlist
from Parallelism.Commands import VCommands, VCommandsType
from Music.VulkanBot import VulkanBot
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
Deals with the creation of shared memory
"""
def __init__(self, bot: VulkanBot = None) -> None:
if not super().created:
self.__bot = bot
VManager.register('Playlist', Playlist)
VManager.register('VoiceChannel', VoiceChannel)
self.__manager = VManager()
self.__manager.start()
self.__playersProcess: Dict[int, PlayerProcessInfo] = {}
self.__playersListeners: Dict[int, Tuple[Thread, bool]] = {}
self.__playersCommandsExecutor: Dict[int, ProcessCommandsExecutor] = {}
async def sendCommandToPlayer(self, command: VCommands, guild: Guild, context: Union[Context, Interaction], forceCreation: bool = False):
if forceCreation:
processInfo = self.createPlayerForGuild(guild, context)
else:
processInfo = self.__getRunningPlayerInfo(guild)
if processInfo == None:
return
if processInfo.getStatus() == ProcessStatus.SLEEPING:
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:
if guild.id not in self.__playersProcess.keys():
self.__playersProcess[guild.id] = self.__createProcessPlayerInfo(guild, context)
else:
# If the process has ended create a new one
if not self.__playersProcess[guild.id].getProcess().is_alive():
self.__playersProcess[guild.id] = self.__recreateProcess(guild, context)
# Start the process
self.__playersProcess[guild.id].getProcess().start()
return self.__playersProcess[guild.id]
except Exception as e:
print(f'[Error In GetPlayerContext] -> {e}')
def resetPlayer(self, guild: Guild, context: Context) -> None:
"""Restart a running process, already start it to return to play"""
if guild.id not in self.__playersProcess.keys():
return None
# Recreate the process keeping the playlist
newProcessInfo = self.__recreateProcess(guild, context)
newProcessInfo.getProcess().start() # Start the process
# Send a command to start the play again
playCommand = VCommands(VCommandsType.PLAY)
self.__putCommandInQueue(newProcessInfo.getQueueToPlayer(), playCommand)
self.__playersProcess[guild.id] = newProcessInfo
def __getRunningPlayerInfo(self, guild: Guild) -> PlayerProcessInfo:
"""Return the process info for the guild, if not, return None"""
if guild.id not in self.__playersProcess.keys():
print('Process Info not found')
return None
return self.__playersProcess[guild.id]
def __createProcessPlayerInfo(self, guild: Guild, context: Context) -> PlayerProcessInfo:
guildID: int = context.guild.id
voiceID: int = context.author.voice.channel.id
playlist: Playlist = self.__manager.Playlist()
lock = Lock()
queueToListen = Queue()
queueToSend = Queue()
process = ProcessPlayer(context.guild.name, playlist, lock, queueToSend,
queueToListen, guildID, voiceID)
processInfo = PlayerProcessInfo(process, queueToSend, queueToListen,
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
thread = Thread(target=self.__listenToCommands,
args=(queueToListen, guild), daemon=True)
self.__playersListeners[guildID] = (thread, False)
thread.start()
# Create a Message Controller for this player
self.__playersCommandsExecutor[guildID] = ProcessCommandsExecutor(self.__bot, guildID)
return processInfo
def __stopPossiblyRunningProcess(self, guild: Guild):
try:
if guild.id in self.__playersProcess.keys():
playerProcess = self.__playersProcess[guild.id]
process = playerProcess.getProcess()
process.close()
process.kill()
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"""
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
playlist: Playlist = self.__playersProcess[guildID].getPlaylist()
lock = Lock()
queueToListen = Queue()
queueToSend = Queue()
process = ProcessPlayer(context.guild.name, playlist, lock, queueToSend,
queueToListen, guildID, voiceID)
processInfo = PlayerProcessInfo(process, queueToSend, queueToListen,
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
thread = Thread(target=self.__listenToCommands,
args=(queueToListen, guild), daemon=True)
self.__playersListeners[guildID] = (thread, False)
thread.start()
return processInfo
def __listenToCommands(self, queue: Queue, guild: Guild) -> None:
guildID = guild.id
while True:
shouldEnd = self.__playersListeners[guildID][1]
if shouldEnd:
break
try:
command: VCommands = queue.get(timeout=5)
commandType = command.getType()
args = command.getArgs()
print(f'Process {guild.name} sended command {commandType}')
if commandType == VCommandsType.NOW_PLAYING:
asyncio.run_coroutine_threadsafe(self.showNowPlaying(
guild.id, args), self.__bot.loop)
elif commandType == VCommandsType.TERMINATE:
# Delete the process elements and return, to finish task
self.__terminateProcess(guildID)
return
elif commandType == VCommandsType.SLEEPING:
# The process might be used again
self.__sleepingProcess(guildID)
return
else:
print(f'[ERROR] -> Unknown Command Received from Process: {commandType}')
except Empty:
continue
except Exception as e:
print(f'[ERROR IN LISTENING PROCESS] -> {guild.name} - {e}')
def __terminateProcess(self, guildID: int) -> None:
# Delete all structures associated with the Player
del self.__playersProcess[guildID]
del self.__playersCommandsExecutor[guildID]
threadListening = self.__playersListeners[guildID]
threadListening._stop()
del self.__playersListeners[guildID]
def __sleepingProcess(self, guildID: int) -> None:
# Disable all process structures, except Playlist
queue1 = self.__playersProcess[guildID].getQueueToMain()
queue2 = self.__playersProcess[guildID].getQueueToPlayer()
queue1.close()
queue1.join_thread()
queue2.close()
queue2.join_thread()
# Set the status of this process as sleeping, only the playlist object remains
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:
commandExecutor = self.__playersCommandsExecutor[guildID]
processInfo = self.__playersProcess[guildID]
playlist = processInfo.getPlaylist()
channel = processInfo.getTextChannel()
await commandExecutor.sendNowPlaying(playlist, channel, song)
class VManager(BaseManager):
pass
class VProxy(NamespaceProxy):
_exposed_ = ('__getattribute__', '__setattr__', '__delattr__')

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)

141
README.md
View File

@@ -1,68 +1,61 @@
# **Vulkan**
A Music Discord bot, written in Python, that plays *Youtube*, *Spotify* and *Deezer* links. Vulkan was designed so that anyone can fork this project, follow the instructions and use it in their own way, Vulkan can also be configured in Heroku to work 24/7.
<h1 align="center">Vulkan</h1>
# **Music**
- Play musics from Youtube, Spotify and Deezer links (Albums, Artists, Playlists and Tracks)
- Control loop of one or all musics
- Allow moving and removing musics in the queue
- Play musics in queue randomly
- Store played songs and allow bidirectional flow
A Music Discord Bot, that plays *Youtube*, *Spotify*, *Deezer* links or raw queries. Vulkan is open source, so everyone can fork this project, follow the instructions and use it in their own way, executing it in your own machine or hosting in others machines to work 24/7.
### Commands
```!play [title, spotify_url, youtube_url, deezer_url]``` - Start playing song
```!resume``` - Resume the song player
```!pause``` - Pause the song player
```!skip``` - Skip the currently playing song
```!prev``` - Return to play the previous song
```!stop``` - Stop the playing of musics
```!queue``` - Show the musics list in queue
```!history``` - Show the played songs list
```!loop [one, all, off]``` - Control the loop of songs
```!shuffle``` - Shuffle the songs in queue
```!remove [x]``` - Remove the song in position x
```!move [x, y]``` - Change the musics in position x and y in Queue
```!np``` - Show information of the currently song
```!clear``` - Clear the songs in queue, doesn't stop the player
```!reset``` - Reset the player, recommended if any error happen
```!invite``` - Send the URL to invite Vulkan to your server
```!help [command]``` - Show more info about the command selected
Vulkan uses multiprocessing and asynchronous Python modules to maximize Music Player response time, so the player doesn't lag when many commands are being processed and it can play in multiples discord serves at the same time without affecting the Music Player response time.
<p align="center">
<img src="./Assets/playermenu.jpg" />
</p>
# **Usage:**
### **API Keys**
# **Music 🎧**
- Play musics from Youtube, Spotify and Deezer links (Albums, Artists, Playlists and Tracks).
- Play musics in multiple discord server at the same time.
- The player contains buttons to shortcut some commands.
- Support for the new Discord Slash commands.
- Search for all musics in Queue using buttons.
- 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 order and remove musics from the queue.
- Shuffle the musics queue order.
- Automatically clean the sended messages so it doesn't fill up your server.
<p align="center">
<img src="./Assets/vulkancommands.jpg" />
</p>
<p align="center">
<img src="./Assets/queuemessage.jpg" />
</p>
<hr>
<br>
## **Setting up for yourself**
### **Requirements**
Installation of ``Python 3.10+`` and the dependencies in the requirements.txt file, creation of your own Bot in Discord and Spotify Keys. <br>
To install the dependencies type this command in the terminal, in the project root folder.
```
pip install -r requirements.txt
```
### **🔑 API Keys**
You have to create your own discord Bot and store your Bot Token
* Your Discord Application - [Discord](https://discord.com/developers)
* You own Spotify Keys - [Spotify](https://developer.spotify.com/dashboard/applications)
- This information must be stored in an .env file, explained further.
### **Requirements**
- Installation of Python 3.8+ and the dependencies in the requirements.txt file.
```
pip install -r requirements.txt
```
- **Installation of FFMPEG**<br>
### **Installation of FFMPEG**<br>
FFMPEG is a module that will be used to play music, you must have this configured in your machine
*FFMPEG must be configured in the PATH for Windows users. Check this [YoutubeVideo](https://www.youtube.com/watch?v=r1AtmY-RMyQ&t=114s&ab_channel=TroubleChute).* <br><br>
You can download the executables in this link `https://www.ffmpeg.org/download.html` and then put the .exe files inside a ffmpeg\bin folder in your C:\ folder. Do not forget to add 'ffmpeg\bin' to your PATH.
@@ -70,42 +63,48 @@ You can download the executables in this link `https://www.ffmpeg.org/download.h
### **.Env File Example**
This is an example of how your .env file (located in root) should look like.
```
```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
```
### **Config File**
The config file, located in ```./config``` folder doesn't require any change, but if you acquire the knowledged of how it works, you can change it to the way you want.
### **⚙️ Configs**
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**
- Go to [Discord](https://discord.com/developers) and invite your Bot to your own server
- 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.
## **Heroku**
To run your Bot in Heroku 24/7, you will need the Procfile located in root, then follow the instructions in this [video](https://www.youtube.com/watch?v=BPvg9bndP1U&ab_channel=TechWithTim). In addition, also add these two buildpacks to your Heroku Application:
- https://github.com/jonathanong/heroku-buildpack-ffmpeg-latest.git
- https://github.com/xrisk/heroku-opus.git
<br>
<hr>
<br>
## Testing
## **🚀 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
The tests were written manually with no package due to problems with async function in other packages, to execute them type in root: <br>
`python run_tests.py`<br>
## License
- This program is free software: you can redistribute it and/or modify it under the terms of the [MIT License](https://github.com/RafaelSolVargas/Vulkan/blob/master/LICENSE).
## 📖 License
This program is free software: you can redistribute it and/or modify it under the terms of the [MIT License](https://github.com/RafaelSolVargas/Vulkan/blob/master/LICENSE).
## Contributing
- If you are interested in upgrading this project i will be very happy to receive a PR or Issue from you. See TODO project to see if i'm working in some feature now.
## Acknowledgment
- See the DingoLingo [project](https://github.com/Raptor123471/DingoLingo) from Raptor123471, it helped me a lot to build Vulkan.
## 🏗️ Contributing
If you are interested in upgrading this project i will be very happy to receive a PR or Issue from you. See TODO project to see if i'm working in some feature now.

View File

@@ -0,0 +1,12 @@
from abc import ABC, abstractmethod
from discord.ui import Item, View
class AbstractItem(ABC, Item):
@abstractmethod
def set_view(self, view: View):
pass
@abstractmethod
def get_view(self) -> View:
pass

View File

@@ -1,22 +0,0 @@
from discord import ButtonStyle, Interaction
from discord.ui import Button
from Config.Emojis import VEmojis
from Handlers.PrevHandler import PrevHandler
from Music.VulkanBot import VulkanBot
class BackButton(Button):
def __init__(self, bot: VulkanBot):
super().__init__(label="Back", style=ButtonStyle.secondary, emoji=VEmojis().BACK)
self.__bot = bot
async def callback(self, interaction: Interaction) -> None:
"""Callback to when Button is clicked"""
# Return to Discord that this command is being processed
await interaction.response.defer()
handler = PrevHandler(interaction, self.__bot)
response = await handler.run()
if response.embed:
await interaction.followup.send(embed=response.embed)

View File

@@ -0,0 +1,48 @@
from typing import Awaitable
from Config.Emojis import VEmojis
from discord import ButtonStyle, Interaction, Message, TextChannel
from discord.ui import Button, View
from Handlers.HandlerResponse import HandlerResponse
from Messages.MessagesCategory import MessagesCategory
from Messages.MessagesManager import MessagesManager
from Music.VulkanBot import VulkanBot
class CallbackButton(Button):
"""When clicked execute an callback passing the args and kwargs"""
def __init__(self, bot: VulkanBot, cb: Awaitable, emoji: VEmojis, textChannel: TextChannel, guildID: int, category: MessagesCategory, label=None, *args, **kwargs):
super().__init__(label=label, style=ButtonStyle.secondary, emoji=emoji)
self.__channel = textChannel
self.__guildID = guildID
self.__category = category
self.__messagesManager = MessagesManager()
self.__bot = bot
self.__args = args
self.__kwargs = kwargs
self.__callback = cb
self.__view: View = None
async def callback(self, interaction: Interaction) -> None:
"""Callback to when Button is clicked"""
# Return to Discord that this command is being processed
await interaction.response.defer()
response: HandlerResponse = await self.__callback(*self.__args, **self.__kwargs)
message = None
if response and response.view is not None:
message: Message = await self.__channel.send(embed=response.embed, view=response.view)
response.view.set_message(message)
elif response.embed:
message: Message = await self.__channel.send(embed=response.embed)
# Clear the last sended message in this category and add the new one
if message:
await self.__messagesManager.addMessageAndClearPrevious(self.__guildID, self.__category, message, response.view)
def set_view(self, view: View):
self.__view = view
def get_view(self) -> View:
return self.__view

View File

@@ -0,0 +1,50 @@
from Config.Emojis import VEmojis
from discord import ButtonStyle, Interaction, Message, TextChannel
from discord.ui import Button, View
from Handlers.HandlerResponse import HandlerResponse
from Messages.MessagesCategory import MessagesCategory
from Music.VulkanBot import VulkanBot
from Handlers.AbstractHandler import AbstractHandler
from Messages.MessagesManager import MessagesManager
class HandlerButton(Button):
"""Button that will create and execute a Handler Object when clicked"""
def __init__(self, bot: VulkanBot, handler: type[AbstractHandler], emoji: VEmojis, textChannel: TextChannel, guildID: int, category: MessagesCategory, label=None, *args, **kwargs):
super().__init__(label=label, style=ButtonStyle.secondary, emoji=emoji)
self.__messagesManager = MessagesManager()
self.__category = category
self.__guildID = guildID
self.__channel = textChannel
self.__bot = bot
self.__args = args
self.__kwargs = kwargs
self.__handlerClass = handler
self.__view: View = None
async def callback(self, interaction: Interaction) -> None:
"""Callback to when Button is clicked"""
# Return to Discord that this command is being processed
await interaction.response.defer()
# Create the handler object
handler = self.__handlerClass(interaction, self.__bot)
response: HandlerResponse = await handler.run(*self.__args, **self.__kwargs)
message = None
if response and response.view is not None:
message: Message = await self.__channel.send(embed=response.embed, view=response.view)
response.view.set_message(message)
elif response.embed:
message: Message = await self.__channel.send(embed=response.embed)
# Clear the last category sended message and add the new one
if message:
await self.__messagesManager.addMessageAndClearPrevious(self.__guildID, self.__category, message, response.view)
def set_view(self, view: View):
self.__view = view
def get_view(self) -> View:
return self.__view

View File

@@ -1,20 +0,0 @@
from discord import ButtonStyle, Interaction
from discord.ui import Button
from Config.Emojis import VEmojis
from Handlers.LoopHandler import LoopHandler
from Music.VulkanBot import VulkanBot
class LoopAllButton(Button):
def __init__(self, bot: VulkanBot):
super().__init__(label="Loop All", style=ButtonStyle.secondary, emoji=VEmojis().LOOP_ALL)
self.__bot = bot
async def callback(self, interaction: Interaction) -> None:
await interaction.response.defer()
handler = LoopHandler(interaction, self.__bot)
response = await handler.run('all')
if response.embed:
await interaction.followup.send(embed=response.embed)

View File

@@ -1,20 +0,0 @@
from discord import ButtonStyle, Interaction
from discord.ui import Button
from Config.Emojis import VEmojis
from Handlers.LoopHandler import LoopHandler
from Music.VulkanBot import VulkanBot
class LoopOffButton(Button):
def __init__(self, bot: VulkanBot):
super().__init__(label="Loop Off", style=ButtonStyle.secondary, emoji=VEmojis().LOOP_OFF)
self.__bot = bot
async def callback(self, interaction: Interaction) -> None:
await interaction.response.defer()
handler = LoopHandler(interaction, self.__bot)
response = await handler.run('off')
if response.embed:
await interaction.followup.send(embed=response.embed)

View File

@@ -1,20 +0,0 @@
from discord import ButtonStyle, Interaction
from discord.ui import Button
from Config.Emojis import VEmojis
from Handlers.LoopHandler import LoopHandler
from Music.VulkanBot import VulkanBot
class LoopOneButton(Button):
def __init__(self, bot: VulkanBot):
super().__init__(label="Loop One", style=ButtonStyle.secondary, emoji=VEmojis().LOOP_ONE)
self.__bot = bot
async def callback(self, interaction: Interaction) -> None:
await interaction.response.defer()
handler = LoopHandler(interaction, self.__bot)
response = await handler.run('one')
if response.embed:
await interaction.followup.send(embed=response.embed)

View File

@@ -1,20 +0,0 @@
from discord import ButtonStyle, Interaction
from discord.ui import Button
from Config.Emojis import VEmojis
from Handlers.PauseHandler import PauseHandler
from Music.VulkanBot import VulkanBot
class PauseButton(Button):
def __init__(self, bot: VulkanBot):
super().__init__(label="Pause", style=ButtonStyle.secondary, emoji=VEmojis().PAUSE)
self.__bot = bot
async def callback(self, interaction: Interaction) -> None:
await interaction.response.defer()
handler = PauseHandler(interaction, self.__bot)
response = await handler.run()
if response.embed:
await interaction.followup.send(embed=response.embed)

View File

@@ -1,20 +0,0 @@
from discord import ButtonStyle, Interaction
from discord.ui import Button
from Config.Emojis import VEmojis
from Music.VulkanBot import VulkanBot
from Handlers.ResumeHandler import ResumeHandler
class PlayButton(Button):
def __init__(self, bot: VulkanBot):
super().__init__(label="Play", style=ButtonStyle.secondary, emoji=VEmojis().PLAY)
self.__bot = bot
async def callback(self, interaction: Interaction) -> None:
await interaction.response.defer()
handler = ResumeHandler(interaction, self.__bot)
response = await handler.run()
if response.embed:
await interaction.followup.send(embed=response.embed)

View File

@@ -0,0 +1,89 @@
import asyncio
from typing import List
from discord import Interaction, Message, TextChannel, SelectOption
from discord.ui import Select, View
from Handlers.HandlerResponse import HandlerResponse
from Messages.MessagesCategory import MessagesCategory
from Messages.MessagesManager import MessagesManager
from Music.VulkanBot import VulkanBot
from Handlers.AbstractHandler import AbstractHandler
from UI.Buttons.AbstractItem import AbstractItem
from UI.Views.AbstractView import AbstractView
from Music.Playlist import Playlist
class PlaylistDropdown(Select, AbstractItem):
"""Receives n elements to put in drop down and return the selected, pass the index value to a handler"""
def __init__(self, bot: VulkanBot, handler: type[AbstractHandler], playlist: Playlist, textChannel: TextChannel, guildID: int, category: MessagesCategory):
songs = list(playlist.getSongs())
values = [str(x) for x in range(1, len(songs) + 1)]
# Get the title of each of the 20 first songs, the pycord library doesn't accept more
songsNames: List[str] = []
songsLength = min(20, len(songs))
for x in range(songsLength):
songsNames.append(f'{x + 1} - {songs[x].title[:80]}')
selectOptions: List[SelectOption] = []
for x in range(len(songsNames)):
selectOptions.append(SelectOption(label=songsNames[x], value=values[x]))
super().__init__(placeholder="Select one music to play now, may be outdated",
min_values=1, max_values=1, options=selectOptions)
self.__playlist = playlist
self.__channel = textChannel
self.__guildID = guildID
self.__category = category
self.__handlerClass = handler
self.__messagesManager = MessagesManager()
self.__bot = bot
self.__view: AbstractView = None
async def callback(self, interaction: Interaction) -> None:
"""Callback to when the selection is selected"""
await interaction.response.defer()
# Execute the handler passing the value selected
handler = self.__handlerClass(interaction, self.__bot)
response: HandlerResponse = await handler.run(self.values[0])
message = None
if response and response.view is not None:
message: Message = await self.__channel.send(embed=response.embed, view=response.view)
elif response.embed:
message: Message = await self.__channel.send(embed=response.embed)
# Clear the last sended message in this category and add the new one
if message:
await self.__messagesManager.addMessageAndClearPrevious(self.__guildID, self.__category, message, response.view)
# Extreme ugly way to wait for the player process to actually retrieve the next song
await asyncio.sleep(2)
await self.__update()
async def __update(self):
songs = list(self.__playlist.getSongs())
values = [str(x) for x in range(1, len(songs) + 1)]
# Get the title of each of the 20 first songs, library doesn't accept more
songsNames = [song.title[:80] for song in songs[:20]]
selectOptions: List[SelectOption] = []
for x in range(len(songsNames)):
selectOptions.append(SelectOption(label=songsNames[x], value=values[x]))
self.options = selectOptions
if self.__view is not None:
await self.__view.update()
def set_view(self, view: View):
self.__view = view
def get_view(self) -> View:
return self.__view

View File

@@ -1,20 +0,0 @@
from discord import ButtonStyle, Interaction
from discord.ui import Button
from Config.Emojis import VEmojis
from Music.VulkanBot import VulkanBot
from Handlers.SkipHandler import SkipHandler
class SkipButton(Button):
def __init__(self, bot: VulkanBot):
super().__init__(label="Skip", style=ButtonStyle.secondary, emoji=VEmojis().SKIP)
self.__bot = bot
async def callback(self, interaction: Interaction) -> None:
await interaction.response.defer()
handler = SkipHandler(interaction, self.__bot)
response = await handler.run()
if response.embed:
await interaction.followup.send(embed=response.embed)

View File

@@ -1,20 +0,0 @@
from Handlers.QueueHandler import QueueHandler
from discord import ButtonStyle, Interaction
from discord.ui import Button
from Config.Emojis import VEmojis
from Music.VulkanBot import VulkanBot
class SongsButton(Button):
def __init__(self, bot: VulkanBot):
super().__init__(label="Songs", style=ButtonStyle.secondary, emoji=VEmojis().QUEUE)
self.__bot = bot
async def callback(self, interaction: Interaction) -> None:
await interaction.response.defer()
handler = QueueHandler(interaction, self.__bot)
response = await handler.run()
if response.embed:
await interaction.followup.send(embed=response.embed)

View File

@@ -1,20 +0,0 @@
from discord import ButtonStyle, Interaction
from discord.ui import Button
from Config.Emojis import VEmojis
from Music.VulkanBot import VulkanBot
from Handlers.StopHandler import StopHandler
class StopButton(Button):
def __init__(self, bot: VulkanBot):
super().__init__(label="Stop", style=ButtonStyle.secondary, emoji=VEmojis().STOP)
self.__bot = bot
async def callback(self, interaction: Interaction) -> None:
await interaction.response.defer()
handler = StopHandler(interaction, self.__bot)
response = await handler.run()
if response.embed:
await interaction.followup.send(embed=response.embed)

View File

@@ -1,11 +0,0 @@
from UI.Responses.AbstractCogResponse import AbstractCommandResponse
from Handlers.HandlerResponse import HandlerResponse
class EmbedCommandResponse(AbstractCommandResponse):
def __init__(self, response: HandlerResponse) -> None:
super().__init__(response)
async def run(self) -> None:
if self.response.embed:
await self.context.send(embed=self.response.embed)

View File

@@ -1,16 +0,0 @@
from Config.Emojis import VEmojis
from UI.Responses.AbstractCogResponse import AbstractCommandResponse
from Handlers.HandlerResponse import HandlerResponse
class EmoteCommandResponse(AbstractCommandResponse):
def __init__(self, response: HandlerResponse) -> None:
super().__init__(response)
self.__emojis = VEmojis()
async def run(self) -> None:
if self.response.success:
await self.message.add_reaction(self.__emojis.SUCCESS)
else:
await self.message.add_reaction(self.__emojis.ERROR)

14
UI/Views/AbstractView.py Normal file
View File

@@ -0,0 +1,14 @@
from abc import ABC, abstractmethod
class AbstractView(ABC):
@abstractmethod
async def update(self) -> None:
pass
def set_message(self, message) -> None:
pass
@abstractmethod
def stopView(self) -> None:
pass

56
UI/Views/BasicView.py Normal file
View File

@@ -0,0 +1,56 @@
from UI.Views.AbstractView import AbstractView
from UI.Buttons.AbstractItem import AbstractItem
from Music.VulkanBot import VulkanBot
from Config.Emojis import VEmojis
from discord import Message
from discord.ui import View
from typing import List
emojis = VEmojis()
class BasicView(View, AbstractView):
"""
View class that inherits from the Discord View Class, managing a list of Buttons
and the message that holds this View.
"""
def __init__(self, bot: VulkanBot, buttons: List[AbstractItem], timeout: float = 6000):
super().__init__(timeout=timeout)
self.__bot = bot
self.__message: Message = None
self.__working = True
for button in buttons:
# Set the buttons to have a instance of the view that contains them
button.set_view(self)
self.add_item(button)
def stopView(self):
self.__working = False
async def on_timeout(self) -> None:
# Disable all itens and, if has the message, edit it
try:
if not self.__working:
return
self.disable_all_items()
if self.__message is not None and isinstance(self.__message, Message):
await self.__message.edit(f"{emojis.MUSIC} - The buttons in this message have been disabled due timeout", view=self)
except Exception as e:
print(f'[ERROR EDITING MESSAGE] -> {e}')
def set_message(self, message: Message) -> None:
self.__message = message
async def update(self):
"""Edit the message sending the view again"""
try:
if not self.__working:
return
if self.__message is not None:
await self.__message.edit(view=self)
except Exception as e:
print(f'[ERROR UPDATING MESSAGE] -> {e}')

View File

@@ -1,40 +0,0 @@
from discord import Message
from discord.ui import View
from Config.Emojis import VEmojis
from UI.Buttons.PauseButton import PauseButton
from UI.Buttons.BackButton import BackButton
from UI.Buttons.SkipButton import SkipButton
from UI.Buttons.StopButton import StopButton
from UI.Buttons.SongsButton import SongsButton
from UI.Buttons.PlayButton import PlayButton
from UI.Buttons.LoopAllButton import LoopAllButton
from UI.Buttons.LoopOneButton import LoopOneButton
from UI.Buttons.LoopOffButton import LoopOffButton
from Music.VulkanBot import VulkanBot
emojis = VEmojis()
class PlayerView(View):
def __init__(self, bot: VulkanBot, timeout: float = 6000):
super().__init__(timeout=timeout)
self.__bot = bot
self.__message: Message = None
self.add_item(BackButton(self.__bot))
self.add_item(PauseButton(self.__bot))
self.add_item(PlayButton(self.__bot))
self.add_item(StopButton(self.__bot))
self.add_item(SkipButton(self.__bot))
self.add_item(SongsButton(self.__bot))
self.add_item(LoopOneButton(self.__bot))
self.add_item(LoopOffButton(self.__bot))
self.add_item(LoopAllButton(self.__bot))
async def on_timeout(self) -> None:
# Disable all itens and, if has the message, edit it
self.disable_all_items()
if self.__message is not None and isinstance(self.__message, Message):
await self.__message.edit(view=self)
def set_message(self, message: Message) -> None:
self.__message = message

8
docker-compose.yaml Normal file
View File

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

View File

@@ -1,7 +1,8 @@
from Music.VulkanInitializer import VulkanInitializer
from Config.Folder import Folder
if __name__ == '__main__':
folder = Folder()
initializer = VulkanInitializer(willListen=True)
vulkanBot = initializer.getBot()
vulkanBot.startBot()

Binary file not shown.