mirror of
https://github.com/RafaelSolVargas/Vulkan.git
synced 2025-10-29 16:57:23 +00:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5885f3093 | ||
|
|
60a36425ee | ||
|
|
5902a0dc72 | ||
|
|
ca754c6f62 | ||
|
|
4f11506c2b | ||
|
|
beb0bc085d | ||
|
|
fededdbb8c | ||
|
|
4a22b43ce9 | ||
|
|
48d7166386 | ||
|
|
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 | ||
|
|
2bc8cd206c | ||
|
|
66962cb274 | ||
|
|
2d7ecd0a0e | ||
|
|
98ee6f9cf2 | ||
|
|
089b47fc44 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,8 +1,8 @@
|
||||
.vscode
|
||||
assets/
|
||||
__pycache__
|
||||
.env
|
||||
.cache
|
||||
Admin.py
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
34
Config/Colors.py
Normal file
34
Config/Colors.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from Config.Singleton import Singleton
|
||||
|
||||
|
||||
class VColors(Singleton):
|
||||
def __init__(self) -> None:
|
||||
self.__red = 0xDC143C
|
||||
self.__green = 0x1F8B4C
|
||||
self.__grey = 0x708090
|
||||
self.__blue = 0x206694
|
||||
self.__black = 0x23272A
|
||||
|
||||
@property
|
||||
def RED(self) -> str:
|
||||
return self.__red
|
||||
|
||||
@property
|
||||
def GREEN(self) -> str:
|
||||
return self.__green
|
||||
|
||||
@property
|
||||
def GREY(self) -> str:
|
||||
return self.__grey
|
||||
|
||||
@property
|
||||
def BLUE(self) -> str:
|
||||
return self.__blue
|
||||
|
||||
@property
|
||||
def BLACK(self) -> str:
|
||||
return self.__black
|
||||
|
||||
@property
|
||||
def RED(self) -> str:
|
||||
return self.__red
|
||||
38
Config/Configs.py
Normal file
38
Config/Configs.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from decouple import config
|
||||
from Config.Singleton import Singleton
|
||||
|
||||
|
||||
class VConfigs(Singleton):
|
||||
def __init__(self) -> None:
|
||||
if not super().created:
|
||||
self.BOT_PREFIX = '!'
|
||||
try:
|
||||
self.BOT_TOKEN = config('BOT_TOKEN')
|
||||
self.SPOTIFY_ID = config('SPOTIFY_ID')
|
||||
self.SPOTIFY_SECRET = config('SPOTIFY_SECRET')
|
||||
self.BOT_PREFIX = config('BOT_PREFIX')
|
||||
except:
|
||||
print(
|
||||
'[ERROR] -> You must create and .env file with all required fields, see documentation for help')
|
||||
|
||||
self.CLEANER_MESSAGES_QUANT = 5
|
||||
self.ACQUIRE_LOCK_TIMEOUT = 10
|
||||
self.COMMANDS_PATH = 'DiscordCogs'
|
||||
self.VC_TIMEOUT = 300
|
||||
|
||||
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'
|
||||
|
||||
def getProcessManager(self):
|
||||
return self.__manager
|
||||
|
||||
def setProcessManager(self, newManager):
|
||||
self.__manager = newManager
|
||||
357
Config/Embeds.py
Normal file
357
Config/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 VConfigs
|
||||
from Config.Colors import VColors
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class VEmbeds:
|
||||
def __init__(self) -> None:
|
||||
self.__config = VConfigs()
|
||||
self.__messages = Messages()
|
||||
self.__colors = VColors()
|
||||
|
||||
def 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 Coroa',
|
||||
description=f'Result: {result}',
|
||||
colour=self.__colors.GREEN
|
||||
)
|
||||
return embed
|
||||
|
||||
def CHOSEN_THING(self, thing: str) -> Embed:
|
||||
embed = Embed(
|
||||
title='Choose something',
|
||||
description=f'Chosen: {thing}',
|
||||
colour=self.__colors.GREEN
|
||||
)
|
||||
return embed
|
||||
|
||||
def BAD_CHOOSE_USE(self) -> Embed:
|
||||
embed = Embed(
|
||||
title='Choose something',
|
||||
description=f'Error: Use {self.__config.BOT_PREFIX}help choose to understand this command.',
|
||||
colour=self.__colors.RED
|
||||
)
|
||||
return embed
|
||||
20
Config/Emojis.py
Normal file
20
Config/Emojis.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from Config.Singleton import Singleton
|
||||
|
||||
|
||||
class VEmojis(Singleton):
|
||||
def __init__(self) -> None:
|
||||
if not super().created:
|
||||
self.SKIP = "⏩"
|
||||
self.BACK = "⏪"
|
||||
self.PAUSE = "⏸️"
|
||||
self.PLAY = "▶️"
|
||||
self.STOP = "⏹️"
|
||||
self.LOOP_ONE = "🔂"
|
||||
self.LOOP_OFF = "➡️"
|
||||
self.LOOP_ALL = "🔁"
|
||||
self.SHUFFLE = "🔀"
|
||||
self.QUEUE = "📜"
|
||||
self.MUSIC = "🎧"
|
||||
self.ERROR = "❌"
|
||||
self.DOWNLOADING = "📥"
|
||||
self.SUCCESS = "✅"
|
||||
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 VConfigs
|
||||
|
||||
|
||||
class Helper(Singleton):
|
||||
def __init__(self) -> None:
|
||||
if not super().created:
|
||||
config = VConfigs()
|
||||
self.HELP_SKIP = 'Skip the current playing song.'
|
||||
self.HELP_SKIP_LONG = 'Skip the playing of the current song, does not work if loop one is activated. \n\nArguments: None.'
|
||||
self.HELP_RESUME = 'Resumes the song player.'
|
||||
self.HELP_RESUME_LONG = 'If the player if paused, return the playing. \n\nArguments: None.'
|
||||
self.HELP_CLEAR = 'Clear the queue and songs history.'
|
||||
self.HELP_CLEAR_LONG = 'Clear the songs queue and songs history. \n\nArguments: None.'
|
||||
self.HELP_STOP = 'Stop the song player.'
|
||||
self.HELP_STOP_LONG = 'Stop the song player, clear queue and history and remove Vulkan from voice channel.\n\nArguments: None.'
|
||||
self.HELP_LOOP = 'Control the loop of songs.'
|
||||
self.HELP_LOOP_LONG = """Control the loop of songs.\n\n Require: A song being played.\nArguments:
|
||||
One - Start looping the current song.
|
||||
All - Start looping all songs in queue.
|
||||
Off - Disable loop."""
|
||||
self.HELP_NP = 'Show the info of the current song.'
|
||||
self.HELP_NP_LONG = 'Show the information of the song being played.\n\nRequire: A song being played.\nArguments: None.'
|
||||
self.HELP_QUEUE = f'Show the first {config.MAX_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.'
|
||||
101
Config/Messages.py
Normal file
101
Config/Messages.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from Config.Singleton import Singleton
|
||||
from Config.Configs import VConfigs
|
||||
from Config.Emojis import VEmojis
|
||||
|
||||
|
||||
class Messages(Singleton):
|
||||
def __init__(self) -> None:
|
||||
if not super().created:
|
||||
self.__emojis = VEmojis()
|
||||
configs = VConfigs()
|
||||
self.STARTUP_MESSAGE = 'Starting Vulkan...'
|
||||
self.STARTUP_COMPLETE_MESSAGE = 'Vulkan is now operating.'
|
||||
|
||||
self.SONGINFO_UPLOADER = "Uploader: "
|
||||
self.SONGINFO_DURATION = "Duration: "
|
||||
self.SONGINFO_REQUESTER = 'Requester: '
|
||||
self.SONGINFO_POSITION = 'Position: '
|
||||
|
||||
self.SONGS_ADDED = 'Downloading `{}` songs to add to the queue'
|
||||
self.SONG_ADDED = 'Downloading the song `{}` to add to the queue'
|
||||
self.SONG_ADDED_TWO = f'{self.__emojis.MUSIC} Song added to the queue'
|
||||
self.SONG_PLAYING = f'{self.__emojis.MUSIC} Song playing now'
|
||||
self.SONG_PLAYER = f'{self.__emojis.MUSIC} Song Player'
|
||||
self.QUEUE_TITLE = f'{self.__emojis.MUSIC} Songs in Queue'
|
||||
self.ONE_SONG_LOOPING = f'{self.__emojis.MUSIC} Looping One Song'
|
||||
self.ALL_SONGS_LOOPING = f'{self.__emojis.MUSIC} Looping All Songs'
|
||||
self.SONG_PAUSED = f'{self.__emojis.PAUSE} Song paused'
|
||||
self.SONG_RESUMED = f'{self.__emojis.PLAY} Song playing'
|
||||
self.EMPTY_QUEUE = f'{self.__emojis.QUEUE} Song queue is empty, use {configs.BOT_PREFIX}play to add new songs'
|
||||
self.SONG_DOWNLOADING = f'{self.__emojis.DOWNLOADING} Downloading...'
|
||||
|
||||
self.HISTORY_TITLE = f'{self.__emojis.MUSIC} Played Songs'
|
||||
self.HISTORY_EMPTY = f'{self.__emojis.QUEUE} There is no musics in history'
|
||||
|
||||
self.SONG_MOVED_SUCCESSFULLY = 'Song `{}` in position `{}` moved to the position `{}` successfully'
|
||||
self.SONG_REMOVED_SUCCESSFULLY = 'Song `{}` removed successfully'
|
||||
|
||||
self.LOOP_ALL_ON = f'{self.__emojis.ERROR} Vulkan is looping all songs, use {configs.BOT_PREFIX}loop off to disable this loop first'
|
||||
self.LOOP_ONE_ON = f'{self.__emojis.ERROR} Vulkan is looping one song, use {configs.BOT_PREFIX}loop off to disable this loop first'
|
||||
self.LOOP_ALL_ALREADY_ON = f'{self.__emojis.LOOP_ALL} Vulkan is already looping all songs'
|
||||
self.LOOP_ONE_ALREADY_ON = f'{self.__emojis.LOOP_ONE} Vulkan is already looping the current song'
|
||||
self.LOOP_ALL_ACTIVATE = f'{self.__emojis.LOOP_ALL} Looping all songs'
|
||||
self.LOOP_ONE_ACTIVATE = f'{self.__emojis.LOOP_ONE} Looping the current song'
|
||||
self.LOOP_DISABLE = f'{self.__emojis.LOOP_OFF} Loop disabled'
|
||||
self.LOOP_ALREADY_DISABLE = f'{self.__emojis.ERROR} Loop is already disabled'
|
||||
self.LOOP_ON = f'{self.__emojis.ERROR} This command cannot be invoked with any loop activated. Use {configs.BOT_PREFIX}loop off to disable loop'
|
||||
self.BAD_USE_OF_LOOP = f"""{self.__emojis.ERROR} Invalid arguments of Loop command. Use {configs.BOT_PREFIX}help loop to more information.
|
||||
-> Available Arguments: ["all", "off", "one", ""]"""
|
||||
|
||||
self.SONGS_SHUFFLED = f'{self.__emojis.SHUFFLE} Songs shuffled successfully'
|
||||
self.ERROR_SHUFFLING = f'{self.__emojis.ERROR} Error while shuffling the songs'
|
||||
self.ERROR_MOVING = f'{self.__emojis.ERROR} Error while moving the songs'
|
||||
self.LENGTH_ERROR = f'{self.__emojis.ERROR} Numbers must be between 1 and queue length, use -1 for the last song'
|
||||
self.ERROR_NUMBER = f'{self.__emojis.ERROR} This command require a number'
|
||||
self.ERROR_PLAYING = f'{self.__emojis.ERROR} Error while playing songs'
|
||||
self.COMMAND_NOT_FOUND = f'{self.__emojis.ERROR} Command not found, type {configs.BOT_PREFIX}help to see all commands'
|
||||
self.UNKNOWN_ERROR = f'{self.__emojis.ERROR} Unknown Error, if needed, use {configs.BOT_PREFIX}reset to reset the player of your server'
|
||||
self.ERROR_MISSING_ARGUMENTS = f'{self.__emojis.ERROR} Missing arguments in this command. Type {configs.BOT_PREFIX}help "command" to see more info about this command'
|
||||
self.NOT_PREVIOUS = f'{self.__emojis.ERROR} There is none previous song to play'
|
||||
self.PLAYER_NOT_PLAYING = f'{self.__emojis.ERROR} No song playing. Use {configs.BOT_PREFIX}play to start the player'
|
||||
self.IMPOSSIBLE_MOVE = 'That is impossible :('
|
||||
self.ERROR_TITLE = 'Error :-('
|
||||
self.COMMAND_NOT_FOUND_TITLE = 'This is strange :-('
|
||||
self.NO_CHANNEL = 'To play some music, connect to any voice channel first.'
|
||||
self.NO_GUILD = f'This server does not has a Player, try {configs.BOT_PREFIX}reset'
|
||||
self.INVALID_INPUT = f'This URL was too strange, try something better or type {configs.BOT_PREFIX}help play'
|
||||
self.DOWNLOADING_ERROR = f"{self.__emojis.ERROR} It's impossible to download and play this video"
|
||||
self.EXTRACTING_ERROR = f'{self.__emojis.ERROR} An error ocurred while searching for the songs'
|
||||
|
||||
self.ERROR_IN_PROCESS = f"{self.__emojis.ERROR} Due to a internal error your player was restarted, skipping the song."
|
||||
self.MY_ERROR_BAD_COMMAND = 'This string serves to verify if some error was raised by myself on purpose'
|
||||
self.BAD_COMMAND_TITLE = 'Misuse of command'
|
||||
self.BAD_COMMAND = f'{self.__emojis.ERROR} Bad usage of this command, type {configs.BOT_PREFIX}help "command" to understand the command better'
|
||||
self.VIDEO_UNAVAILABLE = f'{self.__emojis.ERROR} Sorry. This video is unavailable for download.'
|
||||
self.ERROR_DUE_LOOP_ONE_ON = f'{self.__emojis.ERROR} This command cannot be executed with loop one activated. Use {configs.BOT_PREFIX}loop off to disable loop.'
|
||||
|
||||
|
||||
class SearchMessages(Singleton):
|
||||
def __init__(self) -> None:
|
||||
if not super().created:
|
||||
config = VConfigs()
|
||||
self.UNKNOWN_INPUT = f'This type of input was too strange, try something else or type {config.BOT_PREFIX}help play'
|
||||
self.UNKNOWN_INPUT_TITLE = 'Nothing Found'
|
||||
self.GENERIC_TITLE = 'URL could not be processed'
|
||||
self.SPOTIFY_NOT_FOUND = 'Spotify could not process any songs with this input, verify your link or try again later.'
|
||||
self.YOUTUBE_NOT_FOUND = 'Youtube could not process any songs with this input, verify your link or try again later.'
|
||||
self.DEEZER_NOT_FOUND = 'Deezer could not process any songs with this input, verify your link or try again later.'
|
||||
|
||||
|
||||
class SpotifyMessages(Singleton):
|
||||
def __init__(self) -> None:
|
||||
if not super().created:
|
||||
self.INVALID_SPOTIFY_URL = 'Invalid Spotify URL, verify your link.'
|
||||
self.GENERIC_TITLE = 'URL could not be processed'
|
||||
|
||||
|
||||
class DeezerMessages(Singleton):
|
||||
def __init__(self) -> None:
|
||||
if not super().created:
|
||||
self.INVALID_DEEZER_URL = 'Invalid Deezer URL, verify your link.'
|
||||
self.GENERIC_TITLE = 'URL could not be processed'
|
||||
16
Config/Singleton.py
Normal file
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
95
DiscordCogs/ControlCog.py
Normal file
95
DiscordCogs/ControlCog.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from discord import Embed
|
||||
from discord.ext.commands import Cog, command
|
||||
from Config.Configs import VConfigs
|
||||
from Config.Helper import Helper
|
||||
from Config.Colors import VColors
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from Config.Embeds import VEmbeds
|
||||
|
||||
helper = Helper()
|
||||
|
||||
|
||||
class ControlCog(Cog):
|
||||
"""Class to handle discord events"""
|
||||
|
||||
def __init__(self, bot: VulkanBot):
|
||||
self.__bot = bot
|
||||
self.__config = VConfigs()
|
||||
self.__colors = VColors()
|
||||
self.__embeds = VEmbeds()
|
||||
self.__commands = {
|
||||
'MUSIC': ['resume', 'pause', 'loop', 'stop',
|
||||
'skip', 'play', 'queue', 'clear',
|
||||
'np', 'shuffle', 'move', 'remove',
|
||||
'reset', 'prev', 'history'],
|
||||
'RANDOM': ['choose', 'cara', 'random']
|
||||
|
||||
}
|
||||
|
||||
@command(name="help", help=helper.HELP_HELP, description=helper.HELP_HELP_LONG, aliases=['h', 'ajuda'])
|
||||
async def help_msg(self, ctx, command_help=''):
|
||||
if command_help != '':
|
||||
for command in self.__bot.commands:
|
||||
if command.name == command_help:
|
||||
txt = command.description if command.description else command.help
|
||||
|
||||
embedhelp = Embed(
|
||||
title=f'**Description of {command_help}** command',
|
||||
description=txt,
|
||||
colour=self.__colors.BLUE
|
||||
)
|
||||
|
||||
await ctx.send(embed=embedhelp)
|
||||
return
|
||||
|
||||
embedhelp = Embed(
|
||||
title='Help',
|
||||
description=f'Command {command_help} do not exists, type {self.__config.BOT_PREFIX}help to see all commands',
|
||||
colour=self.__colors.BLACK
|
||||
)
|
||||
|
||||
await ctx.send(embed=embedhelp)
|
||||
else:
|
||||
|
||||
helptxt = ''
|
||||
help_music = '🎧 `MUSIC`\n'
|
||||
help_random = '🎲 `RANDOM`\n'
|
||||
help_help = '👾 `HELP`\n'
|
||||
|
||||
for command in self.__bot.commands:
|
||||
if command.name in self.__commands['MUSIC']:
|
||||
help_music += f'**{command}** - {command.help}\n'
|
||||
|
||||
elif command.name in self.__commands['RANDOM']:
|
||||
help_random += f'**{command}** - {command.help}\n'
|
||||
|
||||
else:
|
||||
help_help += f'**{command}** - {command.help}\n'
|
||||
|
||||
helptxt = f'\n{help_music}\n{help_help}\n{help_random}'
|
||||
helptxt += f'\n\nType {self.__config.BOT_PREFIX}help "command" for more information about the command chosen'
|
||||
embedhelp = Embed(
|
||||
title=f'**Available Commands of {self.__bot.user.name}**',
|
||||
description=helptxt,
|
||||
colour=self.__colors.BLUE
|
||||
)
|
||||
|
||||
embedhelp.set_thumbnail(url=self.__bot.user.avatar)
|
||||
await ctx.send(embed=embedhelp)
|
||||
|
||||
@command(name='invite', help=helper.HELP_INVITE, description=helper.HELP_INVITE_LONG, aliases=['convite', 'inv', 'convidar'])
|
||||
async def invite_bot(self, ctx):
|
||||
invite_url = self.__config.INVITE_URL.format(self.__bot.user.id)
|
||||
txt = self.__config.INVITE_MESSAGE.format(invite_url, invite_url)
|
||||
|
||||
embed = Embed(
|
||||
title="Invite Vulkan",
|
||||
description=txt,
|
||||
colour=self.__colors.BLUE
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(ControlCog(bot))
|
||||
243
DiscordCogs/MusicCog.py
Normal file
243
DiscordCogs/MusicCog.py
Normal file
@@ -0,0 +1,243 @@
|
||||
from discord.ext.commands import Context, command, Cog
|
||||
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 UI.Responses.EmoteCogResponse import EmoteCommandResponse
|
||||
from UI.Responses.EmbedCogResponse import EmbedCommandResponse
|
||||
from UI.Views.PlayerView import PlayerView
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from Config.Configs import VConfigs
|
||||
from Parallelism.ProcessManager import ProcessManager
|
||||
|
||||
helper = Helper()
|
||||
|
||||
|
||||
class MusicCog(Cog):
|
||||
"""
|
||||
Class to listen to Music commands
|
||||
It'll listen for commands from discord, when triggered will create a specific Handler for the command
|
||||
Execute the handler and then create a specific View to be showed in Discord
|
||||
"""
|
||||
|
||||
def __init__(self, bot: VulkanBot) -> None:
|
||||
self.__bot: VulkanBot = bot
|
||||
VConfigs().setProcessManager(ProcessManager(bot))
|
||||
|
||||
@command(name="play", help=helper.HELP_PLAY, description=helper.HELP_PLAY_LONG, aliases=['p', 'tocar'])
|
||||
async def play(self, ctx: Context, *args) -> None:
|
||||
try:
|
||||
controller = PlayHandler(ctx, self.__bot)
|
||||
|
||||
response = await controller.run(args)
|
||||
if response is not None:
|
||||
view1 = EmbedCommandResponse(response)
|
||||
view2 = EmoteCommandResponse(response)
|
||||
await view1.run()
|
||||
await view2.run()
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN COG] -> {e}')
|
||||
|
||||
@command(name="queue", help=helper.HELP_QUEUE, description=helper.HELP_QUEUE_LONG, aliases=['q', 'fila', 'musicas'])
|
||||
async def queue(self, ctx: Context) -> None:
|
||||
try:
|
||||
controller = QueueHandler(ctx, self.__bot)
|
||||
|
||||
response = await controller.run()
|
||||
view2 = EmbedCommandResponse(response)
|
||||
await view2.run()
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN COG] -> {e}')
|
||||
|
||||
@command(name="skip", help=helper.HELP_SKIP, description=helper.HELP_SKIP_LONG, aliases=['s', 'pular', 'next'])
|
||||
async def skip(self, ctx: Context) -> None:
|
||||
try:
|
||||
controller = SkipHandler(ctx, self.__bot)
|
||||
|
||||
response = await controller.run()
|
||||
if response.success:
|
||||
view = EmoteCommandResponse(response)
|
||||
else:
|
||||
view = EmbedCommandResponse(response)
|
||||
|
||||
await view.run()
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN COG] -> {e}')
|
||||
|
||||
@command(name='stop', help=helper.HELP_STOP, description=helper.HELP_STOP_LONG, aliases=['parar'])
|
||||
async def stop(self, ctx: Context) -> None:
|
||||
try:
|
||||
controller = StopHandler(ctx, self.__bot)
|
||||
|
||||
response = await controller.run()
|
||||
if response.success:
|
||||
view = EmoteCommandResponse(response)
|
||||
else:
|
||||
view = EmbedCommandResponse(response)
|
||||
|
||||
await view.run()
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN COG] -> {e}')
|
||||
|
||||
@command(name='pause', help=helper.HELP_PAUSE, description=helper.HELP_PAUSE_LONG, aliases=['pausar', 'pare'])
|
||||
async def pause(self, ctx: Context) -> None:
|
||||
try:
|
||||
controller = PauseHandler(ctx, self.__bot)
|
||||
|
||||
response = await controller.run()
|
||||
view1 = EmoteCommandResponse(response)
|
||||
view2 = EmbedCommandResponse(response)
|
||||
await view1.run()
|
||||
await view2.run()
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN COG] -> {e}')
|
||||
|
||||
@command(name='resume', help=helper.HELP_RESUME, description=helper.HELP_RESUME_LONG, aliases=['soltar', 'despausar'])
|
||||
async def resume(self, ctx: Context) -> None:
|
||||
try:
|
||||
controller = ResumeHandler(ctx, self.__bot)
|
||||
|
||||
response = await controller.run()
|
||||
view1 = EmoteCommandResponse(response)
|
||||
view2 = EmbedCommandResponse(response)
|
||||
await view1.run()
|
||||
await view2.run()
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN COG] -> {e}')
|
||||
|
||||
@command(name='prev', help=helper.HELP_PREV, description=helper.HELP_PREV_LONG, aliases=['anterior', 'return', 'previous', 'back'])
|
||||
async def prev(self, ctx: Context) -> None:
|
||||
try:
|
||||
controller = PrevHandler(ctx, self.__bot)
|
||||
|
||||
response = await controller.run()
|
||||
if response is not None:
|
||||
view1 = EmbedCommandResponse(response)
|
||||
view2 = EmoteCommandResponse(response)
|
||||
await view1.run()
|
||||
await view2.run()
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN COG] -> {e}')
|
||||
|
||||
@command(name='history', help=helper.HELP_HISTORY, description=helper.HELP_HISTORY_LONG, aliases=['historico', 'anteriores', 'hist'])
|
||||
async def history(self, ctx: Context) -> None:
|
||||
try:
|
||||
controller = HistoryHandler(ctx, self.__bot)
|
||||
|
||||
response = await controller.run()
|
||||
view1 = EmbedCommandResponse(response)
|
||||
view2 = EmoteCommandResponse(response)
|
||||
await view1.run()
|
||||
await view2.run()
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN COG] -> {e}')
|
||||
|
||||
@command(name='loop', help=helper.HELP_LOOP, description=helper.HELP_LOOP_LONG, aliases=['l', 'repeat'])
|
||||
async def loop(self, ctx: Context, args='') -> None:
|
||||
try:
|
||||
controller = LoopHandler(ctx, self.__bot)
|
||||
|
||||
response = await controller.run(args)
|
||||
view1 = EmoteCommandResponse(response)
|
||||
view2 = EmbedCommandResponse(response)
|
||||
await view1.run()
|
||||
await view2.run()
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN COG] -> {e}')
|
||||
|
||||
@command(name='clear', help=helper.HELP_CLEAR, description=helper.HELP_CLEAR_LONG, aliases=['c', 'limpar'])
|
||||
async def clear(self, ctx: Context) -> None:
|
||||
try:
|
||||
controller = ClearHandler(ctx, self.__bot)
|
||||
|
||||
response = await controller.run()
|
||||
view = EmoteCommandResponse(response)
|
||||
await view.run()
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN COG] -> {e}')
|
||||
|
||||
@command(name='np', help=helper.HELP_NP, description=helper.HELP_NP_LONG, aliases=['playing', 'now', 'this'])
|
||||
async def now_playing(self, ctx: Context) -> None:
|
||||
try:
|
||||
controller = NowPlayingHandler(ctx, self.__bot)
|
||||
|
||||
response = await controller.run()
|
||||
view1 = EmbedCommandResponse(response)
|
||||
view2 = EmoteCommandResponse(response)
|
||||
await view1.run()
|
||||
await view2.run()
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN COG] -> {e}')
|
||||
|
||||
@command(name='shuffle', help=helper.HELP_SHUFFLE, description=helper.HELP_SHUFFLE_LONG, aliases=['aleatorio', 'misturar'])
|
||||
async def shuffle(self, ctx: Context) -> None:
|
||||
try:
|
||||
controller = ShuffleHandler(ctx, self.__bot)
|
||||
|
||||
response = await controller.run()
|
||||
view1 = EmbedCommandResponse(response)
|
||||
view2 = EmoteCommandResponse(response)
|
||||
await view1.run()
|
||||
await view2.run()
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN COG] -> {e}')
|
||||
|
||||
@command(name='move', help=helper.HELP_MOVE, description=helper.HELP_MOVE_LONG, aliases=['m', 'mover'])
|
||||
async def move(self, ctx: Context, pos1, pos2='1') -> None:
|
||||
try:
|
||||
controller = MoveHandler(ctx, self.__bot)
|
||||
|
||||
response = await controller.run(pos1, pos2)
|
||||
view1 = EmbedCommandResponse(response)
|
||||
view2 = EmoteCommandResponse(response)
|
||||
await view1.run()
|
||||
await view2.run()
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN COG] -> {e}')
|
||||
|
||||
@command(name='remove', help=helper.HELP_REMOVE, description=helper.HELP_REMOVE_LONG, aliases=['remover'])
|
||||
async def remove(self, ctx: Context, position) -> None:
|
||||
try:
|
||||
controller = RemoveHandler(ctx, self.__bot)
|
||||
|
||||
response = await controller.run(position)
|
||||
view1 = EmbedCommandResponse(response)
|
||||
view2 = EmoteCommandResponse(response)
|
||||
await view1.run()
|
||||
await view2.run()
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN COG] -> {e}')
|
||||
|
||||
@command(name='reset', help=helper.HELP_RESET, description=helper.HELP_RESET_LONG, aliases=['resetar'])
|
||||
async def reset(self, ctx: Context) -> None:
|
||||
try:
|
||||
controller = ResetHandler(ctx, self.__bot)
|
||||
|
||||
response = await controller.run()
|
||||
view1 = EmbedCommandResponse(response)
|
||||
view2 = EmoteCommandResponse(response)
|
||||
await view1.run()
|
||||
await view2.run()
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN COG] -> {e}')
|
||||
|
||||
@command(name='rafael')
|
||||
async def rafael(self, ctx: Context) -> None:
|
||||
view = PlayerView(self.__bot)
|
||||
await ctx.send(view=view)
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(MusicCog(bot))
|
||||
64
DiscordCogs/RandomCog.py
Normal file
64
DiscordCogs/RandomCog.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from random import randint, random
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from discord.ext.commands import Context, command, Cog
|
||||
from Config.Helper import Helper
|
||||
from Config.Embeds import VEmbeds
|
||||
|
||||
helper = Helper()
|
||||
|
||||
|
||||
class RandomCog(Cog):
|
||||
"""Class to listen to commands of type Random"""
|
||||
|
||||
def __init__(self, bot: VulkanBot):
|
||||
self.__embeds = VEmbeds()
|
||||
|
||||
@command(name='random', help=helper.HELP_RANDOM, description=helper.HELP_RANDOM_LONG, aliases=['rand'])
|
||||
async def random(self, ctx: Context, arg: str) -> None:
|
||||
try:
|
||||
arg = int(arg)
|
||||
|
||||
except:
|
||||
embed = self.__embeds.ERROR_NUMBER()
|
||||
await ctx.send(embed=embed)
|
||||
return None
|
||||
|
||||
if arg < 1:
|
||||
a = arg
|
||||
b = 1
|
||||
else:
|
||||
a = 1
|
||||
b = arg
|
||||
|
||||
x = randint(a, b)
|
||||
embed = self.__embeds.RANDOM_NUMBER(a, b, x)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@command(name='cara', help=helper.HELP_CARA, description=helper.HELP_CARA_LONG, aliases=['coroa'])
|
||||
async def cara(self, ctx: Context) -> None:
|
||||
x = random()
|
||||
if x < 0.5:
|
||||
result = 'cara'
|
||||
else:
|
||||
result = 'coroa'
|
||||
|
||||
embed = self.__embeds.CARA_COROA(result)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@command(name='choose', help=helper.HELP_CHOOSE, description=helper.HELP_CHOOSE_LONG, aliases=['escolha', 'pick'])
|
||||
async def choose(self, ctx, *args: str) -> None:
|
||||
try:
|
||||
user_input = " ".join(args)
|
||||
itens = user_input.split(sep=',')
|
||||
|
||||
index = randint(0, len(itens)-1)
|
||||
|
||||
embed = self.__embeds.CHOSEN_THING(itens[index])
|
||||
await ctx.send(embed=embed)
|
||||
except:
|
||||
embed = self.__embeds.BAD_CHOOSE_USE()
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(RandomCog(bot))
|
||||
82
Handlers/AbstractHandler.py
Normal file
82
Handlers/AbstractHandler.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Union
|
||||
from discord.ext.commands import Context
|
||||
from discord import Client, Guild, ClientUser, Interaction, Member, User
|
||||
from Config.Messages import Messages
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
from Config.Configs import VConfigs
|
||||
from Config.Helper import Helper
|
||||
from Config.Embeds import VEmbeds
|
||||
|
||||
|
||||
class AbstractHandler(ABC):
|
||||
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
|
||||
self.__bot: VulkanBot = bot
|
||||
self.__guild: Guild = ctx.guild
|
||||
self.__ctx: Context = ctx
|
||||
self.__bot_user: ClientUser = self.__bot.user
|
||||
self.__id = self.__bot_user.id
|
||||
self.__messages = Messages()
|
||||
self.__config = VConfigs()
|
||||
self.__helper = Helper()
|
||||
self.__embeds = VEmbeds()
|
||||
self.__bot_member: Member = self.__get_member()
|
||||
if isinstance(ctx, Context):
|
||||
self.__author = ctx.author
|
||||
else:
|
||||
self.__author = ctx.user
|
||||
|
||||
@abstractmethod
|
||||
async def run(self) -> HandlerResponse:
|
||||
pass
|
||||
|
||||
@property
|
||||
def id(self) -> int:
|
||||
return self.__id
|
||||
|
||||
@property
|
||||
def bot_member(self) -> Member:
|
||||
return self.__bot_member
|
||||
|
||||
@property
|
||||
def bot_user(self) -> ClientUser:
|
||||
return self.__bot_user
|
||||
|
||||
@property
|
||||
def author(self) -> User:
|
||||
return self.__author
|
||||
|
||||
@property
|
||||
def guild(self) -> Guild:
|
||||
return self.__guild
|
||||
|
||||
@property
|
||||
def bot(self) -> Client:
|
||||
return self.__bot
|
||||
|
||||
@property
|
||||
def config(self) -> VConfigs:
|
||||
return self.__config
|
||||
|
||||
@property
|
||||
def messages(self) -> Messages:
|
||||
return self.__messages
|
||||
|
||||
@property
|
||||
def helper(self) -> Helper:
|
||||
return self.__helper
|
||||
|
||||
@property
|
||||
def ctx(self) -> Union[Context, Interaction]:
|
||||
return self.__ctx
|
||||
|
||||
@property
|
||||
def embeds(self) -> VEmbeds:
|
||||
return self.__embeds
|
||||
|
||||
def __get_member(self) -> Member:
|
||||
guild_members: List[Member] = self.__guild.members
|
||||
for member in guild_members:
|
||||
if member.id == self.__id:
|
||||
return member
|
||||
29
Handlers/ClearHandler.py
Normal file
29
Handlers/ClearHandler.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from typing import Union
|
||||
from discord import Interaction
|
||||
from discord.ext.commands import Context
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from Handlers.AbstractHandler import AbstractHandler
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
|
||||
|
||||
class ClearHandler(AbstractHandler):
|
||||
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
|
||||
super().__init__(ctx, bot)
|
||||
|
||||
async def run(self) -> HandlerResponse:
|
||||
# Get the current process of the guild
|
||||
processManager = self.config.getProcessManager()
|
||||
processInfo = 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()
|
||||
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, Interaction
|
||||
|
||||
|
||||
class HandlerResponse:
|
||||
def __init__(self, ctx: Union[Context, Interaction], 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) -> Union[Context, Interaction]:
|
||||
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
|
||||
41
Handlers/HistoryHandler.py
Normal file
41
Handlers/HistoryHandler.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from discord.ext.commands import Context
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from Handlers.AbstractHandler import AbstractHandler
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
from Utils.Utils import Utils
|
||||
from typing import Union
|
||||
from discord import Interaction
|
||||
|
||||
|
||||
class HistoryHandler(AbstractHandler):
|
||||
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
|
||||
super().__init__(ctx, bot)
|
||||
|
||||
async def run(self) -> HandlerResponse:
|
||||
# Get the current process of the guild
|
||||
processManager = self.config.getProcessManager()
|
||||
processInfo = processManager.getRunningPlayerInfo(self.guild)
|
||||
if processInfo:
|
||||
processLock = processInfo.getLock()
|
||||
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
|
||||
if acquired:
|
||||
playlist = processInfo.getPlaylist()
|
||||
history = playlist.getSongsHistory()
|
||||
processLock.release()
|
||||
else:
|
||||
# If the player doesn't respond in time we restart it
|
||||
processManager.resetProcess(self.guild, self.ctx)
|
||||
embed = self.embeds.PLAYER_RESTARTED()
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
else:
|
||||
history = []
|
||||
|
||||
if len(history) == 0:
|
||||
text = self.messages.HISTORY_EMPTY
|
||||
else:
|
||||
text = f'\n📜 History Length: {len(history)} | Max: {self.config.MAX_SONGS_HISTORY}\n'
|
||||
for pos, song in enumerate(history, start=1):
|
||||
text += f"**`{pos}` - ** {song.title} - `{Utils.format_time(song.duration)}`\n"
|
||||
|
||||
embed = self.embeds.HISTORY(text)
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
59
Handlers/LoopHandler.py
Normal file
59
Handlers/LoopHandler.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from discord.ext.commands import Context
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from Handlers.AbstractHandler import AbstractHandler
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
from Config.Exceptions import BadCommandUsage
|
||||
from typing import Union
|
||||
from discord import Interaction
|
||||
|
||||
|
||||
class LoopHandler(AbstractHandler):
|
||||
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
|
||||
super().__init__(ctx, bot)
|
||||
|
||||
async def run(self, args: str) -> HandlerResponse:
|
||||
# Get the current process of the guild
|
||||
processManager = self.config.getProcessManager()
|
||||
processInfo = processManager.getRunningPlayerInfo(self.guild)
|
||||
if not processInfo:
|
||||
embed = self.embeds.NOT_PLAYING()
|
||||
error = BadCommandUsage()
|
||||
return HandlerResponse(self.ctx, embed, error)
|
||||
|
||||
playlist = processInfo.getPlaylist()
|
||||
|
||||
processLock = processInfo.getLock()
|
||||
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
|
||||
if acquired:
|
||||
if args == '' or args is None:
|
||||
playlist.loop_all()
|
||||
embed = self.embeds.LOOP_ALL_ACTIVATED()
|
||||
processLock.release()
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
|
||||
args = args.lower()
|
||||
error = None
|
||||
if playlist.getCurrentSong() is None:
|
||||
embed = self.embeds.NOT_PLAYING()
|
||||
error = BadCommandUsage()
|
||||
return HandlerResponse(self.ctx, embed, error)
|
||||
|
||||
if args == 'one':
|
||||
playlist.loop_one()
|
||||
embed = self.embeds.LOOP_ONE_ACTIVATED()
|
||||
elif args == 'all':
|
||||
playlist.loop_all()
|
||||
embed = self.embeds.LOOP_ALL_ACTIVATED()
|
||||
elif args == 'off':
|
||||
playlist.loop_off()
|
||||
embed = self.embeds.LOOP_DISABLE()
|
||||
else:
|
||||
error = BadCommandUsage()
|
||||
embed = self.embeds.BAD_LOOP_USE()
|
||||
|
||||
processLock.release()
|
||||
return HandlerResponse(self.ctx, embed, error)
|
||||
else:
|
||||
processManager.resetProcess(self.guild, self.ctx)
|
||||
embed = self.embeds.PLAYER_RESTARTED()
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
75
Handlers/MoveHandler.py
Normal file
75
Handlers/MoveHandler.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from typing import Union
|
||||
from discord.ext.commands import Context
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from Handlers.AbstractHandler import AbstractHandler
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
from Config.Exceptions import BadCommandUsage, VulkanError, InvalidInput, NumberRequired, UnknownError
|
||||
from Music.Playlist import Playlist
|
||||
from typing import Union
|
||||
from discord import Interaction
|
||||
|
||||
|
||||
class MoveHandler(AbstractHandler):
|
||||
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
|
||||
super().__init__(ctx, bot)
|
||||
|
||||
async def run(self, pos1: str, pos2: str) -> HandlerResponse:
|
||||
processManager = self.config.getProcessManager()
|
||||
processInfo = processManager.getRunningPlayerInfo(self.guild)
|
||||
if not processInfo:
|
||||
embed = self.embeds.NOT_PLAYING()
|
||||
error = BadCommandUsage()
|
||||
return HandlerResponse(self.ctx, embed, error)
|
||||
|
||||
processLock = processInfo.getLock()
|
||||
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
|
||||
if acquired:
|
||||
error = self.__validateInput(pos1, pos2)
|
||||
if error:
|
||||
embed = self.embeds.ERROR_EMBED(error.message)
|
||||
processLock.release()
|
||||
return HandlerResponse(self.ctx, embed, error)
|
||||
|
||||
playlist = processInfo.getPlaylist()
|
||||
pos1, pos2 = self.__sanitizeInput(playlist, pos1, pos2)
|
||||
|
||||
if not playlist.validate_position(pos1) or not playlist.validate_position(pos2):
|
||||
error = InvalidInput()
|
||||
embed = self.embeds.PLAYLIST_RANGE_ERROR()
|
||||
processLock.release()
|
||||
return HandlerResponse(self.ctx, embed, error)
|
||||
try:
|
||||
song = playlist.move_songs(pos1, pos2)
|
||||
|
||||
song_name = song.title if song.title else song.identifier
|
||||
embed = self.embeds.SONG_MOVED(song_name, pos1, pos2)
|
||||
processLock.release()
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
except:
|
||||
# Release the acquired Lock
|
||||
processLock.release()
|
||||
embed = self.embeds.ERROR_MOVING()
|
||||
error = UnknownError()
|
||||
return HandlerResponse(self.ctx, embed, error)
|
||||
else:
|
||||
processManager.resetProcess(self.guild, self.ctx)
|
||||
embed = self.embeds.PLAYER_RESTARTED()
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
|
||||
def __validateInput(self, pos1: str, pos2: str) -> Union[VulkanError, None]:
|
||||
try:
|
||||
pos1 = int(pos1)
|
||||
pos2 = int(pos2)
|
||||
except:
|
||||
return NumberRequired(self.messages.ERROR_NUMBER)
|
||||
|
||||
def __sanitizeInput(self, playlist: Playlist, pos1: int, pos2: int) -> tuple:
|
||||
pos1 = int(pos1)
|
||||
pos2 = int(pos2)
|
||||
|
||||
if pos1 == -1:
|
||||
pos1 = len(playlist.getSongs())
|
||||
if pos2 == -1:
|
||||
pos2 = len(playlist.getSongs())
|
||||
|
||||
return pos1, pos2
|
||||
36
Handlers/NowPlayingHandler.py
Normal file
36
Handlers/NowPlayingHandler.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from discord.ext.commands import Context
|
||||
from Handlers.AbstractHandler import AbstractHandler
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from Utils.Cleaner import Cleaner
|
||||
from typing import Union
|
||||
from discord import Interaction
|
||||
|
||||
|
||||
class NowPlayingHandler(AbstractHandler):
|
||||
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
|
||||
super().__init__(ctx, bot)
|
||||
self.__cleaner = Cleaner()
|
||||
|
||||
async def run(self) -> HandlerResponse:
|
||||
# Get the current process of the guild
|
||||
processManager = self.config.getProcessManager()
|
||||
processInfo = processManager.getRunningPlayerInfo(self.guild)
|
||||
if not processInfo:
|
||||
embed = self.embeds.NOT_PLAYING()
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
|
||||
playlist = processInfo.getPlaylist()
|
||||
if playlist.getCurrentSong() is None:
|
||||
embed = self.embeds.NOT_PLAYING()
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
|
||||
if playlist.isLoopingOne():
|
||||
title = self.messages.ONE_SONG_LOOPING
|
||||
else:
|
||||
title = self.messages.SONG_PLAYING
|
||||
await self.__cleaner.clean_messages(self.ctx, self.config.CLEANER_MESSAGES_QUANT)
|
||||
|
||||
info = playlist.getCurrentSong().info
|
||||
embed = self.embeds.SONG_INFO(info, title)
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
26
Handlers/PauseHandler.py
Normal file
26
Handlers/PauseHandler.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from discord.ext.commands import Context
|
||||
from Handlers.AbstractHandler import AbstractHandler
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
from Parallelism.Commands import VCommands, VCommandsType
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from typing import Union
|
||||
from discord import Interaction
|
||||
|
||||
|
||||
class PauseHandler(AbstractHandler):
|
||||
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
|
||||
super().__init__(ctx, bot)
|
||||
|
||||
async def run(self) -> HandlerResponse:
|
||||
processManager = self.config.getProcessManager()
|
||||
processInfo = processManager.getRunningPlayerInfo(self.guild)
|
||||
if processInfo:
|
||||
# Send Pause command to be execute by player process
|
||||
command = VCommands(VCommandsType.PAUSE, None)
|
||||
queue = processInfo.getQueueToPlayer()
|
||||
queue.put(command)
|
||||
|
||||
return HandlerResponse(self.ctx)
|
||||
else:
|
||||
embed = self.embeds.NOT_PLAYING()
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
136
Handlers/PlayHandler.py
Normal file
136
Handlers/PlayHandler.py
Normal file
@@ -0,0 +1,136 @@
|
||||
import asyncio
|
||||
from typing import List
|
||||
from Config.Exceptions import DownloadingError, InvalidInput, VulkanError
|
||||
from discord.ext.commands import Context
|
||||
from Handlers.AbstractHandler import AbstractHandler
|
||||
from Config.Exceptions import ImpossibleMove, UnknownError
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
from Music.Downloader import Downloader
|
||||
from Music.Searcher import Searcher
|
||||
from Music.Song import Song
|
||||
from Parallelism.ProcessInfo import ProcessInfo
|
||||
from Parallelism.Commands import VCommands, VCommandsType
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from typing import Union
|
||||
from discord import Interaction
|
||||
|
||||
|
||||
class PlayHandler(AbstractHandler):
|
||||
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
|
||||
super().__init__(ctx, bot)
|
||||
self.__searcher = Searcher()
|
||||
self.__down = Downloader()
|
||||
|
||||
async def run(self, 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 = self.config.getProcessManager()
|
||||
processInfo = processManager.getPlayerInfo(self.guild, self.ctx)
|
||||
playlist = processInfo.getPlaylist()
|
||||
process = processInfo.getProcess()
|
||||
if not process.is_alive(): # If process has not yet started, start
|
||||
process.start()
|
||||
|
||||
# Create the Songs objects
|
||||
songs: List[Song] = []
|
||||
for musicInfo in musicsInfo:
|
||||
songs.append(Song(musicInfo, playlist, requester))
|
||||
|
||||
if len(songs) == 1:
|
||||
# If only one music, download it directly
|
||||
song = self.__down.finish_one_song(songs[0])
|
||||
if song.problematic: # If error in download song return
|
||||
embed = self.embeds.SONG_PROBLEMATIC()
|
||||
error = DownloadingError()
|
||||
return HandlerResponse(self.ctx, embed, error)
|
||||
|
||||
# If not playing
|
||||
if not playlist.getCurrentSong():
|
||||
embed = self.embeds.SONG_ADDED(song.title)
|
||||
response = HandlerResponse(self.ctx, embed)
|
||||
else: # If already playing
|
||||
pos = len(playlist.getSongs())
|
||||
embed = self.embeds.SONG_ADDED_TWO(song.info, pos)
|
||||
response = HandlerResponse(self.ctx, embed)
|
||||
|
||||
# Add the unique song to the playlist and send a command to player process
|
||||
processLock = processInfo.getLock()
|
||||
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
|
||||
if acquired:
|
||||
playlist.add_song(song)
|
||||
# Release the acquired Lock
|
||||
processLock.release()
|
||||
queue = processInfo.getQueueToPlayer()
|
||||
playCommand = VCommands(VCommandsType.PLAY, None)
|
||||
queue.put(playCommand)
|
||||
else:
|
||||
processManager.resetProcess(self.guild, self.ctx)
|
||||
embed = self.embeds.PLAYER_RESTARTED()
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
|
||||
return response
|
||||
else: # If multiple songs added
|
||||
# Trigger a task to download all songs and then store them in the process playlist
|
||||
asyncio.create_task(self.__downloadSongsAndStore(songs, processInfo))
|
||||
|
||||
embed = self.embeds.SONGS_ADDED(len(songs))
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
|
||||
except DownloadingError as error:
|
||||
embed = self.embeds.DOWNLOADING_ERROR()
|
||||
return HandlerResponse(self.ctx, embed, error)
|
||||
except Exception as error:
|
||||
if isinstance(error, VulkanError): # If error was already processed
|
||||
print(f'DEVELOPER NOTE -s> PlayController Error: {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.getQueueToPlayer()
|
||||
playCommand = VCommands(VCommandsType.PLAY, None)
|
||||
# Trigger a task for each song to be downloaded
|
||||
tasks: List[asyncio.Task] = []
|
||||
for song in songs:
|
||||
task = asyncio.create_task(self.__down.download_song(song))
|
||||
tasks.append(task)
|
||||
|
||||
# In the original order, await for the task and then if successfully downloaded add in the playlist
|
||||
processManager = self.config.getProcessManager()
|
||||
for index, task in enumerate(tasks):
|
||||
await task
|
||||
song = songs[index]
|
||||
if not song.problematic: # If downloaded add to the playlist and send play command
|
||||
processInfo = processManager.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
|
||||
54
Handlers/PrevHandler.py
Normal file
54
Handlers/PrevHandler.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from discord.ext.commands import Context
|
||||
from Handlers.AbstractHandler import AbstractHandler
|
||||
from Config.Exceptions import BadCommandUsage, ImpossibleMove
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
from Parallelism.Commands import VCommands, VCommandsType
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from typing import Union
|
||||
from discord import Interaction
|
||||
|
||||
|
||||
class PrevHandler(AbstractHandler):
|
||||
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
|
||||
super().__init__(ctx, bot)
|
||||
|
||||
async def run(self) -> HandlerResponse:
|
||||
processManager = self.config.getProcessManager()
|
||||
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.author.voice.channel.id)
|
||||
queue = processInfo.getQueueToPlayer()
|
||||
queue.put(prevCommand)
|
||||
return HandlerResponse(self.ctx)
|
||||
|
||||
def __user_connected(self) -> bool:
|
||||
if self.author.voice:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
65
Handlers/QueueHandler.py
Normal file
65
Handlers/QueueHandler.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from discord.ext.commands import Context
|
||||
from Handlers.AbstractHandler import AbstractHandler
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
from Music.Downloader import Downloader
|
||||
from Utils.Utils import Utils
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from typing import Union
|
||||
from discord import Interaction
|
||||
|
||||
|
||||
class QueueHandler(AbstractHandler):
|
||||
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
|
||||
super().__init__(ctx, bot)
|
||||
self.__down = Downloader()
|
||||
|
||||
async def run(self) -> HandlerResponse:
|
||||
# Retrieve the process of the guild
|
||||
processManager = self.config.getProcessManager()
|
||||
processInfo = processManager.getRunningPlayerInfo(self.guild)
|
||||
if not processInfo: # If no process return empty list
|
||||
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)
|
||||
65
Handlers/RemoveHandler.py
Normal file
65
Handlers/RemoveHandler.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from typing import Union
|
||||
from discord.ext.commands import Context
|
||||
from Handlers.AbstractHandler import AbstractHandler
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
from Config.Exceptions import BadCommandUsage, VulkanError, ErrorRemoving, InvalidInput, NumberRequired
|
||||
from Music.Playlist import Playlist
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from typing import Union
|
||||
from discord import Interaction
|
||||
|
||||
|
||||
class RemoveHandler(AbstractHandler):
|
||||
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
|
||||
super().__init__(ctx, bot)
|
||||
|
||||
async def run(self, position: str) -> HandlerResponse:
|
||||
# Get the current process of the guild
|
||||
processManager = self.config.getProcessManager()
|
||||
processInfo = 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
|
||||
26
Handlers/ResetHandler.py
Normal file
26
Handlers/ResetHandler.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from discord.ext.commands import Context
|
||||
from Handlers.AbstractHandler import AbstractHandler
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
from Parallelism.Commands import VCommands, VCommandsType
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from typing import Union
|
||||
from discord import Interaction
|
||||
|
||||
|
||||
class ResetHandler(AbstractHandler):
|
||||
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
|
||||
super().__init__(ctx, bot)
|
||||
|
||||
async def run(self) -> HandlerResponse:
|
||||
# Get the current process of the guild
|
||||
processManager = self.config.getProcessManager()
|
||||
processInfo = processManager.getRunningPlayerInfo(self.guild)
|
||||
if processInfo:
|
||||
command = VCommands(VCommandsType.RESET, None)
|
||||
queue = processInfo.getQueueToPlayer()
|
||||
queue.put(command)
|
||||
|
||||
return HandlerResponse(self.ctx)
|
||||
else:
|
||||
embed = self.embeds.NOT_PLAYING()
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
26
Handlers/ResumeHandler.py
Normal file
26
Handlers/ResumeHandler.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from discord.ext.commands import Context
|
||||
from Handlers.AbstractHandler import AbstractHandler
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
from Parallelism.Commands import VCommands, VCommandsType
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from typing import Union
|
||||
from discord import Interaction
|
||||
|
||||
|
||||
class ResumeHandler(AbstractHandler):
|
||||
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
|
||||
super().__init__(ctx, bot)
|
||||
|
||||
async def run(self) -> HandlerResponse:
|
||||
processManager = self.config.getProcessManager()
|
||||
processInfo = processManager.getRunningPlayerInfo(self.guild)
|
||||
if processInfo:
|
||||
# Send Resume command to be execute by player process
|
||||
command = VCommands(VCommandsType.RESUME, None)
|
||||
queue = processInfo.getQueueToPlayer()
|
||||
queue.put(command)
|
||||
|
||||
return HandlerResponse(self.ctx)
|
||||
else:
|
||||
embed = self.embeds.NOT_PLAYING()
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
42
Handlers/ShuffleHandler.py
Normal file
42
Handlers/ShuffleHandler.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from discord.ext.commands import Context
|
||||
from Handlers.AbstractHandler import AbstractHandler
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
from Config.Exceptions import UnknownError
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from typing import Union
|
||||
from discord import Interaction
|
||||
|
||||
|
||||
class ShuffleHandler(AbstractHandler):
|
||||
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
|
||||
super().__init__(ctx, bot)
|
||||
|
||||
async def run(self) -> HandlerResponse:
|
||||
processManager = self.config.getProcessManager()
|
||||
processInfo = processManager.getRunningPlayerInfo(self.guild)
|
||||
if processInfo:
|
||||
try:
|
||||
processLock = processInfo.getLock()
|
||||
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
|
||||
if acquired:
|
||||
playlist = processInfo.getPlaylist()
|
||||
playlist.shuffle()
|
||||
# Release the acquired Lock
|
||||
processLock.release()
|
||||
else:
|
||||
processManager.resetProcess(self.guild, self.ctx)
|
||||
embed = self.embeds.PLAYER_RESTARTED()
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
|
||||
embed = self.embeds.SONGS_SHUFFLED()
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
|
||||
except Exception as e:
|
||||
print(f'DEVELOPER NOTE -> Error Shuffling: {e}')
|
||||
error = UnknownError()
|
||||
embed = self.embeds.ERROR_SHUFFLING()
|
||||
|
||||
return HandlerResponse(self.ctx, embed, error)
|
||||
else:
|
||||
embed = self.embeds.NOT_PLAYING()
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
33
Handlers/SkipHandler.py
Normal file
33
Handlers/SkipHandler.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from discord.ext.commands import Context
|
||||
from Handlers.AbstractHandler import AbstractHandler
|
||||
from Config.Exceptions import BadCommandUsage
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from Parallelism.Commands import VCommands, VCommandsType
|
||||
from typing import Union
|
||||
from discord import Interaction
|
||||
|
||||
|
||||
class SkipHandler(AbstractHandler):
|
||||
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
|
||||
super().__init__(ctx, bot)
|
||||
|
||||
async def run(self) -> HandlerResponse:
|
||||
processManager = self.config.getProcessManager()
|
||||
processInfo = processManager.getRunningPlayerInfo(self.guild)
|
||||
if processInfo: # Verify if there is a running process
|
||||
playlist = processInfo.getPlaylist()
|
||||
if playlist.isLoopingOne():
|
||||
embed = self.embeds.ERROR_DUE_LOOP_ONE_ON()
|
||||
error = BadCommandUsage()
|
||||
return HandlerResponse(self.ctx, embed, error)
|
||||
|
||||
# Send a command to the player process to skip the music
|
||||
command = VCommands(VCommandsType.SKIP, None)
|
||||
queue = processInfo.getQueueToPlayer()
|
||||
queue.put(command)
|
||||
|
||||
return HandlerResponse(self.ctx)
|
||||
else:
|
||||
embed = self.embeds.NOT_PLAYING()
|
||||
return HandlerResponse(self.ctx, embed)
|
||||
26
Handlers/StopHandler.py
Normal file
26
Handlers/StopHandler.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from discord.ext.commands import Context
|
||||
from Handlers.AbstractHandler import AbstractHandler
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from Parallelism.Commands import VCommands, VCommandsType
|
||||
from typing import Union
|
||||
from discord import Interaction
|
||||
|
||||
|
||||
class StopHandler(AbstractHandler):
|
||||
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
|
||||
super().__init__(ctx, bot)
|
||||
|
||||
async def run(self) -> HandlerResponse:
|
||||
processManager = self.config.getProcessManager()
|
||||
processInfo = processManager.getRunningPlayerInfo(self.guild)
|
||||
if processInfo:
|
||||
# Send command to player process stop
|
||||
command = VCommands(VCommandsType.STOP, None)
|
||||
queue = processInfo.getQueueToPlayer()
|
||||
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 VConfigs
|
||||
from yt_dlp import YoutubeDL, DownloadError
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from Music.Song import Song
|
||||
from Utils.Utils import Utils, run_async
|
||||
from Config.Exceptions import DownloadingError
|
||||
|
||||
|
||||
class Downloader:
|
||||
config = VConfigs()
|
||||
__YDL_OPTIONS = {'format': 'bestaudio/best',
|
||||
'default_search': 'auto',
|
||||
'playliststart': 0,
|
||||
'extract_flat': False,
|
||||
'playlistend': config.MAX_PLAYLIST_LENGTH,
|
||||
'quiet': True
|
||||
}
|
||||
__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 = VConfigs()
|
||||
self.__music_keys_only = ['resolution', 'fps', 'quality']
|
||||
self.__not_extracted_keys_only = ['ie_key']
|
||||
self.__not_extracted_not_keys = ['entries']
|
||||
self.__playlist_keys = ['entries']
|
||||
|
||||
def finish_one_song(self, song: Song) -> Song:
|
||||
try:
|
||||
if song.identifier is None:
|
||||
return None
|
||||
|
||||
if Utils.is_url(song.identifier):
|
||||
song_info = self.__download_url(song.identifier)
|
||||
else:
|
||||
song_info = self.__download_title(song.identifier)
|
||||
|
||||
song.finish_down(song_info)
|
||||
return song
|
||||
# Convert yt_dlp error to my own error
|
||||
except DownloadError:
|
||||
raise DownloadingError()
|
||||
|
||||
@run_async
|
||||
def extract_info(self, url: str) -> List[dict]:
|
||||
if url == '':
|
||||
return []
|
||||
|
||||
if Utils.is_url(url): # If Url
|
||||
options = Downloader.__YDL_OPTIONS_EXTRACT
|
||||
with YoutubeDL(options) as ydl:
|
||||
try:
|
||||
extracted_info = ydl.extract_info(url, download=False)
|
||||
# Some links doesn't extract unless extract_flat key is passed as False in options
|
||||
if self.__failed_to_extract(extracted_info):
|
||||
extracted_info = self.__get_forced_extracted_info(url)
|
||||
|
||||
if self.__is_music(extracted_info):
|
||||
return [extracted_info['original_url']]
|
||||
|
||||
elif self.__is_multiple_musics(extracted_info):
|
||||
songs = []
|
||||
for song in extracted_info['entries']:
|
||||
songs.append(self.__BASE_URL.format(song['id']))
|
||||
return songs
|
||||
|
||||
else: # Failed to extract the songs
|
||||
print(f'DEVELOPER NOTE -> Failed to Extract URL {url}')
|
||||
return []
|
||||
# Convert the yt_dlp download error to own error
|
||||
except DownloadError:
|
||||
raise DownloadingError()
|
||||
except Exception as e:
|
||||
print(f'DEVELOPER NOTE -> Error Extracting Music: {e}, {type(e)}')
|
||||
raise e
|
||||
else:
|
||||
return []
|
||||
|
||||
def __get_forced_extracted_info(self, url: str) -> list:
|
||||
options = Downloader.__YDL_OPTIONS_FORCE_EXTRACT
|
||||
with YoutubeDL(options) as ydl:
|
||||
try:
|
||||
extracted_info = ydl.extract_info(url, download=False)
|
||||
return extracted_info
|
||||
|
||||
except Exception as e:
|
||||
print(f'DEVELOPER NOTE -> Error Forcing Extract Music: {e}')
|
||||
return []
|
||||
|
||||
def __download_url(self, url) -> dict:
|
||||
options = Downloader.__YDL_OPTIONS
|
||||
with YoutubeDL(options) as ydl:
|
||||
try:
|
||||
result = ydl.extract_info(url, download=False)
|
||||
|
||||
return result
|
||||
except Exception as e: # Any type of error in download
|
||||
print(f'DEVELOPER NOTE -> Error Downloading URL {e}')
|
||||
return None
|
||||
|
||||
async def download_song(self, song: Song) -> None:
|
||||
if song.source is not None: # If Music already preloaded
|
||||
return None
|
||||
|
||||
def __download_func(song: Song) -> None:
|
||||
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
|
||||
65
Music/MessagesController.py
Normal file
65
Music/MessagesController.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from typing import List
|
||||
from discord import Embed, Message, TextChannel
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from Parallelism.ProcessInfo import ProcessInfo
|
||||
from Config.Configs import VConfigs
|
||||
from Config.Messages import Messages
|
||||
from Music.Song import Song
|
||||
from Config.Embeds import VEmbeds
|
||||
from UI.Views.PlayerView import PlayerView
|
||||
|
||||
|
||||
class MessagesController:
|
||||
def __init__(self, bot: VulkanBot) -> None:
|
||||
self.__bot = bot
|
||||
self.__previousMessages = []
|
||||
self.__configs = VConfigs()
|
||||
self.__messages = Messages()
|
||||
self.__embeds = VEmbeds()
|
||||
|
||||
async def sendNowPlaying(self, processInfo: ProcessInfo, song: Song) -> None:
|
||||
# Get the lock of the playlist
|
||||
playlist = processInfo.getPlaylist()
|
||||
if playlist.isLoopingOne():
|
||||
title = self.__messages.ONE_SONG_LOOPING
|
||||
else:
|
||||
title = self.__messages.SONG_PLAYING
|
||||
|
||||
# Create View and Embed
|
||||
embed = self.__embeds.SONG_INFO(song.info, title)
|
||||
view = PlayerView(self.__bot)
|
||||
channel = processInfo.getTextChannel()
|
||||
# Delete the previous and send the message
|
||||
await self.__deletePreviousNPMessages()
|
||||
await channel.send(embed=embed, view=view)
|
||||
|
||||
# Get the sended message
|
||||
sendedMessage = await self.__getSendedMessage(channel)
|
||||
# Set the message witch contains the view
|
||||
view.set_message(message=sendedMessage)
|
||||
self.__previousMessages.append(sendedMessage)
|
||||
|
||||
async def __deletePreviousNPMessages(self) -> None:
|
||||
for message in self.__previousMessages:
|
||||
try:
|
||||
await message.delete()
|
||||
except:
|
||||
pass
|
||||
self.__previousMessages.clear()
|
||||
|
||||
async def __getSendedMessage(self, channel: TextChannel) -> Message:
|
||||
stringToIdentify = 'Uploader:'
|
||||
last_messages: List[Message] = await channel.history(limit=5).flatten()
|
||||
|
||||
for message in last_messages:
|
||||
try:
|
||||
if message.author == self.__bot.user:
|
||||
if len(message.embeds) > 0:
|
||||
embed: Embed = message.embeds[0]
|
||||
if len(embed.fields) > 0:
|
||||
if embed.fields[0].name == stringToIdentify:
|
||||
return message
|
||||
|
||||
except Exception as e:
|
||||
print(f'DEVELOPER NOTE -> Error cleaning messages {e}')
|
||||
continue
|
||||
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 VConfigs
|
||||
from Music.Song import Song
|
||||
import random
|
||||
|
||||
|
||||
class Playlist:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.__configs = VConfigs()
|
||||
self.__queue = deque() # Store the musics to play
|
||||
self.__songs_history = deque() # Store the musics played
|
||||
|
||||
self.__looping_one = False
|
||||
self.__looping_all = False
|
||||
|
||||
self.__current: Song = None
|
||||
|
||||
def getSongs(self) -> deque[Song]:
|
||||
return self.__queue
|
||||
|
||||
def validate_position(self, position: int) -> bool:
|
||||
if position not in range(1, len(self.__queue) + 1):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def validate_positions_list(self, positions: list) -> bool:
|
||||
for position in positions:
|
||||
if not self.validate_position(position):
|
||||
return False
|
||||
return True
|
||||
|
||||
def getSongsHistory(self) -> deque:
|
||||
return self.__songs_history
|
||||
|
||||
def isLoopingOne(self) -> bool:
|
||||
return self.__looping_one
|
||||
|
||||
def isLoopingAll(self) -> bool:
|
||||
return self.__looping_all
|
||||
|
||||
def getCurrentSong(self) -> Song:
|
||||
return self.__current
|
||||
|
||||
def setCurrentSong(self, song: Song) -> Song:
|
||||
self.__current = song
|
||||
|
||||
def getSongsToPreload(self) -> List[Song]:
|
||||
return list(self.__queue)[:self.__configs.MAX_PRELOAD_SONGS]
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.__queue)
|
||||
|
||||
def next_song(self) -> Song:
|
||||
if self.__current is None and len(self.__queue) == 0:
|
||||
return None
|
||||
|
||||
played_song = self.__current
|
||||
|
||||
# Att played song info
|
||||
if played_song != None:
|
||||
if not self.__looping_one and not self.__looping_all:
|
||||
if not played_song.problematic:
|
||||
self.__songs_history.appendleft(played_song)
|
||||
|
||||
if len(self.__songs_history) > self.__configs.MAX_SONGS_HISTORY:
|
||||
self.__songs_history.pop() # Remove the older
|
||||
|
||||
elif self.__looping_one: # Insert the current song to play again
|
||||
self.__queue.appendleft(played_song)
|
||||
|
||||
elif self.__looping_all: # Insert the current song in the end of queue
|
||||
self.__queue.append(played_song)
|
||||
|
||||
# Get the new song
|
||||
if len(self.__queue) == 0:
|
||||
self.__current = None
|
||||
return None
|
||||
|
||||
self.__current = 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:
|
||||
|
||||
|
||||
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"""
|
||||
def __init__(self, identifier: str, playlist, requester: str) -> None:
|
||||
self.__identifier = identifier
|
||||
self.__info = {'requester': requester}
|
||||
self.__problematic = False
|
||||
self.__playlist: IPlaylist = playlist
|
||||
self.__playlist = playlist
|
||||
|
||||
def finish_down(self, info: dict) -> None:
|
||||
"""Get and store the full information of the song"""
|
||||
self.__usefull_keys = ['url', 'duration',
|
||||
'title', 'webpage_url',
|
||||
'channel', 'id', 'uploader',
|
||||
'thumbnail', 'original_url']
|
||||
if info is None:
|
||||
self.destroy()
|
||||
return None
|
||||
|
||||
for key in self.__usefull_keys:
|
||||
try:
|
||||
self.__useful_keys = ['duration',
|
||||
'title', 'webpage_url',
|
||||
'channel', 'id', 'uploader',
|
||||
'thumbnail', 'original_url']
|
||||
self.__required_keys = ['url']
|
||||
|
||||
for key in self.__required_keys:
|
||||
if key in info.keys():
|
||||
self.__info[key] = info[key]
|
||||
else:
|
||||
print(f'DEVELOPER NOTE -> {key} not found in info of music: {self.identifier}')
|
||||
self.destroy()
|
||||
return
|
||||
|
||||
for key in self.__useful_keys:
|
||||
if key in info.keys():
|
||||
self.__info[key] = info[key]
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@property
|
||||
def source(self) -> str:
|
||||
"""Return the Song Source URL to play"""
|
||||
if 'url' in self.__info.keys():
|
||||
return self.__info['url']
|
||||
else:
|
||||
@@ -34,7 +38,6 @@ class Song(ISong):
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
"""Return the Song Title"""
|
||||
if 'title' in self.__info.keys():
|
||||
return self.__info['title']
|
||||
else:
|
||||
@@ -42,7 +45,6 @@ class Song(ISong):
|
||||
|
||||
@property
|
||||
def duration(self) -> str:
|
||||
"""Return the Song Title"""
|
||||
if 'duration' in self.__info.keys():
|
||||
return self.__info['duration']
|
||||
else:
|
||||
@@ -57,7 +59,7 @@ class Song(ISong):
|
||||
return self.__problematic
|
||||
|
||||
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.__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 VConfigs
|
||||
from Config.Messages import SpotifyMessages
|
||||
|
||||
|
||||
class SpotifySearch():
|
||||
def __init__(self) -> None:
|
||||
self.__messages = SpotifyMessages()
|
||||
self.__config = VConfigs()
|
||||
self.__connected = False
|
||||
self.__connect()
|
||||
|
||||
def __connect(self) -> None:
|
||||
try:
|
||||
auth = SpotifyClientCredentials(self.__config.SPOTIFY_ID, self.__config.SPOTIFY_SECRET)
|
||||
self.__api = Spotify(auth_manager=auth)
|
||||
self.__connected = True
|
||||
except Exception as e:
|
||||
print(f'DEVELOPER NOTE -> Spotify Connection Error {e}')
|
||||
|
||||
def search(self, url: str) -> list:
|
||||
if not self.__checkUrlValid(url):
|
||||
raise SpotifyError(self.__messages.INVALID_SPOTIFY_URL, self.__messages.GENERIC_TITLE)
|
||||
|
||||
type = url.split('/')[3].split('?')[0]
|
||||
code = url.split('/')[4].split('?')[0]
|
||||
musics = []
|
||||
|
||||
try:
|
||||
if self.__connected:
|
||||
if type == 'album':
|
||||
musics = self.__get_album(code)
|
||||
elif type == 'playlist':
|
||||
musics = self.__get_playlist(code)
|
||||
elif type == 'track':
|
||||
musics = self.__get_track(code)
|
||||
elif type == 'artist':
|
||||
musics = self.__get_artist(code)
|
||||
|
||||
return musics
|
||||
except SpotifyException:
|
||||
raise SpotifyError(self.__messages.INVALID_SPOTIFY_URL, self.__messages.GENERIC_TITLE)
|
||||
|
||||
def __get_album(self, code: str) -> list:
|
||||
results = self.__api.album_tracks(code)
|
||||
musics = results['items']
|
||||
|
||||
while results['next']: # Get the next pages
|
||||
results = self.__api.next(results)
|
||||
musics.extend(results['items'])
|
||||
|
||||
musicsTitle = []
|
||||
|
||||
for music in musics:
|
||||
title = self.__extract_title(music)
|
||||
musicsTitle.append(title)
|
||||
|
||||
return musicsTitle
|
||||
|
||||
def __get_playlist(self, code: str) -> list:
|
||||
results = self.__api.playlist_items(code)
|
||||
itens = results['items']
|
||||
|
||||
while results['next']: # Load the next pages
|
||||
results = self.__api.next(results)
|
||||
itens.extend(results['items'])
|
||||
|
||||
musics = []
|
||||
for item in itens:
|
||||
musics.append(item['track'])
|
||||
|
||||
titles = []
|
||||
for music in musics:
|
||||
title = self.__extract_title(music)
|
||||
titles.append(title)
|
||||
|
||||
return titles
|
||||
|
||||
def __get_track(self, code: str) -> list:
|
||||
results = self.__api.track(code)
|
||||
name = results['name']
|
||||
artists = ''
|
||||
for artist in results['artists']:
|
||||
artists += f'{artist["name"]} '
|
||||
|
||||
return [f'{name} {artists}']
|
||||
|
||||
def __get_artist(self, code: str) -> list:
|
||||
results = self.__api.artist_top_tracks(code, country='BR')
|
||||
|
||||
musics_titles = []
|
||||
for music in results['tracks']:
|
||||
title = self.__extract_title(music)
|
||||
musics_titles.append(title)
|
||||
|
||||
return musics_titles
|
||||
|
||||
def __extract_title(self, music: dict) -> str:
|
||||
title = f'{music["name"]} '
|
||||
for artist in music['artists']:
|
||||
title += f'{artist["name"]} '
|
||||
|
||||
return title
|
||||
|
||||
def __checkUrlValid(self, url: str) -> bool:
|
||||
try:
|
||||
type = url.split('/')[3].split('?')[0]
|
||||
code = url.split('/')[4].split('?')[0]
|
||||
|
||||
if type == '' or code == '':
|
||||
return False
|
||||
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
9
Music/Types.py
Normal file
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'
|
||||
73
Music/VulkanBot.py
Normal file
73
Music/VulkanBot.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from asyncio import AbstractEventLoop
|
||||
from discord import Guild, Status, Game, Message
|
||||
from discord.ext.commands.errors import CommandNotFound, MissingRequiredArgument
|
||||
from Config.Configs import VConfigs
|
||||
from discord.ext.commands import Bot, Context
|
||||
from Config.Messages import Messages
|
||||
from Config.Embeds import VEmbeds
|
||||
|
||||
|
||||
class VulkanBot(Bot):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__configs = VConfigs()
|
||||
self.__messages = Messages()
|
||||
self.__embeds = VEmbeds()
|
||||
self.remove_command("help")
|
||||
|
||||
def startBot(self) -> None:
|
||||
"""Blocking function that will start the bot"""
|
||||
if self.__configs.BOT_TOKEN == '':
|
||||
print('DEVELOPER NOTE -> Token not found')
|
||||
exit()
|
||||
|
||||
super().run(self.__configs.BOT_TOKEN, reconnect=True)
|
||||
|
||||
async def startBotCoro(self, loop: AbstractEventLoop) -> None:
|
||||
"""Start a bot coroutine, does not wait for connection to be established"""
|
||||
task = loop.create_task(self.__login())
|
||||
await task
|
||||
loop.create_task(self.__connect())
|
||||
|
||||
async def __login(self):
|
||||
"""Coroutine to login the Bot in discord"""
|
||||
await self.login(token=self.__configs.BOT_TOKEN)
|
||||
|
||||
async def __connect(self):
|
||||
"""Coroutine to connect the Bot in discord"""
|
||||
await self.connect(reconnect=True)
|
||||
|
||||
async def on_ready(self):
|
||||
print(self.__messages.STARTUP_MESSAGE)
|
||||
await self.change_presence(status=Status.online, activity=Game(name=f"Vulkan | {self.__configs.BOT_PREFIX}help"))
|
||||
print(self.__messages.STARTUP_COMPLETE_MESSAGE)
|
||||
|
||||
async def on_command_error(self, ctx, error):
|
||||
if isinstance(error, MissingRequiredArgument):
|
||||
embed = self.__embeds.MISSING_ARGUMENTS()
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
elif isinstance(error, CommandNotFound):
|
||||
embed = self.__embeds.COMMAND_NOT_FOUND()
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
else:
|
||||
print(f'DEVELOPER NOTE -> Command Error: {error}')
|
||||
embed = self.__embeds.UNKNOWN_ERROR()
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
async def process_commands(self, message: Message):
|
||||
if message.author.bot:
|
||||
return
|
||||
|
||||
ctx = await self.get_context(message, cls=Context)
|
||||
|
||||
if ctx.valid and not message.guild:
|
||||
return
|
||||
|
||||
await self.invoke(ctx)
|
||||
|
||||
|
||||
class Context(Context):
|
||||
bot: VulkanBot
|
||||
guild: Guild
|
||||
55
Music/VulkanInitializer.py
Normal file
55
Music/VulkanInitializer.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from random import choices
|
||||
import string
|
||||
from discord.bot import Bot
|
||||
from discord import Intents
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from os import listdir
|
||||
from Config.Configs import VConfigs
|
||||
from Config.Exceptions import VulkanError
|
||||
|
||||
|
||||
class VulkanInitializer:
|
||||
def __init__(self, willListen: bool) -> None:
|
||||
self.__config = VConfigs()
|
||||
self.__intents = Intents.default()
|
||||
self.__intents.message_content = True
|
||||
self.__intents.members = True
|
||||
self.__bot = self.__create_bot(willListen)
|
||||
self.__add_cogs(self.__bot)
|
||||
|
||||
def getBot(self) -> VulkanBot:
|
||||
return self.__bot
|
||||
|
||||
def __create_bot(self, willListen: bool) -> VulkanBot:
|
||||
if willListen:
|
||||
prefix = self.__config.BOT_PREFIX
|
||||
else:
|
||||
prefix = ''.join(choices(string.ascii_uppercase + string.digits, k=4))
|
||||
|
||||
bot = VulkanBot(command_prefix=prefix,
|
||||
pm_help=True,
|
||||
case_insensitive=True,
|
||||
intents=self.__intents)
|
||||
return bot
|
||||
|
||||
def __add_cogs(self, bot: Bot) -> None:
|
||||
try:
|
||||
cogsStatus = []
|
||||
for filename in listdir(f'./{self.__config.COMMANDS_PATH}'):
|
||||
if filename.endswith('.py'):
|
||||
cogPath = f'{self.__config.COMMANDS_PATH}.{filename[:-3]}'
|
||||
cogsStatus.append(bot.load_extension(cogPath, store=True))
|
||||
|
||||
if len(bot.cogs.keys()) != self.__getTotalCogs():
|
||||
print(cogsStatus)
|
||||
raise VulkanError(message='Failed to load some Cog')
|
||||
|
||||
except VulkanError as e:
|
||||
print(f'[Error Loading Vulkan] -> {e.message}')
|
||||
|
||||
def __getTotalCogs(self) -> int:
|
||||
quant = 0
|
||||
for filename in listdir(f'./{self.__config.COMMANDS_PATH}'):
|
||||
if filename.endswith('.py'):
|
||||
quant += 1
|
||||
return quant
|
||||
28
Parallelism/Commands.py
Normal file
28
Parallelism/Commands.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from enum import Enum
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class VCommandsType(Enum):
|
||||
PREV = 'Prev'
|
||||
SKIP = 'Skip'
|
||||
PAUSE = 'Pause'
|
||||
RESUME = 'Resume'
|
||||
CONTEXT = 'Context'
|
||||
PLAY = 'Play'
|
||||
STOP = 'Stop'
|
||||
RESET = 'Reset'
|
||||
NOW_PLAYING = 'Now Playing'
|
||||
TERMINATE = 'Terminate'
|
||||
SLEEPING = 'Sleeping'
|
||||
|
||||
|
||||
class VCommands:
|
||||
def __init__(self, type: VCommandsType, args=None) -> None:
|
||||
self.__type = type
|
||||
self.__args = args
|
||||
|
||||
def getType(self) -> VCommandsType:
|
||||
return self.__type
|
||||
|
||||
def getArgs(self) -> Tuple:
|
||||
return self.__args
|
||||
348
Parallelism/PlayerProcess.py
Normal file
348
Parallelism/PlayerProcess.py
Normal file
@@ -0,0 +1,348 @@
|
||||
import asyncio
|
||||
from Music.VulkanInitializer import VulkanInitializer
|
||||
from discord import User, Member, Message
|
||||
from asyncio import AbstractEventLoop, Semaphore, Queue
|
||||
from multiprocessing import Process, RLock, Lock, Queue
|
||||
from threading import Thread
|
||||
from typing import Callable, List
|
||||
from discord import Guild, FFmpegPCMAudio, VoiceChannel, TextChannel
|
||||
from Music.Playlist import Playlist
|
||||
from Music.Song import Song
|
||||
from Config.Configs import VConfigs
|
||||
from Config.Messages import Messages
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from Config.Embeds import VEmbeds
|
||||
from Parallelism.Commands import VCommands, VCommandsType
|
||||
|
||||
|
||||
class TimeoutClock:
|
||||
def __init__(self, callback: Callable, loop: asyncio.AbstractEventLoop):
|
||||
self.__callback = callback
|
||||
self.__task = loop.create_task(self.__executor())
|
||||
|
||||
async def __executor(self):
|
||||
await asyncio.sleep(VConfigs().VC_TIMEOUT)
|
||||
await self.__callback()
|
||||
|
||||
def cancel(self):
|
||||
self.__task.cancel()
|
||||
|
||||
|
||||
class PlayerProcess(Process):
|
||||
"""Process that will play songs, receive commands from the main process by a Queue"""
|
||||
|
||||
def __init__(self, name: str, playlist: Playlist, lock: Lock, queueToReceive: Queue, queueToSend: Queue, guildID: int, textID: int, voiceID: int, authorID: int) -> None:
|
||||
"""
|
||||
Start a new process that will have his own bot instance
|
||||
Due to pickle serialization, no objects are stored, the values initialization are being made in the run method
|
||||
"""
|
||||
Process.__init__(self, name=name, group=None, target=None, args=(), kwargs={})
|
||||
# Synchronization objects
|
||||
self.__playlist: Playlist = playlist
|
||||
self.__playlistLock: Lock = lock
|
||||
self.__queueReceive: Queue = queueToReceive
|
||||
self.__queueSend: Queue = queueToSend
|
||||
self.__semStopPlaying: Semaphore = None
|
||||
self.__loop: AbstractEventLoop = None
|
||||
# Discord context ID
|
||||
self.__textChannelID = textID
|
||||
self.__guildID = guildID
|
||||
self.__voiceChannelID = voiceID
|
||||
self.__authorID = authorID
|
||||
# All information of discord context will be retrieved directly with discord API
|
||||
self.__guild: Guild = None
|
||||
self.__bot: VulkanBot = None
|
||||
self.__voiceChannel: VoiceChannel = None
|
||||
self.__textChannel: TextChannel = None
|
||||
self.__author: User = None
|
||||
self.__botMember: Member = None
|
||||
|
||||
self.__configs: VConfigs = None
|
||||
self.__embeds: VEmbeds = None
|
||||
self.__messages: Messages = None
|
||||
self.__messagesToDelete: List[Message] = []
|
||||
self.__playing = False
|
||||
self.__forceStop = False
|
||||
self.FFMPEG_OPTIONS = {'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
|
||||
'options': '-vn'}
|
||||
|
||||
def run(self) -> None:
|
||||
"""Method called by process.start(), this will exec the actually _run method in a event loop"""
|
||||
try:
|
||||
print(f'Starting Process {self.name}')
|
||||
self.__playerLock = RLock()
|
||||
self.__loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
asyncio.set_event_loop(self.__loop)
|
||||
|
||||
self.__configs = VConfigs()
|
||||
self.__messages = Messages()
|
||||
self.__embeds = VEmbeds()
|
||||
|
||||
self.__semStopPlaying = Semaphore(0)
|
||||
self.__loop.run_until_complete(self._run())
|
||||
except Exception as e:
|
||||
print(f'[Error in Process {self.name}] -> {e}')
|
||||
|
||||
async def _run(self) -> None:
|
||||
# Recreate the bot instance and objects using discord API
|
||||
self.__bot = await self.__createBotInstance()
|
||||
self.__guild = self.__bot.get_guild(self.__guildID)
|
||||
self.__voiceChannel = self.__bot.get_channel(self.__voiceChannelID)
|
||||
self.__textChannel = self.__bot.get_channel(self.__textChannelID)
|
||||
self.__author = self.__bot.get_channel(self.__authorID)
|
||||
self.__botMember = self.__getBotMember()
|
||||
# Connect to voice Channel
|
||||
await self.__connectToVoiceChannel()
|
||||
|
||||
# Start the timeout function
|
||||
self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop)
|
||||
# Thread that will receive commands to be executed in this Process
|
||||
self.__commandsReceiver = Thread(target=self.__commandsReceiver, daemon=True)
|
||||
self.__commandsReceiver.start()
|
||||
|
||||
# Start a Task to play songs
|
||||
self.__loop.create_task(self.__playPlaylistSongs())
|
||||
# Try to acquire a semaphore, it'll be release when timeout function trigger, we use the Semaphore
|
||||
# from the asyncio lib to not block the event loop
|
||||
await self.__semStopPlaying.acquire()
|
||||
# In this point the process should finalize
|
||||
self.__timer.cancel()
|
||||
|
||||
async def __playPlaylistSongs(self) -> None:
|
||||
"""If the player is not running trigger to play a new song"""
|
||||
if not self.__playing:
|
||||
song = None
|
||||
with self.__playlistLock:
|
||||
with self.__playerLock:
|
||||
if not (self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused()):
|
||||
song = self.__playlist.next_song()
|
||||
|
||||
if song is not None:
|
||||
self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}')
|
||||
|
||||
async def __playSong(self, song: Song) -> None:
|
||||
"""Function that will trigger the player to play the song"""
|
||||
try:
|
||||
self.__playerLock.acquire()
|
||||
if song is None:
|
||||
return
|
||||
|
||||
if song.source is None:
|
||||
return self.__playNext(None)
|
||||
|
||||
# If not connected, connect to bind channel
|
||||
if self.__guild.voice_client is None:
|
||||
await self.__connectToVoiceChannel()
|
||||
|
||||
# If the player is already playing return
|
||||
if self.__guild.voice_client.is_playing():
|
||||
return
|
||||
|
||||
self.__playing = True
|
||||
self.__playingSong = song
|
||||
|
||||
player = FFmpegPCMAudio(song.source, **self.FFMPEG_OPTIONS)
|
||||
self.__guild.voice_client.play(player, after=lambda e: self.__playNext(e))
|
||||
|
||||
self.__timer.cancel()
|
||||
self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop)
|
||||
|
||||
nowPlayingCommand = VCommands(VCommandsType.NOW_PLAYING, song)
|
||||
self.__queueSend.put(nowPlayingCommand)
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN PLAY SONG] -> {e}, {type(e)}')
|
||||
self.__playNext(None)
|
||||
finally:
|
||||
self.__playerLock.release()
|
||||
|
||||
def __playNext(self, error) -> None:
|
||||
with self.__playlistLock:
|
||||
with self.__playerLock:
|
||||
if self.__forceStop: # If it's forced to stop player
|
||||
self.__forceStop = False
|
||||
return None
|
||||
|
||||
song = self.__playlist.next_song()
|
||||
|
||||
if song is not None:
|
||||
self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}')
|
||||
else:
|
||||
self.__playlist.loop_off()
|
||||
self.__playingSong = None
|
||||
self.__playing = False
|
||||
# Send a command to the main process put this one to sleep
|
||||
sleepCommand = VCommands(VCommandsType.SLEEPING)
|
||||
self.__queueSend.put(sleepCommand)
|
||||
# Release the semaphore to finish the process
|
||||
self.__semStopPlaying.release()
|
||||
|
||||
async def __playPrev(self, voiceChannelID: int) -> None:
|
||||
with self.__playlistLock:
|
||||
song = self.__playlist.prev_song()
|
||||
|
||||
with self.__playerLock:
|
||||
if song is not None:
|
||||
if self.__guild.voice_client is None: # If not connect, connect to the user voice channel
|
||||
self.__voiceChannelID = voiceChannelID
|
||||
self.__voiceChannel = self.__guild.get_channel(self.__voiceChannelID)
|
||||
await self.__connectToVoiceChannel()
|
||||
|
||||
# If already playing, stop the current play
|
||||
if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused():
|
||||
# Will forbidden next_song to execute after stopping current player
|
||||
self.__forceStop = True
|
||||
self.__guild.voice_client.stop()
|
||||
self.__playing = False
|
||||
|
||||
self.__loop.create_task(self.__playSong(song), name=f'Song {song.identifier}')
|
||||
|
||||
def __commandsReceiver(self) -> None:
|
||||
while True:
|
||||
command: VCommands = self.__queueReceive.get()
|
||||
type = command.getType()
|
||||
args = command.getArgs()
|
||||
|
||||
try:
|
||||
self.__playerLock.acquire()
|
||||
if type == VCommandsType.PAUSE:
|
||||
self.__pause()
|
||||
elif type == VCommandsType.RESUME:
|
||||
self.__resume()
|
||||
elif type == VCommandsType.SKIP:
|
||||
self.__skip()
|
||||
elif type == VCommandsType.PLAY:
|
||||
asyncio.run_coroutine_threadsafe(self.__playPlaylistSongs(), self.__loop)
|
||||
elif type == VCommandsType.PREV:
|
||||
asyncio.run_coroutine_threadsafe(self.__playPrev(args), self.__loop)
|
||||
elif type == VCommandsType.RESET:
|
||||
asyncio.run_coroutine_threadsafe(self.__reset(), self.__loop)
|
||||
elif type == VCommandsType.STOP:
|
||||
asyncio.run_coroutine_threadsafe(self.__stop(), self.__loop)
|
||||
else:
|
||||
print(f'[ERROR] -> Unknown Command Received: {command}')
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN COMMAND RECEIVER] -> {type} - {e}')
|
||||
finally:
|
||||
self.__playerLock.release()
|
||||
|
||||
def __pause(self) -> None:
|
||||
if self.__guild.voice_client is not None:
|
||||
if self.__guild.voice_client.is_playing():
|
||||
self.__guild.voice_client.pause()
|
||||
|
||||
async def __reset(self) -> None:
|
||||
if self.__guild.voice_client is None:
|
||||
return
|
||||
# Reset the bot
|
||||
self.__guild.voice_client.stop()
|
||||
await self.__guild.voice_client.disconnect()
|
||||
self.__playlist.clear()
|
||||
self.__playlist.loop_off()
|
||||
await self.__botMember.move_to(None)
|
||||
# Release semaphore to finish the current player process
|
||||
self.__semStopPlaying.release()
|
||||
|
||||
async def __stop(self) -> None:
|
||||
if self.__guild.voice_client is not None:
|
||||
if self.__guild.voice_client.is_connected():
|
||||
with self.__playlistLock:
|
||||
self.__playlist.loop_off()
|
||||
|
||||
# Send a command to the main process put this to sleep
|
||||
sleepCommand = VCommands(VCommandsType.SLEEPING)
|
||||
self.__queueSend.put(sleepCommand)
|
||||
self.__guild.voice_client.stop()
|
||||
self.__playingSong = None
|
||||
await self.__guild.voice_client.disconnect()
|
||||
self.__semStopPlaying.release()
|
||||
|
||||
def __resume(self) -> None:
|
||||
# Lock to work with Player
|
||||
with self.__playerLock:
|
||||
if self.__guild.voice_client is not None:
|
||||
if self.__guild.voice_client.is_paused():
|
||||
self.__guild.voice_client.resume()
|
||||
|
||||
def __skip(self) -> None:
|
||||
# Lock to work with Player
|
||||
with self.__playerLock:
|
||||
if self.__guild.voice_client is not None and self.__playing:
|
||||
self.__playing = False
|
||||
self.__guild.voice_client.stop()
|
||||
|
||||
async def __forceStop(self) -> None:
|
||||
# Lock to work with Player
|
||||
with self.__playerLock:
|
||||
if self.__guild.voice_client is None:
|
||||
return
|
||||
|
||||
self.__guild.voice_client.stop()
|
||||
await self.__guild.voice_client.disconnect()
|
||||
with self.__playlistLock:
|
||||
self.__playlist.clear()
|
||||
self.__playlist.loop_off()
|
||||
|
||||
async def __createBotInstance(self) -> VulkanBot:
|
||||
"""Load a new bot instance that should not be directly called."""
|
||||
initializer = VulkanInitializer(willListen=False)
|
||||
bot = initializer.getBot()
|
||||
|
||||
await bot.startBotCoro(self.__loop)
|
||||
await self.__ensureDiscordConnection(bot)
|
||||
return bot
|
||||
|
||||
async def __timeoutHandler(self) -> None:
|
||||
try:
|
||||
if self.__guild.voice_client is None:
|
||||
return
|
||||
|
||||
if self.__guild.voice_client.is_playing() or self.__guild.voice_client.is_paused():
|
||||
if not self.__isBotAloneInChannel(): # If bot is not alone continue to play
|
||||
self.__timer = TimeoutClock(self.__timeoutHandler, self.__loop)
|
||||
return
|
||||
|
||||
# Finish the process
|
||||
if self.__guild.voice_client.is_connected():
|
||||
with self.__playerLock:
|
||||
with self.__playlistLock:
|
||||
self.__playlist.loop_off()
|
||||
self.__playing = False
|
||||
await self.__guild.voice_client.disconnect()
|
||||
# Send command to main process to finish this one
|
||||
sleepCommand = VCommands(VCommandsType.SLEEPING)
|
||||
self.__queueSend.put(sleepCommand)
|
||||
# Release semaphore to finish process
|
||||
self.__semStopPlaying.release()
|
||||
except Exception as e:
|
||||
print(f'[Error in Timeout] -> {e}')
|
||||
|
||||
def __isBotAloneInChannel(self) -> bool:
|
||||
try:
|
||||
if len(self.__guild.voice_client.channel.members) <= 1:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN CHECK BOT ALONE] -> {e}')
|
||||
return False
|
||||
|
||||
async def __ensureDiscordConnection(self, bot: VulkanBot) -> None:
|
||||
"""Await in this point until connection to discord is established"""
|
||||
guild = None
|
||||
while guild is None:
|
||||
guild = bot.get_guild(self.__guildID)
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
async def __connectToVoiceChannel(self) -> bool:
|
||||
try:
|
||||
await self.__voiceChannel.connect(reconnect=True, timeout=None)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f'[ERROR CONNECTING TO VC] -> {e}')
|
||||
return False
|
||||
|
||||
def __getBotMember(self) -> Member:
|
||||
guild_members: List[Member] = self.__guild.members
|
||||
for member in guild_members:
|
||||
if member.id == self.__bot.user.id:
|
||||
return member
|
||||
38
Parallelism/ProcessInfo.py
Normal file
38
Parallelism/ProcessInfo.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from multiprocessing import Process, Queue, Lock
|
||||
from discord import TextChannel
|
||||
from Music.Playlist import Playlist
|
||||
|
||||
|
||||
class ProcessInfo:
|
||||
"""
|
||||
Class to store the reference to all structures to maintain a player process
|
||||
"""
|
||||
|
||||
def __init__(self, process: Process, queueToPlayer: Queue, queueToMain: Queue, playlist: Playlist, lock: Lock, textChannel: TextChannel) -> None:
|
||||
self.__process = process
|
||||
self.__queueToPlayer = queueToPlayer
|
||||
self.__queueToMain = queueToMain
|
||||
self.__playlist = playlist
|
||||
self.__lock = lock
|
||||
self.__textChannel = textChannel
|
||||
|
||||
def setProcess(self, newProcess: Process) -> None:
|
||||
self.__process = newProcess
|
||||
|
||||
def getProcess(self) -> Process:
|
||||
return self.__process
|
||||
|
||||
def getQueueToPlayer(self) -> Queue:
|
||||
return self.__queueToPlayer
|
||||
|
||||
def getQueueToMain(self) -> Queue:
|
||||
return self.__queueToMain
|
||||
|
||||
def getPlaylist(self) -> Playlist:
|
||||
return self.__playlist
|
||||
|
||||
def getLock(self) -> Lock:
|
||||
return self.__lock
|
||||
|
||||
def getTextChannel(self) -> TextChannel:
|
||||
return self.__textChannel
|
||||
188
Parallelism/ProcessManager.py
Normal file
188
Parallelism/ProcessManager.py
Normal file
@@ -0,0 +1,188 @@
|
||||
import asyncio
|
||||
from multiprocessing import Lock, Queue
|
||||
from multiprocessing.managers import BaseManager, NamespaceProxy
|
||||
from queue import Empty
|
||||
from threading import Thread
|
||||
from typing import Dict, Tuple, Union
|
||||
from Config.Singleton import Singleton
|
||||
from discord import Guild, Interaction
|
||||
from discord.ext.commands import Context
|
||||
from Music.MessagesController import MessagesController
|
||||
from Music.Song import Song
|
||||
from Parallelism.PlayerProcess import PlayerProcess
|
||||
from Music.Playlist import Playlist
|
||||
from Parallelism.ProcessInfo import ProcessInfo
|
||||
from Parallelism.Commands import VCommands, VCommandsType
|
||||
from Music.VulkanBot import VulkanBot
|
||||
|
||||
|
||||
class ProcessManager(Singleton):
|
||||
"""
|
||||
Manage all running player process, creating and storing them for future calls
|
||||
Deal with the creation of shared memory
|
||||
"""
|
||||
|
||||
def __init__(self, bot: VulkanBot = None) -> None:
|
||||
if not super().created:
|
||||
self.__bot = bot
|
||||
VManager.register('Playlist', Playlist)
|
||||
self.__manager = VManager()
|
||||
self.__manager.start()
|
||||
self.__playersProcess: Dict[Guild, ProcessInfo] = {}
|
||||
self.__playersListeners: Dict[Guild, Tuple[Thread, bool]] = {}
|
||||
self.__playersMessages: Dict[Guild, MessagesController] = {}
|
||||
|
||||
def setPlayerInfo(self, guild: Guild, info: ProcessInfo):
|
||||
self.__playersProcess[guild.id] = info
|
||||
|
||||
def getPlayerInfo(self, guild: Guild, context: Union[Context, Interaction]) -> ProcessInfo:
|
||||
"""Return the process info for the guild, if not and context is a instance
|
||||
of discord.Context then create one, else return None"""
|
||||
try:
|
||||
if isinstance(context, Interaction):
|
||||
if guild.id not in self.__playersProcess.keys():
|
||||
return None
|
||||
else:
|
||||
return self.__playersProcess[guild.id]
|
||||
|
||||
if guild.id not in self.__playersProcess.keys():
|
||||
self.__playersProcess[guild.id] = self.__createProcessInfo(guild, context)
|
||||
else:
|
||||
# If the process has ended create a new one
|
||||
if not self.__playersProcess[guild.id].getProcess().is_alive():
|
||||
self.__playersProcess[guild.id] = self.__recreateProcess(guild, context)
|
||||
|
||||
return self.__playersProcess[guild.id]
|
||||
except Exception as e:
|
||||
print(f'[Error In GetPlayerContext] -> {e}')
|
||||
|
||||
def resetProcess(self, guild: Guild, context: Context) -> None:
|
||||
"""Restart a running process, already start it to return to play"""
|
||||
if guild.id not in self.__playersProcess.keys():
|
||||
return None
|
||||
|
||||
# Recreate the process keeping the playlist
|
||||
newProcessInfo = self.__recreateProcess(guild, context)
|
||||
newProcessInfo.getProcess().start() # Start the process
|
||||
# Send a command to start the play again
|
||||
playCommand = VCommands(VCommandsType.PLAY)
|
||||
newProcessInfo.getQueueToPlayer().put(playCommand)
|
||||
self.__playersProcess[guild.id] = newProcessInfo
|
||||
|
||||
def getRunningPlayerInfo(self, guild: Guild) -> ProcessInfo:
|
||||
"""Return the process info for the guild, if not, return None"""
|
||||
if guild.id not in self.__playersProcess.keys():
|
||||
return None
|
||||
|
||||
return self.__playersProcess[guild.id]
|
||||
|
||||
def __createProcessInfo(self, guild: Guild, context: Context) -> ProcessInfo:
|
||||
guildID: int = context.guild.id
|
||||
textID: int = context.channel.id
|
||||
voiceID: int = context.author.voice.channel.id
|
||||
authorID: int = context.author.id
|
||||
|
||||
playlist: Playlist = self.__manager.Playlist()
|
||||
lock = Lock()
|
||||
queueToListen = Queue()
|
||||
queueToSend = Queue()
|
||||
process = PlayerProcess(context.guild.name, playlist, lock, queueToSend,
|
||||
queueToListen, guildID, textID, voiceID, authorID)
|
||||
processInfo = ProcessInfo(process, queueToSend, queueToListen,
|
||||
playlist, lock, context.channel)
|
||||
|
||||
# Create a Thread to listen for the queue coming from the Player Process, this will redirect the Queue to a async
|
||||
thread = Thread(target=self.__listenToCommands,
|
||||
args=(queueToListen, guild), daemon=True)
|
||||
self.__playersListeners[guildID] = (thread, False)
|
||||
thread.start()
|
||||
|
||||
# Create a Message Controller for this player
|
||||
self.__playersMessages[guildID] = MessagesController(self.__bot)
|
||||
|
||||
return processInfo
|
||||
|
||||
def __recreateProcess(self, guild: Guild, context: Context) -> ProcessInfo:
|
||||
"""Create a new process info using previous playlist"""
|
||||
guildID: int = context.guild.id
|
||||
textID: int = context.channel.id
|
||||
voiceID: int = context.author.voice.channel.id
|
||||
authorID: int = context.author.id
|
||||
|
||||
playlist: Playlist = self.__playersProcess[guildID].getPlaylist()
|
||||
lock = Lock()
|
||||
queueToListen = Queue()
|
||||
queueToSend = Queue()
|
||||
process = PlayerProcess(context.guild.name, playlist, lock, queueToSend,
|
||||
queueToListen, guildID, textID, voiceID, authorID)
|
||||
processInfo = ProcessInfo(process, queueToSend, queueToListen,
|
||||
playlist, lock, context.channel)
|
||||
|
||||
# Create a Thread to listen for the queue coming from the Player Process, this will redirect the Queue to a async
|
||||
thread = Thread(target=self.__listenToCommands,
|
||||
args=(queueToListen, guild), daemon=True)
|
||||
self.__playersListeners[guildID] = (thread, False)
|
||||
thread.start()
|
||||
|
||||
return processInfo
|
||||
|
||||
def __listenToCommands(self, queue: Queue, guild: Guild) -> None:
|
||||
guildID = guild.id
|
||||
while True:
|
||||
shouldEnd = self.__playersListeners[guildID][1]
|
||||
if shouldEnd:
|
||||
break
|
||||
|
||||
try:
|
||||
command: VCommands = queue.get(timeout=5)
|
||||
commandType = command.getType()
|
||||
args = command.getArgs()
|
||||
|
||||
print(f'Process {guild.name} sended command {commandType}')
|
||||
if commandType == VCommandsType.NOW_PLAYING:
|
||||
asyncio.run_coroutine_threadsafe(self.showNowPlaying(
|
||||
guild.id, args), self.__bot.loop)
|
||||
elif commandType == VCommandsType.TERMINATE:
|
||||
# Delete the process elements and return, to finish task
|
||||
self.__terminateProcess(guildID)
|
||||
return
|
||||
elif commandType == VCommandsType.SLEEPING:
|
||||
# The process might be used again
|
||||
self.__sleepingProcess(guildID)
|
||||
return
|
||||
else:
|
||||
print(f'[ERROR] -> Unknown Command Received from Process: {commandType}')
|
||||
except Empty:
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f'[ERROR IN LISTENING PROCESS] -> {guild.name} - {e}')
|
||||
|
||||
def __terminateProcess(self, guildID: int) -> None:
|
||||
# Delete all structures associated with the Player
|
||||
del self.__playersProcess[guildID]
|
||||
del self.__playersMessages[guildID]
|
||||
threadListening = self.__playersListeners[guildID]
|
||||
threadListening._stop()
|
||||
del self.__playersListeners[guildID]
|
||||
|
||||
def __sleepingProcess(self, guildID: int) -> None:
|
||||
# Disable all process structures, except Playlist
|
||||
queue1 = self.__playersProcess[guildID].getQueueToMain()
|
||||
queue2 = self.__playersProcess[guildID].getQueueToPlayer()
|
||||
queue1.close()
|
||||
queue1.join_thread()
|
||||
queue2.close()
|
||||
queue2.join_thread()
|
||||
|
||||
async def showNowPlaying(self, guildID: int, song: Song) -> None:
|
||||
messagesController = self.__playersMessages[guildID]
|
||||
processInfo = self.__playersProcess[guildID]
|
||||
await messagesController.sendNowPlaying(processInfo, song)
|
||||
|
||||
|
||||
class VManager(BaseManager):
|
||||
pass
|
||||
|
||||
|
||||
class VProxy(NamespaceProxy):
|
||||
_exposed_ = ('__getattribute__', '__setattr__', '__delattr__')
|
||||
19
README.md
19
README.md
@@ -1,17 +1,17 @@
|
||||
# **Vulkan**
|
||||
|
||||
A Music Discord bot, written in Python, that supports Youtube and Spotify sources for playing. 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.
|
||||
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.
|
||||
|
||||
|
||||
# **Music**
|
||||
- Play musics from Youtube and Spotify Playlists
|
||||
- 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]``` - Start playing song
|
||||
```!play [title, spotify_url, youtube_url, deezer_url]``` - Start playing song
|
||||
|
||||
```!resume``` - Resume the song player
|
||||
|
||||
@@ -62,9 +62,11 @@ 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**
|
||||
This is an example of how your .env file (located in root) should look like.
|
||||
@@ -72,6 +74,7 @@ This is an example of how your .env file (located in root) should look like.
|
||||
BOT_TOKEN=Your_Own_Bot_Token
|
||||
SPOTIFY_ID=Your_Own_Spotify_ID
|
||||
SPOTIFY_SECRET=Your_Own_Spotify_Secret
|
||||
BOT_PREFIX=Your_Wanted_Prefix_For_Vulkan
|
||||
|
||||
```
|
||||
|
||||
@@ -92,6 +95,10 @@ To run your Bot in Heroku 24/7, you will need the Procfile located in root, then
|
||||
- https://github.com/xrisk/heroku-opus.git
|
||||
|
||||
|
||||
## Testing
|
||||
The tests were written manually with no package due to problems with async function in other packages, to execute them type in root: <br>
|
||||
`python run_tests.py`<br>
|
||||
|
||||
## License
|
||||
- This program is free software: you can redistribute it and/or modify it under the terms of the [MIT License](https://github.com/RafaelSolVargas/Vulkan/blob/master/LICENSE).
|
||||
|
||||
@@ -101,4 +108,4 @@ To run your Bot in Heroku 24/7, you will need the Procfile located in root, then
|
||||
|
||||
|
||||
## Acknowledgment
|
||||
- See the DingoLingo [project](https://github.com/Raptor123471/DingoLingo) from Raptor123471, it helped me a lot to build 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
|
||||
22
UI/Buttons/BackButton.py
Normal file
22
UI/Buttons/BackButton.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from discord import ButtonStyle, Interaction
|
||||
from discord.ui import Button
|
||||
from Config.Emojis import VEmojis
|
||||
from Handlers.PrevHandler import PrevHandler
|
||||
from Music.VulkanBot import VulkanBot
|
||||
|
||||
|
||||
class BackButton(Button):
|
||||
def __init__(self, bot: VulkanBot):
|
||||
super().__init__(label="Back", style=ButtonStyle.secondary, emoji=VEmojis().BACK)
|
||||
self.__bot = bot
|
||||
|
||||
async def callback(self, interaction: Interaction) -> None:
|
||||
"""Callback to when Button is clicked"""
|
||||
# Return to Discord that this command is being processed
|
||||
await interaction.response.defer()
|
||||
|
||||
handler = PrevHandler(interaction, self.__bot)
|
||||
response = await handler.run()
|
||||
|
||||
if response.embed:
|
||||
await interaction.followup.send(embed=response.embed)
|
||||
20
UI/Buttons/LoopAllButton.py
Normal file
20
UI/Buttons/LoopAllButton.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from discord import ButtonStyle, Interaction
|
||||
from discord.ui import Button
|
||||
from Config.Emojis import VEmojis
|
||||
from Handlers.LoopHandler import LoopHandler
|
||||
from Music.VulkanBot import VulkanBot
|
||||
|
||||
|
||||
class LoopAllButton(Button):
|
||||
def __init__(self, bot: VulkanBot):
|
||||
super().__init__(label="Loop All", style=ButtonStyle.secondary, emoji=VEmojis().LOOP_ALL)
|
||||
self.__bot = bot
|
||||
|
||||
async def callback(self, interaction: Interaction) -> None:
|
||||
await interaction.response.defer()
|
||||
|
||||
handler = LoopHandler(interaction, self.__bot)
|
||||
response = await handler.run('all')
|
||||
|
||||
if response.embed:
|
||||
await interaction.followup.send(embed=response.embed)
|
||||
20
UI/Buttons/LoopOffButton.py
Normal file
20
UI/Buttons/LoopOffButton.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from discord import ButtonStyle, Interaction
|
||||
from discord.ui import Button
|
||||
from Config.Emojis import VEmojis
|
||||
from Handlers.LoopHandler import LoopHandler
|
||||
from Music.VulkanBot import VulkanBot
|
||||
|
||||
|
||||
class LoopOffButton(Button):
|
||||
def __init__(self, bot: VulkanBot):
|
||||
super().__init__(label="Loop Off", style=ButtonStyle.secondary, emoji=VEmojis().LOOP_OFF)
|
||||
self.__bot = bot
|
||||
|
||||
async def callback(self, interaction: Interaction) -> None:
|
||||
await interaction.response.defer()
|
||||
|
||||
handler = LoopHandler(interaction, self.__bot)
|
||||
response = await handler.run('off')
|
||||
|
||||
if response.embed:
|
||||
await interaction.followup.send(embed=response.embed)
|
||||
20
UI/Buttons/LoopOneButton.py
Normal file
20
UI/Buttons/LoopOneButton.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from discord import ButtonStyle, Interaction
|
||||
from discord.ui import Button
|
||||
from Config.Emojis import VEmojis
|
||||
from Handlers.LoopHandler import LoopHandler
|
||||
from Music.VulkanBot import VulkanBot
|
||||
|
||||
|
||||
class LoopOneButton(Button):
|
||||
def __init__(self, bot: VulkanBot):
|
||||
super().__init__(label="Loop One", style=ButtonStyle.secondary, emoji=VEmojis().LOOP_ONE)
|
||||
self.__bot = bot
|
||||
|
||||
async def callback(self, interaction: Interaction) -> None:
|
||||
await interaction.response.defer()
|
||||
|
||||
handler = LoopHandler(interaction, self.__bot)
|
||||
response = await handler.run('one')
|
||||
|
||||
if response.embed:
|
||||
await interaction.followup.send(embed=response.embed)
|
||||
20
UI/Buttons/PauseButton.py
Normal file
20
UI/Buttons/PauseButton.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from discord import ButtonStyle, Interaction
|
||||
from discord.ui import Button
|
||||
from Config.Emojis import VEmojis
|
||||
from Handlers.PauseHandler import PauseHandler
|
||||
from Music.VulkanBot import VulkanBot
|
||||
|
||||
|
||||
class PauseButton(Button):
|
||||
def __init__(self, bot: VulkanBot):
|
||||
super().__init__(label="Pause", style=ButtonStyle.secondary, emoji=VEmojis().PAUSE)
|
||||
self.__bot = bot
|
||||
|
||||
async def callback(self, interaction: Interaction) -> None:
|
||||
await interaction.response.defer()
|
||||
|
||||
handler = PauseHandler(interaction, self.__bot)
|
||||
response = await handler.run()
|
||||
|
||||
if response.embed:
|
||||
await interaction.followup.send(embed=response.embed)
|
||||
20
UI/Buttons/PlayButton.py
Normal file
20
UI/Buttons/PlayButton.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from discord import ButtonStyle, Interaction
|
||||
from discord.ui import Button
|
||||
from Config.Emojis import VEmojis
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from Handlers.ResumeHandler import ResumeHandler
|
||||
|
||||
|
||||
class PlayButton(Button):
|
||||
def __init__(self, bot: VulkanBot):
|
||||
super().__init__(label="Play", style=ButtonStyle.secondary, emoji=VEmojis().PLAY)
|
||||
self.__bot = bot
|
||||
|
||||
async def callback(self, interaction: Interaction) -> None:
|
||||
await interaction.response.defer()
|
||||
|
||||
handler = ResumeHandler(interaction, self.__bot)
|
||||
response = await handler.run()
|
||||
|
||||
if response.embed:
|
||||
await interaction.followup.send(embed=response.embed)
|
||||
20
UI/Buttons/SkipButton.py
Normal file
20
UI/Buttons/SkipButton.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from discord import ButtonStyle, Interaction
|
||||
from discord.ui import Button
|
||||
from Config.Emojis import VEmojis
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from Handlers.SkipHandler import SkipHandler
|
||||
|
||||
|
||||
class SkipButton(Button):
|
||||
def __init__(self, bot: VulkanBot):
|
||||
super().__init__(label="Skip", style=ButtonStyle.secondary, emoji=VEmojis().SKIP)
|
||||
self.__bot = bot
|
||||
|
||||
async def callback(self, interaction: Interaction) -> None:
|
||||
await interaction.response.defer()
|
||||
|
||||
handler = SkipHandler(interaction, self.__bot)
|
||||
response = await handler.run()
|
||||
|
||||
if response.embed:
|
||||
await interaction.followup.send(embed=response.embed)
|
||||
20
UI/Buttons/SongsButton.py
Normal file
20
UI/Buttons/SongsButton.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from Handlers.QueueHandler import QueueHandler
|
||||
from discord import ButtonStyle, Interaction
|
||||
from discord.ui import Button
|
||||
from Config.Emojis import VEmojis
|
||||
from Music.VulkanBot import VulkanBot
|
||||
|
||||
|
||||
class SongsButton(Button):
|
||||
def __init__(self, bot: VulkanBot):
|
||||
super().__init__(label="Songs", style=ButtonStyle.secondary, emoji=VEmojis().QUEUE)
|
||||
self.__bot = bot
|
||||
|
||||
async def callback(self, interaction: Interaction) -> None:
|
||||
await interaction.response.defer()
|
||||
|
||||
handler = QueueHandler(interaction, self.__bot)
|
||||
response = await handler.run()
|
||||
|
||||
if response.embed:
|
||||
await interaction.followup.send(embed=response.embed)
|
||||
20
UI/Buttons/StopButton.py
Normal file
20
UI/Buttons/StopButton.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from discord import ButtonStyle, Interaction
|
||||
from discord.ui import Button
|
||||
from Config.Emojis import VEmojis
|
||||
from Music.VulkanBot import VulkanBot
|
||||
from Handlers.StopHandler import StopHandler
|
||||
|
||||
|
||||
class StopButton(Button):
|
||||
def __init__(self, bot: VulkanBot):
|
||||
super().__init__(label="Stop", style=ButtonStyle.secondary, emoji=VEmojis().STOP)
|
||||
self.__bot = bot
|
||||
|
||||
async def callback(self, interaction: Interaction) -> None:
|
||||
await interaction.response.defer()
|
||||
|
||||
handler = StopHandler(interaction, self.__bot)
|
||||
response = await handler.run()
|
||||
|
||||
if response.embed:
|
||||
await interaction.followup.send(embed=response.embed)
|
||||
33
UI/Responses/AbstractCogResponse.py
Normal file
33
UI/Responses/AbstractCogResponse.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
from discord.ext.commands import Context
|
||||
from discord import Message
|
||||
from Music.VulkanBot import VulkanBot
|
||||
|
||||
|
||||
class AbstractCommandResponse(ABC):
|
||||
def __init__(self, response: HandlerResponse) -> None:
|
||||
self.__response: HandlerResponse = response
|
||||
self.__context: Context = response.ctx
|
||||
self.__message: Message = response.ctx.message
|
||||
self.__bot: VulkanBot = response.ctx.bot
|
||||
|
||||
@property
|
||||
def response(self) -> HandlerResponse:
|
||||
return self.__response
|
||||
|
||||
@property
|
||||
def bot(self) -> VulkanBot:
|
||||
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
UI/Responses/EmbedCogResponse.py
Normal file
11
UI/Responses/EmbedCogResponse.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from UI.Responses.AbstractCogResponse import AbstractCommandResponse
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
|
||||
|
||||
class EmbedCommandResponse(AbstractCommandResponse):
|
||||
def __init__(self, response: HandlerResponse) -> None:
|
||||
super().__init__(response)
|
||||
|
||||
async def run(self) -> None:
|
||||
if self.response.embed:
|
||||
await self.context.send(embed=self.response.embed)
|
||||
16
UI/Responses/EmoteCogResponse.py
Normal file
16
UI/Responses/EmoteCogResponse.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from Config.Emojis import VEmojis
|
||||
from UI.Responses.AbstractCogResponse import AbstractCommandResponse
|
||||
from Handlers.HandlerResponse import HandlerResponse
|
||||
|
||||
|
||||
class EmoteCommandResponse(AbstractCommandResponse):
|
||||
|
||||
def __init__(self, response: HandlerResponse) -> None:
|
||||
super().__init__(response)
|
||||
self.__emojis = VEmojis()
|
||||
|
||||
async def run(self) -> None:
|
||||
if self.response.success:
|
||||
await self.message.add_reaction(self.__emojis.SUCCESS)
|
||||
else:
|
||||
await self.message.add_reaction(self.__emojis.ERROR)
|
||||
40
UI/Views/PlayerView.py
Normal file
40
UI/Views/PlayerView.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from discord import Message
|
||||
from discord.ui import View
|
||||
from Config.Emojis import VEmojis
|
||||
from UI.Buttons.PauseButton import PauseButton
|
||||
from UI.Buttons.BackButton import BackButton
|
||||
from UI.Buttons.SkipButton import SkipButton
|
||||
from UI.Buttons.StopButton import StopButton
|
||||
from UI.Buttons.SongsButton import SongsButton
|
||||
from UI.Buttons.PlayButton import PlayButton
|
||||
from UI.Buttons.LoopAllButton import LoopAllButton
|
||||
from UI.Buttons.LoopOneButton import LoopOneButton
|
||||
from UI.Buttons.LoopOffButton import LoopOffButton
|
||||
from Music.VulkanBot import VulkanBot
|
||||
|
||||
emojis = VEmojis()
|
||||
|
||||
|
||||
class PlayerView(View):
|
||||
def __init__(self, bot: VulkanBot, timeout: float = 6000):
|
||||
super().__init__(timeout=timeout)
|
||||
self.__bot = bot
|
||||
self.__message: Message = None
|
||||
self.add_item(BackButton(self.__bot))
|
||||
self.add_item(PauseButton(self.__bot))
|
||||
self.add_item(PlayButton(self.__bot))
|
||||
self.add_item(StopButton(self.__bot))
|
||||
self.add_item(SkipButton(self.__bot))
|
||||
self.add_item(SongsButton(self.__bot))
|
||||
self.add_item(LoopOneButton(self.__bot))
|
||||
self.add_item(LoopOffButton(self.__bot))
|
||||
self.add_item(LoopAllButton(self.__bot))
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
# Disable all itens and, if has the message, edit it
|
||||
self.disable_all_items()
|
||||
if self.__message is not None and isinstance(self.__message, Message):
|
||||
await self.__message.edit(view=self)
|
||||
|
||||
def set_message(self, message: Message) -> None:
|
||||
self.__message = message
|
||||
33
Utils/Cleaner.py
Normal file
33
Utils/Cleaner.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from typing import List
|
||||
from discord.ext.commands import Context
|
||||
from discord import Message, Embed
|
||||
from Config.Singleton import Singleton
|
||||
from Music.VulkanBot import VulkanBot
|
||||
|
||||
|
||||
class Cleaner(Singleton):
|
||||
def __init__(self, bot: VulkanBot = None) -> None:
|
||||
if not super().created:
|
||||
self.__bot = bot
|
||||
self.__clean_str = 'Uploader:'
|
||||
|
||||
def set_bot(self, bot: VulkanBot) -> None:
|
||||
self.__bot = bot
|
||||
|
||||
async def clean_messages(self, ctx: Context, quant: int) -> None:
|
||||
if self.__bot is None:
|
||||
return
|
||||
|
||||
last_messages: List[Message] = await ctx.channel.history(limit=quant).flatten()
|
||||
|
||||
for message in last_messages:
|
||||
try:
|
||||
if message.author == self.__bot.user:
|
||||
if len(message.embeds) > 0:
|
||||
embed: Embed = message.embeds[0]
|
||||
if len(embed.fields) > 0:
|
||||
if embed.fields[0].name == self.__clean_str:
|
||||
await message.delete()
|
||||
except Exception as e:
|
||||
print(f'DEVELOPER NOTE -> Error cleaning messages {e}')
|
||||
continue
|
||||
34
Utils/UrlAnalyzer.py
Normal file
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 VConfigs
|
||||
from functools import wraps, partial
|
||||
config = VConfigs()
|
||||
|
||||
|
||||
class Utils:
|
||||
@classmethod
|
||||
def format_time(cls, duration) -> str:
|
||||
if not duration:
|
||||
return "00:00"
|
||||
|
||||
hours = duration // 60 // 60
|
||||
minutes = duration // 60 % 60
|
||||
seconds = duration % 60
|
||||
|
||||
return "{}{}{:02d}:{:02d}".format(
|
||||
hours if hours else "",
|
||||
":" if hours else "",
|
||||
minutes,
|
||||
seconds)
|
||||
|
||||
@classmethod
|
||||
def is_url(cls, string) -> bool:
|
||||
regex = re.compile(
|
||||
"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+")
|
||||
|
||||
if re.search(regex, string):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def run_async(func):
|
||||
@wraps(func)
|
||||
async def run(*args, loop=None, executor=None, **kwargs):
|
||||
if loop is None:
|
||||
loop = asyncio.get_event_loop()
|
||||
partial_func = partial(func, *args, **kwargs)
|
||||
return await loop.run_in_executor(executor, partial_func)
|
||||
return run
|
||||
@@ -1,76 +0,0 @@
|
||||
from decouple import config
|
||||
|
||||
BOT_TOKEN = config('BOT_TOKEN')
|
||||
SPOTIFY_ID = config('SPOTIFY_ID')
|
||||
SPOTIFY_SECRET = config('SPOTIFY_SECRET')
|
||||
|
||||
BOT_PREFIX = '!'
|
||||
VC_TIMEOUT = 600
|
||||
|
||||
STARTUP_MESSAGE = 'Starting Vulkan...'
|
||||
STARTUP_COMPLETE_MESSAGE = 'Vulkan is now operating.'
|
||||
|
||||
MAX_PLAYLIST_LENGTH = 50
|
||||
MAX_PRELOAD_SONGS = 10
|
||||
MAX_SONGS_HISTORY = 15
|
||||
|
||||
INVITE_MESSAGE = 'To invite Vulkan to your own server, click [here]({})'
|
||||
|
||||
SONGINFO_UPLOADER = "Uploader: "
|
||||
SONGINFO_DURATION = "Duration: "
|
||||
SONGINFO_REQUESTER = 'Requester: '
|
||||
|
||||
SONGS_ADDED = 'You added {} songs to the queue'
|
||||
SONG_ADDED = 'You added the song `{}` to the queue'
|
||||
SONG_ADDED_TWO = '🎧 Song added to the queue'
|
||||
SONG_PLAYING = '🎧 Song playing now'
|
||||
SONG_PLAYER = '🎧 Song Player'
|
||||
QUEUE_TITLE = '🎧 Songs in Queue'
|
||||
ONE_SONG_LOOPING = '🎧 Looping One Song'
|
||||
ALL_SONGS_LOOPING = '🎧 Looping All Songs'
|
||||
SONG_PAUSED = '⏸️ Song paused'
|
||||
SONG_RESUMED = '▶️ Song playing'
|
||||
EMPTY_QUEUE = f'📜 Song queue is empty, use {BOT_PREFIX}play to add new songs'
|
||||
SONG_DOWNLOADING = '📥 Downloading...'
|
||||
|
||||
HISTORY_TITLE = '🎧 Played Songs'
|
||||
HISTORY_EMPTY = '📜 There is no musics in history'
|
||||
|
||||
SONG_MOVED_SUCCESSFULLY = 'Song `{}` in position `{}` moved with `{}` in position `{}` successfully'
|
||||
SONG_REMOVED_SUCCESSFULLY = 'Song `{}` removed successfully'
|
||||
|
||||
LOOP_ALL_ON = f'❌ Vulkan is looping all songs, use {BOT_PREFIX}loop off to disable this loop first'
|
||||
LOOP_ONE_ON = f'❌ Vulkan is looping one song, use {BOT_PREFIX}loop off to disable this loop first'
|
||||
LOOP_ALL_ALREADY_ON = '🔁 Vulkan is already looping all songs'
|
||||
LOOP_ONE_ALREADY_ON = '🔂 Vulkan is already looping the current song'
|
||||
LOOP_ALL_ACTIVATE = '🔁 Looping all songs'
|
||||
LOOP_ONE_ACTIVATE = '🔂 Looping the current song'
|
||||
LOOP_DISABLE = '➡️ Loop disabled'
|
||||
LOOP_ALREADY_DISABLE = '❌ Loop is already disabled'
|
||||
LOOP_ON = f'❌ This command cannot be invoked with any loop activated. Use {BOT_PREFIX}loop off to disable loop'
|
||||
|
||||
SONGS_SHUFFLED = '🔀 Songs shuffled successfully'
|
||||
ERROR_SHUFFLING = '❌ Error while shuffling the songs'
|
||||
ERROR_MOVING = '❌ Error while moving the songs'
|
||||
LENGTH_ERROR = '❌ Numbers must be between 1 and queue length, use -1 for the last song'
|
||||
ERROR_NUMBER = '❌ This command require a number'
|
||||
ERROR_PLAYING = '❌ Error while playing songs'
|
||||
COMMAND_NOT_FOUND = f'❌ Command not found, type {BOT_PREFIX}help to see all commands'
|
||||
UNKNOWN_ERROR = f'❌ Unknown Error, if needed, use {BOT_PREFIX}reset to reset the player of your server'
|
||||
ERROR_MISSING_ARGUMENTS = f'❌ Missing arguments in this function. Type {BOT_PREFIX}help to see all commands'
|
||||
NOT_PREVIOUS = '❌ There is none previous song to play'
|
||||
PLAYER_NOT_PLAYING = f'❌ No song playing. Use {BOT_PREFIX}play to start the player'
|
||||
IMPOSSIBLE_MOVE = 'That is impossible :('
|
||||
ERROR_TITLE = 'Error :-('
|
||||
NO_CHANNEL = 'To play some music, connect to any voice channel first.'
|
||||
NO_GUILD = f'This server does not has a Player, try {BOT_PREFIX}reset'
|
||||
INVALID_INPUT = f'This type of input was too strange, try something better or type {BOT_PREFIX}help play'
|
||||
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
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
from config.config import *
|
||||
|
||||
HELP_SKIP = 'Skip the current playing song.'
|
||||
HELP_SKIP_LONG = 'Skip the playing of the current song, does not work if loop one is activated. \n\nArguments: None.'
|
||||
HELP_RESUME = 'Resumes the song player.'
|
||||
HELP_RESUME_LONG = 'If the player if paused, return the playing. \n\nArguments: None.'
|
||||
HELP_CLEAR = 'Clear the queue and songs history.'
|
||||
HELP_CLEAR_LONG = 'Clear the songs queue and songs history. \n\nArguments: None.'
|
||||
HELP_STOP = 'Stop the song player.'
|
||||
HELP_STOP_LONG = 'Stop the song player, clear queue and history and remove Vulkan from voice channel.\n\nArguments: None.'
|
||||
HELP_LOOP = 'Control the loop of songs.'
|
||||
HELP_LOOP_LONG = 'Controll the loop of songs.\n\n Require: A song being played.\nArguments:\nOne - Start looping the current song. \
|
||||
\nAll - Start looping all songs in queue.\nOff - Disable loop.'
|
||||
HELP_NP = 'Show the info of the current song.'
|
||||
HELP_NP_LONG = 'Show the information of the song being played.\n\nRequire: A song being played.\nArguments: None.'
|
||||
HELP_QUEUE = f'Show the first {MAX_PRELOAD_SONGS} songs in queue.'
|
||||
HELP_QUEUE_LONG = f'Show the first {MAX_PRELOAD_SONGS} song in the queue.\n\nArguments: None.'
|
||||
HELP_PAUSE = 'Pauses the song player.'
|
||||
HELP_PAUSE_LONG = 'If playing, pauses the song player.\n\nArguments: None'
|
||||
HELP_PREV = 'Play the previous song.'
|
||||
HELP_PREV_LONG = 'Play the previous song. If playing, the current song will return to queue.\n\nRequire: Loop to be disable.\nArguments: None.'
|
||||
HELP_SHUFFLE = 'Shuffle the songs playing.'
|
||||
HELP_SHUFFLE_LONG = 'Randomly shuffle the songs in the queue.\n\nArguments: None.'
|
||||
HELP_PLAY = 'Plays a song.'
|
||||
HELP_PLAY_LONG = 'Play a song in discord. \n\nRequire: You to be connected to a voice channel.\nArguments: Youtube or Spotify song/playlist link or the title of the song to be searched in Youtube.'
|
||||
HELP_HISTORY = f'Show the history of played songs.'
|
||||
HELP_HISTORY_LONG = f'Show the last {MAX_SONGS_HISTORY} played songs'
|
||||
HELP_MOVE = 'Moves a song from position x to y in queue.'
|
||||
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.'
|
||||
HELP_REMOVE = 'Remove a song in position x.'
|
||||
HELP_REMOVE_LONG = 'Remove a song from queue in the position passed.\n\nRequire: Position to be a valid number.\nArguments: 1º Number => Position in queue of the song.'
|
||||
HELP_RESET = 'Reset the Player of the server.'
|
||||
HELP_RESET_LONG = 'Reset the Player of the server. Recommended if you find any type of error.\n\nArguments: None'
|
||||
HELP_HELP = f'Use {BOT_PREFIX}help "command" for more info.'
|
||||
HELP_HELP_LONG = f'Use {BOT_PREFIX}help command for more info about the command selected.'
|
||||
HELP_INVITE = 'Send the invite URL to call Vulkan to your server.'
|
||||
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.'
|
||||
HELP_RANDOM = 'Return a random number between 1 and x.'
|
||||
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.'
|
||||
HELP_CHOOSE = 'Choose randomly one item passed.'
|
||||
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.'
|
||||
HELP_CARA = 'Return cara or coroa.'
|
||||
HELP_CARA_LONG = 'Return cara or coroa.'
|
||||
25
main.py
25
main.py
@@ -1,22 +1,7 @@
|
||||
import discord
|
||||
import os
|
||||
|
||||
from config import config
|
||||
from discord.ext import commands
|
||||
|
||||
intents = discord.Intents.default()
|
||||
intents.members = True
|
||||
|
||||
bot = commands.Bot(command_prefix=config.BOT_PREFIX, pm_help=True,
|
||||
case_insensitive=True, intents=intents)
|
||||
bot.remove_command('help')
|
||||
|
||||
if config.BOT_TOKEN == "":
|
||||
exit()
|
||||
|
||||
for filename in os.listdir('./vulkan/commands'):
|
||||
if filename.endswith('.py'):
|
||||
bot.load_extension(f'vulkan.commands.{filename[:-3]}')
|
||||
from Music.VulkanInitializer import VulkanInitializer
|
||||
|
||||
|
||||
bot.run(config.BOT_TOKEN, bot=True, reconnect=True)
|
||||
if __name__ == '__main__':
|
||||
initializer = VulkanInitializer(willListen=True)
|
||||
vulkanBot = initializer.getBot()
|
||||
vulkanBot.startBot()
|
||||
|
||||
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,122 +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
|
||||
from config import help
|
||||
|
||||
|
||||
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', 'prev', 'history'],
|
||||
'RANDOM': ['choose', 'cara', 'random']
|
||||
|
||||
}
|
||||
|
||||
@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):
|
||||
embed = discord.Embed(
|
||||
title=config.ERROR_TITLE,
|
||||
description=config.ERROR_MISSING_ARGUMENTS,
|
||||
colour=config.COLOURS['red']
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
elif isinstance(error, CommandNotFound):
|
||||
embed = discord.Embed(
|
||||
title=config.ERROR_TITLE,
|
||||
description=config.COMMAND_NOT_FOUND,
|
||||
colour=config.COLOURS['red']
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
else:
|
||||
print(error)
|
||||
embed = discord.Embed(
|
||||
title=config.ERROR_TITLE,
|
||||
description=config.UNKNOWN_ERROR,
|
||||
colour=config.COLOURS['red']
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="help", help=help.HELP_HELP, description=help.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 = discord.Embed(
|
||||
title=f'**Description of {command_help}** command',
|
||||
description=txt,
|
||||
colour=config.COLOURS['blue']
|
||||
)
|
||||
|
||||
await ctx.send(embed=embedhelp)
|
||||
return
|
||||
|
||||
embedhelp = discord.Embed(
|
||||
title='Command Help',
|
||||
description=f'Command {command_help} Not Found',
|
||||
colour=config.COLOURS['red']
|
||||
)
|
||||
|
||||
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.__comandos['MUSIC']:
|
||||
help_music += f'**{command}** - {command.help}\n'
|
||||
|
||||
elif command.name in self.__comandos['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}'
|
||||
|
||||
embedhelp = discord.Embed(
|
||||
title=f'**Available Commands of {self.__bot.user.name}**',
|
||||
description=helptxt,
|
||||
colour=config.COLOURS['blue']
|
||||
)
|
||||
|
||||
embedhelp.set_thumbnail(url=self.__bot.user.avatar_url)
|
||||
await ctx.send(embed=embedhelp)
|
||||
|
||||
@commands.command(name='invite', help=help.HELP_INVITE, description=help.HELP_INVITE_LONG)
|
||||
async def invite_bot(self, ctx):
|
||||
invite_url = 'https://discordapp.com/oauth2/authorize?client_id={}&scope=bot>'.format(
|
||||
self.__bot.user.id)
|
||||
txt = config.INVITE_MESSAGE.format(invite_url)
|
||||
|
||||
embed = discord.Embed(
|
||||
title="Invite Vulkan",
|
||||
description=txt,
|
||||
colour=config.COLOURS['blue']
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Control(bot))
|
||||
@@ -1,219 +0,0 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from config import config
|
||||
from config import help
|
||||
from vulkan.music.Player import Player
|
||||
from vulkan.music.utils import *
|
||||
|
||||
|
||||
class Music(commands.Cog):
|
||||
def __init__(self, bot) -> None:
|
||||
self.__guilds = {}
|
||||
self.__bot: discord.Client = bot
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_ready(self) -> None:
|
||||
"""Load a player for each guild that the Bot are"""
|
||||
for guild in self.__bot.guilds:
|
||||
self.__guilds[guild] = Player(self.__bot, guild)
|
||||
print(f'Player for guild {guild.name} created')
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_guild_join(self, guild) -> None:
|
||||
"""Load a player when joining a guild"""
|
||||
self.__guilds[guild] = Player(self.__bot, guild)
|
||||
print(f'Player for guild {guild.name} created')
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_guild_remove(self, guild) -> None:
|
||||
"""Removes the player of the guild if banned"""
|
||||
if guild in self.__guilds.keys():
|
||||
self.__guilds.pop(guild, None)
|
||||
print(f'Player for guild {guild.name} destroyed')
|
||||
|
||||
@commands.command(name="play", help=help.HELP_PLAY, description=help.HELP_PLAY_LONG, aliases=['p', 'tocar'])
|
||||
async def play(self, ctx, *args) -> None:
|
||||
track = " ".join(args)
|
||||
requester = ctx.author.name
|
||||
|
||||
player = self.__get_player(ctx)
|
||||
if player == None:
|
||||
await self.__send_embed(ctx, config.ERROR_TITLE, config.NO_GUILD, 'red')
|
||||
return
|
||||
|
||||
if is_connected(ctx) == None:
|
||||
success = await player.connect(ctx)
|
||||
if success == False:
|
||||
await self.__send_embed(ctx, config.IMPOSSIBLE_MOVE, config.NO_CHANNEL, 'red')
|
||||
return
|
||||
|
||||
await player.play(ctx, track, requester)
|
||||
|
||||
@commands.command(name="queue", help=help.HELP_QUEUE, description=help.HELP_QUEUE_LONG, aliases=['q', 'fila'])
|
||||
async def queue(self, ctx) -> None:
|
||||
player = self.__get_player(ctx)
|
||||
if player == None:
|
||||
return
|
||||
|
||||
embed = await player.queue()
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="skip", help=help.HELP_SKIP, description=help.HELP_SKIP_LONG, aliases=['s', 'pular'])
|
||||
async def skip(self, ctx) -> None:
|
||||
player = self.__get_player(ctx)
|
||||
if player == None:
|
||||
return
|
||||
else:
|
||||
await player.skip(ctx)
|
||||
|
||||
@commands.command(name='stop', help=help.HELP_STOP, description=help.HELP_STOP_LONG, aliases=['parar'])
|
||||
async def stop(self, ctx) -> None:
|
||||
player = self.__get_player(ctx)
|
||||
if player == None:
|
||||
return
|
||||
else:
|
||||
await player.stop()
|
||||
|
||||
@commands.command(name='pause', help=help.HELP_PAUSE, description=help.HELP_PAUSE_LONG, aliases=['pausar'])
|
||||
async def pause(self, ctx) -> None:
|
||||
player = self.__get_player(ctx)
|
||||
if player == None:
|
||||
return
|
||||
else:
|
||||
success = await player.pause()
|
||||
if success:
|
||||
await self.__send_embed(ctx, config.SONG_PLAYER, config.SONG_PAUSED, 'blue')
|
||||
|
||||
@commands.command(name='resume', help=help.HELP_RESUME, description=help.HELP_RESUME_LONG, aliases=['soltar'])
|
||||
async def resume(self, ctx) -> None:
|
||||
player = self.__get_player(ctx)
|
||||
if player == None:
|
||||
return
|
||||
else:
|
||||
success = await player.resume()
|
||||
if success:
|
||||
await self.__send_embed(ctx, config.SONG_PLAYER, config.SONG_RESUMED, 'blue')
|
||||
|
||||
@commands.command(name='prev', help=help.HELP_PREV, description=help.HELP_PREV_LONG, aliases=['anterior'])
|
||||
async def prev(self, ctx) -> None:
|
||||
player = self.__get_player(ctx)
|
||||
if player == None:
|
||||
return
|
||||
|
||||
if is_connected(ctx) == None:
|
||||
success = await player.connect(ctx)
|
||||
if success == False:
|
||||
await self.__send_embed(ctx, config.IMPOSSIBLE_MOVE, config.NO_CHANNEL, 'red')
|
||||
return
|
||||
|
||||
await player.play_prev(ctx)
|
||||
|
||||
@commands.command(name='history', help=help.HELP_HISTORY, description=help.HELP_HISTORY_LONG, aliases=['historico'])
|
||||
async def history(self, ctx) -> None:
|
||||
player = self.__get_player(ctx)
|
||||
if player == None:
|
||||
return
|
||||
else:
|
||||
embed = player.history()
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name='loop', help=help.HELP_LOOP, description=help.HELP_LOOP_LONG, aliases=['l', 'repeat'])
|
||||
async def loop(self, ctx, args: str) -> None:
|
||||
player = self.__get_player(ctx)
|
||||
if player == None:
|
||||
return
|
||||
else:
|
||||
description = await player.loop(args)
|
||||
await self.__send_embed(ctx, config.SONG_PLAYER, description, 'blue')
|
||||
|
||||
@commands.command(name='clear', help=help.HELP_CLEAR, description=help.HELP_CLEAR_LONG, aliases=['c', 'limpar'])
|
||||
async def clear(self, ctx) -> None:
|
||||
player = self.__get_player(ctx)
|
||||
if player == None:
|
||||
return
|
||||
else:
|
||||
await player.clear()
|
||||
|
||||
@commands.command(name='np', help=help.HELP_NP, description=help.HELP_NP_LONG, aliases=['playing', 'now'])
|
||||
async def now_playing(self, ctx) -> None:
|
||||
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=help.HELP_SHUFFLE, description=help.HELP_SHUFFLE_LONG, aliases=['aleatorio'])
|
||||
async def shuffle(self, ctx) -> None:
|
||||
player = self.__get_player(ctx)
|
||||
if player == None:
|
||||
return
|
||||
else:
|
||||
description = await player.shuffle()
|
||||
await self.__send_embed(ctx, config.SONG_PLAYER, description, 'blue')
|
||||
|
||||
@commands.command(name='move', help=help.HELP_MOVE, description=help.HELP_MOVE_LONG, aliases=['m', 'mover'])
|
||||
async def move(self, ctx, pos1, pos2='1') -> None:
|
||||
player = self.__get_player(ctx)
|
||||
if player == None:
|
||||
return
|
||||
else:
|
||||
description = await player.move(pos1, pos2)
|
||||
await self.__send_embed(ctx, config.SONG_PLAYER, description, 'blue')
|
||||
|
||||
@commands.command(name='remove', help=help.HELP_REMOVE, description=help.HELP_REMOVE_LONG, aliases=['remover'])
|
||||
async def remove(self, ctx, position) -> None:
|
||||
player = self.__get_player(ctx)
|
||||
if player == None:
|
||||
return
|
||||
else:
|
||||
description = await player.remove(position)
|
||||
await self.__send_embed(ctx, config.SONG_PLAYER, description, 'blue')
|
||||
|
||||
@commands.command(name='reset', help=help.HELP_RESET, description=help.HELP_RESET_LONG, aliases=['resetar'])
|
||||
async def reset(self, ctx) -> None:
|
||||
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='grey') -> None:
|
||||
try:
|
||||
colour = config.COLOURS[colour]
|
||||
except:
|
||||
colour = config.COLOURS['grey']
|
||||
|
||||
embedvc = discord.Embed(
|
||||
title=title,
|
||||
description=description,
|
||||
colour=colour
|
||||
)
|
||||
await ctx.send(embed=embedvc)
|
||||
|
||||
async def __clean_messages(self, ctx) -> None:
|
||||
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 len(embed.fields) > 0:
|
||||
if embed.fields[0].name == 'Uploader:':
|
||||
await message.delete()
|
||||
|
||||
except:
|
||||
continue
|
||||
|
||||
def __get_player(self, ctx) -> Player:
|
||||
try:
|
||||
return self.__guilds[ctx.guild]
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Music(bot))
|
||||
@@ -1,81 +0,0 @@
|
||||
from random import randint, random
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from config import config
|
||||
from config import help
|
||||
|
||||
|
||||
class Random(commands.Cog):
|
||||
"""Deal with returning random things"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.__bot = bot
|
||||
|
||||
@commands.command(name='random', help=help.HELP_RANDOM, description=help.HELP_RANDOM_LONG)
|
||||
async def random(self, ctx, arg: str) -> None:
|
||||
try:
|
||||
arg = int(arg)
|
||||
|
||||
except:
|
||||
embed = discord.Embed(
|
||||
description=config.ERROR_NUMBER,
|
||||
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'Random number between [{a, b}]',
|
||||
description=x,
|
||||
colour=config.COLOURS['green']
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name='cara', help=help.HELP_CARA, description=help.HELP_CARA_LONG)
|
||||
async def cara(self, ctx) -> None:
|
||||
x = random()
|
||||
if x < 0.5:
|
||||
result = 'cara'
|
||||
else:
|
||||
result = 'coroa'
|
||||
|
||||
embed = discord.Embed(
|
||||
title='Cara Cora',
|
||||
description=f'Result: {result}',
|
||||
colour=config.COLOURS['green']
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name='choose', help=help.HELP_CHOOSE, description=help.HELP_CHOOSE_LONG)
|
||||
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 = discord.Embed(
|
||||
title='Choose something',
|
||||
description=itens[index],
|
||||
colour=config.COLOURS['green']
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
except:
|
||||
embed = discord.Embed(
|
||||
title='Choose something.',
|
||||
description=f'Error: Use {config.BOT_PREFIX}help choose to understand this command.',
|
||||
colour=config.COLOURS['red']
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Random(bot))
|
||||
@@ -1,138 +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:
|
||||
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,85 +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 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,390 +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.Song import Song
|
||||
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
|
||||
|
||||
# Flag to control if the player should stop totally the playing
|
||||
self.__force_stop = 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) -> bool:
|
||||
if not ctx.author.voice:
|
||||
return False
|
||||
|
||||
if self.__guild.voice_client == None:
|
||||
await ctx.author.voice.channel.connect(reconnect=True, timeout=None)
|
||||
return True
|
||||
|
||||
def __play_next(self, error, ctx) -> None:
|
||||
if self.__force_stop: # If it's forced to stop player
|
||||
self.__force_stop = False
|
||||
return
|
||||
|
||||
song = self.__playlist.next_song()
|
||||
|
||||
if song != None:
|
||||
coro = self.__play_music(ctx, song)
|
||||
self.__bot.loop.create_task(coro)
|
||||
else:
|
||||
self.__playing = False
|
||||
|
||||
async def __play_music(self, ctx, song: Song) -> None:
|
||||
try:
|
||||
source = self.__ensure_source(song)
|
||||
if source == None:
|
||||
self.__play_next(None, ctx)
|
||||
|
||||
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)
|
||||
except:
|
||||
self.__play_next(None, ctx)
|
||||
|
||||
async def play(self, ctx, track=str, requester=str) -> str:
|
||||
try:
|
||||
songs_names, provider = self.__searcher.search(track)
|
||||
if provider == Provider.Unknown or songs_names == None:
|
||||
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_PLAYER,
|
||||
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_TWO)
|
||||
await ctx.send(embed=embed)
|
||||
else:
|
||||
embed = discord.Embed(
|
||||
title=config.SONG_PLAYER,
|
||||
description=config.SONGS_ADDED.format(songs_quant),
|
||||
colour=config.COLOURS['blue'])
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
if not self.__playing:
|
||||
first_song = self.__playlist.next_song()
|
||||
await self.__play_music(ctx, first_song)
|
||||
|
||||
async def play_prev(self, ctx) -> None:
|
||||
"""Stop the currently playing cycle, load the previous song and play"""
|
||||
if self.__playlist.looping_one or self.__playlist.looping_all: # Do not allow play if loop
|
||||
embed = discord.Embed(
|
||||
title=config.SONG_PLAYER,
|
||||
description=config.LOOP_ON,
|
||||
colour=config.COLOURS['blue']
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
song = self.__playlist.prev_song() # Prepare the prev song to play again
|
||||
if song == None:
|
||||
embed = discord.Embed(
|
||||
title=config.SONG_PLAYER,
|
||||
description=config.NOT_PREVIOUS,
|
||||
colour=config.COLOURS['blue']
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
else:
|
||||
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.__force_stop = True
|
||||
self.__guild.voice_client.stop()
|
||||
self.__playing = False
|
||||
|
||||
await self.__play_music(ctx, song)
|
||||
|
||||
async def queue(self) -> discord.Embed:
|
||||
if self.__playlist.looping_one:
|
||||
info = self.__playlist.current.info
|
||||
title = config.ONE_SONG_LOOPING
|
||||
return self.__format_embed(info, title)
|
||||
|
||||
songs_preload = self.__playlist.songs_to_preload
|
||||
|
||||
if len(songs_preload) == 0:
|
||||
title = config.SONG_PLAYER
|
||||
text = config.EMPTY_QUEUE
|
||||
|
||||
else:
|
||||
if self.__playlist.looping_all:
|
||||
title = config.ALL_SONGS_LOOPING
|
||||
else:
|
||||
title = config.QUEUE_TITLE
|
||||
|
||||
await self.__down.preload(songs_preload)
|
||||
|
||||
total_time = format_time(sum([int(song.duration if song.duration else 0)
|
||||
for song in songs_preload]))
|
||||
total_songs = len(self.__playlist)
|
||||
|
||||
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 config.SONG_DOWNLOADING
|
||||
text += f"**`{pos}` - ** {song_name} - `{format_time(song.duration)}`\n"
|
||||
|
||||
embed = discord.Embed(
|
||||
title=title,
|
||||
description=text,
|
||||
colour=config.COLOURS['blue']
|
||||
)
|
||||
|
||||
return embed
|
||||
|
||||
async def skip(self, ctx) -> bool:
|
||||
if self.__playlist.looping_one:
|
||||
embed = discord.Embed(
|
||||
title=config.SONG_PLAYER,
|
||||
description=config.LOOP_ON,
|
||||
colour=config.COLOURS['blue']
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
return False
|
||||
|
||||
if self.__guild.voice_client != None:
|
||||
self.__guild.voice_client.stop()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def history(self) -> discord.Embed:
|
||||
history = self.__playlist.songs_history
|
||||
|
||||
if len(history) == 0:
|
||||
text = config.HISTORY_EMPTY
|
||||
|
||||
else:
|
||||
text = f'\n📜 History Length: {len(history)} | Max: {config.MAX_SONGS_HISTORY}\n'
|
||||
for pos, song in enumerate(history, start=1):
|
||||
text += f"**`{pos}` - ** {song.title} - `{format_time(song.duration)}`\n"
|
||||
|
||||
embed = discord.Embed(
|
||||
title=config.HISTORY_TITLE,
|
||||
description=text,
|
||||
colour=config.COLOURS['blue']
|
||||
)
|
||||
return embed
|
||||
|
||||
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) -> str:
|
||||
args = args.lower()
|
||||
if self.__playlist.current == None:
|
||||
return config.PLAYER_NOT_PLAYING
|
||||
|
||||
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 = help.HELP_LONG_LOOP
|
||||
|
||||
return description
|
||||
|
||||
async def clear(self) -> None:
|
||||
self.__playlist.clear()
|
||||
|
||||
async def now_playing(self) -> discord.Embed:
|
||||
if not self.__playing:
|
||||
embed = discord.Embed(
|
||||
title=config.SONG_PLAYER,
|
||||
description=config.PLAYER_NOT_PLAYING,
|
||||
colour=config.COLOURS['blue']
|
||||
)
|
||||
return embed
|
||||
|
||||
if self.__playlist.looping_one:
|
||||
title = config.ONE_SONG_LOOPING
|
||||
else:
|
||||
title = config.SONG_PLAYING
|
||||
|
||||
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 config.SONGS_SHUFFLED
|
||||
except:
|
||||
return config.ERROR_SHUFFLING
|
||||
|
||||
async def move(self, pos1, pos2='1') -> str:
|
||||
if not self.__playing:
|
||||
return config.PLAYER_NOT_PLAYING
|
||||
|
||||
try:
|
||||
pos1 = int(pos1)
|
||||
pos2 = int(pos2)
|
||||
|
||||
except:
|
||||
return config.ERROR_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"""
|
||||
if not self.__playing:
|
||||
return config.PLAYER_NOT_PLAYING
|
||||
|
||||
try:
|
||||
position = int(position)
|
||||
|
||||
except:
|
||||
return config.ERROR_NUMBER
|
||||
|
||||
result = self.__playlist.remove_song(position)
|
||||
return result
|
||||
|
||||
def __format_embed(self, info=dict, 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()
|
||||
|
||||
def __ensure_source(self, song: Song) -> str:
|
||||
while True:
|
||||
if song.source != None: # If song got downloaded
|
||||
return song.source
|
||||
|
||||
if song.problematic: # If song got any error
|
||||
return None
|
||||
@@ -1,195 +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.__looping_one = False
|
||||
self.__looping_all = False
|
||||
|
||||
self.__current: Song = None
|
||||
|
||||
@property
|
||||
def songs_history(self) -> deque:
|
||||
return self.__songs_history
|
||||
|
||||
@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 in a normal playlist flow"""
|
||||
if self.__current == 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) > config.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:
|
||||
return None
|
||||
|
||||
self.__current = self.__queue.popleft()
|
||||
|
||||
return self.__current
|
||||
|
||||
def prev_song(self) -> Song:
|
||||
"""If playing return it to queue and return the previous song to play"""
|
||||
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, 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()
|
||||
|
||||
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 config.LOOP_ALL_ON
|
||||
|
||||
elif self.__looping_one == True:
|
||||
return config.LOOP_ONE_ALREADY_ON
|
||||
else:
|
||||
self.__looping_one = True
|
||||
return config.LOOP_ONE_ACTIVATE
|
||||
|
||||
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 config.LOOP_ONE_ON
|
||||
|
||||
elif self.__looping_all == True:
|
||||
return config.LOOP_ALL_ALREADY_ON
|
||||
|
||||
else:
|
||||
self.__looping_all = True
|
||||
return config.LOOP_ALL_ACTIVATE
|
||||
|
||||
def loop_off(self) -> str:
|
||||
"""Disable both types of loop"""
|
||||
if self.__looping_all == False and self.__looping_one == False:
|
||||
return config.LOOP_ALREADY_DISABLE
|
||||
|
||||
self.__looping_all = False
|
||||
self.__looping_one = False
|
||||
return config.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 config.LENGTH_ERROR
|
||||
|
||||
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 config.SONG_MOVED_SUCCESSFULLY.format(song1_name, pos1, song2_name, pos2)
|
||||
except:
|
||||
return config.ERROR_MOVING
|
||||
|
||||
def remove_song(self, position) -> str:
|
||||
if position not in range(1, len(self.__queue) + 1) and position != -1:
|
||||
return config.LENGTH_ERROR
|
||||
else:
|
||||
song = self.__queue[position-1]
|
||||
self.__queue.remove(song)
|
||||
|
||||
song_name = song.title if song.title else song.identifier
|
||||
|
||||
return config.SONG_REMOVED_SUCCESSFULLY.format(song_name)
|
||||
|
||||
def history(self) -> list:
|
||||
"""Return a list with the song title of all played songs"""
|
||||
titles = []
|
||||
for song in self.__songs_history:
|
||||
title = song.title if song.title else 'Unknown'
|
||||
titles.append(title)
|
||||
|
||||
return titles
|
||||
@@ -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,136 +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=str) -> 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)
|
||||
elif type == 'artist':
|
||||
musics = self.__get_artist(code)
|
||||
else:
|
||||
return None
|
||||
|
||||
return musics
|
||||
|
||||
def __get_album(self, code=str) -> list:
|
||||
"""Convert a album ID to list of songs names
|
||||
|
||||
ARG: Spotify Code of the Album
|
||||
"""
|
||||
if self.__connected == True:
|
||||
try:
|
||||
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=str) -> list:
|
||||
"""Convert a playlist ID to list of songs names
|
||||
|
||||
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=str) -> list:
|
||||
"""Convert a track ID to the title of the music
|
||||
|
||||
ARG: Spotify Code of the Track
|
||||
"""
|
||||
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:
|
||||
"""Convert a external_url track to the title of the music
|
||||
|
||||
ARG: Spotify Code of the Music
|
||||
"""
|
||||
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:
|
||||
"""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,55 +0,0 @@
|
||||
import re
|
||||
import asyncio
|
||||
from config import config
|
||||
|
||||
|
||||
def is_connected(ctx):
|
||||
try:
|
||||
voice_channel = ctx.guild.voice_client.channel
|
||||
|
||||
if not ctx.guild.voice_client.is_connected():
|
||||
return None
|
||||
else:
|
||||
return voice_channel
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def format_time(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
|
||||
)
|
||||
|
||||
|
||||
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