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
assets/
__pycache__
.env
.cache

BIN
Assets/playermenu.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
Assets/queuemessage.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

BIN
Assets/vulkan-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
Assets/vulkancommands.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -1,5 +1,7 @@
import os
from decouple import config
from Config.Singleton import Singleton
from Config.Folder import Folder
class VConfigs(Singleton):
@@ -17,11 +19,18 @@ class VConfigs(Singleton):
self.CLEANER_MESSAGES_QUANT = 5
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.CHANCE_SHOW_PROJECT = 15
self.PROJECT_URL = 'https://github.com/RafaelSolVargas/Vulkan'
self.SUPPORTING_ICON = 'https://i.pinimg.com/originals/d6/05/b4/d605b4f8c5d1c6ae20dc353ef9f091bd.png'
self.MAX_PLAYLIST_LENGTH = 50
self.MAX_PLAYLIST_FORCED_LENGTH = 5
self.MAX_SONGS_IN_PAGE = 10
self.MAX_PRELOAD_SONGS = 15
self.MAX_SONGS_HISTORY = 15

View File

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

View File

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

19
Config/Folder.py Normal file
View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

109
README.md
View File

@@ -1,68 +1,58 @@
# **Vulkan**
A Music Discord bot, written in Python, that plays *Youtube*, *Spotify* and *Deezer* links. Vulkan was designed so that anyone can fork this project, follow the instructions and use it in their own way, Vulkan can also be configured in Heroku to work 24/7.
<h1 align="center">Vulkan</h1>
# **Music**
- Play musics from Youtube, Spotify and Deezer links (Albums, Artists, Playlists and Tracks)
- Control loop of one or all musics
- Allow moving and removing musics in the queue
- Play musics in queue randomly
- Store played songs and allow bidirectional flow
A Music Discord Bot, that plays *Youtube*, *Spotify*, *Deezer* links or raw queries. Vulkan is open source, so everyone can fork this project, follow the instructions and use it in their own way, executing it in your own machine or hosting in others machines to work 24/7.
### Commands
```!play [title, spotify_url, youtube_url, deezer_url]``` - Start playing song
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.
```!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
```!shuffle``` - Shuffle the songs in queue
```!remove [x]``` - Remove the song in position x
```!move [x, y]``` - Change the musics in position x and y in Queue
```!np``` - Show information of the currently song
```!clear``` - Clear the songs in queue, doesn't stop the player
```!reset``` - Reset the player, recommended if any error happen
```!invite``` - Send the URL to invite Vulkan to your server
```!help [command]``` - Show more info about the command selected
<p align="center">
<img src="./Assets/queuemessage.jpg" />
</p>
# **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)
* You own Spotify Keys - [Spotify](https://developer.spotify.com/dashboard/applications)
- This information must be stored in an .env file, explained further.
### **Requirements**
- Installation of Python 3.8+ and the dependencies in the requirements.txt file.
```
pip install -r requirements.txt
```
- **Installation of FFMPEG**<br>
### **Installation of FFMPEG**<br>
FFMPEG is a module that will be used to play music, you must have this configured in your machine
*FFMPEG must be configured in the PATH for Windows users. Check this [YoutubeVideo](https://www.youtube.com/watch?v=r1AtmY-RMyQ&t=114s&ab_channel=TroubleChute).* <br><br>
You can download the executables in this link `https://www.ffmpeg.org/download.html` and then put the .exe files inside a ffmpeg\bin folder in your C:\ folder. Do not forget to add 'ffmpeg\bin' to your PATH.
@@ -78,8 +68,8 @@ BOT_PREFIX=Your_Wanted_Prefix_For_Vulkan
```
### **Config File**
The config file, located in ```./config``` folder doesn't require any change, but if you acquire the knowledged of how it works, you can change it to the way you want.
### **⚙️ Configs**
The config file is located at ```./config/Configs.py```, it doesn't require any change, but if you can change values to the way you want.
### **Initialization**
@@ -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
## **Heroku**
To run your Bot in Heroku 24/7, you will need the Procfile located in root, then follow the instructions in this [video](https://www.youtube.com/watch?v=BPvg9bndP1U&ab_channel=TechWithTim). In addition, also add these two buildpacks to your Heroku Application:
<br>
<hr>
<br>
- https://github.com/jonathanong/heroku-buildpack-ffmpeg-latest.git
## **🚀 Heroku**
To deploy and run your Bot in Heroku 24/7, follow the instructions in the [Heroku Instructions](HEROKU.md) page.
- https://github.com/xrisk/heroku-opus.git
## Testing
## 🧪 Tests
The tests were written manually with no package due to problems with async function in other packages, to execute them type in root: <br>
`python run_tests.py`<br>
## License
- This program is free software: you can redistribute it and/or modify it under the terms of the [MIT License](https://github.com/RafaelSolVargas/Vulkan/blob/master/LICENSE).
## 📖 License
This program is free software: you can redistribute it and/or modify it under the terms of the [MIT License](https://github.com/RafaelSolVargas/Vulkan/blob/master/LICENSE).
## Contributing
- If you are interested in upgrading this project i will be very happy to receive a PR or Issue from you. See TODO project to see if i'm working in some feature now.
## Acknowledgment
- See the DingoLingo [project](https://github.com/Raptor123471/DingoLingo) from Raptor123471, it helped me a lot to build Vulkan.
## 🏗️ Contributing
If you are interested in upgrading this project i will be very happy to receive a PR or Issue from you. See TODO project to see if i'm working in some feature now.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

Binary file not shown.