101 Commits
v1.0 ... v15.1

Author SHA1 Message Date
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
Rafael Vargas
c5885f3093 Finishing work with the second queue 2022-07-28 11:32:04 -03:00
Rafael Vargas
60a36425ee Adding a queue for player send commands to Main process 2022-07-28 00:38:30 -03:00
Rafael Vargas
5902a0dc72 Others buttons are now working using handlers to do the work 2022-07-27 17:50:27 -03:00
Rafael Vargas
ca754c6f62 Subclassing Button class for each button, changing context interface for handlers 2022-07-27 17:20:57 -03:00
Rafael Vargas
4f11506c2b Creating Player View 2022-07-27 16:14:13 -03:00
Rafael Vargas
beb0bc085d Fixing errors due to new discor d library 2022-07-27 11:27:38 -03:00
Rafael Vargas
fededdbb8c Changing to pycord 2022-07-27 01:36:55 -03:00
Rafael Vargas
4a22b43ce9 Setting new event loop 2022-07-25 21:59:09 -03:00
Rafael Vargas
48d7166386 Merge pull request #21 from RafaelSolVargas/usingProcess
Upgrading the architecture to use both asyncio and multiprocessing modules
2022-07-25 09:54:16 -03:00
Rafael Vargas
140c1640d9 Stability update 2022-07-25 09:49:04 -03:00
Rafael Vargas
f27dc1de93 Upgrading stability in errors occurrences 2022-07-24 23:49:35 -03:00
Rafael Vargas
b904c75caa Fixing erros due to processing 2022-07-24 14:25:44 -03:00
Rafael Vargas
56456bf2ed Modifying play and prev commands to work with Process 2022-07-23 15:51:13 -03:00
Rafael Vargas
7efed8ab89 Adapting History, Loop, Now Playing, Remove, Shuffle and Reset commands to work with process 2022-07-23 13:57:47 -03:00
Rafael Vargas
3eab6176c3 Code style upgrade 2022-07-23 09:52:20 -03:00
Rafael Vargas
cd3eddb125 Modifying pause, resume and move commands to work with process 2022-07-23 00:37:11 -03:00
Rafael Vargas
1ce6deaa48 Modifying clear and skip commands to work with process 2022-07-22 22:57:27 -03:00
Rafael Vargas
19ae59c5b8 Changing some methods signature in Playlist to be compatible in Shared Memory, now finishing process after some time 2022-07-22 18:07:44 -03:00
Rafael Vargas
fc7de9cb4f Starting using multiprocessing module in Vulkan, now creating a new bot for each guild when played, multiple issues yet 2022-07-22 16:03:51 -03:00
Rafael Vargas
7a51c22709 Removing old files 2022-07-10 17:48:57 -03:00
Rafael Vargas
cbf6e84eb1 Removing print 2022-07-10 17:46:08 -03:00
Rafael Vargas
97d49a5709 Merge pull request #20 from RafaelSolVargas/addingDezzer
Updating requirements and Helper messages closing AddDeezer task
2022-07-10 17:37:04 -03:00
Rafael Vargas
d894929662 Updating requirements and Helper messages 2022-07-10 17:30:18 -03:00
Rafael Vargas
d87a0234ba Merge pull request #19 from RafaelSolVargas/addingDezzer
Adding dezzer option to play musics
2022-07-10 16:23:28 -03:00
Rafael Vargas
94194f5d6a Updating README 2022-07-10 16:21:35 -03:00
Rafael Vargas
863b079a01 Adding Deezer Module and Deezer Tests 2022-07-10 16:08:22 -03:00
Rafael Vargas
5099a551a4 Merge pull request #18 from RafaelSolVargas/addingTests
Adding tests and upgrading Spotify stability
2022-07-10 14:30:02 -03:00
Rafael Vargas
c826af229c Upgrading clean code 2022-07-10 14:27:40 -03:00
Rafael Vargas
7e9a6d45c0 Upgrading Spotify Invalid Input Dealing 2022-07-10 14:18:07 -03:00
Rafael Vargas
8336a95eda Adding Spotify Tests 2022-07-10 13:47:08 -03:00
Rafael Vargas
0938dd37e2 Now using own test module to test vulkan downloader methods 2022-07-10 12:09:32 -03:00
Rafael Vargas
cd5f4567be Upgrading tests in Downloader 2022-07-10 01:23:59 -03:00
Rafael Vargas
4fb9d8d1ba Adding new tests using unit test module 2022-07-09 23:22:55 -03:00
Rafael Vargas
2dbc6c3984 Updating requirements 2022-03-26 23:04:51 -04:00
Rafael Vargas
dd4fbff27c Chaging Config file name to work in Heroku 2022-03-26 22:52:30 -04:00
Rafael Vargas
cc0cd6424f Changing config folder name to Config 2022-03-26 22:10:34 -04:00
Rafael Vargas
985d87a470 Trying Fix Heroku Error 2022-03-26 21:59:04 -04:00
Rafael Vargas
14bb43a42e Turning YTD quiet 2022-03-26 21:43:13 -04:00
Rafael Vargas
caaa48ba05 Fixing some errors 2022-03-26 21:40:04 -04:00
Rafael Vargas
b4159c7e86 Adding types and changing some messages from class 2022-03-26 21:24:03 -04:00
Rafael Vargas
f9b46e13ff Finishing PrevController 2022-03-26 18:45:40 -04:00
Rafael Vargas
f09568bd69 Adding more stability to Searcher 2022-03-26 18:12:08 -04:00
Rafael Vargas
4c66c64041 Upgrading PlayController and Spotify Connection 2022-03-26 17:42:49 -04:00
Rafael Vargas
f30513f710 Adding Cleaner and fixing bugs 2022-03-26 14:45:27 -04:00
Rafael Vargas
362ec02fe4 Adding Play and Prev Controller 2022-03-26 12:19:25 -04:00
Rafael Vargas
8ac80c216f Adding Reset and Shuffle Controller and some fix 2022-03-25 14:31:37 -04:00
Rafael Vargas
d30ff93dc1 Adding Moving and Remove Songs Controllers 2022-03-25 00:42:58 -04:00
Rafael Vargas
fd1e58211b Adding new controllers 2022-03-23 13:48:01 -04:00
Rafael Vargas
a828350201 Chaging the folders and creating a separeted class for messages 2022-03-22 17:53:21 -04:00
Rafael Vargas
2240c7535a Changing folders names and adding more concrete classes of controllers and viewers 2022-03-22 17:39:09 -04:00
Rafael Vargas
14705569c1 Chaging config and help to class 2022-03-21 21:51:40 -04:00
Rafael Vargas
5c4d09bf9d Sending initial changes, adding the concept of Handlers, Views, Exceptions and Result, these class will optimaze the code in terms of cleanliness and organizational 2022-03-21 16:11:38 -04:00
Rafael Vargas
2bd100a3e1 Deleting uselles file 2022-03-20 22:05:53 -04:00
Rafael Vargas
1dc708a86b Resolving bug in downloader 2022-03-20 22:04:59 -04:00
Rafael Vargas
e59efb0010 Adding some typing to the project and rebuilding the Downloader 2022-03-19 17:17:38 -04:00
Rafael Vargas
5510b5af78 Stability Commit 2022-02-24 16:16:43 -04:00
Rafael Vargas
5312a729e9 Stability Commit with connections to Discord user 2022-02-24 16:09:45 -04:00
Rafael Vargas
63d86e23f9 [Add] - Position Report | [Fix] - Own Error creation | [Modify] - Move command logic 2022-01-14 22:02:37 -04:00
Rafael Vargas
cbd8ed45f9 [Fix] - Command remove with -1 as argument 2022-01-14 19:51:24 -04:00
Rafael Vargas
c23d4c13cb [Fix] - Error msg in loop command error, colours update 2022-01-13 20:10:28 -04:00
Rafael Vargas
b39e1f0216 Updating README 2022-01-12 13:27:09 -04:00
Rafael Vargas
37f5773c8e Merge pull request #16 from RafaelSolVargas/patch1.2
Patch1.2
2022-01-12 11:38:50 -04:00
Rafael Vargas
0d606361da Refactoring clean messages 2022-01-12 11:31:15 -04:00
Rafael Vargas
173b6a7b43 Stability Update 2022-01-12 10:41:31 -04:00
Rafael Vargas
5b4c5b49f1 Updating help command in Vulkan 2022-01-12 09:41:35 -04:00
Rafael Vargas
ea5245cf95 Removing some useless modules and spliting config file to help file too 2022-01-12 09:40:25 -04:00
Rafael Vargas
5063c45e87 Adding prev and history command 2022-01-11 21:46:09 -04:00
Rafael Vargas
c41a6a6c8a Adding new config variables to prev command 2022-01-11 21:40:20 -04:00
Rafael Vargas
615fd580bc Merge pull request #15 from RafaelSolVargas/stylish-update
Closing branch
2022-01-08 19:54:20 -04:00
Rafael Vargas
9b38ba4434 Updating README 2022-01-08 19:50:53 -04:00
Rafael Vargas
bbddcb555b Fixing my bot prefix 2022-01-08 19:16:35 -04:00
Rafael Vargas
3b1a7d5364 Fixing my bot prefix 2022-01-08 19:08:22 -04:00
Rafael Vargas
37acd1a61b Merge pull request #14 from RafaelSolVargas/stylish-update
Stylish update and Fixing Known Errors
2022-01-08 19:01:01 -04:00
Rafael Vargas
ccad2de97c Fixing Player trying to play a song before it got downloaded 2022-01-08 18:54:03 -04:00
Rafael Vargas
a1881cd46b Fixing aliases 2022-01-08 18:51:44 -04:00
Rafael Vargas
bb8d514525 Setting Spotify to search with artist ID 2022-01-08 18:03:56 -04:00
Rafael Vargas
ef2d7bb8fa Stylish update 2022-01-08 17:07:02 -04:00
Rafael Vargas
13bc51fae7 Updating requirements file with version used 2022-01-07 14:29:34 -04:00
Rafael Vargas
2bc8cd206c empty commit 2022-01-07 12:22:26 -04:00
Rafael Vargas
66962cb274 empty commit 2022-01-07 12:17:00 -04:00
Rafael Vargas
2d7ecd0a0e Teste 2022-01-07 11:56:05 -04:00
Rafael Vargas
98ee6f9cf2 Second try 2022-01-07 11:54:45 -04:00
Rafael Vargas
089b47fc44 First commit 2022-01-07 11:53:12 -04:00
94 changed files with 5231 additions and 1676 deletions

2
.gitignore vendored
View File

@@ -1,7 +1,7 @@
.vscode
__pycache__
.env
.cache
Admin.py
# Byte-compiled / optimized / DLL files
__pycache__/

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: 72 KiB

34
Config/Colors.py Normal file
View File

@@ -0,0 +1,34 @@
from Config.Singleton import Singleton
class VColors(Singleton):
def __init__(self) -> None:
self.__red = 0xDC143C
self.__green = 0x1F8B4C
self.__grey = 0x708090
self.__blue = 0x206694
self.__black = 0x23272A
@property
def RED(self) -> str:
return self.__red
@property
def GREEN(self) -> str:
return self.__green
@property
def GREY(self) -> str:
return self.__grey
@property
def BLUE(self) -> str:
return self.__blue
@property
def BLACK(self) -> str:
return self.__black
@property
def RED(self) -> str:
return self.__red

47
Config/Configs.py Normal file
View File

@@ -0,0 +1,47 @@
import os
from decouple import config
from Config.Singleton import Singleton
from Config.Folder import Folder
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')
self.CLEANER_MESSAGES_QUANT = 5
self.ACQUIRE_LOCK_TIMEOUT = 10
self.QUEUE_VIEW_TIMEOUT = 120
self.COMMANDS_FOLDER_NAME = 'DiscordCogs'
self.COMMANDS_PATH = f'{Folder().rootFolder}{self.COMMANDS_FOLDER_NAME}'
self.VC_TIMEOUT = 300
self.CHANCE_SHOW_PROJECT = 15
self.PROJECT_URL = 'https://github.com/RafaelSolVargas/Vulkan'
self.SUPPORTING_ICON = 'https://i.pinimg.com/originals/d6/05/b4/d605b4f8c5d1c6ae20dc353ef9f091bd.png'
self.MAX_PLAYLIST_LENGTH = 50
self.MAX_PLAYLIST_FORCED_LENGTH = 5
self.MAX_SONGS_IN_PAGE = 10
self.MAX_PRELOAD_SONGS = 15
self.MAX_SONGS_HISTORY = 15
self.INVITE_MESSAGE = """To invite Vulkan to your own server, click [here]({}).
Or use this direct URL: {}"""
self.MY_ERROR_BAD_COMMAND = 'This string serves to verify if some error was raised by myself on purpose'
self.INVITE_URL = 'https://discordapp.com/oauth2/authorize?client_id={}&scope=bot'
def getProcessManager(self):
return self.__manager
def setProcessManager(self, newManager):
self.__manager = newManager

426
Config/Embeds.py Normal file
View File

@@ -0,0 +1,426 @@
from random import random
from Config.Messages import Messages
from Config.Exceptions import VulkanError
from discord import Embed
from Config.Configs import VConfigs
from Config.Colors import VColors
from datetime import timedelta
class VEmbeds:
def __init__(self) -> None:
self.__config = VConfigs()
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)
def EMPTY_QUEUE(self) -> Embed:
title = self.__messages.SONG_PLAYER
text = self.__messages.EMPTY_QUEUE
embed = Embed(
title=title,
description=text,
colour=self.__colors.BLUE
)
return embed
def MISSING_ARGUMENTS(self) -> Embed:
embed = Embed(
title=self.__messages.ERROR_TITLE,
description=self.__messages.ERROR_MISSING_ARGUMENTS,
colour=self.__colors.BLACK
)
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
def INVALID_INPUT(self) -> Embed:
embed = Embed(
title=self.__messages.ERROR_TITLE,
description=self.__messages.INVALID_INPUT,
colour=self.__colors.BLACK)
return embed
def UNAVAILABLE_VIDEO(self) -> Embed:
embed = Embed(
title=self.__messages.ERROR_TITLE,
description=self.__messages.VIDEO_UNAVAILABLE,
colour=self.__colors.BLACK)
return embed
def DOWNLOADING_ERROR(self) -> Embed:
embed = Embed(
title=self.__messages.ERROR_TITLE,
description=self.__messages.DOWNLOADING_ERROR,
colour=self.__colors.BLACK)
return embed
def SONG_ADDED(self, title: str) -> Embed:
embed = Embed(
title=self.__messages.SONG_PLAYER,
description=self.__messages.SONG_ADDED.format(title),
colour=self.__colors.BLUE)
return embed
def SONGS_ADDED(self, quant: int) -> Embed:
embed = Embed(
title=self.__messages.SONG_PLAYER,
description=self.__messages.SONGS_ADDED.format(quant),
colour=self.__colors.BLUE)
return embed
def SONG_INFO(self, info: dict, title: str, position='Playing Now') -> Embed:
embedvc = Embed(
title=title,
description=f"[{info['title']}]({info['original_url']})",
colour=self.__colors.BLUE
)
embedvc.add_field(name=self.__messages.SONGINFO_UPLOADER,
value=info['uploader'],
inline=False)
embedvc.add_field(name=self.__messages.SONGINFO_REQUESTER,
value=info['requester'],
inline=True)
if 'thumbnail' in info.keys():
embedvc.set_thumbnail(url=info['thumbnail'])
if 'duration' in info.keys():
duration = str(timedelta(seconds=info['duration']))
embedvc.add_field(name=self.__messages.SONGINFO_DURATION,
value=f"{duration}",
inline=True)
else:
embedvc.add_field(name=self.__messages.SONGINFO_DURATION,
value=self.__messages.SONGINFO_UNKNOWN_DURATION,
inline=True)
embedvc.add_field(name=self.__messages.SONGINFO_POSITION,
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:
embed = Embed(
title=self.__messages.SONG_PLAYER,
description=self.__messages.SONG_MOVED_SUCCESSFULLY.format(song_name, pos1, pos2),
colour=self.__colors.BLUE
)
return embed
def ERROR_MOVING(self) -> Embed:
embed = Embed(
title=self.__messages.UNKNOWN_ERROR,
description=self.__messages.ERROR_MOVING,
colour=self.__colors.BLACK
)
return embed
def ERROR_EMBED(self, description: str) -> Embed:
embed = Embed(
description=description,
colour=self.__colors.BLACK
)
return embed
def CUSTOM_ERROR(self, error: VulkanError) -> Embed:
embed = Embed(
title=error.title,
description=error.message,
colour=self.__colors.BLACK
)
return embed
def WRONG_LENGTH_INPUT(self) -> Embed:
embed = Embed(
title=self.__messages.BAD_COMMAND_TITLE,
description=self.__messages.LENGTH_ERROR,
colour=self.__colors.BLACK
)
return embed
def BAD_LOOP_USE(self) -> Embed:
embed = Embed(
title=self.__messages.BAD_COMMAND_TITLE,
description=self.__messages.BAD_USE_OF_LOOP,
colour=self.__colors.BLACK
)
return embed
def COMMAND_ERROR(self):
embed = Embed(
title=self.__messages.ERROR_TITLE,
description=self.__messages.ERROR_MISSING_ARGUMENTS,
colour=self.__colors.BLACK
)
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,
description=self.__messages.COMMAND_NOT_FOUND,
colour=self.__colors.BLACK
)
return embed
def MY_ERROR_BAD_COMMAND(self) -> Embed:
embed = Embed(
title=self.__messages.BAD_COMMAND_TITLE,
description=self.__messages.BAD_COMMAND,
colour=self.__colors.BLACK
)
return embed
def UNKNOWN_ERROR(self) -> Embed:
embed = Embed(
title=self.__messages.ERROR_TITLE,
description=self.__messages.UNKNOWN_ERROR,
colour=self.__colors.RED
)
return embed
def FAIL_DUE_TO_LOOP_ON(self) -> Embed:
embed = Embed(
title=self.__messages.SONG_PLAYER,
description=self.__messages.LOOP_ON,
colour=self.__colors.BLACK
)
return embed
def ERROR_SHUFFLING(self) -> Embed:
embed = Embed(
title=self.__messages.SONG_PLAYER,
description=self.__messages.ERROR_SHUFFLING,
colour=self.__colors.BLACK
)
return embed
def SONGS_SHUFFLED(self) -> Embed:
embed = Embed(
title=self.__messages.SONG_PLAYER,
description=self.__messages.SONGS_SHUFFLED,
colour=self.__colors.BLUE
)
return embed
def LOOP_ONE_ACTIVATED(self) -> Embed:
embed = Embed(
title=self.__messages.LOOP_ONE_ACTIVATE,
colour=self.__colors.BLUE
)
return embed
def LOOP_ALL_ACTIVATED(self) -> Embed:
embed = Embed(
title=self.__messages.LOOP_ALL_ACTIVATE,
colour=self.__colors.BLUE
)
return embed
def SONG_PROBLEMATIC(self) -> Embed:
embed = Embed(
title=self.__messages.ERROR_TITLE,
description=self.__messages.DOWNLOADING_ERROR,
colour=self.__colors.BLACK)
return embed
def PLAYER_RESTARTED(self) -> Embed:
embed = Embed(
title=self.__messages.ERROR_TITLE,
description=self.__messages.ERROR_IN_PROCESS,
colour=self.__colors.BLACK)
return embed
def NO_CHANNEL(self) -> Embed:
embed = Embed(
title=self.__messages.IMPOSSIBLE_MOVE,
description=self.__messages.NO_CHANNEL,
colour=self.__colors.BLACK
)
return embed
def ERROR_DUE_LOOP_ONE_ON(self) -> Embed:
embed = Embed(
title=self.__messages.BAD_COMMAND_TITLE,
description=self.__messages.ERROR_DUE_LOOP_ONE_ON,
colour=self.__colors.BLACK
)
return embed
def LOOP_DISABLE(self) -> Embed:
embed = Embed(
title=self.__messages.LOOP_DISABLE,
colour=self.__colors.BLUE
)
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,
description=self.__messages.NOT_PREVIOUS,
colour=self.__colors.BLUE
)
return embed
def HISTORY(self, description: str) -> Embed:
embed = Embed(
title=self.__messages.HISTORY_TITLE,
description=description,
colour=self.__colors.BLUE)
return embed
def NOT_PLAYING(self) -> Embed:
embed = Embed(
title=self.__messages.SONG_PLAYER,
description=self.__messages.PLAYER_NOT_PLAYING,
colour=self.__colors.BLUE)
return embed
def QUEUE(self, title: str, description: str) -> Embed:
embed = Embed(
title=title,
description=description,
colour=self.__colors.BLUE
)
if self.__willShowProject():
embed = self.__addFooterContent(embed)
return embed
def INVITE(self, bot_id: str) -> Embed:
link = self.__messages.INVITE_URL
link.format(bot_id)
text = self.__messages.INVITE_MESSAGE.format(link, link)
embed = Embed(
title="Invite Vulkan",
description=text,
colour=self.__colors.BLUE
)
return embed
def ERROR_NUMBER(self) -> Embed:
embed = Embed(
description=self.__messages.ERROR_NUMBER,
colour=self.__colors.BLACK
)
return embed
def RANDOM_NUMBER(self, a: int, b: int, x: int) -> Embed:
embed = Embed(
title=f'Random number between [{a}, {b}]',
description=x,
colour=self.__colors.GREEN
)
return embed
def SONG_REMOVED(self, song_name: str) -> Embed:
embed = Embed(
description=self.__messages.SONG_REMOVED_SUCCESSFULLY.format(song_name),
colour=self.__colors.BLUE
)
return embed
def PLAYLIST_RANGE_ERROR(self) -> Embed:
embed = Embed(
description=self.__messages.LENGTH_ERROR,
colour=self.__colors.BLACK
)
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',
description=f'Result: {result}',
colour=self.__colors.GREEN
)
return embed
def CHOSEN_THING(self, thing: str) -> Embed:
embed = Embed(
title='Choose something',
description=f'Chosen: {thing}',
colour=self.__colors.GREEN
)
return embed
def BAD_CHOOSE_USE(self) -> Embed:
embed = Embed(
title='Choose something',
description=f'Error: Use {self.__config.BOT_PREFIX}help choose to understand this command.',
colour=self.__colors.RED
)
return embed

20
Config/Emojis.py Normal file
View File

@@ -0,0 +1,20 @@
from Config.Singleton import Singleton
class VEmojis(Singleton):
def __init__(self) -> None:
if not super().created:
self.SKIP = ""
self.BACK = ""
self.PAUSE = "⏸️"
self.PLAY = "▶️"
self.STOP = "⏹️"
self.LOOP_ONE = "🔂"
self.LOOP_OFF = "➡️"
self.LOOP_ALL = "🔁"
self.SHUFFLE = "🔀"
self.QUEUE = "📜"
self.MUSIC = "🎧"
self.ERROR = ""
self.DOWNLOADING = "📥"
self.SUCCESS = ""

89
Config/Exceptions.py Normal file
View File

@@ -0,0 +1,89 @@
from Config.Messages import Messages
class VulkanError(Exception):
def __init__(self, message='', title='', *args: object) -> None:
self.__message = message
self.__title = title
super().__init__(*args)
@property
def message(self) -> str:
return self.__message
@property
def title(self) -> str:
return self.__title
class ImpossibleMove(VulkanError):
def __init__(self, message='', title='', *args: object) -> None:
message = Messages()
if title == '':
title = message.IMPOSSIBLE_MOVE
super().__init__(message, title, *args)
class MusicUnavailable(VulkanError):
def __init__(self, message='', title='', *args: object) -> None:
super().__init__(message, title, *args)
class YoutubeError(VulkanError):
def __init__(self, message='', title='', *args: object) -> None:
super().__init__(message, title, *args)
class BadCommandUsage(VulkanError):
def __init__(self, message='', title='', *args: object) -> None:
super().__init__(message, title, *args)
class DownloadingError(VulkanError):
def __init__(self, message='', title='', *args: object) -> None:
super().__init__(message, title, *args)
class SpotifyError(VulkanError):
def __init__(self, message='', title='', *args: object) -> None:
super().__init__(message, title, *args)
class DeezerError(VulkanError):
def __init__(self, message='', title='', *args: object) -> None:
super().__init__(message, title, *args)
class UnknownError(VulkanError):
def __init__(self, message='', title='', *args: object) -> None:
super().__init__(message, title, *args)
class InvalidInput(VulkanError):
def __init__(self, message='', title='', *args: object) -> None:
super().__init__(message, title, *args)
class WrongLength(VulkanError):
def __init__(self, message='', title='', *args: object) -> None:
super().__init__(message, title, *args)
class ErrorMoving(VulkanError):
def __init__(self, message='', title='', *args: object) -> None:
super().__init__(message, title, *args)
class ErrorRemoving(VulkanError):
def __init__(self, message='', title='', *args: object) -> None:
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

54
Config/Helper.py Normal file
View File

@@ -0,0 +1,54 @@
from Config.Singleton import Singleton
from Config.Configs import VConfigs
class Helper(Singleton):
def __init__(self) -> None:
if not super().created:
config = VConfigs()
self.HELP_SKIP = 'Skip the current playing song.'
self.HELP_SKIP_LONG = 'Skip the playing of the current song, does not work if loop one is activated. \n\nArguments: None.'
self.HELP_RESUME = 'Resumes the song player.'
self.HELP_RESUME_LONG = 'If the player if paused, return the playing. \n\nArguments: None.'
self.HELP_CLEAR = 'Clear the queue and songs history.'
self.HELP_CLEAR_LONG = 'Clear the songs queue and songs history. \n\nArguments: None.'
self.HELP_STOP = 'Stop the song player.'
self.HELP_STOP_LONG = 'Stop the song player, clear queue and history and remove Vulkan from voice channel.\n\nArguments: None.'
self.HELP_LOOP = 'Control the loop of songs.'
self.HELP_LOOP_LONG = """Control the loop of songs.\n\n Require: A song being played.\nArguments:
One - Start looping the current song.
All - Start looping all songs in queue.
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_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_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 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.'
self.HELP_RESET = 'Reset the Player of the server.'
self.HELP_RESET_LONG = 'Reset the Player of the server. Recommended if you find any type of error.\n\nArguments: None'
self.HELP_HELP = f'Use {config.BOT_PREFIX}help "command" for more info.'
self.HELP_HELP_LONG = f'Use {config.BOT_PREFIX}help command for more info about the command selected.'
self.HELP_INVITE = 'Send the invite URL to call Vulkan to your server.'
self.HELP_INVITE_LONG = 'Send an message in text channel with a URL to be used to invite Vulkan to your own server.\n\nArguments: None.'
self.HELP_RANDOM = 'Return a random number between 1 and x.'
self.HELP_RANDOM_LONG = 'Send a randomly selected number between 1 and the number you pass.\n\nRequired: Number to be a valid number.\nArguments: 1º Any number to be used as range.'
self.HELP_CHOOSE = 'Choose randomly one item passed.'
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.'

107
Config/Messages.py Normal file
View File

@@ -0,0 +1,107 @@
from Config.Singleton import Singleton
from Config.Configs import VConfigs
from Config.Emojis import VEmojis
class Messages(Singleton):
def __init__(self) -> None:
if not super().created:
self.__emojis = VEmojis()
configs = VConfigs()
self.STARTUP_MESSAGE = 'Starting Vulkan...'
self.STARTUP_COMPLETE_MESSAGE = 'Vulkan is now operating.'
self.SONGINFO_UPLOADER = "Uploader: "
self.SONGINFO_DURATION = "Duration: "
self.SONGINFO_REQUESTER = 'Requester: '
self.SONGINFO_POSITION = 'Position: '
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'
self.SONG_PLAYING = f'{self.__emojis.MUSIC} Song playing now'
self.SONG_PLAYER = f'{self.__emojis.MUSIC} Song Player'
self.QUEUE_TITLE = f'{self.__emojis.MUSIC} Songs in Queue'
self.ONE_SONG_LOOPING = f'{self.__emojis.MUSIC} Looping One Song'
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'
self.SONG_MOVED_SUCCESSFULLY = 'Song `{}` in position `{}` moved to the position `{}` successfully'
self.SONG_REMOVED_SUCCESSFULLY = 'Song `{}` removed successfully'
self.LOOP_ALL_ON = f'{self.__emojis.ERROR} Vulkan is looping all songs, use {configs.BOT_PREFIX}loop off to disable this loop first'
self.LOOP_ONE_ON = f'{self.__emojis.ERROR} Vulkan is looping one song, use {configs.BOT_PREFIX}loop off to disable this loop first'
self.LOOP_ALL_ALREADY_ON = f'{self.__emojis.LOOP_ALL} Vulkan is already looping all songs'
self.LOOP_ONE_ALREADY_ON = f'{self.__emojis.LOOP_ONE} Vulkan is already looping the current song'
self.LOOP_ALL_ACTIVATE = f'{self.__emojis.LOOP_ALL} Looping all songs'
self.LOOP_ONE_ACTIVATE = f'{self.__emojis.LOOP_ONE} Looping the current song'
self.LOOP_DISABLE = f'{self.__emojis.LOOP_OFF} Loop disabled'
self.LOOP_ALREADY_DISABLE = f'{self.__emojis.ERROR} Loop is already disabled'
self.LOOP_ON = f'{self.__emojis.ERROR} This command cannot be invoked with any loop activated. Use {configs.BOT_PREFIX}loop off to disable loop'
self.BAD_USE_OF_LOOP = f"""{self.__emojis.ERROR} Invalid arguments of Loop command. Use {configs.BOT_PREFIX}help loop to more information.
-> Available Arguments: ["all", "off", "one", ""]"""
self.SONGS_SHUFFLED = f'{self.__emojis.SHUFFLE} Songs shuffled successfully'
self.ERROR_SHUFFLING = f'{self.__emojis.ERROR} Error while shuffling the songs'
self.ERROR_MOVING = f'{self.__emojis.ERROR} Error while moving the songs'
self.LENGTH_ERROR = f'{self.__emojis.ERROR} Numbers must be between 1 and queue length, use -1 for the last song'
self.ERROR_NUMBER = f'{self.__emojis.ERROR} This command require a number'
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'
self.ERROR_MISSING_ARGUMENTS = f'{self.__emojis.ERROR} Missing arguments in this command. Type {configs.BOT_PREFIX}help "command" to see more info about this command'
self.NOT_PREVIOUS = f'{self.__emojis.ERROR} There is none previous song to play'
self.PLAYER_NOT_PLAYING = f'{self.__emojis.ERROR} No song playing. Use {configs.BOT_PREFIX}play to start the player'
self.IMPOSSIBLE_MOVE = 'That is impossible :('
self.ERROR_TITLE = 'Error :-('
self.COMMAND_NOT_FOUND_TITLE = 'This is strange :-('
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'
self.ERROR_IN_PROCESS = f"{self.__emojis.ERROR} Due to a internal error your player was restarted, skipping the song."
self.MY_ERROR_BAD_COMMAND = 'This string serves to verify if some error was raised by myself on purpose'
self.BAD_COMMAND_TITLE = 'Misuse of command'
self.BAD_COMMAND = f'{self.__emojis.ERROR} Bad usage of this command, type {configs.BOT_PREFIX}help "command" to understand the command better'
self.VIDEO_UNAVAILABLE = f'{self.__emojis.ERROR} Sorry. This video is unavailable for download.'
self.ERROR_DUE_LOOP_ONE_ON = f'{self.__emojis.ERROR} This command cannot be executed with loop one activated. Use {configs.BOT_PREFIX}loop off to disable loop.'
class SearchMessages(Singleton):
def __init__(self) -> None:
if not super().created:
config = VConfigs()
self.UNKNOWN_INPUT = f'This type of input was too strange, try something else or type {config.BOT_PREFIX}help play'
self.UNKNOWN_INPUT_TITLE = 'Nothing Found'
self.GENERIC_TITLE = 'URL could not be processed'
self.SPOTIFY_NOT_FOUND = 'Spotify could not process any songs with this input, verify your link or try again later.'
self.YOUTUBE_NOT_FOUND = 'Youtube could not process any songs with this input, verify your link or try again later.'
self.DEEZER_NOT_FOUND = 'Deezer could not process any songs with this input, verify your link or try again later.'
class SpotifyMessages(Singleton):
def __init__(self) -> None:
if not super().created:
self.INVALID_SPOTIFY_URL = 'Invalid Spotify URL, verify your link.'
self.GENERIC_TITLE = 'URL could not be processed'
class DeezerMessages(Singleton):
def __init__(self) -> None:
if not super().created:
self.INVALID_DEEZER_URL = 'Invalid Deezer URL, verify your link.'
self.GENERIC_TITLE = 'URL could not be processed'

16
Config/Singleton.py Normal file
View File

@@ -0,0 +1,16 @@
class Singleton(object):
__instance = None
__created = False
def __new__(cls, *args, **kwargs):
if cls.__instance is None:
cls.__instance = object.__new__(cls)
return cls.__instance
@property
def created(cls):
if cls.__created == False:
cls.__created = True
return False
else:
return True

0
Config/__init__.py Normal file
View File

95
DiscordCogs/ControlCog.py Normal file
View File

@@ -0,0 +1,95 @@
from discord import Embed
from discord.ext.commands import Cog, command
from Config.Configs import VConfigs
from Config.Helper import Helper
from Config.Colors import VColors
from Music.VulkanBot import VulkanBot
from Config.Embeds import VEmbeds
helper = Helper()
class ControlCog(Cog):
"""Class to handle discord events"""
def __init__(self, bot: VulkanBot):
self.__bot = bot
self.__config = VConfigs()
self.__colors = VColors()
self.__embeds = VEmbeds()
self.__commands = {
'MUSIC': ['resume', 'pause', 'loop', 'stop',
'skip', 'play', 'queue', 'clear',
'np', 'shuffle', 'move', 'remove',
'reset', 'prev', 'history'],
'RANDOM': ['choose', 'cara', 'random']
}
@command(name="help", help=helper.HELP_HELP, description=helper.HELP_HELP_LONG, aliases=['h', 'ajuda'])
async def help_msg(self, ctx, command_help=''):
if command_help != '':
for command in self.__bot.commands:
if command.name == command_help:
txt = command.description if command.description else command.help
embedhelp = Embed(
title=f'**Description of {command_help}** command',
description=txt,
colour=self.__colors.BLUE
)
await ctx.send(embed=embedhelp)
return
embedhelp = Embed(
title='Help',
description=f'Command {command_help} do not exists, type {self.__config.BOT_PREFIX}help to see all commands',
colour=self.__colors.BLACK
)
await ctx.send(embed=embedhelp)
else:
helptxt = ''
help_music = '🎧 `MUSIC`\n'
help_random = '🎲 `RANDOM`\n'
help_help = '👾 `HELP`\n'
for command in self.__bot.commands:
if command.name in self.__commands['MUSIC']:
help_music += f'**{command}** - {command.help}\n'
elif command.name in self.__commands['RANDOM']:
help_random += f'**{command}** - {command.help}\n'
else:
help_help += f'**{command}** - {command.help}\n'
helptxt = f'\n{help_music}\n{help_help}\n{help_random}'
helptxt += f'\n\nType {self.__config.BOT_PREFIX}help "command" for more information about the command chosen'
embedhelp = Embed(
title=f'**Available Commands of {self.__bot.user.name}**',
description=helptxt,
colour=self.__colors.BLUE
)
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'])
async def invite_bot(self, ctx):
invite_url = self.__config.INVITE_URL.format(self.__bot.user.id)
txt = self.__config.INVITE_MESSAGE.format(invite_url, invite_url)
embed = Embed(
title="Invite Vulkan",
description=txt,
colour=self.__colors.BLUE
)
await ctx.send(embed=embed)
def setup(bot):
bot.add_cog(ControlCog(bot))

261
DiscordCogs/MusicCog.py Normal file
View File

@@ -0,0 +1,261 @@
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
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 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 Config.Embeds import VEmbeds
from Parallelism.ProcessManager import ProcessManager
helper = Helper()
class MusicCog(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()
VConfigs().setProcessManager(ProcessManager(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)
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, *args) -> None:
try:
pageNumber = " ".join(args)
controller = QueueHandler(ctx, self.__bot)
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}')
@command(name="skip", help=helper.HELP_SKIP, description=helper.HELP_SKIP_LONG, aliases=['s', 'pular', 'next'])
async def skip(self, ctx: Context) -> None:
try:
controller = SkipHandler(ctx, self.__bot)
response = await controller.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='stop', help=helper.HELP_STOP, description=helper.HELP_STOP_LONG, aliases=['parar'])
async def stop(self, ctx: Context) -> None:
try:
controller = StopHandler(ctx, self.__bot)
response = await controller.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='pause', help=helper.HELP_PAUSE, description=helper.HELP_PAUSE_LONG, aliases=['pausar', 'pare'])
async def pause(self, ctx: Context) -> None:
try:
controller = PauseHandler(ctx, self.__bot)
response = await controller.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}')
@command(name='resume', help=helper.HELP_RESUME, description=helper.HELP_RESUME_LONG, aliases=['soltar', 'despausar'])
async def resume(self, ctx: Context) -> None:
try:
controller = ResumeHandler(ctx, self.__bot)
response = await controller.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}')
@command(name='prev', help=helper.HELP_PREV, description=helper.HELP_PREV_LONG, aliases=['anterior', 'return', 'previous', 'back'])
async def prev(self, ctx: Context) -> None:
try:
controller = PrevHandler(ctx, self.__bot)
response = await controller.run()
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='history', help=helper.HELP_HISTORY, description=helper.HELP_HISTORY_LONG, aliases=['historico', 'anteriores', 'hist'])
async def history(self, ctx: Context) -> None:
try:
controller = HistoryHandler(ctx, self.__bot)
response = await controller.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}')
@command(name='loop', help=helper.HELP_LOOP, description=helper.HELP_LOOP_LONG, aliases=['l', 'repeat'])
async def loop(self, ctx: Context, args='') -> None:
try:
controller = LoopHandler(ctx, self.__bot)
response = await controller.run(args)
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}')
@command(name='clear', help=helper.HELP_CLEAR, description=helper.HELP_CLEAR_LONG, aliases=['c', 'limpar'])
async def clear(self, ctx: Context) -> None:
try:
controller = ClearHandler(ctx, self.__bot)
response = await controller.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='np', help=helper.HELP_NP, description=helper.HELP_NP_LONG, aliases=['playing', 'now', 'this'])
async def now_playing(self, ctx: Context) -> None:
try:
controller = NowPlayingHandler(ctx, self.__bot)
response = await controller.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}')
@command(name='shuffle', help=helper.HELP_SHUFFLE, description=helper.HELP_SHUFFLE_LONG, aliases=['aleatorio', 'misturar'])
async def shuffle(self, ctx: Context) -> None:
try:
controller = ShuffleHandler(ctx, self.__bot)
response = await controller.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='move', help=helper.HELP_MOVE, description=helper.HELP_MOVE_LONG, aliases=['m', 'mover'])
async def move(self, ctx: Context, pos1, pos2='1') -> None:
try:
controller = MoveHandler(ctx, self.__bot)
response = await controller.run(pos1, pos2)
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}')
@command(name='remove', help=helper.HELP_REMOVE, description=helper.HELP_REMOVE_LONG, aliases=['remover'])
async def remove(self, ctx: Context, position) -> None:
try:
controller = RemoveHandler(ctx, self.__bot)
response = await controller.run(position)
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}')
@command(name='reset', help=helper.HELP_RESET, description=helper.HELP_RESET_LONG, aliases=['resetar'])
async def reset(self, ctx: Context) -> None:
try:
controller = ResetHandler(ctx, self.__bot)
response = await controller.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}')
def setup(bot):
bot.add_cog(MusicCog(bot))

64
DiscordCogs/RandomCog.py Normal file
View File

@@ -0,0 +1,64 @@
from random import randint, random
from Music.VulkanBot import VulkanBot
from discord.ext.commands import Context, command, Cog
from Config.Helper import Helper
from Config.Embeds import VEmbeds
helper = Helper()
class RandomCog(Cog):
"""Class to listen to commands of type Random"""
def __init__(self, bot: VulkanBot):
self.__embeds = VEmbeds()
@command(name='random', help=helper.HELP_RANDOM, description=helper.HELP_RANDOM_LONG, aliases=['rand'])
async def random(self, ctx: Context, arg: str) -> None:
try:
arg = int(arg)
except:
embed = self.__embeds.ERROR_NUMBER()
await ctx.send(embed=embed)
return None
if arg < 1:
a = arg
b = 1
else:
a = 1
b = arg
x = randint(a, b)
embed = self.__embeds.RANDOM_NUMBER(a, b, x)
await ctx.send(embed=embed)
@command(name='cara', help=helper.HELP_CARA, description=helper.HELP_CARA_LONG, aliases=['coroa'])
async def cara(self, ctx: Context) -> None:
x = random()
if x < 0.5:
result = 'cara'
else:
result = 'coroa'
embed = self.__embeds.CARA_COROA(result)
await ctx.send(embed=embed)
@command(name='choose', help=helper.HELP_CHOOSE, description=helper.HELP_CHOOSE_LONG, aliases=['escolha', 'pick'])
async def choose(self, ctx, *args: str) -> None:
try:
user_input = " ".join(args)
itens = user_input.split(sep=',')
index = randint(0, len(itens)-1)
embed = self.__embeds.CHOSEN_THING(itens[index])
await ctx.send(embed=embed)
except:
embed = self.__embeds.BAD_CHOOSE_USE()
await ctx.send(embed=embed)
def setup(bot):
bot.add_cog(RandomCog(bot))

271
DiscordCogs/SlashCog.py Normal file
View File

@@ -0,0 +1,271 @@
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 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='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))

23
HEROKU.md Normal file
View File

@@ -0,0 +1,23 @@
<h1 align="center">Configuring Heroku</h1>
Nobody wants to run the Vulkan process on their machine, so we host the process on Heroku, a cloud platform that contains free accounts.<br>
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.

View File

@@ -0,0 +1,82 @@
from abc import ABC, abstractmethod
from typing import List, Union
from discord.ext.commands import Context
from discord import Client, Guild, ClientUser, Interaction, Member, User
from Config.Messages import Messages
from Music.VulkanBot import VulkanBot
from Handlers.HandlerResponse import HandlerResponse
from Config.Configs import VConfigs
from Config.Helper import Helper
from Config.Embeds import VEmbeds
class AbstractHandler(ABC):
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
self.__bot: VulkanBot = bot
self.__guild: Guild = ctx.guild
self.__ctx: Context = ctx
self.__bot_user: ClientUser = self.__bot.user
self.__id = self.__bot_user.id
self.__messages = Messages()
self.__config = VConfigs()
self.__helper = Helper()
self.__embeds = VEmbeds()
self.__bot_member: Member = self.__get_member()
if isinstance(ctx, Context):
self.__author = ctx.author
else:
self.__author = ctx.user
@abstractmethod
async def run(self) -> HandlerResponse:
pass
@property
def id(self) -> int:
return self.__id
@property
def bot_member(self) -> Member:
return self.__bot_member
@property
def bot_user(self) -> ClientUser:
return self.__bot_user
@property
def author(self) -> User:
return self.__author
@property
def guild(self) -> Guild:
return self.__guild
@property
def bot(self) -> Client:
return self.__bot
@property
def config(self) -> VConfigs:
return self.__config
@property
def messages(self) -> Messages:
return self.__messages
@property
def helper(self) -> Helper:
return self.__helper
@property
def ctx(self) -> Union[Context, Interaction]:
return self.__ctx
@property
def embeds(self) -> VEmbeds:
return self.__embeds
def __get_member(self) -> Member:
guild_members: List[Member] = self.__guild.members
for member in guild_members:
if member.id == self.__id:
return member

31
Handlers/ClearHandler.py Normal file
View File

@@ -0,0 +1,31 @@
from typing import Union
from discord import Interaction
from discord.ext.commands import Context
from Music.VulkanBot import VulkanBot
from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse
from Parallelism.ProcessInfo import ProcessInfo
class ClearHandler(AbstractHandler):
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
super().__init__(ctx, bot)
async def run(self) -> HandlerResponse:
# Get the current process of the guild
processManager = self.config.getProcessManager()
processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild)
if processInfo:
# Clear the playlist
playlist = processInfo.getPlaylist()
processLock = processInfo.getLock()
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired:
playlist.clear()
processLock.release()
embed = self.embeds.PLAYLIST_CLEAR()
return HandlerResponse(self.ctx, embed)
else:
processManager.resetProcess(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed)

View File

@@ -0,0 +1,33 @@
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, 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]:
return self.__ctx
@property
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
@property
def success(self) -> bool:
return self.__success

View File

@@ -0,0 +1,41 @@
from discord.ext.commands import Context
from Music.VulkanBot import VulkanBot
from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse
from Utils.Utils import Utils
from typing import Union
from discord import Interaction
class HistoryHandler(AbstractHandler):
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
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:
processLock = processInfo.getLock()
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired:
playlist = processInfo.getPlaylist()
history = playlist.getSongsHistory()
processLock.release()
else:
# If the player doesn't respond in time we restart it
processManager.resetProcess(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed)
else:
history = []
if len(history) == 0:
text = self.messages.HISTORY_EMPTY
else:
text = f'\n📜 History Length: {len(history)} | Max: {self.config.MAX_SONGS_HISTORY}\n'
for pos, song in enumerate(history, start=1):
text += f"**`{pos}` - ** {song.title} - `{Utils.format_time(song.duration)}`\n"
embed = self.embeds.HISTORY(text)
return HandlerResponse(self.ctx, embed)

View File

@@ -0,0 +1,80 @@
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.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:
processManager = self.config.getProcessManager()
processInfo = processManager.getRunningPlayerInfo(self.guild)
if not processInfo:
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)
if acquired:
# Try to convert input to int
error = self.__validateInput(musicPos)
if error:
embed = self.embeds.ERROR_EMBED(error.message)
processLock.release()
return HandlerResponse(self.ctx, embed, error)
# Sanitize the input
playlist: Playlist = processInfo.getPlaylist()
musicPos = self.__sanitizeInput(playlist, musicPos)
# Validate the position
if not playlist.validate_position(musicPos):
error = InvalidInput()
embed = self.embeds.PLAYLIST_RANGE_ERROR()
processLock.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)
queue = processInfo.getQueueToPlayer()
queue.put(command)
processLock.release()
return HandlerResponse(self.ctx)
except:
# Release the acquired Lock
processLock.release()
embed = self.embeds.ERROR_MOVING()
error = UnknownError()
return HandlerResponse(self.ctx, embed, error)
else:
processManager.resetProcess(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

59
Handlers/LoopHandler.py Normal file
View File

@@ -0,0 +1,59 @@
from discord.ext.commands import Context
from Music.VulkanBot import VulkanBot
from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse
from Config.Exceptions import BadCommandUsage
from typing import Union
from discord import Interaction
class LoopHandler(AbstractHandler):
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
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:
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)
if acquired:
if args == '' or args is None:
playlist.loop_all()
embed = self.embeds.LOOP_ALL_ACTIVATED()
processLock.release()
return HandlerResponse(self.ctx, embed)
args = args.lower()
error = None
if playlist.getCurrentSong() is None:
embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error)
if args == 'one':
playlist.loop_one()
embed = self.embeds.LOOP_ONE_ACTIVATED()
elif args == 'all':
playlist.loop_all()
embed = self.embeds.LOOP_ALL_ACTIVATED()
elif args == 'off':
playlist.loop_off()
embed = self.embeds.LOOP_DISABLE()
else:
error = BadCommandUsage()
embed = self.embeds.BAD_LOOP_USE()
processLock.release()
return HandlerResponse(self.ctx, embed, error)
else:
processManager.resetProcess(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed)

75
Handlers/MoveHandler.py Normal file
View File

@@ -0,0 +1,75 @@
from typing import Union
from discord.ext.commands import Context
from Music.VulkanBot import VulkanBot
from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse
from Config.Exceptions import BadCommandUsage, VulkanError, InvalidInput, NumberRequired, UnknownError
from Music.Playlist import Playlist
from typing import Union
from discord import Interaction
class MoveHandler(AbstractHandler):
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
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:
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)
if acquired:
error = self.__validateInput(pos1, pos2)
if error:
embed = self.embeds.ERROR_EMBED(error.message)
processLock.release()
return HandlerResponse(self.ctx, embed, error)
playlist = processInfo.getPlaylist()
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()
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()
return HandlerResponse(self.ctx, embed)
except:
# Release the acquired Lock
processLock.release()
embed = self.embeds.ERROR_MOVING()
error = UnknownError()
return HandlerResponse(self.ctx, embed, error)
else:
processManager.resetProcess(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed)
def __validateInput(self, pos1: str, pos2: str) -> Union[VulkanError, None]:
try:
pos1 = int(pos1)
pos2 = int(pos2)
except:
return NumberRequired(self.messages.ERROR_NUMBER)
def __sanitizeInput(self, playlist: Playlist, pos1: int, pos2: int) -> tuple:
pos1 = int(pos1)
pos2 = int(pos2)
if pos1 == -1:
pos1 = len(playlist.getSongs())
if pos2 == -1:
pos2 = len(playlist.getSongs())
return pos1, pos2

View File

@@ -0,0 +1,36 @@
from discord.ext.commands import Context
from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse
from Music.VulkanBot import VulkanBot
from Utils.Cleaner import Cleaner
from typing import Union
from discord import Interaction
class NowPlayingHandler(AbstractHandler):
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
super().__init__(ctx, bot)
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:
embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed)
playlist = processInfo.getPlaylist()
if playlist.getCurrentSong() is None:
embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed)
if playlist.isLoopingOne():
title = self.messages.ONE_SONG_LOOPING
else:
title = self.messages.SONG_PLAYING
await self.__cleaner.clean_messages(self.ctx, self.config.CLEANER_MESSAGES_QUANT)
info = playlist.getCurrentSong().info
embed = self.embeds.SONG_INFO(info, title)
return HandlerResponse(self.ctx, embed)

32
Handlers/PauseHandler.py Normal file
View File

@@ -0,0 +1,32 @@
from discord.ext.commands import Context
from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse
from Parallelism.Commands import VCommands, VCommandsType
from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus
from Music.VulkanBot import VulkanBot
from typing import Union
from discord import Interaction
class PauseHandler(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: ProcessInfo = processManager.getRunningPlayerInfo(self.guild)
if processInfo:
if processInfo.getStatus() == ProcessStatus.SLEEPING:
embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed)
# Send Pause command to be execute by player process
command = VCommands(VCommandsType.PAUSE, None)
queue = processInfo.getQueueToPlayer()
queue.put(command)
embed = self.embeds.PLAYER_PAUSED()
return HandlerResponse(self.ctx, embed)
else:
embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed)

137
Handlers/PlayHandler.py Normal file
View File

@@ -0,0 +1,137 @@
import asyncio
import traceback
from typing import List
from Config.Exceptions import DownloadingError, InvalidInput, VulkanError
from discord.ext.commands import Context
from Handlers.AbstractHandler import AbstractHandler
from Config.Exceptions import ImpossibleMove, UnknownError
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.Commands import VCommands, VCommandsType
from Music.VulkanBot import VulkanBot
from typing import Union
from discord import Interaction
class PlayHandler(AbstractHandler):
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
super().__init__(ctx, bot)
self.__searcher = Searcher()
self.__down = Downloader()
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.getOrCreatePlayerInfo(self.guild, self.ctx)
playlist = processInfo.getPlaylist()
process = processInfo.getProcess()
if not process.is_alive(): # If process has not yet started, start
process.start()
# Create the Songs objects
songs: List[Song] = []
for musicInfo in musicsInfo:
songs.append(Song(musicInfo, playlist, requester))
if len(songs) == 1:
# If only one music, download it directly
song = self.__down.finish_one_song(songs[0])
if song.problematic: # If error in download song return
embed = self.embeds.SONG_PROBLEMATIC()
error = DownloadingError()
return HandlerResponse(self.ctx, embed, error)
# If not playing
if not playlist.getCurrentSong():
embed = self.embeds.SONG_ADDED(song.title)
response = HandlerResponse(self.ctx, embed)
else: # If already playing
pos = len(playlist.getSongs())
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)
if acquired:
playlist.add_song(song)
# Release the acquired Lock
processLock.release()
queue = processInfo.getQueueToPlayer()
playCommand = VCommands(VCommandsType.PLAY, None)
queue.put(playCommand)
else:
processManager.resetProcess(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))
embed = self.embeds.SONGS_ADDED(len(songs))
return HandlerResponse(self.ctx, embed)
except DownloadingError as error:
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: {traceback.format_exc()}', {type(error)})
embed = self.embeds.CUSTOM_ERROR(error)
else:
print(
f'DEVELOPER NOTE -> PlayController Error: {traceback.format_exc()}, {type(error)}')
error = UnknownError()
embed = self.embeds.UNKNOWN_ERROR()
return HandlerResponse(self.ctx, embed, error)
async def __downloadSongsAndStore(self, songs: List[Song], processInfo: ProcessInfo) -> None:
playlist = processInfo.getPlaylist()
queue = processInfo.getQueueToPlayer()
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)
# 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.getOrCreatePlayerInfo(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)
def __isUserConnected(self) -> bool:
if self.ctx.author.voice:
return True
else:
return False

56
Handlers/PrevHandler.py Normal file
View File

@@ -0,0 +1,56 @@
from discord.ext.commands import Context
from Handlers.AbstractHandler import AbstractHandler
from Config.Exceptions import BadCommandUsage, ImpossibleMove
from Handlers.HandlerResponse import HandlerResponse
from Parallelism.Commands import VCommands, VCommandsType
from Music.VulkanBot import VulkanBot
from typing import Union
from discord import Interaction
class PrevHandler(AbstractHandler):
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
super().__init__(ctx, bot)
async def run(self) -> HandlerResponse:
if not self.__user_connected():
error = ImpossibleMove()
embed = self.embeds.NO_CHANNEL()
return HandlerResponse(self.ctx, embed, error)
processManager = self.config.getProcessManager()
processInfo = processManager.getOrCreatePlayerInfo(self.guild, self.ctx)
if not processInfo:
embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error)
playlist = processInfo.getPlaylist()
if len(playlist.getHistory()) == 0:
error = ImpossibleMove()
embed = self.embeds.NOT_PREVIOUS_SONG()
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)
embed = self.embeds.RETURNING_SONG()
return HandlerResponse(self.ctx, embed)
def __user_connected(self) -> bool:
if self.author.voice:
return True
else:
return False

117
Handlers/QueueHandler.py Normal file
View File

@@ -0,0 +1,117 @@
from discord.ext.commands import Context
from Config.Exceptions import InvalidIndex
from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse
from Handlers.JumpMusicHandler import JumpMusicHandler
from Messages.MessagesCategory import MessagesCategory
from UI.Views.BasicView import BasicView
from Utils.Utils import Utils
from Music.VulkanBot import VulkanBot
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)
async def run(self, pageNumber=0) -> 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
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)
if acquired:
playlist: Playlist = processInfo.getPlaylist()
if playlist.isLoopingOne():
song = playlist.getCurrentSong()
embed = self.embeds.ONE_SONG_LOOPING(song.info)
processLock.release() # Release the Lock
return HandlerResponse(self.ctx, embed)
allSongs = playlist.getSongs()
if len(allSongs) == 0:
embed = self.embeds.EMPTY_QUEUE()
processLock.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:
title = self.messages.QUEUE_TITLE
total_time = Utils.format_time(sum([int(song.duration if song.duration else 0)
for song in allSongs]))
total_songs = len(playlist.getSongs())
text = f'📜 Queue length: {total_songs} | Page Number: {pageNumber+1}/{len(songsPages)} | ⌛ Duration: `{total_time}` downloaded \n\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, view=queueView)
else:
processManager.resetProcess(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)]

64
Handlers/RemoveHandler.py Normal file
View File

@@ -0,0 +1,64 @@
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.ProcessInfo import ProcessInfo
from typing import Union
from discord import Interaction
class RemoveHandler(AbstractHandler):
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
super().__init__(ctx, bot)
async def run(self, position: str) -> HandlerResponse:
# Get the current process of the guild
processManager = self.config.getProcessManager()
processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild)
if not processInfo:
embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error)
playlist = processInfo.getPlaylist()
if playlist is None:
embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error)
error = self.__validateInput(position)
if error:
embed = self.embeds.ERROR_EMBED(error.message)
return HandlerResponse(self.ctx, embed, error)
position = self.__sanitizeInput(playlist, position)
if not playlist.validate_position(position):
error = InvalidInput()
embed = self.embeds.PLAYLIST_RANGE_ERROR()
return HandlerResponse(self.ctx, embed, error)
try:
song = playlist.remove_song(position)
name = song.title if song.title else song.identifier
embed = self.embeds.SONG_REMOVED(name)
return HandlerResponse(self.ctx, embed)
except:
error = ErrorRemoving()
embed = self.embeds.ERROR_REMOVING()
return HandlerResponse(self.ctx, embed, error)
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: str) -> int:
position = int(position)
if position == -1:
position = len(playlist.getSongs())
return position

31
Handlers/ResetHandler.py Normal file
View File

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

32
Handlers/ResumeHandler.py Normal file
View File

@@ -0,0 +1,32 @@
from discord.ext.commands import Context
from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse
from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus
from Parallelism.Commands import VCommands, VCommandsType
from Music.VulkanBot import VulkanBot
from typing import Union
from discord import Interaction
class ResumeHandler(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: ProcessInfo = processManager.getRunningPlayerInfo(self.guild)
if processInfo:
if processInfo.getStatus() == ProcessStatus.SLEEPING:
embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed)
# Send Resume command to be execute by player process
command = VCommands(VCommandsType.RESUME, None)
queue = processInfo.getQueueToPlayer()
queue.put(command)
embed = self.embeds.PLAYER_RESUMED()
return HandlerResponse(self.ctx, embed)
else:
embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed)

View File

@@ -0,0 +1,42 @@
from discord.ext.commands import Context
from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse
from Config.Exceptions import UnknownError
from Music.VulkanBot import VulkanBot
from typing import Union
from discord import Interaction
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:
try:
processLock = processInfo.getLock()
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired:
playlist = processInfo.getPlaylist()
playlist.shuffle()
# Release the acquired Lock
processLock.release()
else:
processManager.resetProcess(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed)
embed = self.embeds.SONGS_SHUFFLED()
return HandlerResponse(self.ctx, embed)
except Exception as e:
print(f'DEVELOPER NOTE -> Error Shuffling: {e}')
error = UnknownError()
embed = self.embeds.ERROR_SHUFFLING()
return HandlerResponse(self.ctx, embed, error)
else:
embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed)

44
Handlers/SkipHandler.py Normal file
View File

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

32
Handlers/StopHandler.py Normal file
View File

@@ -0,0 +1,32 @@
from discord.ext.commands import Context
from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse
from Music.VulkanBot import VulkanBot
from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus
from Parallelism.Commands import VCommands, VCommandsType
from typing import Union
from discord import Interaction
class StopHandler(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: ProcessInfo = processManager.getRunningPlayerInfo(self.guild)
if processInfo:
if processInfo.getStatus() == ProcessStatus.SLEEPING:
embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed)
# Send command to player process stop
command = VCommands(VCommandsType.STOP, None)
queue = processInfo.getQueueToPlayer()
queue.put(command)
embed = self.embeds.STOPPING_PLAYER()
return HandlerResponse(self.ctx, embed)
else:
embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed)

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

@@ -0,0 +1,45 @@
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, 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
@property
def response(self) -> HandlerResponse:
return self.__response
@property
def category(self) -> MessagesCategory:
return self.__category
@property
def bot(self) -> VulkanBot:
return self.__bot
@property
def message(self) -> Message:
return self.__message
@property
def context(self) -> Context:
return self.__context
@property
def manager(self) -> MessagesManager:
return self.__messagesManager
@abstractmethod
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)

72
Music/DeezerSearcher.py Normal file
View File

@@ -0,0 +1,72 @@
import deezer
from Config.Exceptions import DeezerError
from Config.Messages import DeezerMessages
class DeezerSearcher:
def __init__(self) -> None:
self.__client = deezer.Client()
self.__messages = DeezerMessages()
self.__acceptedTypes = ['track', 'artist', 'playlist', 'album']
def search(self, url: str) -> None:
if not self.__verifyValidUrl(url):
raise DeezerError(self.__messages.INVALID_DEEZER_URL, self.__messages.GENERIC_TITLE)
urlType = url.split('/')[4].split('?')[0]
code = int(url.split('/')[5].split('?')[0])
try:
musics = []
if urlType == 'album':
musics = self.__get_album(code)
elif urlType == 'playlist':
musics = self.__get_playlist(code)
elif urlType == 'track':
musics = self.__get_track(code)
elif urlType == 'artist':
musics = self.__get_artist(code)
return musics
except Exception as e:
print(f'[DEEZER ERROR] -> {e}')
raise DeezerError(self.__messages.INVALID_DEEZER_URL, self.__messages.GENERIC_TITLE)
def __get_album(self, code: int) -> list:
album = self.__client.get_album(code)
return [track.title for track in album.tracks]
def __get_track(self, code: int) -> list:
track = self.__client.get_track(code)
return [track.title]
def __get_playlist(self, code: int) -> list:
playlist = self.__client.get_playlist(code)
return [track.title for track in playlist.tracks]
def __get_artist(self, code: int) -> list:
artist = self.__client.get_artist(code)
topMusics = artist.get_top()
return [track.title for track in topMusics]
def __verifyValidUrl(self, url: str) -> bool:
try:
urlType = url.split('/')[4].split('?')[0]
code = url.split('/')[5].split('?')[0]
code = int(code)
if urlType == '' or code == '':
return False
if urlType not in self.__acceptedTypes:
return False
return True
except:
return False

184
Music/Downloader.py Normal file
View File

@@ -0,0 +1,184 @@
import asyncio
from typing import List
from Config.Configs import VConfigs
from yt_dlp import YoutubeDL, DownloadError
from concurrent.futures import ThreadPoolExecutor
from Music.Song import Song
from Utils.Utils import Utils, run_async
from Config.Exceptions import DownloadingError
class Downloader:
config = VConfigs()
__YDL_OPTIONS = {'format': 'bestaudio/best',
'default_search': 'auto',
'playliststart': 0,
'extract_flat': False,
'playlistend': config.MAX_PLAYLIST_LENGTH,
'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,
'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,
'ignore_no_formats_error': True
}
__BASE_URL = 'https://www.youtube.com/watch?v={}'
def __init__(self) -> None:
self.__config = VConfigs()
self.__music_keys_only = ['resolution', 'fps', 'quality']
self.__not_extracted_keys_only = ['ie_key']
self.__not_extracted_not_keys = ['entries']
self.__playlist_keys = ['entries']
def finish_one_song(self, song: Song) -> Song:
try:
if song.identifier is None:
return None
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)
return song
# Convert yt_dlp error to my own error
except DownloadError:
raise DownloadingError()
@run_async
def extract_info(self, url: str) -> List[dict]:
if url == '':
return []
if Utils.is_url(url): # If Url
options = Downloader.__YDL_OPTIONS_EXTRACT
with YoutubeDL(options) as ydl:
try:
extracted_info = ydl.extract_info(url, download=False)
# Some links doesn't extract unless extract_flat key is passed as False in options
if self.__failed_to_extract(extracted_info):
extracted_info = self.__get_forced_extracted_info(url)
if self.__is_music(extracted_info):
return [extracted_info['original_url']]
elif self.__is_multiple_musics(extracted_info):
songs = []
for song in extracted_info['entries']:
songs.append(self.__BASE_URL.format(song['id']))
return songs
else: # Failed to extract the songs
print(f'DEVELOPER NOTE -> Failed to Extract URL {url}')
return []
# Convert the yt_dlp download error to own error
except DownloadError:
raise DownloadingError()
except Exception as e:
print(f'DEVELOPER NOTE -> Error Extracting Music: {e}, {type(e)}')
raise e
else:
return []
def __get_forced_extracted_info(self, url: str) -> list:
options = Downloader.__YDL_OPTIONS_FORCE_EXTRACT
with YoutubeDL(options) as ydl:
try:
extracted_info = ydl.extract_info(url, download=False)
return extracted_info
except Exception as e:
print(f'DEVELOPER NOTE -> Error Forcing Extract Music: {e}')
return []
def __download_url(self, url) -> dict:
options = Downloader.__YDL_OPTIONS
with YoutubeDL(options) as ydl:
try:
result = ydl.extract_info(url, download=False)
return result
except Exception as e: # Any type of error in download
print(f'DEVELOPER NOTE -> Error Downloading {url} -> {e}')
return None
async def download_song(self, song: Song) -> None:
if song.source is not None: # If Music already preloaded
return None
def __download_func(song: Song) -> None:
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)
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()
executor = ThreadPoolExecutor(max_workers=self.__config.MAX_PRELOAD_SONGS)
fs = {loop.run_in_executor(executor, __download_func, song)}
await asyncio.wait(fs=fs, return_when=asyncio.ALL_COMPLETED)
def __download_title(self, title: str) -> dict:
options = Downloader.__YDL_OPTIONS
with YoutubeDL(options) as ydl:
try:
search = f'ytsearch:{title}'
extracted_info = ydl.extract_info(search, download=False)
if self.__failed_to_extract(extracted_info):
extracted_info = self.__get_forced_extracted_info(title)
if extracted_info is None:
return {}
if self.__is_multiple_musics(extracted_info):
return extracted_info['entries'][0]
else:
print(f'DEVELOPER NOTE -> Failed to extract title {title}')
return {}
except Exception as e:
print(f'DEVELOPER NOTE -> Error downloading title {title}: {e}')
return {}
def __is_music(self, extracted_info: dict) -> bool:
for key in self.__music_keys_only:
if key not in extracted_info.keys():
return False
return True
def __is_multiple_musics(self, extracted_info: dict) -> bool:
for key in self.__playlist_keys:
if key not in extracted_info.keys():
return False
return True
def __failed_to_extract(self, extracted_info: dict) -> bool:
if type(extracted_info) is not dict:
return False
for key in self.__not_extracted_keys_only:
if key not in extracted_info.keys():
return False
for key in self.__not_extracted_not_keys:
if key in extracted_info.keys():
return False
return True

155
Music/Playlist.py Normal file
View File

@@ -0,0 +1,155 @@
from collections import deque
from typing import List
from Config.Configs import VConfigs
from Music.Song import Song
import random
class Playlist:
def __init__(self) -> None:
self.__configs = VConfigs()
self.__queue = deque() # Store the musics to play
self.__songs_history = deque() # Store the musics played
self.__looping_one = False
self.__looping_all = False
self.__current: Song = None
def getSongs(self) -> deque[Song]:
return self.__queue
def validate_position(self, position: int) -> bool:
if position not in range(1, len(self.__queue) + 1):
return False
else:
return True
def validate_positions_list(self, positions: list) -> bool:
for position in positions:
if not self.validate_position(position):
return False
return True
def getSongsHistory(self) -> deque:
return self.__songs_history
def isLoopingOne(self) -> bool:
return self.__looping_one
def isLoopingAll(self) -> bool:
return self.__looping_all
def getCurrentSong(self) -> Song:
return self.__current
def setCurrentSong(self, song: Song) -> Song:
self.__current = song
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)
def next_song(self) -> Song:
if self.__current is None and len(self.__queue) == 0:
return None
played_song = self.__current
# Att played song info
if played_song != None:
if not self.__looping_one and not self.__looping_all:
if not played_song.problematic:
self.__songs_history.appendleft(played_song)
if len(self.__songs_history) > self.__configs.MAX_SONGS_HISTORY:
self.__songs_history.pop() # Remove the older
elif self.__looping_one: # Insert the current song to play again
self.__queue.appendleft(played_song)
elif self.__looping_all: # Insert the current song in the end of queue
self.__queue.append(played_song)
# Get the new song
if len(self.__queue) == 0:
self.__current = None
return None
self.__current: Song = self.__queue.popleft()
return self.__current
def prev_song(self) -> Song:
if len(self.__songs_history) == 0:
return None
else:
if self.__current != None:
self.__queue.appendleft(self.__current)
last_song = self.__songs_history.popleft() # Get the last song
self.__current = last_song
return self.__current # return the song
def add_song(self, song: Song) -> Song:
self.__queue.append(song)
return song
def shuffle(self) -> None:
random.shuffle(self.__queue)
def revert(self) -> None:
self.__queue.reverse()
def clear(self) -> None:
self.__queue.clear()
def loop_one(self) -> None:
self.__looping_one = True
self.__looping_all = False
def loop_all(self) -> None:
self.__looping_all = True
self.__looping_one = False
def loop_off(self) -> str:
self.__looping_all = False
self.__looping_one = False
def destroy_song(self, song_destroy: Song) -> None:
for song in self.__queue:
if song == song_destroy:
self.__queue.remove(song)
break
def move_songs(self, pos1, pos2) -> str:
song = self.__queue[pos1-1]
self.__queue.remove(song)
self.__queue.insert(pos2-1, song)
return song
def remove_song(self, position) -> str:
song = self.__queue[position-1]
self.__queue.remove(song)
return song
def getHistory(self) -> list:
titles = []
for song in self.__songs_history:
title = song.title if song.title else 'Unknown'
titles.append(title)
return titles

91
Music/Searcher.py Normal file
View File

@@ -0,0 +1,91 @@
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
class Searcher():
def __init__(self) -> None:
self.__spotify = SpotifySearch()
self.__deezer = DeezerSearcher()
self.__messages = SearchMessages()
self.__down = Downloader()
async def search(self, track: str) -> list:
provider = self.__identify_source(track)
if provider == Provider.Unknown:
raise InvalidInput(self.__messages.UNKNOWN_INPUT, self.__messages.UNKNOWN_INPUT_TITLE)
elif provider == Provider.YouTube:
try:
track = self.__cleanYoutubeInput(track)
musics = await self.__down.extract_info(track)
return musics
except VulkanError as error:
raise error
except Exception as error:
print(f'[Error in Searcher] -> {error}, {type(error)}')
raise YoutubeError(self.__messages.YOUTUBE_NOT_FOUND, self.__messages.GENERIC_TITLE)
elif provider == Provider.Spotify:
try:
musics = self.__spotify.search(track)
if musics == None or len(musics) == 0:
raise SpotifyError(self.__messages.SPOTIFY_NOT_FOUND,
self.__messages.GENERIC_TITLE)
return musics
except SpotifyError as error:
raise error # Redirect already processed error
except Exception as e:
print(f'[Spotify Error] -> {e}')
raise SpotifyError(self.__messages.SPOTIFY_NOT_FOUND, self.__messages.GENERIC_TITLE)
elif provider == Provider.Deezer:
try:
musics = self.__deezer.search(track)
if musics == None or len(musics) == 0:
raise DeezerError(self.__messages.DEEZER_NOT_FOUND,
self.__messages.GENERIC_TITLE)
return musics
except DeezerError as error:
raise error # Redirect already processed error
except Exception as e:
print(f'[Deezer Error] -> {e}')
raise DeezerError(self.__messages.DEEZER_NOT_FOUND, self.__messages.GENERIC_TITLE)
elif provider == Provider.Name:
return [track]
def __cleanYoutubeInput(self, track: str) -> str:
trackAnalyzer = URLAnalyzer(track)
# Just ID and List arguments probably
if trackAnalyzer.queryParamsQuant <= 2:
return track
# Arguments used in Mix Youtube Playlists
if 'start_radio' or 'index' in trackAnalyzer.queryParams.keys():
return trackAnalyzer.getCleanedUrl()
def __identify_source(self, track: str) -> Provider:
if track == '':
return Provider.Unknown
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:
return Provider.YouTube
if "https://open.spotify.com" in track:
return Provider.Spotify
if "https://www.deezer.com" in track:
return Provider.Deezer
return Provider.Unknown

73
Music/Song.py Normal file
View File

@@ -0,0 +1,73 @@
class Song:
def __init__(self, identifier: str, playlist, requester: str) -> None:
self.__identifier = identifier
self.__info = {'requester': requester}
self.__problematic = False
self.__playlist = playlist
def finish_down(self, info: dict) -> None:
if info is None:
self.destroy()
return None
self.__useful_keys = ['duration',
'title', 'webpage_url',
'channel', 'id', 'uploader',
'thumbnail', 'original_url']
self.__required_keys = ['url']
for key in self.__required_keys:
if key in info.keys():
self.__info[key] = info[key]
else:
print(f'DEVELOPER NOTE -> {key} not found in info of music: {self.identifier}')
self.destroy()
return
for key in self.__useful_keys:
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 source(self) -> str:
if 'url' in self.__info.keys():
return self.__info['url']
else:
return None
@property
def title(self) -> str:
if 'title' in self.__info.keys():
return self.__info['title']
else:
return None
@property
def duration(self) -> str:
if 'duration' in self.__info.keys():
return self.__info['duration']
else:
return 0.0
@property
def identifier(self) -> str:
return self.__identifier
@property
def problematic(self) -> bool:
return self.__problematic
def destroy(self) -> None:
print(f'DEVELOPER NOTE -> Music self destroying {self.__identifier}')
self.__problematic = True
self.__playlist.destroy_song(self)
@property
def info(self):
return self.__info

118
Music/SpotifySearcher.py Normal file
View File

@@ -0,0 +1,118 @@
from spotipy import Spotify
from spotipy.oauth2 import SpotifyClientCredentials
from spotipy.exceptions import SpotifyException
from Config.Exceptions import SpotifyError
from Config.Configs import VConfigs
from Config.Messages import SpotifyMessages
class SpotifySearch():
def __init__(self) -> None:
self.__messages = SpotifyMessages()
self.__config = VConfigs()
self.__connected = False
self.__connect()
def __connect(self) -> None:
try:
auth = SpotifyClientCredentials(self.__config.SPOTIFY_ID, self.__config.SPOTIFY_SECRET)
self.__api = Spotify(auth_manager=auth)
self.__connected = True
except Exception as e:
print(f'DEVELOPER NOTE -> Spotify Connection Error {e}')
def search(self, url: str) -> list:
if not self.__checkUrlValid(url):
raise SpotifyError(self.__messages.INVALID_SPOTIFY_URL, self.__messages.GENERIC_TITLE)
type = url.split('/')[3].split('?')[0]
code = url.split('/')[4].split('?')[0]
musics = []
try:
if self.__connected:
if type == 'album':
musics = self.__get_album(code)
elif type == 'playlist':
musics = self.__get_playlist(code)
elif type == 'track':
musics = self.__get_track(code)
elif type == 'artist':
musics = self.__get_artist(code)
return musics
except SpotifyException:
raise SpotifyError(self.__messages.INVALID_SPOTIFY_URL, self.__messages.GENERIC_TITLE)
def __get_album(self, code: str) -> list:
results = self.__api.album_tracks(code)
musics = results['items']
while results['next']: # Get the next pages
results = self.__api.next(results)
musics.extend(results['items'])
musicsTitle = []
for music in musics:
title = self.__extract_title(music)
musicsTitle.append(title)
return musicsTitle
def __get_playlist(self, code: str) -> list:
results = self.__api.playlist_items(code)
itens = results['items']
while results['next']: # Load the next pages
results = self.__api.next(results)
itens.extend(results['items'])
musics = []
for item in itens:
musics.append(item['track'])
titles = []
for music in musics:
title = self.__extract_title(music)
titles.append(title)
return titles
def __get_track(self, code: str) -> list:
results = self.__api.track(code)
name = results['name']
artists = ''
for artist in results['artists']:
artists += f'{artist["name"]} '
return [f'{name} {artists}']
def __get_artist(self, code: str) -> list:
results = self.__api.artist_top_tracks(code, country='BR')
musics_titles = []
for music in results['tracks']:
title = self.__extract_title(music)
musics_titles.append(title)
return musics_titles
def __extract_title(self, music: dict) -> str:
title = f'{music["name"]} '
for artist in music['artists']:
title += f'{artist["name"]} '
return title
def __checkUrlValid(self, url: str) -> bool:
try:
type = url.split('/')[3].split('?')[0]
code = url.split('/')[4].split('?')[0]
if type == '' or code == '':
return False
return True
except:
return False

9
Music/Types.py Normal file
View File

@@ -0,0 +1,9 @@
from enum import Enum
class Provider(str, Enum):
Spotify = 'Spotify'
Deezer = 'Deezer'
YouTube = 'YouTube'
Name = 'Track Name'
Unknown = 'Unknown'

78
Music/VulkanBot.py Normal file
View File

@@ -0,0 +1,78 @@
from asyncio import AbstractEventLoop
from discord import Guild, Status, Game, Message
from discord.ext.commands.errors import CommandNotFound, MissingRequiredArgument
from Config.Configs import VConfigs
from discord.ext.commands import Bot, Context
from Config.Messages import Messages
from Config.Embeds import VEmbeds
class VulkanBot(Bot):
def __init__(self, listingSlash: bool = False, *args, **kwargs):
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 == '':
print('DEVELOPER NOTE -> Token not found')
exit()
super().run(self.__configs.BOT_TOKEN, reconnect=True)
async def startBotCoro(self, loop: AbstractEventLoop) -> None:
"""Start a bot coroutine, does not wait for connection to be established"""
task = loop.create_task(self.__login())
await task
loop.create_task(self.__connect())
async def __login(self):
"""Coroutine to login the Bot in discord"""
await self.login(token=self.__configs.BOT_TOKEN)
async def __connect(self):
"""Coroutine to connect the Bot in discord"""
await self.connect(reconnect=True)
async def on_ready(self):
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)
async def on_command_error(self, ctx, error):
if isinstance(error, MissingRequiredArgument):
embed = self.__embeds.MISSING_ARGUMENTS()
await ctx.send(embed=embed)
elif isinstance(error, CommandNotFound):
embed = self.__embeds.COMMAND_NOT_FOUND()
await ctx.send(embed=embed)
else:
print(f'DEVELOPER NOTE -> Command Error: {error}')
embed = self.__embeds.UNKNOWN_ERROR()
await ctx.send(embed=embed)
async def process_commands(self, message: Message):
if message.author.bot:
return
ctx = await self.get_context(message, cls=Context)
if ctx.valid and not message.guild:
return
await self.invoke(ctx)
class Context(Context):
bot: VulkanBot
guild: Guild

View File

@@ -0,0 +1,60 @@
from random import choices
import string
from discord.bot import Bot
from discord import Intents
from Music.VulkanBot import VulkanBot
from os import listdir
from Config.Configs import VConfigs
from Config.Exceptions import VulkanError
class VulkanInitializer:
def __init__(self, willListen: bool) -> None:
self.__config = VConfigs()
self.__intents = Intents.default()
self.__intents.message_content = True
self.__intents.members = True
self.__bot = self.__create_bot(willListen)
self.__add_cogs(self.__bot)
def getBot(self) -> VulkanBot:
return self.__bot
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(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(self.__config.COMMANDS_PATH):
if filename.endswith('.py'):
cogPath = f'{self.__config.COMMANDS_FOLDER_NAME}.{filename[:-3]}'
cogsStatus.append(bot.load_extension(cogPath, store=True))
if len(bot.cogs.keys()) != self.__getTotalCogs():
print(cogsStatus)
raise VulkanError(message='Failed to load some Cog')
except VulkanError as e:
print(f'[Error Loading Vulkan] -> {e.message}')
def __getTotalCogs(self) -> int:
quant = 0
for filename in listdir(self.__config.COMMANDS_PATH):
if filename.endswith('.py'):
quant += 1
return quant

28
Parallelism/Commands.py Normal file
View File

@@ -0,0 +1,28 @@
from enum import Enum
from typing import Tuple
class VCommandsType(Enum):
PREV = 'Prev'
SKIP = 'Skip'
PAUSE = 'Pause'
RESUME = 'Resume'
CONTEXT = 'Context'
PLAY = 'Play'
STOP = 'Stop'
RESET = 'Reset'
NOW_PLAYING = 'Now Playing'
TERMINATE = 'Terminate'
SLEEPING = 'Sleeping'
class VCommands:
def __init__(self, type: VCommandsType, args=None) -> None:
self.__type = type
self.__args = args
def getType(self) -> VCommandsType:
return self.__type
def getArgs(self) -> Tuple:
return self.__args

View File

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

View File

@@ -0,0 +1,79 @@
from typing import List
from discord import Button, TextChannel
from discord.ui import View
from Config.Emojis import VEmojis
from Messages.MessagesCategory import MessagesCategory
from Music.VulkanBot import VulkanBot
from Parallelism.ProcessInfo import ProcessInfo
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:
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()
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)
channel = processInfo.getTextChannel()
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

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

View File

@@ -0,0 +1,187 @@
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 Parallelism.ProcessExecutor import ProcessCommandsExecutor
from Music.Song import Song
from Parallelism.PlayerProcess import PlayerProcess
from Music.Playlist import Playlist
from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus
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.__playersCommandsExecutor: Dict[Guild, ProcessCommandsExecutor] = {}
def setPlayerInfo(self, guild: Guild, info: ProcessInfo):
self.__playersProcess[guild.id] = info
def getOrCreatePlayerInfo(self, guild: Guild, context: Union[Context, Interaction]) -> ProcessInfo:
"""Return the process info for the guild, the user in context must be connected to a voice_channel"""
try:
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.__playersCommandsExecutor[guildID] = ProcessCommandsExecutor(self.__bot, guildID)
return processInfo
def __recreateProcess(self, guild: Guild, context: Union[Context, Interaction]) -> ProcessInfo:
"""Create a new process info using previous playlist"""
guildID: int = context.guild.id
textID: int = context.channel.id
if isinstance(context, Interaction):
authorID: int = context.user.id
voiceID: int = context.user.voice.channel.id
else:
authorID: int = context.author.id
voiceID: int = context.author.voice.channel.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.__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)
async def showNowPlaying(self, guildID: int, song: Song) -> None:
commandExecutor = self.__playersCommandsExecutor[guildID]
processInfo = self.__playersProcess[guildID]
await commandExecutor.sendNowPlaying(processInfo, song)
class VManager(BaseManager):
pass
class VProxy(NamespaceProxy):
_exposed_ = ('__getattribute__', '__setattr__', '__delattr__')

133
README.md
View File

@@ -1,47 +1,75 @@
# **Vulkan**
A multi-purpose Discord bot, including music, written in Python and supports Youtube and Spotify sources for playing. Vulkan was designed so that everyone could fork this project and use it, including a 24/7 host on Heroku.
<h1 align="center">Vulkan</h1>
Vulkan is also able to get data from Warframe and send it to users, get random phrases and handle random operations to help users decide what to do.
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.
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.
## **Prerequisites:**
<p align="center">
<img src="./Assets/playermenu.jpg" />
</p>
### **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 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>
# **How to use it**
### **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>
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.
- Installation of FFMPEG
*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).*
### **.Env File Example**
This is an example of how your .env file (located in root) should look like, those API url could be the same.
This is an example of how your .env file (located in root) should look like.
```
CETUS_API=https://api.warframestat.us/pc/cetusCycle
CAMBION_API=https://api.warframestat.us/pc/cambionCycle
FISSURES_API=https://api.warframestat.us/pc/fissures
PHRASES_API='http://api.forismatic.com/api/1.0/?method=getQuote&key=457653&format=json&lang=en'
BOT_TOKEN=Your_Own_Bot_Token
SPOTIFY_ID=Your_Own_Spotify_ID
SPOTIFY_SECRET=Your_Own_Spotify_Secret
SECRET_MESSAGE=Your_Own_Secret_Message
BOT_PREFIX=Your_Wanted_Prefix_For_Vulkan
```
### **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 config file is located at ```./config/Configs.py```, it doesn't require any change, but if you can change values to the way you want.
### **Initialization**
@@ -49,55 +77,22 @@ The config file, located in ```./config``` folder doesn't require any change, bu
- Run ```python main.py``` in console to start
## **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:
<br>
<hr>
<br>
- https://github.com/jonathanong/heroku-buildpack-ffmpeg-latest.git
## **🚀 Heroku**
To deploy and run your Bot in Heroku 24/7, follow the instructions in the [Heroku Instructions](HEROKU.md) page.
- https://github.com/xrisk/heroku-opus.git
## 🧪 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>
# **Music**
- Play musics from Youtube and Spotify Playlists
- Controll loop of one or all musics
- Allow realocating musics in the queue
- Play musics in queue randomly
### Commands
```!play [title, spotify_url, youtube_url]``` - Start playing song in Discord
```!resume``` - Resume the song player
```!pause``` - Pause the song player
```!move [x, y]``` - Change the musics in position x and y in Queue
```!skip``` - Skip the currently playing song
```!stop``` - Stop the playing of musics
```!queue``` - Show the musics list in queue
```!shuffle``` - Shuffle the songs in queue
```!remove [x]``` - Remove the song in position x
```!reset``` - Reset the player, recommended if any error happen
```!loop [one, all, off]``` - Control the loop of songs
```!np``` - Show information of the currently song
```!clear``` - Clear the songs in queue, doesn't stop the player
## 📖 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 building 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.

10
Tests/Colors.py Normal file
View File

@@ -0,0 +1,10 @@
class Colors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'

50
Tests/LoopRunner.py Normal file
View File

@@ -0,0 +1,50 @@
import asyncio
from asyncio import AbstractEventLoop
from threading import Thread
from typing import Any, Coroutine, List
class LoopRunner(Thread):
"""
Class to help deal with asyncio coroutines and loops
Copyright: https://agariinc.medium.com/advanced-strategies-for-testing-async-code-in-python-6196a032d8d7
"""
def __init__(self, loop: AbstractEventLoop) -> None:
# We ensure to always use the same loop
self.loop = loop
Thread.__init__(self, name='runner')
def run(self) -> None:
asyncio.set_event_loop(self.loop)
try:
self.loop.run_forever()
finally:
if self.loop.is_running():
self.loop.close()
def run_coroutine(self, coroutine: Coroutine) -> Any:
"""Run a coroutine inside the loop and return the result, doesn't allow concurrency"""
result = asyncio.run_coroutine_threadsafe(coroutine, self.loop)
return result.result()
def _stop(self):
self.loop.stop()
def run_in_thread(self, callback, *args):
return self.loop.call_soon_threadsafe(callback, *args)
def stop(self):
return self.run_in_thread(self._stop)
def run_coroutines_list(self, coroutineList: List[Coroutine]) -> None:
"""Create multiple tasks in the loop and wait for them, use concurrency"""
tasks = []
for coroutine in coroutineList:
tasks.append(self.loop.create_task(coroutine))
self.run_coroutine(self.__waitForMultipleTasks(tasks))
async def __waitForMultipleTasks(self, coroutines: List[Coroutine]) -> None:
"""Function to trigger the await for asyncio.wait coroutines"""
await asyncio.wait(coroutines)

86
Tests/TestBase.py Normal file
View File

@@ -0,0 +1,86 @@
import asyncio
from time import time
from typing import Callable, List, Tuple
from Tests.Colors import Colors
from Music.Downloader import Downloader
from Music.Searcher import Searcher
from Tests.TestsHelper import TestsConstants
from Tests.LoopRunner import LoopRunner
class VulkanTesterBase:
"""My own module to execute asyncio tests"""
def __init__(self) -> None:
self._downloader = Downloader()
self._searcher = Searcher()
self._constants = TestsConstants()
# Get the list of methods objects of this class if start with test
self._methodsList: List[Callable] = [getattr(self, func) for func in dir(self) if callable(
getattr(self, func)) and func.startswith("test")]
def run(self) -> None:
self.__printSeparator()
methodsSummary: List[Tuple[Callable, bool]] = []
testsSuccessQuant = 0
testsStartTime = time()
for method in self._methodsList:
currentTestStartTime = time()
self.__printTestStart(method)
success = False
try:
self._setUp()
success = method()
except Exception as e:
success = False
print(f'ERROR -> {e}')
finally:
self._tearDown()
methodsSummary.append((method, success))
runTime = time() - currentTestStartTime # Get the run time of the current test
if success:
testsSuccessQuant += 1
self.__printTestSuccess(method, runTime)
else:
self.__printTestFailure(method, runTime)
self.__printSeparator()
testsRunTime = time() - testsStartTime
self.__printTestsSummary(methodsSummary, testsSuccessQuant, testsRunTime)
def _setUp(self) -> None:
self._runner = LoopRunner(asyncio.new_event_loop())
self._runner.start()
def _tearDown(self) -> None:
self._runner.stop()
self._runner.join()
def __printTestsSummary(self, methods: List[Tuple[Callable, bool]], totalSuccess: int, runTime: int) -> None:
for index, methodResult in enumerate(methods):
method = methodResult[0]
success = methodResult[1]
if success:
print(f'{Colors.OKGREEN} {index} -> {method.__name__} = Success {Colors.ENDC}')
else:
print(f'{Colors.FAIL} {index} -> {method.__name__} = Failed {Colors.ENDC}')
print()
print(
f'TESTS EXECUTED: {len(methods)} | SUCCESS: {totalSuccess} | FAILED: {len(methods) - totalSuccess} | TIME: {runTime:.2f}sec')
def __printTestStart(self, method: Callable) -> None:
print(f'🧪 - Starting {method.__name__}')
def __printTestSuccess(self, method: Callable, runTime: int) -> None:
print(f'{method.__name__} -> {Colors.OKGREEN} Success {Colors.ENDC} | ⏰ - {runTime:.2f}sec')
def __printTestFailure(self, method: Callable, runTime: int) -> None:
print(f'{method.__name__} -> {Colors.FAIL} Test Failed {Colors.ENDC} | ⏰ - {runTime:.2f}sec')
def __printSeparator(self) -> None:
print('=-=' * 15)

28
Tests/TestsHelper.py Normal file
View File

@@ -0,0 +1,28 @@
from Config.Configs import Singleton
class TestsConstants(Singleton):
def __init__(self) -> None:
if not super().created:
self.EMPTY_STRING_ERROR_MSG = 'Downloader with Empty String should be empty list.'
self.MUSIC_TITLE_STRING = 'Experience || AMV || Anime Mix'
self.YT_MUSIC_URL = 'https://www.youtube.com/watch?v=MvJoiv842mk'
self.YT_MIX_URL = 'https://www.youtube.com/watch?v=ePjtnSPFWK8&list=RDMMePjtnSPFWK8&start_radio=1'
self.YT_PERSONAL_PLAYLIST_URL = 'https://www.youtube.com/playlist?list=PLbbKJHHZR9ShYuKAr71cLJCFbYE-83vhS'
# Links from playlists in channels some times must be extracted with force by Downloader
self.YT_CHANNEL_PLAYLIST_URL = 'https://www.youtube.com/watch?v=MvJoiv842mk&list=PLAI1099Tvk0zWU8X4dwc4vv4MpePQ4DLl'
self.SPOTIFY_TRACK_URL = 'https://open.spotify.com/track/7wpnz7hje4FbnjZuWQtJHP'
self.SPOTIFY_PLAYLIST_URL = 'https://open.spotify.com/playlist/37i9dQZF1EIV9u4LtkBkSF'
self.SPOTIFY_ARTIST_URL = 'https://open.spotify.com/artist/4HF14RSTZQcEafvfPCFEpI'
self.SPOTIFY_ALBUM_URL = 'https://open.spotify.com/album/71O60S5gIJSIAhdnrDIh3N'
self.SPOTIFY_WRONG1_URL = 'https://open.spotify.com/wrongUrl'
self.SPOTIFY_WRONG2_URL = 'https://open.spotify.com/track/WrongID'
self.DEEZER_TRACK_URL = 'https://www.deezer.com/br/track/33560861'
self.DEEZER_ARTIST_URL = 'https://www.deezer.com/br/artist/180'
self.DEEZER_PLAYLIST_URL = 'https://www.deezer.com/br/playlist/1001939451'
self.DEEZER_ALBUM_URL = 'https://www.deezer.com/en/album/236107012'
self.DEEZER_WRONG1_URL = 'xxxhttps://www.deezer.com/br/album/5'
self.DEEZER_WRONG2_URL = 'https://www.deezer.com/en/album/23610701252'

66
Tests/VDeezerTests.py Normal file
View File

@@ -0,0 +1,66 @@
from Tests.TestBase import VulkanTesterBase
from Config.Exceptions import DeezerError
class VulkanDeezerTest(VulkanTesterBase):
def __init__(self) -> None:
super().__init__()
def test_deezerTrack(self) -> bool:
musics = self._runner.run_coroutine(
self._searcher.search(self._constants.DEEZER_TRACK_URL))
if len(musics) > 0:
return True
else:
return False
def test_deezerPlaylist(self) -> bool:
musics = self._runner.run_coroutine(
self._searcher.search(self._constants.DEEZER_PLAYLIST_URL))
if len(musics) > 0:
return True
else:
return False
def test_deezerArtist(self) -> bool:
musics = self._runner.run_coroutine(
self._searcher.search(self._constants.DEEZER_ARTIST_URL))
if len(musics) > 0:
return True
else:
return False
def test_deezerAlbum(self) -> bool:
musics = self._runner.run_coroutine(
self._searcher.search(self._constants.DEEZER_ALBUM_URL))
if len(musics) > 0:
return True
else:
return False
def test_deezerWrongUrlShouldThrowException(self) -> bool:
try:
musics = self._runner.run_coroutine(
self._searcher.search(self._constants.DEEZER_WRONG1_URL))
except DeezerError as e:
print(f'Deezer Error -> {e.message}')
return True
except Exception as e:
print(e)
return False
def test_deezerWrongUrlTwoShouldThrowException(self) -> bool:
try:
musics = self._runner.run_coroutine(
self._searcher.search(self._constants.DEEZER_WRONG2_URL))
except DeezerError as e:
print(f'Deezer Error -> {e.message}')
return True
except Exception as e:
return False

120
Tests/VDownloaderTests.py Normal file
View File

@@ -0,0 +1,120 @@
from typing import List
from Tests.TestBase import VulkanTesterBase
from Music.Playlist import Playlist
from Music.Song import Song
from asyncio import Task
class VulkanDownloaderTest(VulkanTesterBase):
def __init__(self) -> None:
super().__init__()
def test_emptyString(self) -> bool:
musicsList = self._runner.run_coroutine(self._downloader.extract_info(''))
if musicsList == []:
return True
else:
return False
def test_YoutubeMusicUrl(self) -> bool:
musicsList = self._runner.run_coroutine(self._searcher.search(self._constants.YT_MUSIC_URL))
if len(musicsList) > 0:
print(musicsList[0])
return True
else:
return False
def test_YoutubeChannelPlaylist(self) -> None:
# Search the link to determine names
musicsList = self._runner.run_coroutine(
self._searcher.search(self._constants.YT_CHANNEL_PLAYLIST_URL))
if len(musicsList) == 0:
return False
# Create and store songs in list
playlist = Playlist()
songsList: List[Song] = []
for info in musicsList:
song = Song(identifier=info, playlist=playlist, requester='')
playlist.add_song(song)
songsList.append(song)
# Create a list of coroutines without waiting for them
tasks: List[Task] = []
for song in songsList:
tasks.append(self._downloader.download_song(song))
# Send for runner to execute them concurrently
self._runner.run_coroutines_list(tasks)
for song in songsList:
if song.problematic or song.title == None:
return False
return True
def test_YoutubeMixPlaylist(self) -> None:
# Search the link to determine names
musics = self._runner.run_coroutine(
self._searcher.search(self._constants.YT_MIX_URL))
# Musics from Mix should download only the first music
if len(musics) != 1:
return False
playlist = Playlist()
song = Song(musics[0], playlist, '')
playlist.add_song(song)
self._runner.run_coroutine(self._downloader.download_song(song))
if song.problematic:
return False
else:
print(song.title)
return True
def test_musicTitle(self):
playlist = Playlist()
song = Song(self._constants.MUSIC_TITLE_STRING, playlist, '')
playlist.add_song(song)
self._runner.run_coroutine(self._downloader.download_song(song))
if song.problematic:
return False
else:
print(song.title)
return True
def test_YoutubePersonalPlaylist(self) -> None:
musicsList = self._runner.run_coroutine(
self._searcher.search(self._constants.YT_PERSONAL_PLAYLIST_URL))
if len(musicsList) == 0:
return False
# Create and store songs in list
playlist = Playlist()
songsList: List[Song] = []
for info in musicsList:
song = Song(identifier=info, playlist=playlist, requester='')
playlist.add_song(song)
songsList.append(song)
# Create a list of coroutines without waiting for them
tasks: List[Task] = []
for song in songsList:
tasks.append(self._downloader.download_song(song))
# Send for runner to execute them concurrently
self._runner.run_coroutines_list(tasks)
for song in songsList:
if not song.problematic and song.title == None:
return False
return True

66
Tests/VSpotifyTests.py Normal file
View File

@@ -0,0 +1,66 @@
from Tests.TestBase import VulkanTesterBase
from Config.Exceptions import SpotifyError
class VulkanSpotifyTest(VulkanTesterBase):
def __init__(self) -> None:
super().__init__()
def test_spotifyTrack(self) -> bool:
musics = self._runner.run_coroutine(
self._searcher.search(self._constants.SPOTIFY_TRACK_URL))
if len(musics) > 0:
return True
else:
return False
def test_spotifyPlaylist(self) -> bool:
musics = self._runner.run_coroutine(
self._searcher.search(self._constants.SPOTIFY_PLAYLIST_URL))
if len(musics) > 0:
return True
else:
return False
def test_spotifyArtist(self) -> bool:
musics = self._runner.run_coroutine(
self._searcher.search(self._constants.SPOTIFY_ARTIST_URL))
if len(musics) > 0:
return True
else:
return False
def test_spotifyAlbum(self) -> bool:
musics = self._runner.run_coroutine(
self._searcher.search(self._constants.SPOTIFY_ARTIST_URL))
if len(musics) > 0:
return True
else:
return False
def test_spotifyWrongUrlShouldThrowException(self) -> bool:
try:
musics = self._runner.run_coroutine(
self._searcher.search(self._constants.SPOTIFY_WRONG1_URL))
except SpotifyError as e:
print(f'Spotify Error -> {e.message}')
return True
except Exception as e:
print(e)
return False
def test_spotifyWrongUrlTwoShouldThrowException(self) -> bool:
try:
musics = self._runner.run_coroutine(
self._searcher.search(self._constants.SPOTIFY_WRONG2_URL))
except SpotifyError as e:
print(f'Spotify Error -> {e.message}')
return True
except Exception as e:
return False

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

@@ -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

@@ -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

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}')

33
Utils/Cleaner.py Normal file
View File

@@ -0,0 +1,33 @@
from typing import List
from discord.ext.commands import Context
from discord import Message, Embed
from Config.Singleton import Singleton
from Music.VulkanBot import VulkanBot
class Cleaner(Singleton):
def __init__(self, bot: VulkanBot = None) -> None:
if not super().created:
self.__bot = bot
self.__clean_str = 'Uploader:'
def set_bot(self, bot: VulkanBot) -> None:
self.__bot = bot
async def clean_messages(self, ctx: Context, quant: int) -> None:
if self.__bot is None:
return
last_messages: List[Message] = await ctx.channel.history(limit=quant).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 == self.__clean_str:
await message.delete()
except Exception as e:
print(f'DEVELOPER NOTE -> Error cleaning messages {e}')
continue

34
Utils/UrlAnalyzer.py Normal file
View File

@@ -0,0 +1,34 @@
from typing import Dict
class URLAnalyzer:
def __init__(self, url: str) -> None:
self.__url = url
self.__queryParamsQuant = self.__url.count('&') + self.__url.count('?')
self.__queryParams: Dict[str, str] = self.__getAllQueryParams()
@property
def queryParams(self) -> dict:
return self.__queryParams
@property
def queryParamsQuant(self) -> int:
return self.__queryParamsQuant
def getCleanedUrl(self) -> str:
firstE = self.__url.index('&')
return self.__url[:firstE]
def __getAllQueryParams(self) -> dict:
if self.__queryParamsQuant <= 1:
return {}
params = {}
arguments = self.__url.split('&')
arguments.pop(0)
for queryParam in arguments:
queryName, queryValue = queryParam.split('=')
params[queryName] = queryValue
return params

42
Utils/Utils.py Normal file
View File

@@ -0,0 +1,42 @@
import re
import asyncio
from Config.Configs import VConfigs
from functools import wraps, partial
config = VConfigs()
class Utils:
@classmethod
def format_time(cls, duration) -> str:
if not duration:
return "00:00"
hours = duration // 60 // 60
minutes = duration // 60 % 60
seconds = duration % 60
return "{}{}{:02d}:{:02d}".format(
hours if hours else "",
":" if hours else "",
minutes,
seconds)
@classmethod
def is_url(cls, string) -> bool:
regex = re.compile(
"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+")
if re.search(regex, string):
return True
else:
return False
def run_async(func):
@wraps(func)
async def run(*args, loop=None, executor=None, **kwargs):
if loop is None:
loop = asyncio.get_event_loop()
partial_func = partial(func, *args, **kwargs)
return await loop.run_in_executor(executor, partial_func)
return run

View File

@@ -1,66 +0,0 @@
from decouple import config
CETUS_API = config('CETUS_API')
CAMBION_API = config('CAMBION_API')
FISSURES_API = config('FISSURES_API')
BOT_TOKEN = config('BOT_TOKEN')
SPOTIFY_ID = config('SPOTIFY_ID')
SPOTIFY_SECRET = config('SPOTIFY_SECRET')
SECRET_MESSAGE = config('SECRET_MESSAGE')
PHRASES_API = config('PHRASES_API')
BOT_PREFIX = '!'
VC_TIMEOUT = 600
STARTUP_MESSAGE = 'Starting Vulkan...'
STARTUP_COMPLETE_MESSAGE = 'Vulkan is now operating.'
MAX_PLAYLIST_LENGTH = 50
MAX_API_PHRASES_TRIES = 10
MAX_API_CETUS_TRIES = 10
MAX_API_CAMBION_TRIES = 10
MAX_API_FISSURES_TRIES = 10
MAX_PRELOAD_SONGS = 10
SONGINFO_UPLOADER = "Uploader: "
SONGINFO_DURATION = "Duration: "
SONGINFO_REQUESTER = 'Requester: '
HELP_SKIP = 'Skip the current playing song'
HELP_RESUME = 'Resumes the song player'
HELP_CLEAR = 'Clear the queue'
HELP_STOP = 'Stop the song player, removing Vulkan from voice channel'
HELP_LOOP = '(one/all/off) - Control the loop of songs'
HELP_NP = 'Show the info of the current song'
HELP_QUEUE = f'Show the first {MAX_PRELOAD_SONGS} songs in queue'
HELP_PAUSE = 'Pauses the song player'
HELP_SHUFFLE = 'Shuffle the songs playing'
HELP_PLAY = '(title/youtube/spotify) - Plays a song'
HELP_MOVE = '(x, y) - Moves a song from position x to y in queue'
HELP_REMOVE = '(x, -1) - Remove a song in the position x or -1 for the last song'
HELP_RESET = 'Reset the Player of a guild'
HELP_WARFRAME = f'({BOT_PREFIX}warframe help for more)'
HELP_RANDOM = '(x) - Return a random number between 1 and x'
HELP_ESCOLHA = '(x, y, z...) - Choose randomly one item passed'
HELP_CARA = 'Return cara or coroa'
HELP_DROP = '(user_name) - Try to remove the user from the current voice channel'
HELP_FRASE = "Send a randomly phrase, perhaps you get the braba"
HELP_HELP = 'This command :)'
SONGS_ADDED = 'You added {} songs to the queue'
SONG_ADDED = 'You added the song {} to the queue'
SONG_QUEUE_TITLE = 'Songs Queue'
ERROR_TITLE = 'Error :/'
NO_CHANNEL = 'To play some music, connect to any voice channel first.'
NO_GUILD = 'This guild are not connected to Vulkan'
INVALID_INPUT = 'This type of input was too strange, try something better'
DOWNLOADING_ERROR = 'An error occurred while downloading'
EXTRACTING_ERROR = 'An error ocurred while searching for the songs'
COLOURS = {
'red': 0xDC143C,
'green': 0x58D68D,
'grey': 0x708090,
'blue': 0x3498DB
}

28
main.py
View File

@@ -1,22 +1,8 @@
import discord
import os
from Music.VulkanInitializer import VulkanInitializer
from Config.Folder import Folder
from config import config
from discord.ext import commands
intents = discord.Intents.default()
intents.members = True
bot = commands.Bot(command_prefix=config.BOT_PREFIX, pm_help=True,
case_insensitive=True, intents=intents)
bot.remove_command('help')
if config.BOT_TOKEN == "":
exit()
for filename in os.listdir('./vulkan/commands'):
if filename.endswith('.py'):
bot.load_extension(f'vulkan.commands.{filename[:-3]}')
bot.run(config.BOT_TOKEN, bot=True, reconnect=True)
if __name__ == '__main__':
folder = Folder()
initializer = VulkanInitializer(willListen=True)
vulkanBot = initializer.getBot()
vulkanBot.startBot()

Binary file not shown.

11
run_tests.py Normal file
View File

@@ -0,0 +1,11 @@
from Tests.VDownloaderTests import VulkanDownloaderTest
from Tests.VSpotifyTests import VulkanSpotifyTest
from Tests.VDeezerTests import VulkanDeezerTest
tester = VulkanDownloaderTest()
tester.run()
tester = VulkanSpotifyTest()
tester.run()
tester = VulkanDeezerTest()
tester.run()

View File

@@ -1,71 +0,0 @@
import discord
from discord import Client
from discord.ext.commands.errors import CommandNotFound, MissingRequiredArgument
from discord.ext import commands
from config import config
class Control(commands.Cog):
"""Control the flow of the Bot"""
def __init__(self, bot: Client):
self.__bot = bot
self.__comandos = {
'MUSIC': ['resume', 'pause', 'loop', 'stop', 'skip', 'play', 'queue', 'clear', 'np', 'shuffle', 'move', 'remove', 'reset'],
'WARFRAME': ['warframe'],
'RANDOM': ['escolha', 'cara', 'random'],
'HELP': ['help'],
'OTHERS': ['frase', 'drop']
}
@commands.Cog.listener()
async def on_ready(self):
print(config.STARTUP_MESSAGE)
await self.__bot.change_presence(status=discord.Status.online, activity=discord.Game(name=f"Vulkan | {config.BOT_PREFIX}help"))
print(config.STARTUP_COMPLETE_MESSAGE)
@commands.Cog.listener()
async def on_command_error(self, ctx, error):
if isinstance(error, MissingRequiredArgument):
await ctx.channel.send(f'Falta argumentos. Digite {config.BOT_PREFIX}help para ver todos os comandos\n\nOu tente {config.BOT_PREFIX}command help para mais informações')
elif isinstance(error, CommandNotFound):
await ctx.channel.send(f'O comando não existe')
else:
await ctx.channel.send(f'Teve um erro aí bicho')
raise error
@commands.command(name="help", alisases=['ajuda'], help=config.HELP_HELP)
async def help_msg(self, ctx):
helptxt = ''
help_music = '-- MUSIC\n'
help_random = '-- RANDOM\n'
help_warframe = '-- WARFRAME\n'
help_help = '-- HELP\n'
help_others = '-- OTHERS\n'
for command in self.__bot.commands:
if command.name in self.__comandos['MUSIC']:
help_music += f'**{command}** - {command.help}\n'
elif command.name in self.__comandos['HELP']:
help_help += f'**{command}** - {command.help}\n'
elif command.name in self.__comandos['OTHERS']:
help_others += f'**{command}** - {command.help}\n'
elif command.name in self.__comandos['WARFRAME']:
help_warframe += f'**{command}** - {command.help}\n'
else:
help_random += f'**{command}** - {command.help}\n'
helptxt = f'{help_music}\n{help_warframe}\n{help_random}\n{help_others}\n{help_help}'
embedhelp = discord.Embed(
colour=config.COLOURS['grey'],
title=f'Comandos do {self.__bot.user.name}',
description=helptxt
)
embedhelp.set_thumbnail(url=self.__bot.user.avatar_url)
await ctx.send(embed=embedhelp)
def setup(bot):
bot.add_cog(Control(bot))

View File

@@ -1,181 +0,0 @@
import discord
from discord.ext import commands
from config import config
from vulkan.music.Player import Player
from vulkan.music.utils import *
class Music(commands.Cog):
def __init__(self, bot):
self.__guilds = {}
self.__bot: discord.Client = bot
@commands.Cog.listener()
async def on_ready(self):
for guild in self.__bot.guilds:
self.__guilds[guild] = Player(self.__bot, guild)
@commands.command(name="play", help=config.HELP_PLAY, aliases=['p', 'tocar'])
async def play(self, ctx, *args):
user_input = " ".join(args)
requester = ctx.author.name
player = self.__get_player(ctx)
if player == None:
await self.__send_embed(ctx, description=config.NO_GUILD, colour_name='red')
return
if is_connected(ctx) == None:
result = await player.connect(ctx)
if result['success'] == False:
await self.__send_embed(ctx, description=result['reason'], colour_name='red')
return
await player.play(ctx, user_input, requester)
@commands.command(name="queue", help=config.HELP_QUEUE, aliases=['q', 'fila'])
async def queue(self, ctx):
player = self.__get_player(ctx)
if player == None:
return
embed = await player.queue()
await ctx.send(embed=embed)
@commands.command(name="skip", help=config.HELP_SKIP, aliases=['pular'])
async def skip(self, ctx):
player = self.__get_player(ctx)
if player == None:
return
else:
await player.skip()
@commands.command(name='stop', help=config.HELP_STOP, aliases=['parar'])
async def stop(self, ctx):
player = self.__get_player(ctx)
if player == None:
return
else:
await player.stop()
@commands.command(name='pause', help=config.HELP_PAUSE, aliases=['pausar'])
async def pause(self, ctx):
player = self.__get_player(ctx)
if player == None:
print('No player')
return
else:
success = await player.pause()
if success:
await self.__send_embed(ctx, description='Song paused', colour_name='blue')
@commands.command(name='resume', help=config.HELP_RESUME, aliases=['soltar'])
async def resume(self, ctx):
player = self.__get_player(ctx)
if player == None:
return
else:
success = await player.resume()
if success:
await self.__send_embed(ctx, description='Song Playing', colour_name='blue')
@commands.command(name='loop', help=config.HELP_LOOP, aliases=['repeat'])
async def loop(self, ctx, args: str):
player = self.__get_player(ctx)
if player == None:
return
else:
result = await player.loop(args)
await self.__send_embed(ctx, description=result, colour_name='blue')
@commands.command(name='clear', help=config.HELP_CLEAR, aliases=['limpar'])
async def clear(self, ctx):
player = self.__get_player(ctx)
if player == None:
return
else:
await player.clear()
@commands.command(name='np', help=config.HELP_NP, aliases=['playing', 'now'])
async def now_playing(self, ctx):
player = self.__get_player(ctx)
if player == None:
return
else:
embed = await player.now_playing()
await self.__clean_messages(ctx)
await ctx.send(embed=embed)
@commands.command(name='shuffle', help=config.HELP_SHUFFLE, aliases=['aleatorio'])
async def shuffle(self, ctx):
player = self.__get_player(ctx)
if player == None:
return
else:
result = await player.shuffle()
await self.__send_embed(ctx, description=result, colour_name='blue')
@commands.command(name='move', help=config.HELP_MOVE, aliases=['mover'])
async def move(self, ctx, pos1, pos2='1'):
player = self.__get_player(ctx)
if player == None:
return
else:
result = await player.move(pos1, pos2)
await self.__send_embed(ctx, description=result, colour_name='blue')
@commands.command(name='remove', help=config.HELP_REMOVE, aliases=['remover'])
async def remove(self, ctx, position):
"""Remove a song from the queue in the position"""
player = self.__get_player(ctx)
if player == None:
return
else:
result = await player.remove(position)
await self.__send_embed(ctx, description=result, colour_name='blue')
@commands.command(name='reset', help=config.HELP_RESET, aliases=['resetar'])
async def reset(self, ctx):
player = self.__get_player(ctx)
if player != None:
await player.stop()
self.__guilds[ctx.guild] = Player(self.__bot, ctx.guild)
async def __send_embed(self, ctx, title='', description='', colour_name='grey'):
try:
colour = config.COLOURS[colour_name]
except Exception as e:
colour = config.COLOURS['grey']
embedvc = discord.Embed(
title=title,
description=description,
colour=colour
)
await ctx.send(embed=embedvc)
async def __clean_messages(self, ctx):
"""Clear Bot messages if send recently"""
last_messages = await ctx.channel.history(limit=5).flatten()
for message in last_messages:
try:
if message.author == self.__bot.user:
if len(message.embeds) > 0:
embed = message.embeds[0]
if embed.title == 'Song Playing Now' or embed.title == 'Song Looping Now':
await message.delete()
except:
continue
def __get_player(self, ctx):
try:
return self.__guilds[ctx.guild]
except:
return None
def setup(bot):
bot.add_cog(Music(bot))

View File

@@ -1,59 +0,0 @@
from discord.client import Client
import requests
import json
from config import config
from discord.ext import commands
from random import random as rand
class Phrases(commands.Cog):
"""Deal with the generation of motivational phrases"""
def __init__(self, bot: Client):
self.__bot = bot
@commands.command(name='frase', help=config.HELP_FRASE)
async def phrase(self, ctx):
"""Send some phrase to the requester"""
secret = await self.__calculate_rgn()
if secret != None:
await ctx.send(secret)
else:
phrase = await self.__get_phrase()
await ctx.send(phrase)
async def __calculate_rgn(self):
"""Calculate the chance from the phrase function return a secret custom message"""
x = rand()
if x < 0.15:
return config.SECRET_MESSAGE
else:
return None
async def __get_phrase(self):
"""Get the phrase from the server"""
tries = 0
while True:
tries += 1
if tries > config.MAX_API_PHRASES_TRIES:
return 'O banco de dados dos cara tá off, bando de vagabundo, tenta depois aí bicho'
try:
response = requests.get(config.PHRASES_API)
data = json.loads(response.content)
phrase = data['quoteText']
author = data['quoteAuthor']
if phrase == '' or author == '':
continue
text = f'{phrase} \nBy: {author}'
return text
except Exception as e:
continue
def setup(bot):
bot.add_cog(Phrases(bot))

View File

@@ -1,80 +0,0 @@
from random import randint, random
import discord
from discord.ext import commands
from config import config
class Random(commands.Cog):
"""Deal with returning random things"""
def __init__(self, bot):
self.__bot = bot
@commands.command(name='random', help=config.HELP_RANDOM)
async def random(self, ctx, arg: str):
try:
arg = int(arg)
except Exception as e:
embed = discord.Embed(
description='Manda um número aí ow animal',
colour=config.COLOURS['red']
)
await ctx.send(embed=embed)
return
if arg < 1:
a = arg
b = 1
else:
a = 1
b = arg
x = randint(a, b)
embed = discord.Embed(
title=f'Número Aleatório entre {a, b}',
description=x,
colour=config.COLOURS['green']
)
await ctx.send(embed=embed)
@commands.command(name='cara', help=config.HELP_CARA)
async def cara(self, ctx):
x = random()
if x < 0.5:
result = 'cara'
else:
result = 'coroa'
embed = discord.Embed(
title='Cara Cora',
description=f'Resultado: {result}',
colour=config.COLOURS['green']
)
await ctx.send(embed=embed)
@commands.command(name='escolha', help=config.HELP_ESCOLHA)
async def escolher(self, ctx, *args: str):
try:
user_input = " ".join(args)
itens = user_input.split(sep=',')
index = randint(0, len(itens)-1)
embed = discord.Embed(
title='Escolha de algo',
description=itens[index],
colour=config.COLOURS['green']
)
await ctx.send(embed=embed)
except Exception as e:
embed = discord.Embed(
title='Escolha de algo',
description='Erro: Envie várias coisas separadas por vírgula',
colour=config.COLOURS['green']
)
await ctx.send(embed=embed)
def setup(bot):
bot.add_cog(Random(bot))

View File

@@ -1,118 +0,0 @@
import requests
import json
import discord
from discord.ext import commands
from config import config
from discord import Embed
class Warframe(commands.Cog):
"""Deal with the generation of warframe data"""
def __init__(self, bot: discord.Client):
self.__bot = bot
self.__open_functions = ['cetus', 'cambion', 'fissures']
@commands.command(name='warframe', help=config.HELP_WARFRAME)
async def warframe(self, ctx, arg) -> Embed:
if arg in self.__open_functions:
function = getattr(Warframe, f'_Warframe__{arg}')
embed = await function(self)
await ctx.send(embed=embed)
else:
info = f'Warframe commands: {self.__open_functions}'
embed = Embed(
title='Invalid Command',
description=info,
colour=config.COLOURS['blue']
)
await ctx.send(embed=embed)
async def __cetus(self) -> Embed:
description = await self.__get_cetus()
embed = discord.Embed(
title='Warframe Cetus Timing',
description=description,
colour=config.COLOURS['blue']
)
return embed
async def __get_cetus(self) -> str:
"""Return the information of the Warframe API"""
tries = 0
while True:
tries += 1
if tries > config.MAX_API_CETUS_TRIES:
return 'Os DE baiano não tão com o banco de dados ligado'
try:
response = requests.get(config.CETUS_API)
data = json.loads(response.content)
short = data['shortString']
return short
except Exception as e:
continue
async def __cambion(self) -> Embed:
description = await self.__get_cambion()
embed = discord.Embed(
title='Warframe Cambion Timing',
description=description,
colour=config.COLOURS['blue']
)
return embed
async def __get_cambion(self) -> str:
"""Return the information of the Warframe API"""
tries = 0
while True:
tries += 1
if tries > config.MAX_API_CAMBION_TRIES:
return 'Os DE baiano não tão com o banco de dados ligado'
try:
response = requests.get(config.CAMBION_API)
data = json.loads(response.content)
info = f'**Active:** {data["active"]}\n**Time Left:** {data["timeLeft"]}'
return info
except:
continue
async def __fissures(self) -> Embed:
description = await self.__get_fissures()
embed = discord.Embed(
title='Warframe Fissures Status',
description=description,
colour=config.COLOURS['blue']
)
return embed
async def __get_fissures(self) -> str:
"""Return the information of the Warframe API"""
tries = 0
while True:
tries += 1
if tries > config.MAX_API_FISSURES_TRIES:
return 'Os DE baiano não tão com o banco de dados ligado'
try:
response = requests.get(config.FISSURES_API)
data = json.loads(response.content)
info = ''
for pos, fissure in enumerate(data, start=1):
info += f'`{pos}` - **Mission:** {fissure["missionType"]} | **Type:** {fissure["tier"]} | **Timing:** {fissure["eta"]} | **Storm:** {fissure["isStorm"]}\n'
return info
except Exception as e:
continue
def setup(bot):
bot.add_cog(Warframe(bot))

View File

@@ -1,139 +0,0 @@
import asyncio
import concurrent.futures
from config import config
from yt_dlp import YoutubeDL
from yt_dlp.utils import ExtractorError, DownloadError
from vulkan.music.Song import Song
from vulkan.music.utils import is_url
class Downloader():
"""Download musics direct URL and title or Source from Youtube using a music name or Youtube URL"""
def __init__(self) -> None:
self.__YDL_OPTIONS = {'format': 'bestaudio/best',
'default_search': 'auto',
'playliststart': 0,
'extract_flat': True,
'playlistend': config.MAX_PLAYLIST_LENGTH,
}
def download_one(self, song: Song) -> Song:
"""Receives a song object, finish his download and return it"""
if song.identifier == None:
return None
if is_url(song.identifier): # Youtube URL
song_info = self.__download_url(song.identifier)
else: # Song name
song_info = self.__download_title(song.identifier)
if song_info == None:
song.destroy() # Destroy the music with problems
return None
else:
song.finish_down(song_info)
return song
def extract_youtube_link(self, playlist_url: str) -> list:
"""Extract all songs direct URL from a Youtube Link
Arg: Url String
Return: List with the direct youtube URL of each song
"""
if is_url(playlist_url): # If Url
options = self.__YDL_OPTIONS
options['extract_flat'] = True
with YoutubeDL(options) as ydl:
try:
result = ydl.extract_info(playlist_url, download=False)
songs_identifiers = []
if result.get('entries'): # If got a dict of musics
for entry in result['entries']:
songs_identifiers.append(
f"https://www.youtube.com/watch?v={entry['id']}")
else: # Or a single music
songs_identifiers.append(result['original_url'])
return songs_identifiers # Return a list
except (ExtractorError, DownloadError) as e:
return None
else:
print('Invalid type of playlist URL')
return None
async def preload(self, songs: list) -> None:
"""Download the full info of the song object"""
for song in songs:
asyncio.ensure_future(self.__download_songs(song))
def __download_url(self, url) -> dict:
"""Download musics full info and source from Music URL
Arg: URL from Youtube
Return: Dict with the full youtube information of the music, including source to play it
"""
options = self.__YDL_OPTIONS
options['extract_flat'] = False
with YoutubeDL(options) as ydl:
try:
result = ydl.extract_info(url, download=False)
return result
except (ExtractorError, DownloadError) as e: # Any type of error in download
return None
async def __download_songs(self, song: Song) -> None:
"""Download a music object asynchronously"""
if song.source != None: # If Music already preloaded
return
def download_song(song):
if is_url(song.identifier): # Youtube URL
song_info = self.__download_url(song.identifier)
else: # Song name
song_info = self.__download_title(song.identifier)
if song_info == None:
song.destroy() # Remove the song with problems from the playlist
else:
song.finish_down(song_info)
# Creating a loop task to download each song
loop = asyncio.get_event_loop()
executor = concurrent.futures.ThreadPoolExecutor(
max_workers=config.MAX_PRELOAD_SONGS
)
await asyncio.wait(fs={loop.run_in_executor(executor, download_song, song)},
return_when=asyncio.ALL_COMPLETED)
def __download_title(self, title: str) -> dict:
"""Download a music full information using his name.
Arg: Music Name
Return: A dict containing the song information
"""
if type(title) != str:
return None
config = self.__YDL_OPTIONS
config['extract_flat'] = False
with YoutubeDL(self.__YDL_OPTIONS) as ydl:
try:
search = f"ytsearch:{title}"
result = ydl.extract_info(search, download=False)
if result == None:
return None
# Return a dict with the full info of first music
return result['entries'][0]
except Exception as e:
return None

View File

@@ -1,89 +0,0 @@
from abc import ABC, abstractproperty, abstractmethod
class IPlaylist(ABC):
"""Class to manage and control the songs to play and played"""
@abstractproperty
def looping_one(self):
pass
@abstractproperty
def looping_all(self):
pass
@abstractproperty
def songs_to_preload(self) -> list:
pass
@abstractmethod
def __len__(self):
pass
@abstractmethod
def next_song(self):
pass
@abstractmethod
def prev_song(self):
pass
@abstractmethod
def add_song(self, identifier: str) -> None:
pass
@abstractmethod
def shuffle(self) -> None:
pass
@abstractmethod
def revert(self) -> None:
pass
@abstractmethod
def clear(self) -> None:
pass
@abstractmethod
def loop_one(self) -> str:
pass
@abstractmethod
def loop_all(self) -> str:
pass
@abstractmethod
def loop_off(self) -> str:
pass
@abstractmethod
def destroy_song(self, song_destroy) -> None:
pass
class ISong(ABC):
"""Store the usefull information about a Song"""
@abstractmethod
def finish_down(self, info: dict) -> None:
pass
@abstractmethod
def source(self) -> str:
pass
@abstractmethod
def title(self) -> str:
pass
@abstractmethod
def duration(self) -> str:
pass
@abstractmethod
def identifier(self) -> str:
pass
@abstractmethod
def destroy(self) -> None:
pass

View File

@@ -1,308 +0,0 @@
import discord
from discord.ext import commands
from config import config
import datetime
from vulkan.music.Downloader import Downloader
from vulkan.music.Playlist import Playlist
from vulkan.music.Searcher import Searcher
from vulkan.music.Types import Provider
from vulkan.music.utils import *
class Player(commands.Cog):
def __init__(self, bot, guild):
self.__searcher: Searcher = Searcher()
self.__down: Downloader = Downloader()
self.__playlist: Playlist = Playlist()
self.__bot: discord.Client = bot
self.__guild: discord.Guild = guild
self.__timer = Timer(self.__timeout_handler)
self.__playing = False
self.YDL_OPTIONS = {'format': 'bestaudio', 'noplaylist': 'True'}
self.FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
'options': '-vn'}
async def connect(self, ctx):
if not ctx.author.voice:
return {'success': False, 'reason': config.NO_CHANNEL}
if self.__guild.voice_client == None:
await ctx.author.voice.channel.connect(reconnect=True, timeout=None)
return {'success': True, 'reason': ''}
def __play_next(self, error, ctx):
song = self.__playlist.next_song()
if song != None: # If there is not a song for the song
coro = self.__play_music(ctx, song)
self.__bot.loop.create_task(coro)
else:
self.__playing = False
async def __play_music(self, ctx, song):
self.__playing = True
player = discord.FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS)
self.__guild.voice_client.play(
player, after=lambda e: self.__play_next(e, ctx))
self.__timer.cancel()
self.__timer = Timer(self.__timeout_handler)
await ctx.invoke(self.__bot.get_command('np'))
songs = self.__playlist.songs_to_preload
await self.__down.preload(songs)
async def play(self, ctx, track, requester) -> str:
try:
songs_names, provider = self.__searcher.search(track)
if provider == Provider.Unknown:
embed = discord.Embed(
title=config.ERROR_TITLE,
description=config.INVALID_INPUT,
colours=config.COLOURS['blue'])
await ctx.send(embed=embed)
return
elif provider == Provider.YouTube:
songs_names = self.__down.extract_youtube_link(songs_names[0])
songs_quant = 0
for name in songs_names:
song = self.__playlist.add_song(name, requester)
songs_quant += 1
songs_preload = self.__playlist.songs_to_preload
await self.__down.preload(songs_preload)
except:
embed = discord.Embed(
title=config.ERROR_TITLE,
description=config.DOWNLOADING_ERROR,
colours=config.COLOURS['blue'])
await ctx.send(embed=embed)
return
if songs_quant == 1:
song = self.__down.download_one(song)
if song == None:
embed = discord.Embed(
title=config.ERROR_TITLE,
description=config.DOWNLOADING_ERROR,
colours=config.COLOURS['blue'])
await ctx.send(embed=embed)
return
elif not self.__playing:
embed = discord.Embed(
title=config.SONG_QUEUE_TITLE,
description=config.SONG_ADDED.format(song.title),
colour=config.COLOURS['blue'])
await ctx.send(embed=embed)
else:
embed = self.__format_embed(song.info, config.SONG_ADDED)
await ctx.send(embed=embed)
else:
embed = discord.Embed(
title=config.SONG_QUEUE_TITLE,
description=config.SONGS_ADDED.format(songs_quant),
colour=config.COLOURS['blue'])
await ctx.send(embed=embed)
if not self.__playing:
try_another = True
while try_another: # This will ensure the first song source to be ready
first_song = self.__playlist.next_song()
if first_song == None:
embed = discord.Embed(
title=config.ERROR_TITLE,
description=config.DOWNLOADING_ERROR,
colour=config.COLOURS['blue'])
await ctx.send(embed=embed)
break
while True:
if first_song.source != None: # If song got downloaded
try_another = False
break
if first_song.problematic: # If song got any error, try another one
break
if first_song != None:
await self.__play_music(ctx, first_song)
async def queue(self) -> discord.Embed:
if self.__playlist.looping_one:
info = self.__playlist.current.info
title = 'Song Looping Now'
return self.__format_embed(info, title)
songs_preload = self.__playlist.songs_to_preload
await self.__down.preload(songs_preload)
total_time = format_time(sum([int(song.duration if song.duration else 0)
for song in songs_preload])) # Sum the duration
total_songs = len(self.__playlist)
text = f'Total musics: {total_songs} | Duration: `{total_time}` downloaded \n\n'
for pos, song in enumerate(songs_preload, start=1):
title = song.title if song.title else 'Downloading...'
text += f"**`{pos}` - ** {title} - `{format_time(song.duration)}`\n"
title = 'Songs in Queue'
if len(songs_preload) > 0:
if self.__playlist.looping_all:
title = 'Repeating All'
else:
text = 'There is no musics in queue'
embed = discord.Embed(
title=title,
description=text,
colour=config.COLOURS['blue']
)
return embed
async def skip(self) -> bool:
if self.__guild.voice_client != None:
self.__guild.voice_client.stop()
return True
else:
return False
async def stop(self) -> bool:
if self.__guild.voice_client == None:
return False
if self.__guild.voice_client.is_connected():
self.__playlist.clear()
self.__playlist.loop_off()
self.__guild.voice_client.stop()
await self.__guild.voice_client.disconnect()
return True
async def pause(self) -> bool:
if self.__guild.voice_client == None:
return False
if self.__guild.voice_client.is_playing():
self.__guild.voice_client.pause()
return True
async def resume(self) -> bool:
if self.__guild.voice_client == None:
return False
if self.__guild.voice_client.is_paused():
self.__guild.voice_client.resume()
return True
async def loop(self, args: str):
args = args.lower()
if args == 'one':
description = self.__playlist.loop_one()
elif args == 'all':
description = self.__playlist.loop_all()
elif args == 'off':
description = self.__playlist.loop_off()
else:
description = 'Comando Loop\nOne - Repete a música atual\nAll - Repete as músicas atuais\nOff - Desativa o loop'
return description
async def clear(self) -> None:
self.__playlist.clear()
async def now_playing(self) -> discord.Embed:
if self.__playlist.looping_one:
title = 'Song Looping Now'
else:
title = 'Song Playing Now'
current_song = self.__playlist.current
embed = self.__format_embed(current_song.info, title)
return embed
async def shuffle(self) -> str:
try:
self.__playlist.shuffle()
songs = self.__playlist.songs_to_preload
await self.__down.preload(songs)
return 'Musics shuffled successfully'
except:
return 'An error ocurred :/'
async def move(self, pos1, pos2='1') -> str:
try:
pos1 = int(pos1)
pos2 = int(pos2)
except:
return 'This command require a number'
result = self.__playlist.move_songs(pos1, pos2)
songs = self.__playlist.songs_to_preload
await self.__down.preload(songs)
return result
async def remove(self, position) -> str:
"""Remove a song from the queue in the position"""
try:
position = int(position)
except:
return 'This command require a number'
result = self.__playlist.remove_song(position)
return result
def __format_embed(self, info, title='') -> discord.Embed:
"""Configure the embed to show the song information"""
embedvc = discord.Embed(
title=title,
description=f"[{info['title']}]({info['original_url']})",
color=config.COLOURS['blue']
)
embedvc.add_field(name=config.SONGINFO_UPLOADER,
value=info['uploader'],
inline=True)
embedvc.add_field(name=config.SONGINFO_REQUESTER,
value=info['requester'],
inline=True)
if 'thumbnail' in info.keys():
embedvc.set_thumbnail(url=info['thumbnail'])
if 'duration' in info.keys():
duration = str(datetime.timedelta(seconds=info['duration']))
embedvc.add_field(name=config.SONGINFO_DURATION,
value=f"{duration}",
inline=True)
else:
embedvc.add_field(name=config.SONGINFO_DURATION,
value=config.SONGINFO_UNKNOWN_DURATION,
inline=True)
return embedvc
async def __timeout_handler(self) -> None:
if self.__guild.voice_client == None:
return
if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused():
self.__timer = Timer(self.__timeout_handler)
elif self.__guild.voice_client.is_connected():
self.__playlist.clear()
self.__playlist.loop_off()
await self.__guild.voice_client.disconnect()

View File

@@ -1,179 +0,0 @@
from collections import deque
from config import config
import random
from vulkan.music.Interfaces import IPlaylist
from vulkan.music.Song import Song
class Playlist(IPlaylist):
"""Class to manage and control the songs to play and played"""
def __init__(self) -> None:
self.__queue = deque() # Store the musics to play
self.__songs_history = deque() # Store the musics played
self.__name_history = deque() # Store the name of musics played
self.__looping_one = False
self.__looping_all = False
self.__current: Song = None
@property
def looping_one(self) -> bool:
return self.__looping_one
@property
def looping_all(self) -> bool:
return self.__looping_all
@property
def current(self) -> Song:
return self.__current
@property
def songs_to_preload(self) -> list:
return list(self.__queue)[:config.MAX_PRELOAD_SONGS]
def __len__(self) -> int:
return len(self.__queue)
def next_song(self) -> Song:
"""Return the next song to play"""
if self.__current == None and len(self.__queue) == 0:
# If not playing and nothing to play
return None
# If playing
played_song = self.__current
# Check if need to repeat the played song
if self.__looping_one: # Insert the current song to play again
self.__queue.appendleft(played_song)
if self.__looping_all: # Insert the current song in the end of queue
self.__queue.append(played_song)
while True: # Try to get the source of next song
if len(self.__queue) == 0: # If no more song to play, return None
return None
# Att the current with the first one
self.__current = self.__queue[0]
self.__queue.popleft() # Remove the current from queue
self.__name_history.append(
self.__current.identifier) # Add to name history
self.__songs_history.append(self.__current) # Add to song history
return self.__current
def prev_song(self) -> Song:
"""Return the source of the last song played
Return None or the source of the prev song
"""
if len(self.__songs_history) == 0:
return None
else:
return self.__songs_history[0].source
def add_song(self, identifier: str, requester: str) -> Song:
"""Create a song object, add to queue and return it"""
song = Song(identifier=identifier, playlist=self, requester=requester)
self.__queue.append(song)
return song
def shuffle(self) -> None:
"""Shuffle the order of the songs to play"""
random.shuffle(self.__queue)
def revert(self) -> None:
"""Revert the order of the songs to play"""
self.__queue.reverse()
def clear(self) -> None:
"""Clear the songs to play song history"""
self.__queue.clear()
self.__songs_history.clear()
def loop_one(self) -> str:
"""Try to start the loop of the current song
Return: Embed descrition to show to user
"""
if self.__looping_all == True:
return 'Vulkan already looping one music, disable loop first'
elif self.__looping_one == True:
return "I'm already doing this, you dumb ass"
else:
self.__looping_one = True
return 'Repeating the current song'
def loop_all(self) -> str:
"""Try to start the loop of all songs
Return: Embed descrition to show to user
"""
if self.__looping_one == True:
return 'Vulkan already looping one music, disable loop first'
elif self.__looping_all == True:
return "I'm already doing this, you dumb ass"
else:
self.__looping_all = True
return 'Repeating all songs in queue'
def loop_off(self) -> str:
"""Disable both types of loop"""
if self.__looping_all == False and self.__looping_one == False:
return "The loop is already off, you fucking dick head"
self.__looping_all = False
self.__looping_one = False
return 'Loop disable'
def destroy_song(self, song_destroy: Song) -> None:
"""Destroy a song object from the queue"""
for song in self.__queue:
if song == song_destroy:
self.__queue.remove(song)
break
def move_songs(self, pos1, pos2) -> str:
"""Receive two position and try to change the songs in those positions, -1 is the last
Positions: First music is 1
Return (Error bool, string) with the status of the function, to show to user
"""
if pos1 == -1:
pos1 = len(self.__queue)
if pos2 == -1:
pos2 = len(self.__queue)
if pos2 not in range(1, len(self.__queue) + 1) or pos1 not in range(1, len(self.__queue) + 1):
return 'Numbers must be between 1 and queue length, or -1 for the last song'
try:
song1 = self.__queue[pos1-1]
song2 = self.__queue[pos2-1]
self.__queue[pos1-1] = song2
self.__queue[pos2-1] = song1
song1_name = song1.title if song1.title else song1.identifier
song2_name = song2.title if song2.title else song2.identifier
return f'Song `{song1_name}` in position `{pos1}` moved with `{song2_name}` in position `{pos2}` successfully'
except Exception as e:
print(e)
return 'There was a problem with the moving of songs'
def remove_song(self, position) -> tuple:
if position not in range(1, len(self.__queue) + 1) and position != -1:
return 'Numbers must be between 1 and queue length, or -1 for the last song'
else:
song = self.__queue[position-1]
self.__queue.remove(song)
song_name = song.title if song.title else song.identifier
return f'Song `{song_name}` removed successfully'

View File

@@ -1,48 +0,0 @@
from vulkan.music.Types import Provider
from vulkan.music.Spotify import SpotifySearch
from vulkan.music.utils import is_url
class Searcher():
"""Turn the user input into list of musics names, support youtube and spotify"""
def __init__(self) -> None:
self.__Spotify = SpotifySearch()
def search(self, music: str) -> list:
"""Return a list with the song names or an URL
Arg -> User Input, a string with the
Return -> A list of musics names and Provider Type
"""
url_type = self.__identify_source(music)
if url_type == Provider.YouTube:
return [music], Provider.YouTube
elif url_type == Provider.Spotify:
if self.__Spotify.connected == True:
musics = self.__Spotify.search(music)
return musics, Provider.Name
else:
return [], Provider.Unknown
elif url_type == Provider.Name:
return [music], Provider.Name
elif url_type == Provider.Unknown:
return None, Provider.Unknown
def __identify_source(self, music) -> Provider:
"""Identify the provider of a music"""
if not is_url(music):
return Provider.Name
if "https://www.youtu" in music or "https://youtu.be" in music:
return Provider.YouTube
if "https://open.spotify.com" in music:
return Provider.Spotify
# If no match
return Provider.Unknown

View File

@@ -1,66 +0,0 @@
from vulkan.music.Interfaces import ISong, IPlaylist
class Song(ISong):
"""Store the usefull information about a Song"""
def __init__(self, identifier: str, playlist: IPlaylist, requester: str) -> None:
"""Create a song with only the URL to the youtube song"""
self.__identifier = identifier
self.__info = {'requester': requester}
self.__problematic = False
self.__playlist: IPlaylist = playlist
def finish_down(self, info: dict) -> None:
"""Get and store the full information of the song"""
self.__usefull_keys = ['url', 'duration',
'title', 'webpage_url',
'channel', 'id', 'uploader',
'thumbnail', 'original_url']
for key in self.__usefull_keys:
try:
self.__info[key] = info[key]
except Exception as e:
raise e
@property
def source(self) -> str:
"""Return the Song Source URL to play"""
if 'url' in self.__info.keys():
return self.__info['url']
else:
return None
@property
def title(self) -> str:
"""Return the Song Title"""
if 'title' in self.__info.keys():
return self.__info['title']
else:
return None
@property
def duration(self) -> str:
"""Return the Song Title"""
if 'duration' in self.__info.keys():
return self.__info['duration']
else:
return 0.0
@property
def identifier(self) -> str:
return self.__identifier
@property
def problematic(self) -> bool:
return self.__problematic
def destroy(self) -> None:
"""Mark this song with problems and removed from the playlist due to any type of error"""
self.__problematic = True
self.__playlist.destroy_song(self)
@property
def info(self):
return self.__info

View File

@@ -1,121 +0,0 @@
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
from config import config
class SpotifySearch():
"""Search a Spotify music or playlist and return the musics names"""
def __init__(self) -> None:
self.__connected = False
self.__connect()
@property
def connected(self):
return self.__connected
def __connect(self) -> bool:
try:
# Initialize the connection with Spotify API
self.__api = spotipy.Spotify(auth_manager=SpotifyClientCredentials(
client_id=config.SPOTIFY_ID, client_secret=config.SPOTIFY_SECRET))
self.__connected = True
return True
except:
return False
def search(self, music) -> list:
"""Search and return the title of musics on Spotify"""
type = music.split('/')[3].split('?')[0]
code = music.split('/')[4].split('?')[0]
if type == 'album':
musics = self.__get_album(code)
elif type == 'playlist':
musics = self.__get_playlist(code)
elif type == 'track':
musics = self.__get_track(code)
else:
return None
return musics
def __get_album(self, code) -> list:
"""Get the externals urls of a album
ARG: Spotify Code of the Album
"""
if self.__connected == True:
try:
# Load all music objects
results = self.__api.album_tracks(code)
musics = results['items']
while results['next']: # Get the next pages
results = self.__api.next(results)
musics.extend(results['items'])
musicsTitle = []
for music in musics:
try:
title = self.__extract_title(music)
musicsTitle.append(title)
except:
pass
return musicsTitle
except Exception as e:
raise e
def __get_playlist(self, code) -> list:
"""Get the externals urls of a playlist
Arg: Spotify Code of the Playlist
"""
try:
results = self.__api.playlist_items(code)
itens = results['items']
while results['next']: # Load the next pages
results = self.__api.next(results)
itens.extend(results['items'])
musics = []
for item in itens:
musics.append(item['track'])
titles = []
for music in musics:
try:
title = self.__extract_title(music)
titles.append(title)
except Exception as e:
raise e
return titles
except Exception as e:
raise e
def __get_track(self, code) -> list:
"""Convert a external_url track to the title of the music
ARG: Spotify Code of the Music
"""
results = self.__api.track(code)
name = results['name']
artists = ''
for artist in results['artists']:
artists += f'{artist["name"]} '
return [f'{name} {artists}']
def __extract_title(self, music: dict) -> str:
"""Receive a spotify music object and return his title
ARG: music dict returned by Spotify
"""
title = f'{music["name"]} '
for artist in music['artists']:
title += f'{artist["name"]} '
return title

View File

@@ -1,9 +0,0 @@
from enum import Enum
class Provider(Enum):
"""Store Enum Types of the Providers"""
Spotify = "Spotify"
YouTube = "YouTube"
Name = 'Track Name'
Unknown = "Unknown"

View File

@@ -1,51 +0,0 @@
import re
import asyncio
from config import config
def is_connected(ctx):
try:
voice_channel = ctx.guild.voice_client.channel
return voice_channel
except:
return None
def format_time(duration):
if not duration:
return "00:00"
hours = duration // 60 // 60
minutes = duration // 60 % 60
seconds = duration % 60
return "{}{}{:02d}:{:02d}".format(
hours if hours else "",
":" if hours else "",
minutes,
seconds
)
def is_url(string) -> bool:
"""Verify if a string is a url"""
regex = re.compile(
"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+")
if re.search(regex, string):
return True
else:
return False
class Timer:
def __init__(self, callback):
self.__callback = callback
self.__task = asyncio.create_task(self.__executor())
async def __executor(self):
await asyncio.sleep(config.VC_TIMEOUT)
await self.__callback()
def cancel(self):
self.__task.cancel()