17 Commits
v1.4 ... v1.5

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

1
.gitignore vendored
View File

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

BIN
Assets/playermenu.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
Assets/queuemessage.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

BIN
Assets/vulkan-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
Assets/vulkancommands.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -1,5 +1,7 @@
import os
from decouple import config from decouple import config
from Config.Singleton import Singleton from Config.Singleton import Singleton
from Config.Folder import Folder
class VConfigs(Singleton): class VConfigs(Singleton):
@@ -17,11 +19,18 @@ class VConfigs(Singleton):
self.CLEANER_MESSAGES_QUANT = 5 self.CLEANER_MESSAGES_QUANT = 5
self.ACQUIRE_LOCK_TIMEOUT = 10 self.ACQUIRE_LOCK_TIMEOUT = 10
self.COMMANDS_PATH = 'DiscordCogs' self.QUEUE_VIEW_TIMEOUT = 120
self.COMMANDS_FOLDER_NAME = 'DiscordCogs'
self.COMMANDS_PATH = f'{Folder().rootFolder}{self.COMMANDS_FOLDER_NAME}'
self.VC_TIMEOUT = 300 self.VC_TIMEOUT = 300
self.CHANCE_SHOW_PROJECT = 15
self.PROJECT_URL = 'https://github.com/RafaelSolVargas/Vulkan'
self.SUPPORTING_ICON = 'https://i.pinimg.com/originals/d6/05/b4/d605b4f8c5d1c6ae20dc353ef9f091bd.png'
self.MAX_PLAYLIST_LENGTH = 50 self.MAX_PLAYLIST_LENGTH = 50
self.MAX_PLAYLIST_FORCED_LENGTH = 5 self.MAX_PLAYLIST_FORCED_LENGTH = 5
self.MAX_SONGS_IN_PAGE = 10
self.MAX_PRELOAD_SONGS = 15 self.MAX_PRELOAD_SONGS = 15
self.MAX_SONGS_HISTORY = 15 self.MAX_SONGS_HISTORY = 15

View File

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

View File

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

19
Config/Folder.py Normal file
View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
from discord.ext.commands import Context, command, Cog from discord.ext.commands import Context, command, Cog
from Config.Exceptions import InvalidInput
from Config.Helper import Helper from Config.Helper import Helper
from Handlers.ClearHandler import ClearHandler from Handlers.ClearHandler import ClearHandler
from Handlers.HandlerResponse import HandlerResponse
from Handlers.MoveHandler import MoveHandler from Handlers.MoveHandler import MoveHandler
from Handlers.NowPlayingHandler import NowPlayingHandler from Handlers.NowPlayingHandler import NowPlayingHandler
from Handlers.PlayHandler import PlayHandler from Handlers.PlayHandler import PlayHandler
@@ -15,11 +17,12 @@ from Handlers.ResumeHandler import ResumeHandler
from Handlers.HistoryHandler import HistoryHandler from Handlers.HistoryHandler import HistoryHandler
from Handlers.QueueHandler import QueueHandler from Handlers.QueueHandler import QueueHandler
from Handlers.LoopHandler import LoopHandler from Handlers.LoopHandler import LoopHandler
from UI.Responses.EmoteCogResponse import EmoteCommandResponse from Messages.MessagesCategory import MessagesCategory
from UI.Responses.EmbedCogResponse import EmbedCommandResponse from Messages.Responses.EmoteCogResponse import EmoteCommandResponse
from UI.Views.PlayerView import PlayerView from Messages.Responses.EmbedCogResponse import EmbedCommandResponse
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
from Config.Configs import VConfigs from Config.Configs import VConfigs
from Config.Embeds import VEmbeds
from Parallelism.ProcessManager import ProcessManager from Parallelism.ProcessManager import ProcessManager
helper = Helper() helper = Helper()
@@ -34,6 +37,7 @@ class MusicCog(Cog):
def __init__(self, bot: VulkanBot) -> None: def __init__(self, bot: VulkanBot) -> None:
self.__bot: VulkanBot = bot self.__bot: VulkanBot = bot
self.__embeds = VEmbeds()
VConfigs().setProcessManager(ProcessManager(bot)) VConfigs().setProcessManager(ProcessManager(bot))
@command(name="play", help=helper.HELP_PLAY, description=helper.HELP_PLAY_LONG, aliases=['p', 'tocar']) @command(name="play", help=helper.HELP_PLAY, description=helper.HELP_PLAY_LONG, aliases=['p', 'tocar'])
@@ -41,23 +45,44 @@ class MusicCog(Cog):
try: try:
controller = PlayHandler(ctx, self.__bot) controller = PlayHandler(ctx, self.__bot)
response = await controller.run(args) if len(args) > 1:
track = " ".join(args)
else:
track = args
response = await controller.run(track)
if response is not None: if response is not None:
view1 = EmbedCommandResponse(response) cogResponser1 = EmbedCommandResponse(response, MessagesCategory.PLAYER)
view2 = EmoteCommandResponse(response) cogResponser2 = EmoteCommandResponse(response, MessagesCategory.PLAYER)
await view1.run() await cogResponser1.run()
await view2.run() await cogResponser2.run()
except Exception as e: except Exception as e:
print(f'[ERROR IN COG] -> {e}') print(f'[ERROR IN COG] -> {e}')
@command(name="queue", help=helper.HELP_QUEUE, description=helper.HELP_QUEUE_LONG, aliases=['q', 'fila', 'musicas']) @command(name="queue", help=helper.HELP_QUEUE, description=helper.HELP_QUEUE_LONG, aliases=['q', 'fila', 'musicas'])
async def queue(self, ctx: Context) -> None: async def queue(self, ctx: Context, *args) -> None:
try: try:
pageNumber = " ".join(args)
controller = QueueHandler(ctx, self.__bot) controller = QueueHandler(ctx, self.__bot)
response = await controller.run() if pageNumber == "":
view2 = EmbedCommandResponse(response) response = await controller.run()
await view2.run() else:
pageNumber = int(pageNumber)
pageNumber -= 1 # Change index 1 to 0
response = await controller.run(pageNumber)
cogResponser = EmbedCommandResponse(response, MessagesCategory.QUEUE)
await cogResponser.run()
except ValueError as e:
# Draft a Handler Response to pass to cogResponser
error = InvalidInput()
embed = self.__embeds.INVALID_ARGUMENTS()
response = HandlerResponse(ctx, embed, error)
cogResponser = EmbedCommandResponse(response, MessagesCategory.QUEUE)
await cogResponser.run(deleteLast=False)
except Exception as e: except Exception as e:
print(f'[ERROR IN COG] -> {e}') print(f'[ERROR IN COG] -> {e}')
@@ -67,12 +92,10 @@ class MusicCog(Cog):
controller = SkipHandler(ctx, self.__bot) controller = SkipHandler(ctx, self.__bot)
response = await controller.run() response = await controller.run()
if response.success: cogResponser1 = EmbedCommandResponse(response, MessagesCategory.PLAYER)
view = EmoteCommandResponse(response) cogResponser2 = EmoteCommandResponse(response, MessagesCategory.PLAYER)
else: await cogResponser1.run()
view = EmbedCommandResponse(response) await cogResponser2.run()
await view.run()
except Exception as e: except Exception as e:
print(f'[ERROR IN COG] -> {e}') print(f'[ERROR IN COG] -> {e}')
@@ -82,12 +105,10 @@ class MusicCog(Cog):
controller = StopHandler(ctx, self.__bot) controller = StopHandler(ctx, self.__bot)
response = await controller.run() response = await controller.run()
if response.success: cogResponser1 = EmbedCommandResponse(response, MessagesCategory.PLAYER)
view = EmoteCommandResponse(response) cogResponser2 = EmoteCommandResponse(response, MessagesCategory.PLAYER)
else: await cogResponser1.run()
view = EmbedCommandResponse(response) await cogResponser2.run()
await view.run()
except Exception as e: except Exception as e:
print(f'[ERROR IN COG] -> {e}') print(f'[ERROR IN COG] -> {e}')
@@ -97,10 +118,10 @@ class MusicCog(Cog):
controller = PauseHandler(ctx, self.__bot) controller = PauseHandler(ctx, self.__bot)
response = await controller.run() response = await controller.run()
view1 = EmoteCommandResponse(response) cogResponser1 = EmoteCommandResponse(response, MessagesCategory.PLAYER)
view2 = EmbedCommandResponse(response) cogResponser2 = EmbedCommandResponse(response, MessagesCategory.PLAYER)
await view1.run() await cogResponser1.run()
await view2.run() await cogResponser2.run()
except Exception as e: except Exception as e:
print(f'[ERROR IN COG] -> {e}') print(f'[ERROR IN COG] -> {e}')
@@ -110,10 +131,10 @@ class MusicCog(Cog):
controller = ResumeHandler(ctx, self.__bot) controller = ResumeHandler(ctx, self.__bot)
response = await controller.run() response = await controller.run()
view1 = EmoteCommandResponse(response) cogResponser1 = EmoteCommandResponse(response, MessagesCategory.PLAYER)
view2 = EmbedCommandResponse(response) cogResponser2 = EmbedCommandResponse(response, MessagesCategory.PLAYER)
await view1.run() await cogResponser1.run()
await view2.run() await cogResponser2.run()
except Exception as e: except Exception as e:
print(f'[ERROR IN COG] -> {e}') print(f'[ERROR IN COG] -> {e}')
@@ -124,10 +145,10 @@ class MusicCog(Cog):
response = await controller.run() response = await controller.run()
if response is not None: if response is not None:
view1 = EmbedCommandResponse(response) cogResponser1 = EmbedCommandResponse(response, MessagesCategory.PLAYER)
view2 = EmoteCommandResponse(response) cogResponser2 = EmoteCommandResponse(response, MessagesCategory.PLAYER)
await view1.run() await cogResponser1.run()
await view2.run() await cogResponser2.run()
except Exception as e: except Exception as e:
print(f'[ERROR IN COG] -> {e}') print(f'[ERROR IN COG] -> {e}')
@@ -137,10 +158,10 @@ class MusicCog(Cog):
controller = HistoryHandler(ctx, self.__bot) controller = HistoryHandler(ctx, self.__bot)
response = await controller.run() response = await controller.run()
view1 = EmbedCommandResponse(response) cogResponser1 = EmbedCommandResponse(response, MessagesCategory.HISTORY)
view2 = EmoteCommandResponse(response) cogResponser2 = EmoteCommandResponse(response, MessagesCategory.HISTORY)
await view1.run() await cogResponser1.run()
await view2.run() await cogResponser2.run()
except Exception as e: except Exception as e:
print(f'[ERROR IN COG] -> {e}') print(f'[ERROR IN COG] -> {e}')
@@ -150,10 +171,10 @@ class MusicCog(Cog):
controller = LoopHandler(ctx, self.__bot) controller = LoopHandler(ctx, self.__bot)
response = await controller.run(args) response = await controller.run(args)
view1 = EmoteCommandResponse(response) cogResponser1 = EmoteCommandResponse(response, MessagesCategory.LOOP)
view2 = EmbedCommandResponse(response) cogResponser2 = EmbedCommandResponse(response, MessagesCategory.LOOP)
await view1.run() await cogResponser1.run()
await view2.run() await cogResponser2.run()
except Exception as e: except Exception as e:
print(f'[ERROR IN COG] -> {e}') print(f'[ERROR IN COG] -> {e}')
@@ -163,8 +184,10 @@ class MusicCog(Cog):
controller = ClearHandler(ctx, self.__bot) controller = ClearHandler(ctx, self.__bot)
response = await controller.run() response = await controller.run()
view = EmoteCommandResponse(response) cogResponser1 = EmbedCommandResponse(response, MessagesCategory.PLAYER)
await view.run() cogResponser2 = EmoteCommandResponse(response, MessagesCategory.PLAYER)
await cogResponser1.run()
await cogResponser2.run()
except Exception as e: except Exception as e:
print(f'[ERROR IN COG] -> {e}') print(f'[ERROR IN COG] -> {e}')
@@ -174,10 +197,10 @@ class MusicCog(Cog):
controller = NowPlayingHandler(ctx, self.__bot) controller = NowPlayingHandler(ctx, self.__bot)
response = await controller.run() response = await controller.run()
view1 = EmbedCommandResponse(response) cogResponser1 = EmbedCommandResponse(response, MessagesCategory.NOW_PLAYING)
view2 = EmoteCommandResponse(response) cogResponser2 = EmoteCommandResponse(response, MessagesCategory.NOW_PLAYING)
await view1.run() await cogResponser1.run()
await view2.run() await cogResponser2.run()
except Exception as e: except Exception as e:
print(f'[ERROR IN COG] -> {e}') print(f'[ERROR IN COG] -> {e}')
@@ -187,10 +210,10 @@ class MusicCog(Cog):
controller = ShuffleHandler(ctx, self.__bot) controller = ShuffleHandler(ctx, self.__bot)
response = await controller.run() response = await controller.run()
view1 = EmbedCommandResponse(response) cogResponser1 = EmbedCommandResponse(response, MessagesCategory.PLAYER)
view2 = EmoteCommandResponse(response) cogResponser2 = EmoteCommandResponse(response, MessagesCategory.PLAYER)
await view1.run() await cogResponser1.run()
await view2.run() await cogResponser2.run()
except Exception as e: except Exception as e:
print(f'[ERROR IN COG] -> {e}') print(f'[ERROR IN COG] -> {e}')
@@ -200,10 +223,10 @@ class MusicCog(Cog):
controller = MoveHandler(ctx, self.__bot) controller = MoveHandler(ctx, self.__bot)
response = await controller.run(pos1, pos2) response = await controller.run(pos1, pos2)
view1 = EmbedCommandResponse(response) cogResponser1 = EmbedCommandResponse(response, MessagesCategory.MANAGING_QUEUE)
view2 = EmoteCommandResponse(response) cogResponser2 = EmoteCommandResponse(response, MessagesCategory.MANAGING_QUEUE)
await view1.run() await cogResponser1.run()
await view2.run() await cogResponser2.run()
except Exception as e: except Exception as e:
print(f'[ERROR IN COG] -> {e}') print(f'[ERROR IN COG] -> {e}')
@@ -213,10 +236,10 @@ class MusicCog(Cog):
controller = RemoveHandler(ctx, self.__bot) controller = RemoveHandler(ctx, self.__bot)
response = await controller.run(position) response = await controller.run(position)
view1 = EmbedCommandResponse(response) cogResponser1 = EmbedCommandResponse(response, MessagesCategory.MANAGING_QUEUE)
view2 = EmoteCommandResponse(response) cogResponser2 = EmoteCommandResponse(response, MessagesCategory.MANAGING_QUEUE)
await view1.run() await cogResponser1.run()
await view2.run() await cogResponser2.run()
except Exception as e: except Exception as e:
print(f'[ERROR IN COG] -> {e}') print(f'[ERROR IN COG] -> {e}')
@@ -226,18 +249,13 @@ class MusicCog(Cog):
controller = ResetHandler(ctx, self.__bot) controller = ResetHandler(ctx, self.__bot)
response = await controller.run() response = await controller.run()
view1 = EmbedCommandResponse(response) cogResponser1 = EmbedCommandResponse(response, MessagesCategory.PLAYER)
view2 = EmoteCommandResponse(response) cogResponser2 = EmoteCommandResponse(response, MessagesCategory.PLAYER)
await view1.run() await cogResponser1.run()
await view2.run() await cogResponser2.run()
except Exception as e: except Exception as e:
print(f'[ERROR IN COG] -> {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): def setup(bot):
bot.add_cog(MusicCog(bot)) bot.add_cog(MusicCog(bot))

271
DiscordCogs/SlashCog.py Normal file
View File

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

23
HEROKU.md Normal file
View File

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

View File

@@ -4,6 +4,7 @@ from discord.ext.commands import Context
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
from Handlers.AbstractHandler import AbstractHandler from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse from Handlers.HandlerResponse import HandlerResponse
from Parallelism.ProcessInfo import ProcessInfo
class ClearHandler(AbstractHandler): class ClearHandler(AbstractHandler):
@@ -13,7 +14,7 @@ class ClearHandler(AbstractHandler):
async def run(self) -> HandlerResponse: async def run(self) -> HandlerResponse:
# Get the current process of the guild # Get the current process of the guild
processManager = self.config.getProcessManager() processManager = self.config.getProcessManager()
processInfo = processManager.getRunningPlayerInfo(self.guild) processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild)
if processInfo: if processInfo:
# Clear the playlist # Clear the playlist
playlist = processInfo.getPlaylist() playlist = processInfo.getPlaylist()
@@ -22,7 +23,8 @@ class ClearHandler(AbstractHandler):
if acquired: if acquired:
playlist.clear() playlist.clear()
processLock.release() processLock.release()
return HandlerResponse(self.ctx) embed = self.embeds.PLAYLIST_CLEAR()
return HandlerResponse(self.ctx, embed)
else: else:
processManager.resetProcess(self.guild, self.ctx) processManager.resetProcess(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED() embed = self.embeds.PLAYER_RESTARTED()

View File

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

View File

@@ -0,0 +1,80 @@
from typing import Union
from Config.Exceptions import BadCommandUsage, InvalidInput, NumberRequired, UnknownError, VulkanError
from Handlers.AbstractHandler import AbstractHandler
from discord.ext.commands import Context
from discord import Interaction
from Handlers.HandlerResponse import HandlerResponse
from Music.Playlist import Playlist
from Music.VulkanBot import VulkanBot
from Parallelism.Commands import VCommands, VCommandsType
class JumpMusicHandler(AbstractHandler):
"""Move a music from a specific position and play it directly"""
def __init__(self, ctx: Union[Context, Interaction], bot: VulkanBot) -> None:
super().__init__(ctx, bot)
async def run(self, musicPos: str) -> HandlerResponse:
processManager = self.config.getProcessManager()
processInfo = processManager.getRunningPlayerInfo(self.guild)
if not processInfo:
embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage()
return HandlerResponse(self.ctx, embed, error)
processLock = processInfo.getLock()
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired:
# Try to convert input to int
error = self.__validateInput(musicPos)
if error:
embed = self.embeds.ERROR_EMBED(error.message)
processLock.release()
return HandlerResponse(self.ctx, embed, error)
# Sanitize the input
playlist: Playlist = processInfo.getPlaylist()
musicPos = self.__sanitizeInput(playlist, musicPos)
# Validate the position
if not playlist.validate_position(musicPos):
error = InvalidInput()
embed = self.embeds.PLAYLIST_RANGE_ERROR()
processLock.release()
return HandlerResponse(self.ctx, embed, error)
try:
# Move the selected song
playlist.move_songs(musicPos, 1)
# Send a command to the player to skip the music
command = VCommands(VCommandsType.SKIP, None)
queue = processInfo.getQueueToPlayer()
queue.put(command)
processLock.release()
return HandlerResponse(self.ctx)
except:
# Release the acquired Lock
processLock.release()
embed = self.embeds.ERROR_MOVING()
error = UnknownError()
return HandlerResponse(self.ctx, embed, error)
else:
processManager.resetProcess(self.guild, self.ctx)
embed = self.embeds.PLAYER_RESTARTED()
return HandlerResponse(self.ctx, embed)
def __validateInput(self, position: str) -> Union[VulkanError, None]:
try:
position = int(position)
except:
return NumberRequired(self.messages.ERROR_NUMBER)
def __sanitizeInput(self, playlist: Playlist, position: int) -> int:
position = int(position)
if position == -1:
position = len(playlist.getSongs())
return position

View File

@@ -2,6 +2,7 @@ from discord.ext.commands import Context
from Handlers.AbstractHandler import AbstractHandler from Handlers.AbstractHandler import AbstractHandler
from Handlers.HandlerResponse import HandlerResponse from Handlers.HandlerResponse import HandlerResponse
from Parallelism.Commands import VCommands, VCommandsType from Parallelism.Commands import VCommands, VCommandsType
from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
from typing import Union from typing import Union
from discord import Interaction from discord import Interaction
@@ -13,14 +14,19 @@ class PauseHandler(AbstractHandler):
async def run(self) -> HandlerResponse: async def run(self) -> HandlerResponse:
processManager = self.config.getProcessManager() processManager = self.config.getProcessManager()
processInfo = processManager.getRunningPlayerInfo(self.guild) processInfo: ProcessInfo = processManager.getRunningPlayerInfo(self.guild)
if processInfo: if processInfo:
if processInfo.getStatus() == ProcessStatus.SLEEPING:
embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed)
# Send Pause command to be execute by player process # Send Pause command to be execute by player process
command = VCommands(VCommandsType.PAUSE, None) command = VCommands(VCommandsType.PAUSE, None)
queue = processInfo.getQueueToPlayer() queue = processInfo.getQueueToPlayer()
queue.put(command) queue.put(command)
return HandlerResponse(self.ctx) embed = self.embeds.PLAYER_PAUSED()
return HandlerResponse(self.ctx, embed)
else: else:
embed = self.embeds.NOT_PLAYING() embed = self.embeds.NOT_PLAYING()
return HandlerResponse(self.ctx, embed) return HandlerResponse(self.ctx, embed)

View File

@@ -21,8 +21,7 @@ class PlayHandler(AbstractHandler):
self.__searcher = Searcher() self.__searcher = Searcher()
self.__down = Downloader() self.__down = Downloader()
async def run(self, args: str) -> HandlerResponse: async def run(self, track: str) -> HandlerResponse:
track = " ".join(args)
requester = self.ctx.author.name requester = self.ctx.author.name
if not self.__isUserConnected(): if not self.__isUserConnected():
@@ -38,7 +37,7 @@ class PlayHandler(AbstractHandler):
# Get the process context for the current guild # Get the process context for the current guild
processManager = self.config.getProcessManager() processManager = self.config.getProcessManager()
processInfo = processManager.getPlayerInfo(self.guild, self.ctx) processInfo = processManager.getOrCreatePlayerInfo(self.guild, self.ctx)
playlist = processInfo.getPlaylist() playlist = processInfo.getPlaylist()
process = processInfo.getProcess() process = processInfo.getProcess()
if not process.is_alive(): # If process has not yet started, start if not process.is_alive(): # If process has not yet started, start
@@ -119,7 +118,7 @@ class PlayHandler(AbstractHandler):
await task await task
song = songs[index] song = songs[index]
if not song.problematic: # If downloaded add to the playlist and send play command if not song.problematic: # If downloaded add to the playlist and send play command
processInfo = processManager.getPlayerInfo(self.guild, self.ctx) processInfo = processManager.getOrCreatePlayerInfo(self.guild, self.ctx)
processLock = processInfo.getLock() processLock = processInfo.getLock()
acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT) acquired = processLock.acquire(timeout=self.config.ACQUIRE_LOCK_TIMEOUT)
if acquired: if acquired:

View File

@@ -13,8 +13,13 @@ class PrevHandler(AbstractHandler):
super().__init__(ctx, bot) super().__init__(ctx, bot)
async def run(self) -> HandlerResponse: async def run(self) -> HandlerResponse:
if not self.__user_connected():
error = ImpossibleMove()
embed = self.embeds.NO_CHANNEL()
return HandlerResponse(self.ctx, embed, error)
processManager = self.config.getProcessManager() processManager = self.config.getProcessManager()
processInfo = processManager.getPlayerInfo(self.guild, self.ctx) processInfo = processManager.getOrCreatePlayerInfo(self.guild, self.ctx)
if not processInfo: if not processInfo:
embed = self.embeds.NOT_PLAYING() embed = self.embeds.NOT_PLAYING()
error = BadCommandUsage() error = BadCommandUsage()
@@ -26,11 +31,6 @@ class PrevHandler(AbstractHandler):
embed = self.embeds.NOT_PREVIOUS_SONG() embed = self.embeds.NOT_PREVIOUS_SONG()
return HandlerResponse(self.ctx, embed, error) 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(): if playlist.isLoopingAll() or playlist.isLoopingOne():
error = BadCommandUsage() error = BadCommandUsage()
embed = self.embeds.FAIL_DUE_TO_LOOP_ON() embed = self.embeds.FAIL_DUE_TO_LOOP_ON()
@@ -45,7 +45,9 @@ class PrevHandler(AbstractHandler):
prevCommand = VCommands(VCommandsType.PREV, self.author.voice.channel.id) prevCommand = VCommands(VCommandsType.PREV, self.author.voice.channel.id)
queue = processInfo.getQueueToPlayer() queue = processInfo.getQueueToPlayer()
queue.put(prevCommand) queue.put(prevCommand)
return HandlerResponse(self.ctx)
embed = self.embeds.RETURNING_SONG()
return HandlerResponse(self.ctx, embed)
def __user_connected(self) -> bool: def __user_connected(self) -> bool:
if self.author.voice: if self.author.voice:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,21 +15,24 @@ class Downloader:
'playliststart': 0, 'playliststart': 0,
'extract_flat': False, 'extract_flat': False,
'playlistend': config.MAX_PLAYLIST_LENGTH, 'playlistend': config.MAX_PLAYLIST_LENGTH,
'quiet': True 'quiet': True,
'ignore_no_formats_error': True
} }
__YDL_OPTIONS_EXTRACT = {'format': 'bestaudio/best', __YDL_OPTIONS_EXTRACT = {'format': 'bestaudio/best',
'default_search': 'auto', 'default_search': 'auto',
'playliststart': 0, 'playliststart': 0,
'extract_flat': True, 'extract_flat': True,
'playlistend': config.MAX_PLAYLIST_LENGTH, 'playlistend': config.MAX_PLAYLIST_LENGTH,
'quiet': True 'quiet': True,
'ignore_no_formats_error': True
} }
__YDL_OPTIONS_FORCE_EXTRACT = {'format': 'bestaudio/best', __YDL_OPTIONS_FORCE_EXTRACT = {'format': 'bestaudio/best',
'default_search': 'auto', 'default_search': 'auto',
'playliststart': 0, 'playliststart': 0,
'extract_flat': False, 'extract_flat': False,
'playlistend': config.MAX_PLAYLIST_LENGTH, 'playlistend': config.MAX_PLAYLIST_LENGTH,
'quiet': True 'quiet': True,
'ignore_no_formats_error': True
} }
__BASE_URL = 'https://www.youtube.com/watch?v={}' __BASE_URL = 'https://www.youtube.com/watch?v={}'
@@ -110,7 +113,7 @@ class Downloader:
return result return result
except Exception as e: # Any type of error in download except Exception as e: # Any type of error in download
print(f'DEVELOPER NOTE -> Error Downloading URL {e}') print(f'DEVELOPER NOTE -> Error Downloading {url} -> {e}')
return None return None
async def download_song(self, song: Song) -> None: async def download_song(self, song: Song) -> None:
@@ -118,12 +121,15 @@ class Downloader:
return None return None
def __download_func(song: Song) -> None: def __download_func(song: Song) -> None:
if Utils.is_url(song.identifier): try:
song_info = self.__download_url(song.identifier) if Utils.is_url(song.identifier):
else: song_info = self.__download_url(song.identifier)
song_info = self.__download_title(song.identifier) else:
song_info = self.__download_title(song.identifier)
song.finish_down(song_info) song.finish_down(song_info)
except Exception as e:
print(f'DEVELOPER NOTE -> Error Downloading {song.identifier} -> {e}')
# Creating a loop task to download each song # Creating a loop task to download each song
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
class Song: class Song:
def __init__(self, identifier: str, playlist, requester: str) -> None: def __init__(self, identifier: str, playlist, requester: str) -> None:
self.__identifier = identifier self.__identifier = identifier
self.__info = {'requester': requester} self.__info = {'requester': requester}
@@ -29,6 +28,12 @@ class Song:
if key in info.keys(): if key in info.keys():
self.__info[key] = info[key] self.__info[key] = info[key]
self.__cleanTitle()
def __cleanTitle(self) -> None:
self.__info['title'] = ''.join(char if char.isalnum() or char ==
' ' else ' ' for char in self.__info['title'])
@property @property
def source(self) -> str: def source(self) -> str:
if 'url' in self.__info.keys(): if 'url' in self.__info.keys():

View File

@@ -8,13 +8,18 @@ from Config.Embeds import VEmbeds
class VulkanBot(Bot): class VulkanBot(Bot):
def __init__(self, *args, **kwargs): def __init__(self, listingSlash: bool = False, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.__listingSlash = listingSlash
self.__configs = VConfigs() self.__configs = VConfigs()
self.__messages = Messages() self.__messages = Messages()
self.__embeds = VEmbeds() self.__embeds = VEmbeds()
self.remove_command("help") self.remove_command("help")
@property
def listingSlash(self) -> bool:
return self.__listingSlash
def startBot(self) -> None: def startBot(self) -> None:
"""Blocking function that will start the bot""" """Blocking function that will start the bot"""
if self.__configs.BOT_TOKEN == '': if self.__configs.BOT_TOKEN == '':

View File

@@ -23,21 +23,26 @@ class VulkanInitializer:
def __create_bot(self, willListen: bool) -> VulkanBot: def __create_bot(self, willListen: bool) -> VulkanBot:
if willListen: if willListen:
prefix = self.__config.BOT_PREFIX prefix = self.__config.BOT_PREFIX
bot = VulkanBot(listingSlash=True,
command_prefix=prefix,
pm_help=True,
case_insensitive=True,
intents=self.__intents)
else: else:
prefix = ''.join(choices(string.ascii_uppercase + string.digits, k=4)) prefix = ''.join(choices(string.ascii_uppercase + string.digits, k=4))
bot = VulkanBot(listingSlash=False,
bot = VulkanBot(command_prefix=prefix, command_prefix=prefix,
pm_help=True, pm_help=True,
case_insensitive=True, case_insensitive=True,
intents=self.__intents) intents=self.__intents)
return bot return bot
def __add_cogs(self, bot: Bot) -> None: def __add_cogs(self, bot: Bot) -> None:
try: try:
cogsStatus = [] cogsStatus = []
for filename in listdir(f'./{self.__config.COMMANDS_PATH}'): for filename in listdir(self.__config.COMMANDS_PATH):
if filename.endswith('.py'): if filename.endswith('.py'):
cogPath = f'{self.__config.COMMANDS_PATH}.{filename[:-3]}' cogPath = f'{self.__config.COMMANDS_FOLDER_NAME}.{filename[:-3]}'
cogsStatus.append(bot.load_extension(cogPath, store=True)) cogsStatus.append(bot.load_extension(cogPath, store=True))
if len(bot.cogs.keys()) != self.__getTotalCogs(): if len(bot.cogs.keys()) != self.__getTotalCogs():
@@ -49,7 +54,7 @@ class VulkanInitializer:
def __getTotalCogs(self) -> int: def __getTotalCogs(self) -> int:
quant = 0 quant = 0
for filename in listdir(f'./{self.__config.COMMANDS_PATH}'): for filename in listdir(self.__config.COMMANDS_PATH):
if filename.endswith('.py'): if filename.endswith('.py'):
quant += 1 quant += 1
return quant return quant

View File

@@ -247,6 +247,7 @@ class PlayerProcess(Process):
if self.__guild.voice_client.is_connected(): if self.__guild.voice_client.is_connected():
with self.__playlistLock: with self.__playlistLock:
self.__playlist.loop_off() self.__playlist.loop_off()
self.__playlist.clear()
# Send a command to the main process put this to sleep # Send a command to the main process put this to sleep
sleepCommand = VCommands(VCommandsType.SLEEPING) sleepCommand = VCommands(VCommandsType.SLEEPING)

View File

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

View File

@@ -1,8 +1,14 @@
from enum import Enum
from multiprocessing import Process, Queue, Lock from multiprocessing import Process, Queue, Lock
from discord import TextChannel from discord import TextChannel
from Music.Playlist import Playlist from Music.Playlist import Playlist
class ProcessStatus(Enum):
RUNNING = 'Running'
SLEEPING = 'Sleeping'
class ProcessInfo: class ProcessInfo:
""" """
Class to store the reference to all structures to maintain a player process Class to store the reference to all structures to maintain a player process
@@ -15,10 +21,17 @@ class ProcessInfo:
self.__playlist = playlist self.__playlist = playlist
self.__lock = lock self.__lock = lock
self.__textChannel = textChannel self.__textChannel = textChannel
self.__status = ProcessStatus.RUNNING
def setProcess(self, newProcess: Process) -> None: def setProcess(self, newProcess: Process) -> None:
self.__process = newProcess self.__process = newProcess
def getStatus(self) -> ProcessStatus:
return self.__status
def setStatus(self, status: ProcessStatus) -> None:
self.__status = status
def getProcess(self) -> Process: def getProcess(self) -> Process:
return self.__process return self.__process

View File

@@ -7,11 +7,11 @@ from typing import Dict, Tuple, Union
from Config.Singleton import Singleton from Config.Singleton import Singleton
from discord import Guild, Interaction from discord import Guild, Interaction
from discord.ext.commands import Context from discord.ext.commands import Context
from Music.MessagesController import MessagesController from Parallelism.ProcessExecutor import ProcessCommandsExecutor
from Music.Song import Song from Music.Song import Song
from Parallelism.PlayerProcess import PlayerProcess from Parallelism.PlayerProcess import PlayerProcess
from Music.Playlist import Playlist from Music.Playlist import Playlist
from Parallelism.ProcessInfo import ProcessInfo from Parallelism.ProcessInfo import ProcessInfo, ProcessStatus
from Parallelism.Commands import VCommands, VCommandsType from Parallelism.Commands import VCommands, VCommandsType
from Music.VulkanBot import VulkanBot from Music.VulkanBot import VulkanBot
@@ -30,21 +30,14 @@ class ProcessManager(Singleton):
self.__manager.start() self.__manager.start()
self.__playersProcess: Dict[Guild, ProcessInfo] = {} self.__playersProcess: Dict[Guild, ProcessInfo] = {}
self.__playersListeners: Dict[Guild, Tuple[Thread, bool]] = {} self.__playersListeners: Dict[Guild, Tuple[Thread, bool]] = {}
self.__playersMessages: Dict[Guild, MessagesController] = {} self.__playersCommandsExecutor: Dict[Guild, ProcessCommandsExecutor] = {}
def setPlayerInfo(self, guild: Guild, info: ProcessInfo): def setPlayerInfo(self, guild: Guild, info: ProcessInfo):
self.__playersProcess[guild.id] = info self.__playersProcess[guild.id] = info
def getPlayerInfo(self, guild: Guild, context: Union[Context, Interaction]) -> ProcessInfo: def getOrCreatePlayerInfo(self, guild: Guild, context: Union[Context, Interaction]) -> ProcessInfo:
"""Return the process info for the guild, if not and context is a instance """Return the process info for the guild, the user in context must be connected to a voice_channel"""
of discord.Context then create one, else return None"""
try: 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(): if guild.id not in self.__playersProcess.keys():
self.__playersProcess[guild.id] = self.__createProcessInfo(guild, context) self.__playersProcess[guild.id] = self.__createProcessInfo(guild, context)
else: else:
@@ -98,16 +91,20 @@ class ProcessManager(Singleton):
thread.start() thread.start()
# Create a Message Controller for this player # Create a Message Controller for this player
self.__playersMessages[guildID] = MessagesController(self.__bot) self.__playersCommandsExecutor[guildID] = ProcessCommandsExecutor(self.__bot, guildID)
return processInfo return processInfo
def __recreateProcess(self, guild: Guild, context: Context) -> ProcessInfo: def __recreateProcess(self, guild: Guild, context: Union[Context, Interaction]) -> ProcessInfo:
"""Create a new process info using previous playlist""" """Create a new process info using previous playlist"""
guildID: int = context.guild.id guildID: int = context.guild.id
textID: int = context.channel.id textID: int = context.channel.id
voiceID: int = context.author.voice.channel.id if isinstance(context, Interaction):
authorID: int = context.author.id authorID: int = context.user.id
voiceID: int = context.user.voice.channel.id
else:
authorID: int = context.author.id
voiceID: int = context.author.voice.channel.id
playlist: Playlist = self.__playersProcess[guildID].getPlaylist() playlist: Playlist = self.__playersProcess[guildID].getPlaylist()
lock = Lock() lock = Lock()
@@ -160,7 +157,7 @@ class ProcessManager(Singleton):
def __terminateProcess(self, guildID: int) -> None: def __terminateProcess(self, guildID: int) -> None:
# Delete all structures associated with the Player # Delete all structures associated with the Player
del self.__playersProcess[guildID] del self.__playersProcess[guildID]
del self.__playersMessages[guildID] del self.__playersCommandsExecutor[guildID]
threadListening = self.__playersListeners[guildID] threadListening = self.__playersListeners[guildID]
threadListening._stop() threadListening._stop()
del self.__playersListeners[guildID] del self.__playersListeners[guildID]
@@ -173,11 +170,13 @@ class ProcessManager(Singleton):
queue1.join_thread() queue1.join_thread()
queue2.close() queue2.close()
queue2.join_thread() queue2.join_thread()
# Set the status of this process as sleeping, only the playlist object remains
self.__playersProcess[guildID].setStatus(ProcessStatus.SLEEPING)
async def showNowPlaying(self, guildID: int, song: Song) -> None: async def showNowPlaying(self, guildID: int, song: Song) -> None:
messagesController = self.__playersMessages[guildID] commandExecutor = self.__playersCommandsExecutor[guildID]
processInfo = self.__playersProcess[guildID] processInfo = self.__playersProcess[guildID]
await messagesController.sendNowPlaying(processInfo, song) await commandExecutor.sendNowPlaying(processInfo, song)
class VManager(BaseManager): class VManager(BaseManager):

109
README.md
View File

@@ -1,68 +1,58 @@
# **Vulkan** <h1 align="center">Vulkan</h1>
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** A Music Discord Bot, that plays *Youtube*, *Spotify*, *Deezer* links or raw queries. Vulkan is open source, so everyone can fork this project, follow the instructions and use it in their own way, executing it in your own machine or hosting in others machines to work 24/7.
- 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 Vulkan uses multiprocessing and asynchronous Python modules to maximize Music Player response time, so the player doesn't lag when many commands are being processed and it can play in multiples discord serves at the same time without affecting the Music Player response time.
```!play [title, spotify_url, youtube_url, deezer_url]``` - Start playing song
```!resume``` - Resume the song player
```!pause``` - Pause the song player <p align="center">
<img src="./Assets/playermenu.jpg" />
</p>
```!skip``` - Skip the currently playing song
```!prev``` - Return to play the previous song # **Music 🎧**
- Play musics from Youtube, Spotify and Deezer links (Albums, Artists, Playlists and Tracks).
- Play musics in multiple discord server at the same time.
- The player contains buttons to shortcut some commands.
- Support for the new Discord Slash commands.
- Search for all musics in Queue using buttons.
- Shortcut the playing of one song using dropdown menu.
- Manage the loop of one or all playing musics.
- Manage the order and remove musics from the queue.
- Shuffle the musics queue order.
- Automatically clean the sended messages so it doesn't fill up your server.
```!stop``` - Stop the playing of musics
```!queue``` - Show the musics list in queue <p align="center">
<img src="./Assets/vulkancommands.jpg" />
</p>
```!history``` - Show the played songs list
```!loop [one, all, off]``` - Control the loop of songs <p align="center">
<img src="./Assets/queuemessage.jpg" />
```!shuffle``` - Shuffle the songs in queue </p>
```!remove [x]``` - Remove the song in position x
```!move [x, y]``` - Change the musics in position x and y in Queue
```!np``` - Show information of the currently song
```!clear``` - Clear the songs in queue, doesn't stop the player
```!reset``` - Reset the player, recommended if any error happen
```!invite``` - Send the URL to invite Vulkan to your server
```!help [command]``` - Show more info about the command selected
# **Usage:** # **How to use it**
### **API Keys**
### **Requirements**
Installation of ``Python 3.10+`` and the dependencies in the requirements.txt file, creation of your own Bot in Discord and Spotify Keys. <br>
To install the dependencies type this command in the terminal, in the project root folder.
```
pip install -r requirements.txt
```
### **🔑 API Keys**
You have to create your own discord Bot and store your Bot Token
* Your Discord Application - [Discord](https://discord.com/developers) * Your Discord Application - [Discord](https://discord.com/developers)
* You own Spotify Keys - [Spotify](https://developer.spotify.com/dashboard/applications) * You own Spotify Keys - [Spotify](https://developer.spotify.com/dashboard/applications)
- This information must be stored in an .env file, explained further. - This information must be stored in an .env file, explained further.
### **Requirements** ### **Installation of FFMPEG**<br>
- Installation of Python 3.8+ and the dependencies in the requirements.txt file.
```
pip install -r requirements.txt
```
- **Installation of FFMPEG**<br>
FFMPEG is a module that will be used to play music, you must have this configured in your machine FFMPEG 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> *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. 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.
@@ -78,8 +68,8 @@ BOT_PREFIX=Your_Wanted_Prefix_For_Vulkan
``` ```
### **Config File** ### **⚙️ Configs**
The config file, located in ```./config``` folder doesn't require any change, but if you acquire the knowledged of how it works, you can change it to the way you want. The config file is located at ```./config/Configs.py```, it doesn't require any change, but if you can change values to the way you want.
### **Initialization** ### **Initialization**
@@ -87,25 +77,22 @@ The config file, located in ```./config``` folder doesn't require any change, bu
- Run ```python main.py``` in console to start - Run ```python main.py``` in console to start
## **Heroku** <br>
To run your Bot in Heroku 24/7, you will need the Procfile located in root, then follow the instructions in this [video](https://www.youtube.com/watch?v=BPvg9bndP1U&ab_channel=TechWithTim). In addition, also add these two buildpacks to your Heroku Application: <hr>
<br>
- https://github.com/jonathanong/heroku-buildpack-ffmpeg-latest.git ## **🚀 Heroku**
To deploy and run your Bot in Heroku 24/7, follow the instructions in the [Heroku Instructions](HEROKU.md) page.
- https://github.com/xrisk/heroku-opus.git ## 🧪 Tests
## 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> 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> `python run_tests.py`<br>
## License
- This program is free software: you can redistribute it and/or modify it under the terms of the [MIT License](https://github.com/RafaelSolVargas/Vulkan/blob/master/LICENSE). ## 📖 License
This program is free software: you can redistribute it and/or modify it under the terms of the [MIT License](https://github.com/RafaelSolVargas/Vulkan/blob/master/LICENSE).
## Contributing
- If you are interested in upgrading this project i will be very happy to receive a PR or Issue from you. See TODO project to see if i'm working in some feature now.
## 🏗️ Contributing
## Acknowledgment If you are interested in upgrading this project i will be very happy to receive a PR or Issue from you. See TODO project to see if i'm working in some feature now.
- See the DingoLingo [project](https://github.com/Raptor123471/DingoLingo) from Raptor123471, it helped me a lot to build Vulkan.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

Binary file not shown.