mirror of
https://github.com/RafaelSolVargas/Vulkan.git
synced 2025-10-29 16:57:23 +00:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
140c1640d9 | ||
|
|
f27dc1de93 | ||
|
|
b904c75caa | ||
|
|
56456bf2ed | ||
|
|
7efed8ab89 | ||
|
|
3eab6176c3 | ||
|
|
cd3eddb125 | ||
|
|
1ce6deaa48 | ||
|
|
19ae59c5b8 | ||
|
|
fc7de9cb4f | ||
|
|
7a51c22709 | ||
|
|
cbf6e84eb1 | ||
|
|
97d49a5709 | ||
|
|
d894929662 | ||
|
|
d87a0234ba | ||
|
|
94194f5d6a | ||
|
|
863b079a01 | ||
|
|
5099a551a4 | ||
|
|
c826af229c | ||
|
|
7e9a6d45c0 | ||
|
|
8336a95eda | ||
|
|
0938dd37e2 | ||
|
|
cd5f4567be | ||
|
|
4fb9d8d1ba | ||
|
|
2dbc6c3984 | ||
|
|
dd4fbff27c | ||
|
|
cc0cd6424f | ||
|
|
985d87a470 | ||
|
|
14bb43a42e | ||
|
|
caaa48ba05 | ||
|
|
b4159c7e86 | ||
|
|
f9b46e13ff | ||
|
|
f09568bd69 | ||
|
|
4c66c64041 | ||
|
|
f30513f710 | ||
|
|
362ec02fe4 | ||
|
|
8ac80c216f | ||
|
|
d30ff93dc1 | ||
|
|
fd1e58211b | ||
|
|
a828350201 | ||
|
|
2240c7535a | ||
|
|
14705569c1 | ||
|
|
5c4d09bf9d | ||
|
|
2bd100a3e1 | ||
|
|
1dc708a86b | ||
|
|
e59efb0010 | ||
|
|
5510b5af78 | ||
|
|
5312a729e9 | ||
|
|
63d86e23f9 | ||
|
|
cbd8ed45f9 | ||
|
|
c23d4c13cb | ||
|
|
b39e1f0216 | ||
|
|
37f5773c8e | ||
|
|
0d606361da | ||
|
|
173b6a7b43 | ||
|
|
5b4c5b49f1 | ||
|
|
ea5245cf95 | ||
|
|
5063c45e87 | ||
|
|
c41a6a6c8a | ||
|
|
615fd580bc | ||
|
|
9b38ba4434 | ||
|
|
bbddcb555b | ||
|
|
3b1a7d5364 | ||
|
|
37acd1a61b | ||
|
|
ccad2de97c | ||
|
|
a1881cd46b | ||
|
|
bb8d514525 | ||
|
|
ef2d7bb8fa | ||
|
|
13bc51fae7 | ||
|
|
2bc8cd206c | ||
|
|
66962cb274 | ||
|
|
2d7ecd0a0e | ||
|
|
98ee6f9cf2 | ||
|
|
089b47fc44 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,7 +1,8 @@
|
|||||||
|
.vscode
|
||||||
|
assets/
|
||||||
__pycache__
|
__pycache__
|
||||||
.env
|
.env
|
||||||
.cache
|
.cache
|
||||||
Admin.py
|
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
34
Config/Colors.py
Normal file
34
Config/Colors.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from Config.Singleton import Singleton
|
||||||
|
|
||||||
|
|
||||||
|
class Colors(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
|
||||||
32
Config/Configs.py
Normal file
32
Config/Configs.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from decouple import config
|
||||||
|
from Config.Singleton import Singleton
|
||||||
|
|
||||||
|
|
||||||
|
class Configs(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.COMMANDS_PATH = 'DiscordCogs'
|
||||||
|
self.VC_TIMEOUT = 600
|
||||||
|
|
||||||
|
self.MAX_PLAYLIST_LENGTH = 50
|
||||||
|
self.MAX_PLAYLIST_FORCED_LENGTH = 5
|
||||||
|
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'
|
||||||
84
Config/Exceptions.py
Normal file
84
Config/Exceptions.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
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 NumberRequired(VulkanError):
|
||||||
|
def __init__(self, message='', title='', *args: object) -> None:
|
||||||
|
super().__init__(message, title, *args)
|
||||||
51
Config/Helper.py
Normal file
51
Config/Helper.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from Config.Singleton import Singleton
|
||||||
|
from Config.Configs import Configs
|
||||||
|
|
||||||
|
|
||||||
|
class Helper(Singleton):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
if not super().created:
|
||||||
|
config = Configs()
|
||||||
|
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_PRELOAD_SONGS} songs in queue.'
|
||||||
|
self.HELP_QUEUE_LONG = f'Show the first {config.MAX_PRELOAD_SONGS} song in the queue.\n\nArguments: None.'
|
||||||
|
self.HELP_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 x to y 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.'
|
||||||
99
Config/Messages.py
Normal file
99
Config/Messages.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
from Config.Singleton import Singleton
|
||||||
|
from Config.Configs import Configs
|
||||||
|
|
||||||
|
|
||||||
|
class Messages(Singleton):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
if not super().created:
|
||||||
|
configs = Configs()
|
||||||
|
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 = '🎧 Song added to the queue'
|
||||||
|
self.SONG_PLAYING = '🎧 Song playing now'
|
||||||
|
self.SONG_PLAYER = '🎧 Song Player'
|
||||||
|
self.QUEUE_TITLE = '🎧 Songs in Queue'
|
||||||
|
self.ONE_SONG_LOOPING = '🎧 Looping One Song'
|
||||||
|
self.ALL_SONGS_LOOPING = '🎧 Looping All Songs'
|
||||||
|
self.SONG_PAUSED = '⏸️ Song paused'
|
||||||
|
self.SONG_RESUMED = '▶️ Song playing'
|
||||||
|
self.EMPTY_QUEUE = f'📜 Song queue is empty, use {configs.BOT_PREFIX}play to add new songs'
|
||||||
|
self.SONG_DOWNLOADING = '📥 Downloading...'
|
||||||
|
|
||||||
|
self.HISTORY_TITLE = '🎧 Played Songs'
|
||||||
|
self.HISTORY_EMPTY = '📜 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'❌ Vulkan is looping all songs, use {configs.BOT_PREFIX}loop off to disable this loop first'
|
||||||
|
self.LOOP_ONE_ON = f'❌ Vulkan is looping one song, use {configs.BOT_PREFIX}loop off to disable this loop first'
|
||||||
|
self.LOOP_ALL_ALREADY_ON = '🔁 Vulkan is already looping all songs'
|
||||||
|
self.LOOP_ONE_ALREADY_ON = '🔂 Vulkan is already looping the current song'
|
||||||
|
self.LOOP_ALL_ACTIVATE = '🔁 Looping all songs'
|
||||||
|
self.LOOP_ONE_ACTIVATE = '🔂 Looping the current song'
|
||||||
|
self.LOOP_DISABLE = '➡️ Loop disabled'
|
||||||
|
self.LOOP_ALREADY_DISABLE = '❌ Loop is already disabled'
|
||||||
|
self.LOOP_ON = f'❌ This command cannot be invoked with any loop activated. Use {configs.BOT_PREFIX}loop off to disable loop'
|
||||||
|
self.BAD_USE_OF_LOOP = f"""❌ Invalid arguments of Loop command. Use {configs.BOT_PREFIX}help loop to more information.
|
||||||
|
-> Available Arguments: ["all", "off", "one", ""]"""
|
||||||
|
|
||||||
|
self.SONGS_SHUFFLED = '🔀 Songs shuffled successfully'
|
||||||
|
self.ERROR_SHUFFLING = '❌ Error while shuffling the songs'
|
||||||
|
self.ERROR_MOVING = '❌ Error while moving the songs'
|
||||||
|
self.LENGTH_ERROR = '❌ Numbers must be between 1 and queue length, use -1 for the last song'
|
||||||
|
self.ERROR_NUMBER = '❌ This command require a number'
|
||||||
|
self.ERROR_PLAYING = '❌ Error while playing songs'
|
||||||
|
self.COMMAND_NOT_FOUND = f'❌ Command not found, type {configs.BOT_PREFIX}help to see all commands'
|
||||||
|
self.UNKNOWN_ERROR = f'❌ Unknown Error, if needed, use {configs.BOT_PREFIX}reset to reset the player of your server'
|
||||||
|
self.ERROR_MISSING_ARGUMENTS = f'❌ Missing arguments in this command. Type {configs.BOT_PREFIX}help "command" to see more info about this command'
|
||||||
|
self.NOT_PREVIOUS = '❌ There is none previous song to play'
|
||||||
|
self.PLAYER_NOT_PLAYING = f'❌ 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.DOWNLOADING_ERROR = "❌ It's impossible to download and play this video"
|
||||||
|
self.EXTRACTING_ERROR = '❌ An error ocurred while searching for the songs'
|
||||||
|
|
||||||
|
self.ERROR_IN_PROCESS = "❌ 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'❌ Bad usage of this command, type {configs.BOT_PREFIX}help "command" to understand the command better'
|
||||||
|
self.VIDEO_UNAVAILABLE = '❌ Sorry. This video is unavailable for download.'
|
||||||
|
self.ERROR_DUE_LOOP_ONE_ON = f'❌ 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 = Configs()
|
||||||
|
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
16
Config/Singleton.py
Normal 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
0
Config/__init__.py
Normal file
118
DiscordCogs/ControlCog.py
Normal file
118
DiscordCogs/ControlCog.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
from discord import Client, Game, Status, Embed
|
||||||
|
from discord.ext.commands.errors import CommandNotFound, MissingRequiredArgument
|
||||||
|
from discord.ext import commands
|
||||||
|
from Config.Configs import Configs
|
||||||
|
from Config.Helper import Helper
|
||||||
|
from Config.Messages import Messages
|
||||||
|
from Config.Colors import Colors
|
||||||
|
from Views.Embeds import Embeds
|
||||||
|
|
||||||
|
helper = Helper()
|
||||||
|
|
||||||
|
|
||||||
|
class ControlCog(commands.Cog):
|
||||||
|
"""Class to handle discord events"""
|
||||||
|
|
||||||
|
def __init__(self, bot: Client):
|
||||||
|
self.__bot = bot
|
||||||
|
self.__config = Configs()
|
||||||
|
self.__messages = Messages()
|
||||||
|
self.__colors = Colors()
|
||||||
|
self.__embeds = Embeds()
|
||||||
|
self.__commands = {
|
||||||
|
'MUSIC': ['resume', 'pause', 'loop', 'stop',
|
||||||
|
'skip', 'play', 'queue', 'clear',
|
||||||
|
'np', 'shuffle', 'move', 'remove',
|
||||||
|
'reset', 'prev', 'history'],
|
||||||
|
'RANDOM': ['choose', 'cara', 'random']
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_ready(self):
|
||||||
|
print(self.__messages.STARTUP_MESSAGE)
|
||||||
|
await self.__bot.change_presence(status=Status.online, activity=Game(name=f"Vulkan | {self.__config.BOT_PREFIX}help"))
|
||||||
|
print(self.__messages.STARTUP_COMPLETE_MESSAGE)
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
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)
|
||||||
|
|
||||||
|
@commands.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_url)
|
||||||
|
await ctx.send(embed=embedhelp)
|
||||||
|
|
||||||
|
@commands.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))
|
||||||
235
DiscordCogs/MusicCog.py
Normal file
235
DiscordCogs/MusicCog.py
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
from discord import Guild, Client
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord.ext.commands import Context
|
||||||
|
from Config.Helper import Helper
|
||||||
|
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 Views.EmoteView import EmoteView
|
||||||
|
from Views.EmbedView import EmbedView
|
||||||
|
|
||||||
|
helper = Helper()
|
||||||
|
|
||||||
|
|
||||||
|
class MusicCog(commands.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) -> None:
|
||||||
|
self.__bot: Client = bot
|
||||||
|
|
||||||
|
@commands.command(name="play", help=helper.HELP_PLAY, description=helper.HELP_PLAY_LONG, aliases=['p', 'tocar'])
|
||||||
|
async def play(self, ctx: Context, *args) -> None:
|
||||||
|
try:
|
||||||
|
controller = PlayHandler(ctx, self.__bot)
|
||||||
|
|
||||||
|
response = await controller.run(args)
|
||||||
|
if response is not None:
|
||||||
|
view1 = EmbedView(response)
|
||||||
|
view2 = EmoteView(response)
|
||||||
|
await view1.run()
|
||||||
|
await view2.run()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[ERROR IN COG] -> {e}')
|
||||||
|
|
||||||
|
@commands.command(name="queue", help=helper.HELP_QUEUE, description=helper.HELP_QUEUE_LONG, aliases=['q', 'fila', 'musicas'])
|
||||||
|
async def queue(self, ctx: Context) -> None:
|
||||||
|
try:
|
||||||
|
controller = QueueHandler(ctx, self.__bot)
|
||||||
|
|
||||||
|
response = await controller.run()
|
||||||
|
view2 = EmbedView(response)
|
||||||
|
await view2.run()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[ERROR IN COG] -> {e}')
|
||||||
|
|
||||||
|
@commands.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()
|
||||||
|
if response.success:
|
||||||
|
view = EmoteView(response)
|
||||||
|
else:
|
||||||
|
view = EmbedView(response)
|
||||||
|
|
||||||
|
await view.run()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[ERROR IN COG] -> {e}')
|
||||||
|
|
||||||
|
@commands.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()
|
||||||
|
if response.success:
|
||||||
|
view = EmoteView(response)
|
||||||
|
else:
|
||||||
|
view = EmbedView(response)
|
||||||
|
|
||||||
|
await view.run()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[ERROR IN COG] -> {e}')
|
||||||
|
|
||||||
|
@commands.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()
|
||||||
|
view1 = EmoteView(response)
|
||||||
|
view2 = EmbedView(response)
|
||||||
|
await view1.run()
|
||||||
|
await view2.run()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[ERROR IN COG] -> {e}')
|
||||||
|
|
||||||
|
@commands.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()
|
||||||
|
view1 = EmoteView(response)
|
||||||
|
view2 = EmbedView(response)
|
||||||
|
await view1.run()
|
||||||
|
await view2.run()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[ERROR IN COG] -> {e}')
|
||||||
|
|
||||||
|
@commands.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:
|
||||||
|
view1 = EmbedView(response)
|
||||||
|
view2 = EmoteView(response)
|
||||||
|
await view1.run()
|
||||||
|
await view2.run()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[ERROR IN COG] -> {e}')
|
||||||
|
|
||||||
|
@commands.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()
|
||||||
|
view1 = EmbedView(response)
|
||||||
|
view2 = EmoteView(response)
|
||||||
|
await view1.run()
|
||||||
|
await view2.run()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[ERROR IN COG] -> {e}')
|
||||||
|
|
||||||
|
@commands.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)
|
||||||
|
view1 = EmoteView(response)
|
||||||
|
view2 = EmbedView(response)
|
||||||
|
await view1.run()
|
||||||
|
await view2.run()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[ERROR IN COG] -> {e}')
|
||||||
|
|
||||||
|
@commands.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()
|
||||||
|
view = EmoteView(response)
|
||||||
|
await view.run()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[ERROR IN COG] -> {e}')
|
||||||
|
|
||||||
|
@commands.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()
|
||||||
|
view1 = EmbedView(response)
|
||||||
|
view2 = EmoteView(response)
|
||||||
|
await view1.run()
|
||||||
|
await view2.run()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[ERROR IN COG] -> {e}')
|
||||||
|
|
||||||
|
@commands.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()
|
||||||
|
view1 = EmbedView(response)
|
||||||
|
view2 = EmoteView(response)
|
||||||
|
await view1.run()
|
||||||
|
await view2.run()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[ERROR IN COG] -> {e}')
|
||||||
|
|
||||||
|
@commands.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)
|
||||||
|
view1 = EmbedView(response)
|
||||||
|
view2 = EmoteView(response)
|
||||||
|
await view1.run()
|
||||||
|
await view2.run()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[ERROR IN COG] -> {e}')
|
||||||
|
|
||||||
|
@commands.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)
|
||||||
|
view1 = EmbedView(response)
|
||||||
|
view2 = EmoteView(response)
|
||||||
|
await view1.run()
|
||||||
|
await view2.run()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[ERROR IN COG] -> {e}')
|
||||||
|
|
||||||
|
@commands.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()
|
||||||
|
view1 = EmbedView(response)
|
||||||
|
view2 = EmoteView(response)
|
||||||
|
await view1.run()
|
||||||
|
await view2.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
64
DiscordCogs/RandomCog.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from random import randint, random
|
||||||
|
from discord import Client
|
||||||
|
from discord.ext.commands import Context, command, Cog
|
||||||
|
from Config.Helper import Helper
|
||||||
|
from Views.Embeds import Embeds
|
||||||
|
|
||||||
|
helper = Helper()
|
||||||
|
|
||||||
|
|
||||||
|
class RandomCog(Cog):
|
||||||
|
"""Class to listen to commands of type Random"""
|
||||||
|
|
||||||
|
def __init__(self, bot: Client):
|
||||||
|
self.__embeds = Embeds()
|
||||||
|
|
||||||
|
@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))
|
||||||
73
Handlers/AbstractHandler.py
Normal file
73
Handlers/AbstractHandler.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import List
|
||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client, Guild, ClientUser, Member
|
||||||
|
from Config.Messages import Messages
|
||||||
|
from Handlers.HandlerResponse import HandlerResponse
|
||||||
|
from Config.Configs import Configs
|
||||||
|
from Config.Helper import Helper
|
||||||
|
from Views.Embeds import Embeds
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractHandler(ABC):
|
||||||
|
def __init__(self, ctx: Context, bot: Client) -> None:
|
||||||
|
self.__bot: Client = 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 = Configs()
|
||||||
|
self.__helper = Helper()
|
||||||
|
self.__embeds = Embeds()
|
||||||
|
self.__bot_member: Member = self.__get_member()
|
||||||
|
|
||||||
|
@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 guild(self) -> Guild:
|
||||||
|
return self.__guild
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bot(self) -> Client:
|
||||||
|
return self.__bot
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self) -> Configs:
|
||||||
|
return self.__config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def messages(self) -> Messages:
|
||||||
|
return self.__messages
|
||||||
|
|
||||||
|
@property
|
||||||
|
def helper(self) -> Helper:
|
||||||
|
return self.__helper
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ctx(self) -> Context:
|
||||||
|
return self.__ctx
|
||||||
|
|
||||||
|
@property
|
||||||
|
def embeds(self) -> Embeds:
|
||||||
|
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
|
||||||
29
Handlers/ClearHandler.py
Normal file
29
Handlers/ClearHandler.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client
|
||||||
|
from Handlers.AbstractHandler import AbstractHandler
|
||||||
|
from Handlers.HandlerResponse import HandlerResponse
|
||||||
|
from Parallelism.ProcessManager import ProcessManager
|
||||||
|
|
||||||
|
|
||||||
|
class ClearHandler(AbstractHandler):
|
||||||
|
def __init__(self, ctx: Context, bot: Client) -> None:
|
||||||
|
super().__init__(ctx, bot)
|
||||||
|
|
||||||
|
async def run(self) -> HandlerResponse:
|
||||||
|
# Get the current process of the guild
|
||||||
|
processManager = ProcessManager()
|
||||||
|
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()
|
||||||
|
processLock.release()
|
||||||
|
return HandlerResponse(self.ctx)
|
||||||
|
else:
|
||||||
|
processManager.resetProcess(self.guild, self.ctx)
|
||||||
|
embed = self.embeds.PLAYER_RESTARTED()
|
||||||
|
return HandlerResponse(self.ctx, embed)
|
||||||
27
Handlers/HandlerResponse.py
Normal file
27
Handlers/HandlerResponse.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from typing import Union
|
||||||
|
from discord.ext.commands import Context
|
||||||
|
from Config.Exceptions import VulkanError
|
||||||
|
from discord import Embed
|
||||||
|
|
||||||
|
|
||||||
|
class HandlerResponse:
|
||||||
|
def __init__(self, ctx: Context, embed: Embed = None, error: VulkanError = None) -> None:
|
||||||
|
self.__ctx: Context = ctx
|
||||||
|
self.__error: VulkanError = error
|
||||||
|
self.__embed: Embed = embed
|
||||||
|
self.__success = False if error else True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ctx(self) -> Context:
|
||||||
|
return self.__ctx
|
||||||
|
|
||||||
|
@property
|
||||||
|
def embed(self) -> Union[Embed, None]:
|
||||||
|
return self.__embed
|
||||||
|
|
||||||
|
def error(self) -> Union[VulkanError, None]:
|
||||||
|
return self.__error
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success(self) -> bool:
|
||||||
|
return self.__success
|
||||||
40
Handlers/HistoryHandler.py
Normal file
40
Handlers/HistoryHandler.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client
|
||||||
|
from Handlers.AbstractHandler import AbstractHandler
|
||||||
|
from Handlers.HandlerResponse import HandlerResponse
|
||||||
|
from Utils.Utils import Utils
|
||||||
|
from Parallelism.ProcessManager import ProcessManager
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryHandler(AbstractHandler):
|
||||||
|
def __init__(self, ctx: Context, bot: Client) -> None:
|
||||||
|
super().__init__(ctx, bot)
|
||||||
|
|
||||||
|
async def run(self) -> HandlerResponse:
|
||||||
|
# Get the current process of the guild
|
||||||
|
processManager = ProcessManager()
|
||||||
|
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)
|
||||||
58
Handlers/LoopHandler.py
Normal file
58
Handlers/LoopHandler.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client
|
||||||
|
from Handlers.AbstractHandler import AbstractHandler
|
||||||
|
from Handlers.HandlerResponse import HandlerResponse
|
||||||
|
from Config.Exceptions import BadCommandUsage
|
||||||
|
from Parallelism.ProcessManager import ProcessManager
|
||||||
|
|
||||||
|
|
||||||
|
class LoopHandler(AbstractHandler):
|
||||||
|
def __init__(self, ctx: Context, bot: Client) -> None:
|
||||||
|
super().__init__(ctx, bot)
|
||||||
|
|
||||||
|
async def run(self, args: str) -> HandlerResponse:
|
||||||
|
# Get the current process of the guild
|
||||||
|
processManager = ProcessManager()
|
||||||
|
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)
|
||||||
74
Handlers/MoveHandler.py
Normal file
74
Handlers/MoveHandler.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
from typing import Union
|
||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client
|
||||||
|
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 Parallelism.ProcessManager import ProcessManager
|
||||||
|
|
||||||
|
|
||||||
|
class MoveHandler(AbstractHandler):
|
||||||
|
def __init__(self, ctx: Context, bot: Client) -> None:
|
||||||
|
super().__init__(ctx, bot)
|
||||||
|
|
||||||
|
async def run(self, pos1: str, pos2: str) -> HandlerResponse:
|
||||||
|
processManager = ProcessManager()
|
||||||
|
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
|
||||||
35
Handlers/NowPlayingHandler.py
Normal file
35
Handlers/NowPlayingHandler.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client
|
||||||
|
from Handlers.AbstractHandler import AbstractHandler
|
||||||
|
from Handlers.HandlerResponse import HandlerResponse
|
||||||
|
from Utils.Cleaner import Cleaner
|
||||||
|
from Parallelism.ProcessManager import ProcessManager
|
||||||
|
|
||||||
|
|
||||||
|
class NowPlayingHandler(AbstractHandler):
|
||||||
|
def __init__(self, ctx: Context, bot: Client) -> None:
|
||||||
|
super().__init__(ctx, bot)
|
||||||
|
self.__cleaner = Cleaner()
|
||||||
|
|
||||||
|
async def run(self) -> HandlerResponse:
|
||||||
|
# Get the current process of the guild
|
||||||
|
processManager = ProcessManager()
|
||||||
|
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)
|
||||||
25
Handlers/PauseHandler.py
Normal file
25
Handlers/PauseHandler.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client
|
||||||
|
from Handlers.AbstractHandler import AbstractHandler
|
||||||
|
from Handlers.HandlerResponse import HandlerResponse
|
||||||
|
from Parallelism.ProcessManager import ProcessManager
|
||||||
|
from Parallelism.Commands import VCommands, VCommandsType
|
||||||
|
|
||||||
|
|
||||||
|
class PauseHandler(AbstractHandler):
|
||||||
|
def __init__(self, ctx: Context, bot: Client) -> None:
|
||||||
|
super().__init__(ctx, bot)
|
||||||
|
|
||||||
|
async def run(self) -> HandlerResponse:
|
||||||
|
processManager = ProcessManager()
|
||||||
|
processInfo = processManager.getRunningPlayerInfo(self.guild)
|
||||||
|
if processInfo:
|
||||||
|
# Send Pause command to be execute by player process
|
||||||
|
command = VCommands(VCommandsType.PAUSE, None)
|
||||||
|
queue = processInfo.getQueue()
|
||||||
|
queue.put(command)
|
||||||
|
|
||||||
|
return HandlerResponse(self.ctx)
|
||||||
|
else:
|
||||||
|
embed = self.embeds.NOT_PLAYING()
|
||||||
|
return HandlerResponse(self.ctx, embed)
|
||||||
135
Handlers/PlayHandler.py
Normal file
135
Handlers/PlayHandler.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import asyncio
|
||||||
|
from typing import List
|
||||||
|
from Config.Exceptions import DownloadingError, InvalidInput, VulkanError
|
||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client
|
||||||
|
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.ProcessManager import ProcessManager
|
||||||
|
from Parallelism.ProcessInfo import ProcessInfo
|
||||||
|
from Parallelism.Commands import VCommands, VCommandsType
|
||||||
|
|
||||||
|
|
||||||
|
class PlayHandler(AbstractHandler):
|
||||||
|
def __init__(self, ctx: Context, bot: Client) -> None:
|
||||||
|
super().__init__(ctx, bot)
|
||||||
|
self.__searcher = Searcher()
|
||||||
|
self.__down = Downloader()
|
||||||
|
|
||||||
|
async def run(self, args: str) -> HandlerResponse:
|
||||||
|
track = " ".join(args)
|
||||||
|
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 = ProcessManager()
|
||||||
|
processInfo = processManager.getPlayerInfo(self.guild, self.ctx)
|
||||||
|
playlist = processInfo.getPlaylist()
|
||||||
|
process = processInfo.getProcess()
|
||||||
|
if not process.is_alive(): # If process has not yet started, start
|
||||||
|
process.start()
|
||||||
|
|
||||||
|
# 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.getQueue()
|
||||||
|
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: {error.message}', {type(error)})
|
||||||
|
embed = self.embeds.CUSTOM_ERROR(error)
|
||||||
|
else:
|
||||||
|
print(f'DEVELOPER NOTE -> PlayController Error: {error}, {type(error)}')
|
||||||
|
error = UnknownError()
|
||||||
|
embed = self.embeds.UNKNOWN_ERROR()
|
||||||
|
|
||||||
|
return HandlerResponse(self.ctx, embed, error)
|
||||||
|
|
||||||
|
async def __downloadSongsAndStore(self, songs: List[Song], processInfo: ProcessInfo) -> None:
|
||||||
|
playlist = processInfo.getPlaylist()
|
||||||
|
queue = processInfo.getQueue()
|
||||||
|
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 = ProcessManager()
|
||||||
|
for index, task in enumerate(tasks):
|
||||||
|
await task
|
||||||
|
song = songs[index]
|
||||||
|
if not song.problematic: # If downloaded add to the playlist and send play command
|
||||||
|
processInfo = processManager.getPlayerInfo(self.guild, self.ctx)
|
||||||
|
processLock = processInfo.getLock()
|
||||||
|
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
|
||||||
|
if acquired:
|
||||||
|
playlist.add_song(song)
|
||||||
|
queue.put(playCommand)
|
||||||
|
processLock.release()
|
||||||
|
else:
|
||||||
|
processManager.resetProcess(self.guild, self.ctx)
|
||||||
|
|
||||||
|
def __isUserConnected(self) -> bool:
|
||||||
|
if self.ctx.author.voice:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
53
Handlers/PrevHandler.py
Normal file
53
Handlers/PrevHandler.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client
|
||||||
|
from Handlers.AbstractHandler import AbstractHandler
|
||||||
|
from Config.Exceptions import BadCommandUsage, ImpossibleMove
|
||||||
|
from Handlers.HandlerResponse import HandlerResponse
|
||||||
|
from Parallelism.ProcessManager import ProcessManager
|
||||||
|
from Parallelism.Commands import VCommands, VCommandsType
|
||||||
|
|
||||||
|
|
||||||
|
class PrevHandler(AbstractHandler):
|
||||||
|
def __init__(self, ctx: Context, bot: Client) -> None:
|
||||||
|
super().__init__(ctx, bot)
|
||||||
|
|
||||||
|
async def run(self) -> HandlerResponse:
|
||||||
|
processManager = ProcessManager()
|
||||||
|
processInfo = processManager.getPlayerInfo(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 not self.__user_connected():
|
||||||
|
error = ImpossibleMove()
|
||||||
|
embed = self.embeds.NO_CHANNEL()
|
||||||
|
return HandlerResponse(self.ctx, embed, error)
|
||||||
|
|
||||||
|
if playlist.isLoopingAll() or playlist.isLoopingOne():
|
||||||
|
error = BadCommandUsage()
|
||||||
|
embed = self.embeds.FAIL_DUE_TO_LOOP_ON()
|
||||||
|
return HandlerResponse(self.ctx, embed, error)
|
||||||
|
|
||||||
|
# If not started, start the player process
|
||||||
|
process = processInfo.getProcess()
|
||||||
|
if not process.is_alive():
|
||||||
|
process.start()
|
||||||
|
|
||||||
|
# Send a prev command, together with the user voice channel
|
||||||
|
prevCommand = VCommands(VCommandsType.PREV, self.ctx.author.voice.channel.id)
|
||||||
|
queue = processInfo.getQueue()
|
||||||
|
queue.put(prevCommand)
|
||||||
|
return HandlerResponse(self.ctx)
|
||||||
|
|
||||||
|
def __user_connected(self) -> bool:
|
||||||
|
if self.ctx.author.voice:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
64
Handlers/QueueHandler.py
Normal file
64
Handlers/QueueHandler.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client
|
||||||
|
from Handlers.AbstractHandler import AbstractHandler
|
||||||
|
from Handlers.HandlerResponse import HandlerResponse
|
||||||
|
from Music.Downloader import Downloader
|
||||||
|
from Utils.Utils import Utils
|
||||||
|
from Parallelism.ProcessManager import ProcessManager
|
||||||
|
|
||||||
|
|
||||||
|
class QueueHandler(AbstractHandler):
|
||||||
|
def __init__(self, ctx: Context, bot: Client) -> None:
|
||||||
|
super().__init__(ctx, bot)
|
||||||
|
self.__down = Downloader()
|
||||||
|
|
||||||
|
async def run(self) -> HandlerResponse:
|
||||||
|
# Retrieve the process of the guild
|
||||||
|
processManager = ProcessManager()
|
||||||
|
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 = 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)
|
||||||
|
|
||||||
|
songs_preload = playlist.getSongsToPreload()
|
||||||
|
allSongs = playlist.getSongs()
|
||||||
|
if len(songs_preload) == 0:
|
||||||
|
embed = self.embeds.EMPTY_QUEUE()
|
||||||
|
processLock.release() # Release the Lock
|
||||||
|
return HandlerResponse(self.ctx, embed)
|
||||||
|
|
||||||
|
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} | ⌛ Duration: `{total_time}` downloaded \n\n'
|
||||||
|
|
||||||
|
for pos, song in enumerate(songs_preload, start=1):
|
||||||
|
song_name = song.title if song.title else self.messages.SONG_DOWNLOADING
|
||||||
|
text += f"**`{pos}` - ** {song_name} - `{Utils.format_time(song.duration)}`\n"
|
||||||
|
|
||||||
|
embed = self.embeds.QUEUE(title, text)
|
||||||
|
# Release the acquired Lock
|
||||||
|
processLock.release()
|
||||||
|
return HandlerResponse(self.ctx, embed)
|
||||||
|
else:
|
||||||
|
processManager.resetProcess(self.guild, self.ctx)
|
||||||
|
embed = self.embeds.PLAYER_RESTARTED()
|
||||||
|
return HandlerResponse(self.ctx, embed)
|
||||||
64
Handlers/RemoveHandler.py
Normal file
64
Handlers/RemoveHandler.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from typing import Union
|
||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client
|
||||||
|
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 Parallelism.ProcessManager import ProcessManager
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveHandler(AbstractHandler):
|
||||||
|
def __init__(self, ctx: Context, bot: Client) -> None:
|
||||||
|
super().__init__(ctx, bot)
|
||||||
|
|
||||||
|
async def run(self, position: str) -> HandlerResponse:
|
||||||
|
# Get the current process of the guild
|
||||||
|
processManager = ProcessManager()
|
||||||
|
processInfo = processManager.getRunningPlayerInfo(self.guild)
|
||||||
|
if not processInfo:
|
||||||
|
# Clear the playlist
|
||||||
|
embed = self.embeds.NOT_PLAYING()
|
||||||
|
error = BadCommandUsage()
|
||||||
|
return HandlerResponse(self.ctx, embed, error)
|
||||||
|
|
||||||
|
playlist = processInfo.getPlaylist()
|
||||||
|
if playlist.getCurrentSong() 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
|
||||||
25
Handlers/ResetHandler.py
Normal file
25
Handlers/ResetHandler.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client
|
||||||
|
from Handlers.AbstractHandler import AbstractHandler
|
||||||
|
from Handlers.HandlerResponse import HandlerResponse
|
||||||
|
from Parallelism.ProcessManager import ProcessManager
|
||||||
|
from Parallelism.Commands import VCommands, VCommandsType
|
||||||
|
|
||||||
|
|
||||||
|
class ResetHandler(AbstractHandler):
|
||||||
|
def __init__(self, ctx: Context, bot: Client) -> None:
|
||||||
|
super().__init__(ctx, bot)
|
||||||
|
|
||||||
|
async def run(self) -> HandlerResponse:
|
||||||
|
# Get the current process of the guild
|
||||||
|
processManager = ProcessManager()
|
||||||
|
processInfo = processManager.getRunningPlayerInfo(self.guild)
|
||||||
|
if processInfo:
|
||||||
|
command = VCommands(VCommandsType.RESET, None)
|
||||||
|
queue = processInfo.getQueue()
|
||||||
|
queue.put(command)
|
||||||
|
|
||||||
|
return HandlerResponse(self.ctx)
|
||||||
|
else:
|
||||||
|
embed = self.embeds.NOT_PLAYING()
|
||||||
|
return HandlerResponse(self.ctx, embed)
|
||||||
25
Handlers/ResumeHandler.py
Normal file
25
Handlers/ResumeHandler.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client
|
||||||
|
from Handlers.AbstractHandler import AbstractHandler
|
||||||
|
from Handlers.HandlerResponse import HandlerResponse
|
||||||
|
from Parallelism.ProcessManager import ProcessManager
|
||||||
|
from Parallelism.Commands import VCommands, VCommandsType
|
||||||
|
|
||||||
|
|
||||||
|
class ResumeHandler(AbstractHandler):
|
||||||
|
def __init__(self, ctx: Context, bot: Client) -> None:
|
||||||
|
super().__init__(ctx, bot)
|
||||||
|
|
||||||
|
async def run(self) -> HandlerResponse:
|
||||||
|
processManager = ProcessManager()
|
||||||
|
processInfo = processManager.getRunningPlayerInfo(self.guild)
|
||||||
|
if processInfo:
|
||||||
|
# Send Resume command to be execute by player process
|
||||||
|
command = VCommands(VCommandsType.RESUME, None)
|
||||||
|
queue = processInfo.getQueue()
|
||||||
|
queue.put(command)
|
||||||
|
|
||||||
|
return HandlerResponse(self.ctx)
|
||||||
|
else:
|
||||||
|
embed = self.embeds.NOT_PLAYING()
|
||||||
|
return HandlerResponse(self.ctx, embed)
|
||||||
41
Handlers/ShuffleHandler.py
Normal file
41
Handlers/ShuffleHandler.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client
|
||||||
|
from Handlers.AbstractHandler import AbstractHandler
|
||||||
|
from Handlers.HandlerResponse import HandlerResponse
|
||||||
|
from Config.Exceptions import UnknownError
|
||||||
|
from Parallelism.ProcessManager import ProcessManager
|
||||||
|
|
||||||
|
|
||||||
|
class ShuffleHandler(AbstractHandler):
|
||||||
|
def __init__(self, ctx: Context, bot: Client) -> None:
|
||||||
|
super().__init__(ctx, bot)
|
||||||
|
|
||||||
|
async def run(self) -> HandlerResponse:
|
||||||
|
processManager = ProcessManager()
|
||||||
|
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)
|
||||||
32
Handlers/SkipHandler.py
Normal file
32
Handlers/SkipHandler.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client
|
||||||
|
from Handlers.AbstractHandler import AbstractHandler
|
||||||
|
from Config.Exceptions import BadCommandUsage
|
||||||
|
from Handlers.HandlerResponse import HandlerResponse
|
||||||
|
from Parallelism.ProcessManager import ProcessManager
|
||||||
|
from Parallelism.Commands import VCommands, VCommandsType
|
||||||
|
|
||||||
|
|
||||||
|
class SkipHandler(AbstractHandler):
|
||||||
|
def __init__(self, ctx: Context, bot: Client) -> None:
|
||||||
|
super().__init__(ctx, bot)
|
||||||
|
|
||||||
|
async def run(self) -> HandlerResponse:
|
||||||
|
processManager = ProcessManager()
|
||||||
|
processInfo = processManager.getRunningPlayerInfo(self.guild)
|
||||||
|
if processInfo: # Verify if there is a running process
|
||||||
|
playlist = processInfo.getPlaylist()
|
||||||
|
if playlist.isLoopingOne():
|
||||||
|
embed = self.embeds.ERROR_DUE_LOOP_ONE_ON()
|
||||||
|
error = BadCommandUsage()
|
||||||
|
return HandlerResponse(self.ctx, embed, error)
|
||||||
|
|
||||||
|
# Send a command to the player process to skip the music
|
||||||
|
command = VCommands(VCommandsType.SKIP, None)
|
||||||
|
queue = processInfo.getQueue()
|
||||||
|
queue.put(command)
|
||||||
|
|
||||||
|
return HandlerResponse(self.ctx)
|
||||||
|
else:
|
||||||
|
embed = self.embeds.NOT_PLAYING()
|
||||||
|
return HandlerResponse(self.ctx, embed)
|
||||||
25
Handlers/StopHandler.py
Normal file
25
Handlers/StopHandler.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client
|
||||||
|
from Handlers.AbstractHandler import AbstractHandler
|
||||||
|
from Handlers.HandlerResponse import HandlerResponse
|
||||||
|
from Parallelism.ProcessManager import ProcessManager
|
||||||
|
from Parallelism.Commands import VCommands, VCommandsType
|
||||||
|
|
||||||
|
|
||||||
|
class StopHandler(AbstractHandler):
|
||||||
|
def __init__(self, ctx: Context, bot: Client) -> None:
|
||||||
|
super().__init__(ctx, bot)
|
||||||
|
|
||||||
|
async def run(self) -> HandlerResponse:
|
||||||
|
processManager = ProcessManager()
|
||||||
|
processInfo = processManager.getRunningPlayerInfo(self.guild)
|
||||||
|
if processInfo:
|
||||||
|
# Send command to player process stop
|
||||||
|
command = VCommands(VCommandsType.STOP, None)
|
||||||
|
queue = processInfo.getQueue()
|
||||||
|
queue.put(command)
|
||||||
|
|
||||||
|
return HandlerResponse(self.ctx)
|
||||||
|
else:
|
||||||
|
embed = self.embeds.NOT_PLAYING()
|
||||||
|
return HandlerResponse(self.ctx, embed)
|
||||||
72
Music/DeezerSearcher.py
Normal file
72
Music/DeezerSearcher.py
Normal 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
|
||||||
178
Music/Downloader.py
Normal file
178
Music/Downloader.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import asyncio
|
||||||
|
from typing import List
|
||||||
|
from Config.Configs import Configs
|
||||||
|
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 = Configs()
|
||||||
|
__YDL_OPTIONS = {'format': 'bestaudio/best',
|
||||||
|
'default_search': 'auto',
|
||||||
|
'playliststart': 0,
|
||||||
|
'extract_flat': False,
|
||||||
|
'playlistend': config.MAX_PLAYLIST_LENGTH,
|
||||||
|
'quiet': True
|
||||||
|
}
|
||||||
|
__YDL_OPTIONS_EXTRACT = {'format': 'bestaudio/best',
|
||||||
|
'default_search': 'auto',
|
||||||
|
'playliststart': 0,
|
||||||
|
'extract_flat': True,
|
||||||
|
'playlistend': config.MAX_PLAYLIST_LENGTH,
|
||||||
|
'quiet': True
|
||||||
|
}
|
||||||
|
__YDL_OPTIONS_FORCE_EXTRACT = {'format': 'bestaudio/best',
|
||||||
|
'default_search': 'auto',
|
||||||
|
'playliststart': 0,
|
||||||
|
'extract_flat': False,
|
||||||
|
'playlistend': config.MAX_PLAYLIST_LENGTH,
|
||||||
|
'quiet': True
|
||||||
|
}
|
||||||
|
__BASE_URL = 'https://www.youtube.com/watch?v={}'
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.__config = Configs()
|
||||||
|
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:
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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
|
||||||
146
Music/Playlist.py
Normal file
146
Music/Playlist.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
from collections import deque
|
||||||
|
from typing import List
|
||||||
|
from Config.Configs import Configs
|
||||||
|
from Music.Song import Song
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
class Playlist:
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.__configs = Configs()
|
||||||
|
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 __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 played_song.problematic == False:
|
||||||
|
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 = 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
91
Music/Searcher.py
Normal 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
|
||||||
@@ -1,32 +1,36 @@
|
|||||||
from vulkan.music.Interfaces import ISong, IPlaylist
|
class Song:
|
||||||
|
|
||||||
|
def __init__(self, identifier: str, playlist, requester: str) -> None:
|
||||||
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.__identifier = identifier
|
||||||
self.__info = {'requester': requester}
|
self.__info = {'requester': requester}
|
||||||
self.__problematic = False
|
self.__problematic = False
|
||||||
self.__playlist: IPlaylist = playlist
|
self.__playlist = playlist
|
||||||
|
|
||||||
def finish_down(self, info: dict) -> None:
|
def finish_down(self, info: dict) -> None:
|
||||||
"""Get and store the full information of the song"""
|
if info is None:
|
||||||
self.__usefull_keys = ['url', 'duration',
|
self.destroy()
|
||||||
'title', 'webpage_url',
|
return None
|
||||||
'channel', 'id', 'uploader',
|
|
||||||
'thumbnail', 'original_url']
|
|
||||||
|
|
||||||
for key in self.__usefull_keys:
|
self.__useful_keys = ['duration',
|
||||||
try:
|
'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.__info[key] = info[key]
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source(self) -> str:
|
def source(self) -> str:
|
||||||
"""Return the Song Source URL to play"""
|
|
||||||
if 'url' in self.__info.keys():
|
if 'url' in self.__info.keys():
|
||||||
return self.__info['url']
|
return self.__info['url']
|
||||||
else:
|
else:
|
||||||
@@ -34,7 +38,6 @@ class Song(ISong):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self) -> str:
|
def title(self) -> str:
|
||||||
"""Return the Song Title"""
|
|
||||||
if 'title' in self.__info.keys():
|
if 'title' in self.__info.keys():
|
||||||
return self.__info['title']
|
return self.__info['title']
|
||||||
else:
|
else:
|
||||||
@@ -42,7 +45,6 @@ class Song(ISong):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def duration(self) -> str:
|
def duration(self) -> str:
|
||||||
"""Return the Song Title"""
|
|
||||||
if 'duration' in self.__info.keys():
|
if 'duration' in self.__info.keys():
|
||||||
return self.__info['duration']
|
return self.__info['duration']
|
||||||
else:
|
else:
|
||||||
@@ -57,7 +59,7 @@ class Song(ISong):
|
|||||||
return self.__problematic
|
return self.__problematic
|
||||||
|
|
||||||
def destroy(self) -> None:
|
def destroy(self) -> None:
|
||||||
"""Mark this song with problems and removed from the playlist due to any type of error"""
|
print(f'DEVELOPER NOTE -> Music self destroying {self.__identifier}')
|
||||||
self.__problematic = True
|
self.__problematic = True
|
||||||
self.__playlist.destroy_song(self)
|
self.__playlist.destroy_song(self)
|
||||||
|
|
||||||
118
Music/SpotifySearcher.py
Normal file
118
Music/SpotifySearcher.py
Normal 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 Configs
|
||||||
|
from Config.Messages import SpotifyMessages
|
||||||
|
|
||||||
|
|
||||||
|
class SpotifySearch():
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.__messages = SpotifyMessages()
|
||||||
|
self.__config = Configs()
|
||||||
|
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
9
Music/Types.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class Provider(str, Enum):
|
||||||
|
Spotify = 'Spotify'
|
||||||
|
Deezer = 'Deezer'
|
||||||
|
YouTube = 'YouTube'
|
||||||
|
Name = 'Track Name'
|
||||||
|
Unknown = 'Unknown'
|
||||||
25
Parallelism/Commands.py
Normal file
25
Parallelism/Commands.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
380
Parallelism/PlayerProcess.py
Normal file
380
Parallelism/PlayerProcess.py
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
import asyncio
|
||||||
|
from os import listdir
|
||||||
|
from discord import Intents, User, Member, Message, Embed
|
||||||
|
from asyncio import AbstractEventLoop, Semaphore
|
||||||
|
from multiprocessing import Process, Queue, RLock
|
||||||
|
from threading import Lock, Thread
|
||||||
|
from typing import Callable, List
|
||||||
|
from discord import Client, Guild, FFmpegPCMAudio, VoiceChannel, TextChannel
|
||||||
|
from Music.Playlist import Playlist
|
||||||
|
from Music.Song import Song
|
||||||
|
from Config.Configs import Configs
|
||||||
|
from Config.Messages import Messages
|
||||||
|
from discord.ext.commands import Bot
|
||||||
|
from Views.Embeds import Embeds
|
||||||
|
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(Configs().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, queue: 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.__queue: Queue = queue
|
||||||
|
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: Client = None
|
||||||
|
self.__voiceChannel: VoiceChannel = None
|
||||||
|
self.__textChannel: TextChannel = None
|
||||||
|
self.__author: User = None
|
||||||
|
self.__botMember: Member = None
|
||||||
|
|
||||||
|
self.__configs: Configs = None
|
||||||
|
self.__embeds: Embeds = 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()
|
||||||
|
|
||||||
|
self.__configs = Configs()
|
||||||
|
self.__messages = Messages()
|
||||||
|
self.__embeds = Embeds()
|
||||||
|
|
||||||
|
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 not self.__playing:
|
||||||
|
with self.__playlistLock:
|
||||||
|
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)
|
||||||
|
|
||||||
|
await self.__deletePrevNowPlaying()
|
||||||
|
await self.__showNowPlaying()
|
||||||
|
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.__playerLock:
|
||||||
|
if self.__forceStop: # If it's forced to stop player
|
||||||
|
self.__forceStop = False
|
||||||
|
return None
|
||||||
|
|
||||||
|
with self.__playlistLock:
|
||||||
|
song = self.__playlist.next_song()
|
||||||
|
|
||||||
|
if song is not None:
|
||||||
|
self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}')
|
||||||
|
else:
|
||||||
|
with self.__playlistLock:
|
||||||
|
self.__playlist.loop_off()
|
||||||
|
self.__playingSong = None
|
||||||
|
self.__playing = False
|
||||||
|
|
||||||
|
async def __playPrev(self, voiceChannelID: int) -> None:
|
||||||
|
with self.__playlistLock:
|
||||||
|
song = self.__playlist.prev_song()
|
||||||
|
|
||||||
|
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.__queue.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.clear()
|
||||||
|
self.__playlist.loop_off()
|
||||||
|
|
||||||
|
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) -> Client:
|
||||||
|
"""Load a new bot instance that should not be directly called.
|
||||||
|
Get the guild, voice and text Channel in discord API using IDs passed in constructor
|
||||||
|
"""
|
||||||
|
intents = Intents.default()
|
||||||
|
intents.members = True
|
||||||
|
bot = Bot(command_prefix='Rafael',
|
||||||
|
pm_help=True,
|
||||||
|
case_insensitive=True,
|
||||||
|
intents=intents)
|
||||||
|
bot.remove_command('help')
|
||||||
|
|
||||||
|
# Add the Cogs for this bot too
|
||||||
|
for filename in listdir(f'./{self.__configs.COMMANDS_PATH}'):
|
||||||
|
if filename.endswith('.py'):
|
||||||
|
bot.load_extension(f'{self.__configs.COMMANDS_PATH}.{filename[:-3]}')
|
||||||
|
|
||||||
|
# Login and connect the bot instance to discord API
|
||||||
|
task = self.__loop.create_task(bot.login(token=self.__configs.BOT_TOKEN, bot=True))
|
||||||
|
await task
|
||||||
|
self.__loop.create_task(bot.connect(reconnect=True))
|
||||||
|
# Sleep to wait connection to be established
|
||||||
|
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():
|
||||||
|
self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop)
|
||||||
|
|
||||||
|
elif self.__guild.voice_client.is_connected():
|
||||||
|
with self.__playerLock:
|
||||||
|
with self.__playlistLock:
|
||||||
|
self.__playlist.clear()
|
||||||
|
self.__playlist.loop_off()
|
||||||
|
self.__playing = False
|
||||||
|
await self.__guild.voice_client.disconnect()
|
||||||
|
# Release semaphore to finish process
|
||||||
|
self.__semStopPlaying.release()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[Error in Timeout] -> {e}')
|
||||||
|
|
||||||
|
async def __ensureDiscordConnection(self, bot: Client) -> 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
|
||||||
|
|
||||||
|
async def __showNowPlaying(self) -> None:
|
||||||
|
# Get the lock of the playlist
|
||||||
|
with self.__playlistLock:
|
||||||
|
if not self.__playing or self.__playingSong is None:
|
||||||
|
embed = self.__embeds.NOT_PLAYING()
|
||||||
|
await self.__textChannel.send(embed=embed)
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.__playlist.isLoopingOne():
|
||||||
|
title = self.__messages.ONE_SONG_LOOPING
|
||||||
|
else:
|
||||||
|
title = self.__messages.SONG_PLAYING
|
||||||
|
|
||||||
|
info = self.__playingSong.info
|
||||||
|
embed = self.__embeds.SONG_INFO(info, title)
|
||||||
|
await self.__textChannel.send(embed=embed)
|
||||||
|
self.__messagesToDelete.append(await self.__getSendedMessage())
|
||||||
|
|
||||||
|
async def __deletePrevNowPlaying(self) -> None:
|
||||||
|
for message in self.__messagesToDelete:
|
||||||
|
try:
|
||||||
|
await message.delete()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.__messagesToDelete.clear()
|
||||||
|
|
||||||
|
async def __getSendedMessage(self) -> Message:
|
||||||
|
stringToIdentify = 'Uploader:'
|
||||||
|
last_messages: List[Message] = await self.__textChannel.history(limit=5).flatten()
|
||||||
|
|
||||||
|
for message in last_messages:
|
||||||
|
try:
|
||||||
|
if message.author == self.__bot.user:
|
||||||
|
if len(message.embeds) > 0:
|
||||||
|
embed: Embed = message.embeds[0]
|
||||||
|
if len(embed.fields) > 0:
|
||||||
|
if embed.fields[0].name == stringToIdentify:
|
||||||
|
return message
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f'DEVELOPER NOTE -> Error cleaning messages {e}')
|
||||||
|
continue
|
||||||
29
Parallelism/ProcessInfo.py
Normal file
29
Parallelism/ProcessInfo.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from multiprocessing import Process, Queue, Lock
|
||||||
|
from Music.Playlist import Playlist
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessInfo:
|
||||||
|
"""
|
||||||
|
Class to store the reference to all structures to maintain a player process
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, process: Process, queue: Queue, playlist: Playlist, lock: Lock) -> None:
|
||||||
|
self.__process = process
|
||||||
|
self.__queue = queue
|
||||||
|
self.__playlist = playlist
|
||||||
|
self.__lock = lock
|
||||||
|
|
||||||
|
def setProcess(self, newProcess: Process) -> None:
|
||||||
|
self.__process = newProcess
|
||||||
|
|
||||||
|
def getProcess(self) -> Process:
|
||||||
|
return self.__process
|
||||||
|
|
||||||
|
def getQueue(self) -> Queue:
|
||||||
|
return self.__queue
|
||||||
|
|
||||||
|
def getPlaylist(self) -> Playlist:
|
||||||
|
return self.__playlist
|
||||||
|
|
||||||
|
def getLock(self) -> Lock:
|
||||||
|
return self.__lock
|
||||||
101
Parallelism/ProcessManager.py
Normal file
101
Parallelism/ProcessManager.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
from multiprocessing import Queue, Lock
|
||||||
|
from multiprocessing.managers import BaseManager, NamespaceProxy
|
||||||
|
from typing import Dict
|
||||||
|
from Config.Singleton import Singleton
|
||||||
|
from discord import Guild
|
||||||
|
from discord.ext.commands import Context
|
||||||
|
from Parallelism.PlayerProcess import PlayerProcess
|
||||||
|
from Music.Playlist import Playlist
|
||||||
|
from Parallelism.ProcessInfo import ProcessInfo
|
||||||
|
from Parallelism.Commands import VCommands, VCommandsType
|
||||||
|
|
||||||
|
|
||||||
|
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) -> None:
|
||||||
|
if not super().created:
|
||||||
|
VManager.register('Playlist', Playlist)
|
||||||
|
self.__manager = VManager()
|
||||||
|
self.__manager.start()
|
||||||
|
self.__playersProcess: Dict[Guild, ProcessInfo] = {}
|
||||||
|
|
||||||
|
def setPlayerContext(self, guild: Guild, context: ProcessInfo):
|
||||||
|
self.__playersProcess[guild.id] = context
|
||||||
|
|
||||||
|
def getPlayerInfo(self, guild: Guild, context: Context) -> ProcessInfo:
|
||||||
|
"""Return the process info for the guild, if not, create one"""
|
||||||
|
try:
|
||||||
|
if guild.id not in self.__playersProcess.keys():
|
||||||
|
self.__playersProcess[guild.id] = self.__createProcessInfo(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(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(context)
|
||||||
|
newProcessInfo.getProcess().start() # Start the process
|
||||||
|
# Send a command to start the play again
|
||||||
|
playCommand = VCommands(VCommandsType.PLAY)
|
||||||
|
newProcessInfo.getQueue().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, 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()
|
||||||
|
queue = Queue()
|
||||||
|
process = PlayerProcess(context.guild.name, playlist, lock, queue,
|
||||||
|
guildID, textID, voiceID, authorID)
|
||||||
|
processInfo = ProcessInfo(process, queue, playlist, lock)
|
||||||
|
|
||||||
|
return processInfo
|
||||||
|
|
||||||
|
def __recreateProcess(self, context: Context) -> ProcessInfo:
|
||||||
|
"""Create a new process info using previous playlist"""
|
||||||
|
guildID: int = context.guild.id
|
||||||
|
textID: int = context.channel.id
|
||||||
|
voiceID: int = context.author.voice.channel.id
|
||||||
|
authorID: int = context.author.id
|
||||||
|
|
||||||
|
playlist: Playlist = self.__playersProcess[guildID].getPlaylist()
|
||||||
|
lock = Lock()
|
||||||
|
queue = Queue()
|
||||||
|
|
||||||
|
process = PlayerProcess(context.guild.name, playlist, lock, queue,
|
||||||
|
guildID, textID, voiceID, authorID)
|
||||||
|
processInfo = ProcessInfo(process, queue, playlist, lock)
|
||||||
|
|
||||||
|
return processInfo
|
||||||
|
|
||||||
|
|
||||||
|
class VManager(BaseManager):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class VProxy(NamespaceProxy):
|
||||||
|
_exposed_ = ('__getattribute__', '__setattr__', '__delattr__')
|
||||||
100
README.md
100
README.md
@@ -1,12 +1,53 @@
|
|||||||
# **Vulkan**
|
# **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.
|
A Music Discord bot, written in Python, that plays *Youtube*, *Spotify* and *Deezer* links. Vulkan was designed so that anyone can fork this project, follow the instructions and use it in their own way, Vulkan can also be configured in Heroku to work 24/7.
|
||||||
|
|
||||||
|
|
||||||
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.
|
# **Music**
|
||||||
|
- Play musics from Youtube, Spotify and Deezer links (Albums, Artists, Playlists and Tracks)
|
||||||
|
- Control loop of one or all musics
|
||||||
|
- Allow moving and removing musics in the queue
|
||||||
|
- Play musics in queue randomly
|
||||||
|
- Store played songs and allow bidirectional flow
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
```!play [title, spotify_url, youtube_url, deezer_url]``` - Start playing song
|
||||||
|
|
||||||
|
```!resume``` - Resume the song player
|
||||||
|
|
||||||
|
```!pause``` - Pause the song player
|
||||||
|
|
||||||
|
```!skip``` - Skip the currently playing song
|
||||||
|
|
||||||
|
```!prev``` - Return to play the previous song
|
||||||
|
|
||||||
|
```!stop``` - Stop the playing of musics
|
||||||
|
|
||||||
|
```!queue``` - Show the musics list in queue
|
||||||
|
|
||||||
|
```!history``` - Show the played songs list
|
||||||
|
|
||||||
|
```!loop [one, all, off]``` - Control the loop of songs
|
||||||
|
|
||||||
|
```!shuffle``` - Shuffle the songs in queue
|
||||||
|
|
||||||
|
```!remove [x]``` - Remove the song in position x
|
||||||
|
|
||||||
|
```!move [x, y]``` - Change the musics in position x and y in Queue
|
||||||
|
|
||||||
|
```!np``` - Show information of the currently song
|
||||||
|
|
||||||
|
```!clear``` - Clear the songs in queue, doesn't stop the player
|
||||||
|
|
||||||
|
```!reset``` - Reset the player, recommended if any error happen
|
||||||
|
|
||||||
|
```!invite``` - Send the URL to invite Vulkan to your server
|
||||||
|
|
||||||
|
```!help [command]``` - Show more info about the command selected
|
||||||
|
|
||||||
|
|
||||||
## **Prerequisites:**
|
|
||||||
|
# **Usage:**
|
||||||
|
|
||||||
### **API Keys**
|
### **API Keys**
|
||||||
* Your Discord Application - [Discord](https://discord.com/developers)
|
* Your Discord Application - [Discord](https://discord.com/developers)
|
||||||
@@ -21,22 +62,19 @@ pip install -r requirements.txt
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
- Installation of FFMPEG
|
- **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.
|
||||||
|
|
||||||
*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**
|
### **.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
|
BOT_TOKEN=Your_Own_Bot_Token
|
||||||
SPOTIFY_ID=Your_Own_Spotify_ID
|
SPOTIFY_ID=Your_Own_Spotify_ID
|
||||||
SPOTIFY_SECRET=Your_Own_Spotify_Secret
|
SPOTIFY_SECRET=Your_Own_Spotify_Secret
|
||||||
SECRET_MESSAGE=Your_Own_Secret_Message
|
BOT_PREFIX=Your_Wanted_Prefix_For_Vulkan
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -57,39 +95,9 @@ To run your Bot in Heroku 24/7, you will need the Procfile located in root, then
|
|||||||
- https://github.com/xrisk/heroku-opus.git
|
- https://github.com/xrisk/heroku-opus.git
|
||||||
|
|
||||||
|
|
||||||
# **Music**
|
## Testing
|
||||||
- Play musics from Youtube and Spotify Playlists
|
The tests were written manually with no package due to problems with async function in other packages, to execute them type in root: <br>
|
||||||
- Controll loop of one or all musics
|
`python run_tests.py`<br>
|
||||||
- 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
|
## 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).
|
- 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).
|
||||||
@@ -100,4 +108,4 @@ To run your Bot in Heroku 24/7, you will need the Procfile located in root, then
|
|||||||
|
|
||||||
|
|
||||||
## Acknowledgment
|
## Acknowledgment
|
||||||
- See the DingoLingo [project](https://github.com/Raptor123471/DingoLingo) from Raptor123471, it helped me a lot building Vulkan
|
- See the DingoLingo [project](https://github.com/Raptor123471/DingoLingo) from Raptor123471, it helped me a lot to build Vulkan.
|
||||||
10
Tests/Colors.py
Normal file
10
Tests/Colors.py
Normal 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
50
Tests/LoopRunner.py
Normal 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
86
Tests/TestBase.py
Normal 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
28
Tests/TestsHelper.py
Normal 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
66
Tests/VDeezerTests.py
Normal 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
120
Tests/VDownloaderTests.py
Normal 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
66
Tests/VSpotifyTests.py
Normal 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
|
||||||
32
Utils/Cleaner.py
Normal file
32
Utils/Cleaner.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from typing import List
|
||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client, Message, Embed
|
||||||
|
from Config.Singleton import Singleton
|
||||||
|
|
||||||
|
|
||||||
|
class Cleaner(Singleton):
|
||||||
|
def __init__(self, bot: Client = None) -> None:
|
||||||
|
if not super().created:
|
||||||
|
self.__bot = bot
|
||||||
|
self.__clean_str = 'Uploader:'
|
||||||
|
|
||||||
|
def set_bot(self, bot: Client) -> 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
34
Utils/UrlAnalyzer.py
Normal 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
42
Utils/Utils.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import re
|
||||||
|
import asyncio
|
||||||
|
from Config.Configs import Configs
|
||||||
|
from functools import wraps, partial
|
||||||
|
config = Configs()
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
32
Views/AbstractView.py
Normal file
32
Views/AbstractView.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from Handlers.HandlerResponse import HandlerResponse
|
||||||
|
from discord.ext.commands import Context
|
||||||
|
from discord import Client, Message
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractView(ABC):
|
||||||
|
def __init__(self, response: HandlerResponse) -> None:
|
||||||
|
self.__response: HandlerResponse = response
|
||||||
|
self.__context: Context = response.ctx
|
||||||
|
self.__message: Message = response.ctx.message
|
||||||
|
self.__bot: Client = response.ctx.bot
|
||||||
|
|
||||||
|
@property
|
||||||
|
def response(self) -> HandlerResponse:
|
||||||
|
return self.__response
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bot(self) -> Client:
|
||||||
|
return self.__bot
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self) -> Message:
|
||||||
|
return self.__message
|
||||||
|
|
||||||
|
@property
|
||||||
|
def context(self) -> Context:
|
||||||
|
return self.__context
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def run(self) -> None:
|
||||||
|
pass
|
||||||
11
Views/EmbedView.py
Normal file
11
Views/EmbedView.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from Views.AbstractView import AbstractView
|
||||||
|
from Handlers.HandlerResponse import HandlerResponse
|
||||||
|
|
||||||
|
|
||||||
|
class EmbedView(AbstractView):
|
||||||
|
def __init__(self, response: HandlerResponse) -> None:
|
||||||
|
super().__init__(response)
|
||||||
|
|
||||||
|
async def run(self) -> None:
|
||||||
|
if self.response.embed:
|
||||||
|
await self.context.send(embed=self.response.embed)
|
||||||
357
Views/Embeds.py
Normal file
357
Views/Embeds.py
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
from Config.Messages import Messages
|
||||||
|
from Config.Exceptions import VulkanError
|
||||||
|
from discord import Embed
|
||||||
|
from Config.Configs import Configs
|
||||||
|
from Config.Colors import Colors
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
|
class Embeds:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.__config = Configs()
|
||||||
|
self.__messages = Messages()
|
||||||
|
self.__colors = Colors()
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
|
||||||
|
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 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 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
|
||||||
|
)
|
||||||
|
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 CARA_COROA(self, result: str) -> Embed:
|
||||||
|
embed = Embed(
|
||||||
|
title='Cara Cora',
|
||||||
|
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
|
||||||
14
Views/EmoteView.py
Normal file
14
Views/EmoteView.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from Views.AbstractView import AbstractView
|
||||||
|
from Handlers.HandlerResponse import HandlerResponse
|
||||||
|
|
||||||
|
|
||||||
|
class EmoteView(AbstractView):
|
||||||
|
|
||||||
|
def __init__(self, response: HandlerResponse) -> None:
|
||||||
|
super().__init__(response)
|
||||||
|
|
||||||
|
async def run(self) -> None:
|
||||||
|
if self.response.success:
|
||||||
|
await self.message.add_reaction('✅')
|
||||||
|
else:
|
||||||
|
await self.message.add_reaction('❌')
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
56
main.py
56
main.py
@@ -1,22 +1,38 @@
|
|||||||
import discord
|
from discord import Intents, Client
|
||||||
import os
|
from os import listdir
|
||||||
|
from Config.Configs import Configs
|
||||||
from config import config
|
from discord.ext.commands import Bot
|
||||||
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)
|
class VulkanInitializer:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.__config = Configs()
|
||||||
|
self.__intents = Intents.default()
|
||||||
|
self.__intents.members = True
|
||||||
|
self.__bot = self.__create_bot()
|
||||||
|
self.__add_cogs(self.__bot)
|
||||||
|
|
||||||
|
def __create_bot(self) -> Client:
|
||||||
|
bot = Bot(command_prefix=self.__config.BOT_PREFIX,
|
||||||
|
pm_help=True,
|
||||||
|
case_insensitive=True,
|
||||||
|
intents=self.__intents)
|
||||||
|
bot.remove_command('help')
|
||||||
|
return bot
|
||||||
|
|
||||||
|
def __add_cogs(self, bot: Client) -> None:
|
||||||
|
for filename in listdir(f'./{self.__config.COMMANDS_PATH}'):
|
||||||
|
if filename.endswith('.py'):
|
||||||
|
bot.load_extension(f'{self.__config.COMMANDS_PATH}.{filename[:-3]}')
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
if self.__config.BOT_TOKEN == '':
|
||||||
|
print('DEVELOPER NOTE -> Token not found')
|
||||||
|
exit()
|
||||||
|
|
||||||
|
self.__bot.run(self.__config.BOT_TOKEN, bot=True, reconnect=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
vulkan = VulkanInitializer()
|
||||||
|
vulkan.run()
|
||||||
|
|||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
11
run_tests.py
Normal file
11
run_tests.py
Normal 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()
|
||||||
@@ -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))
|
|
||||||
@@ -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))
|
|
||||||
@@ -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))
|
|
||||||
@@ -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))
|
|
||||||
@@ -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))
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
@@ -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'
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
|
||||||
@@ -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()
|
|
||||||
Reference in New Issue
Block a user