From 0581be7f6d87f9f2874aed793c16ef2b5e6007bc Mon Sep 17 00:00:00 2001 From: Mylloon Date: Tue, 2 Nov 2021 01:58:53 +0100 Subject: [PATCH] first attempt to add promice support --- src/cogs/music.py | 1191 ++++++++------------------------------------- src/main.py | 9 +- 2 files changed, 211 insertions(+), 989 deletions(-) diff --git a/src/cogs/music.py b/src/cogs/music.py index 0c676ef..9fce52b 100644 --- a/src/cogs/music.py +++ b/src/cogs/music.py @@ -1,375 +1,74 @@ -"""The MIT License (MIT) -Copyright (c) 2019-2020 PythonistaGuild -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" -import asyncio -import async_timeout -import copy -import datetime import discord -import math -import random -import re -import typing -import wavelink -from discord.ext import commands, menus -from time import sleep +import pomice import socket -from discord_slash import cog_ext -from utils.core import isSlash, multipleArgsToTuple, mySendHidden, addReaction -from sys import _getframe +import re -# Genius API +from discord.ext import commands +from time import sleep +from utils.core import isSlash, multipleArgsToTuple, mySendHidden, addReaction from lyricsgenius import Genius from utils.core import ligneFormatage, userOrNick, load from utils.time import nowCustom +from sys import _getframe genius = Genius(load(["TOKEN_GENIUS"])["TOKEN_GENIUS"]) genius.response_format = "markdown" - def setup(client): client.add_cog(Music(client)) -# URL matching REGEX... -URL_REG = re.compile(r'https?://(?:www\.)?.+') - - -class NoChannelProvided(commands.CommandError): - """Error raised when no suitable voice channel was supplied.""" - pass - - -class IncorrectChannelError(commands.CommandError): - """Error raised when commands are issued outside of the players session channel.""" - pass - - -class Track(wavelink.Track): - """Wavelink Track object with a requester attribute.""" - - __slots__ = ('requester', ) - - def __init__(self, *args, **kwargs): - super().__init__(*args) - - self.requester = kwargs.get('requester') - - -class Player(wavelink.Player): - """Custom wavelink Player class.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.context: commands.Context = kwargs.get('context', None) - if self.context: - self.dj: discord.Member = self.context.author - - self.queue = asyncio.Queue() - self.controller = None - - self.waiting = False - self.updating = False - - self.pause_votes = set() - self.resume_votes = set() - self.skip_votes = set() - self.shuffle_votes = set() - self.stop_votes = set() - - async def do_next(self) -> None: - if self.is_playing or self.waiting: - return - - # Clear the votes for a new song... - self.pause_votes.clear() - self.resume_votes.clear() - self.skip_votes.clear() - self.shuffle_votes.clear() - self.stop_votes.clear() - - try: - self.waiting = True - with async_timeout.timeout(300): - track = await self.queue.get() - except asyncio.TimeoutError: - # No music has been played for 5 minutes, cleanup and disconnect... - return await self.teardown() - - await self.play(track) - self.waiting = False - - # Invoke our players controller... - await self.invoke_controller() - - async def invoke_controller(self) -> None: - """Method which updates or sends a new player controller.""" - if self.updating: - return - - self.updating = True - - if not self.controller: - self.controller = InteractiveController(embed=self.build_embed(), player=self) - await self.controller.start(self.context) - - elif not await self.is_position_fresh(): - try: - await self.controller.message.delete() - except discord.HTTPException: - pass - - self.controller.stop() - - self.controller = InteractiveController(embed=self.build_embed(), player=self) - await self.controller.start(self.context) - - else: - embed = self.build_embed() - await self.controller.message.edit(content=None, embed=embed) - - self.updating = False - - def build_embed(self) -> typing.Optional[discord.Embed]: - """Method which builds our players controller embed.""" - track = self.current - if not track: - return - - channel = self.bot.get_channel(int(self.channel_id)) - qsize = self.queue.qsize() - - embed = discord.Embed(title=f"Musique dans {channel.name}", colour=0xebb145) - embed.description = f"En cours :\n**`{track.title}`**\n\n" - embed.set_thumbnail(url=track.thumb) - - embed.add_field(name="Durée", value=str(datetime.timedelta(milliseconds=int(track.length)))) - embed.add_field(name="Longueur file d'attente", value=str(qsize)) - embed.add_field(name="Volume", value=f"**`{self.volume}%`**") - embed.add_field(name="Demandé par", value=track.requester.mention) - embed.add_field(name="DJ", value=self.dj.mention) - embed.add_field(name="Lien vidéo", value=f"[Clique ici]({track.uri})") - - return embed - - async def is_position_fresh(self) -> bool: - """Method which checks whether the player controller should be remade or updated.""" - try: - async for message in self.context.channel.history(limit=5): - if message.id == self.controller.message.id: - return True - except (discord.HTTPException, AttributeError): - return False - - return False - - async def teardown(self): - """Clear internal states, remove player controller and disconnect.""" - if self.controller: - try: - await self.controller.message.delete() - except discord.HTTPException: - pass - - self.controller.stop() - - try: - await self.destroy() - except KeyError: - pass - - -class InteractiveController(menus.Menu): - """The Players interactive controller menu class.""" - - def __init__(self, *, embed: discord.Embed, player: Player): - super().__init__(timeout=None) - - self.embed = embed - self.player = player - - def update_context(self, payload: discord.RawReactionActionEvent): - """Update our context with the user who reacted.""" - ctx = copy.copy(self.ctx) - ctx.author = payload.member - - return ctx - - def reaction_check(self, payload: discord.RawReactionActionEvent): - if payload.event_type == 'REACTION_REMOVE': - return False - - if not payload.member: - return False - if payload.member.bot: - return False - if payload.message_id != self.message.id: - return False - if payload.member not in self.bot.get_channel(int(self.player.channel_id)).members: - return False - - return payload.emoji in self.buttons - - async def send_initial_message(self, _: commands.Context, channel: discord.TextChannel) -> discord.Message: - return await channel.send(embed=self.embed) - - @menus.button(emoji='\u25B6') - async def resume_command(self, payload: discord.RawReactionActionEvent): - """Resume button.""" - ctx = self.update_context(payload) - - command = self.bot.get_command('resume') - ctx.command = command - - await self.bot.invoke(ctx) - - @menus.button(emoji='\u23F8') - async def pause_command(self, payload: discord.RawReactionActionEvent): - """Pause button""" - ctx = self.update_context(payload) - - command = self.bot.get_command('pause') - ctx.command = command - - await self.bot.invoke(ctx) - - @menus.button(emoji='\u23F9') - async def stop_command(self, payload: discord.RawReactionActionEvent): - """Stop button.""" - ctx = self.update_context(payload) - - command = self.bot.get_command('stop') - ctx.command = command - - await self.bot.invoke(ctx) - - @menus.button(emoji='\u23ED') - async def skip_command(self, payload: discord.RawReactionActionEvent): - """Skip button.""" - ctx = self.update_context(payload) - - command = self.bot.get_command('skip') - ctx.command = command - - await self.bot.invoke(ctx) - - @menus.button(emoji='\U0001F500') - async def shuffle_command(self, payload: discord.RawReactionActionEvent): - """Shuffle button.""" - ctx = self.update_context(payload) - - command = self.bot.get_command('shuffle') - ctx.command = command - - await self.bot.invoke(ctx) - - @menus.button(emoji='\u2795') - async def volup_command(self, payload: discord.RawReactionActionEvent): - """Volume up button""" - ctx = self.update_context(payload) - - command = self.bot.get_command('vol_up') - ctx.command = command - - await self.bot.invoke(ctx) - - @menus.button(emoji='\u2796') - async def voldown_command(self, payload: discord.RawReactionActionEvent): - """Volume down button.""" - ctx = self.update_context(payload) - - command = self.bot.get_command('vol_down') - ctx.command = command - - await self.bot.invoke(ctx) - - @menus.button(emoji='\U0001F1F6') - async def queue_command(self, payload: discord.RawReactionActionEvent): - """Player queue button.""" - ctx = self.update_context(payload) - - command = self.bot.get_command('queue') - ctx.command = command - - await self.bot.invoke(ctx) - - -class PaginatorSource(menus.ListPageSource): - """Player queue paginator class.""" - - def __init__(self, entries, *, per_page=8): - super().__init__(entries, per_page=per_page) - - async def format_page(self, _: menus.Menu, page): - embed = discord.Embed(title='À suivre...', colour=0x4f0321) - embed.description = '\n'.join(f'`{index}. {title}`' for index, title in enumerate(page, 1)) - - return embed - - def is_paginating(self): - # We always want to embed even on 1 page of results... - return True - - -class Music(commands.Cog, wavelink.WavelinkMixin): - """Commandes de musique.""" - - def __init__(self, bot: commands.Bot): +class Music(commands.Cog): + def __init__(self, bot: commands.Bot) -> None: self.bot = bot - self.keys = load(["REGION_DISCORD", "DOCKER_KASSOUBOT"]) - if not hasattr(bot, 'wavelink'): - self.bot.wavelink = wavelink.Client(bot = bot) + # In order to initialize a node, or really do anything in this library, + # you need to make a node pool + self.pomice = pomice.NodePool() + self.keys = load(["REGION_DISCORD", "DOCKER_KASSOUBOT", "SPOTIFY_CLIENT_ID", "SPOTIFY_CLIENT_SECRET"]) if self.keys["DOCKER_KASSOUBOT"] == True: self.url = "lavalink" # name of the service, working fine with the DNS in docker else: - self.url = "localhost" # localhost + self.url = "127.0.0.1" # localhost + + if self.keys["SPOTIFY_CLIENT_ID"] != False and self.keys["SPOTIFY_CLIENT_SECRET"] != False: + self.spotify_client_id = self.keys["SPOTIFY_CLIENT_ID"] + self.spotify_client_secret = self.keys["SPOTIFY_CLIENT_SECRET"] + else: + self.spotify_client_id = None + self.spotify_client_secret = None self.bot.loop.create_task(self.start_nodes()) - async def start_nodes(self) -> None: - """Connect and intiate nodes.""" - await self.bot.wait_until_ready() - - if self.bot.wavelink.nodes: - previous = self.bot.wavelink.nodes.copy() - - for node in previous.values(): - await node.destroy() - - nodes = {'MAIN': {'host': f'{self.url}', - 'port': 2333, - 'rest_uri': f'http://{self.url}:2333', - 'password': 'youshallnotpass', - 'identifier': 'MAIN', - 'region': self.keys["REGION_DISCORD"] - }} - - nodeValues = list(nodes.values()) - n = 0 - timeout = 60 # en secondes - while n < len(nodeValues): - location = (nodeValues[n]["host"], nodeValues[n]["port"]) + async def start_nodes(self): + # You can pass in Spotify credentials to enable Spotify querying. + # If you do not pass in valid Spotify credentials, Spotify querying will not work + working = True + timeout = 60 # in seconds + identifications = { # change here in case you need to + "bot": self.bot, + "host": self.url, + "port": 2333, + "password": "youshallnotpass", + "identifier": "MAIN", + "spotify_client_id": self.spotify_client_id, + "spotify_client_secret": self.spotify_client_secret + } + while working: + location = (identifications["host"], identifications["port"]) a_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if a_socket.connect_ex(location) == 0: - await self.bot.wavelink.initiate_node(**nodeValues[n]) # Port isn't open -> a service is behind, connecting... - n += 1 + await self.pomice.create_node( # Port isn't open -> a service is behind, connecting... + bot=identifications["bot"], + host=identifications["host"], + port=str(identifications["port"]), + password=identifications["password"], + identifier=identifications["identifier"], + spotify_client_id=identifications["spotify_client_id"], + spotify_client_secret=identifications["spotify_client_secret"] + ) + print(f'Node Lavalink prête !') + working = False else: timeout -= 1 sleep(1) # Port is open -> no service behind, retrying in 1 second... @@ -382,639 +81,163 @@ class Music(commands.Cog, wavelink.WavelinkMixin): self.bot.unload_extension(f"{self.bot.cogs_folder}.{fileName}") raise TimeoutError - @wavelink.WavelinkMixin.listener() - async def on_node_ready(self, node: wavelink.Node): - print(f'Node Lavalink \"{node.identifier}\" prête !') - - @wavelink.WavelinkMixin.listener('on_track_stuck') - @wavelink.WavelinkMixin.listener('on_track_end') - @wavelink.WavelinkMixin.listener('on_track_exception') - async def on_player_stop(self, _: wavelink.Node, payload): - await payload.player.do_next() - - @commands.Cog.listener() - async def on_voice_state_update(self, member: discord.Member, _: discord.VoiceState, after: discord.VoiceState): - if member.bot: - return - - player: Player = self.bot.wavelink.get_player(member.guild.id, cls=Player) - - if not player.channel_id or not player.context: - player.node.players.pop(member.guild.id) - return - - channel = self.bot.get_channel(int(player.channel_id)) - - if member == player.dj and after.channel is None: - for m in channel.members: - if m.bot: - continue - else: - player.dj = m - return - - elif after.channel == channel and player.dj not in channel.members: - player.dj = member - - async def cog_command_error(self, ctx: commands.Context, error: Exception): - """Cog wide error handler.""" - if isinstance(error, IncorrectChannelError): - return - - if isinstance(error, NoChannelProvided): - return await ctx.send("Vous devez être dans un channel vocal ou m'en donner un pour m'y connecter.") - - async def cog_check(self, ctx: commands.Context): - """Cog wide check, which disallows commands in DMs.""" - if not ctx.guild: - await ctx.send("Les commandes de musique ne sont pas disponibles dans les messages privés.") - return False - - return True - - async def cog_before_invoke(self, ctx: commands.Context): - """Coroutine called before command invocation. - We mainly just want to check whether the user is in the players controller channel. - """ - player: Player = self.bot.wavelink.get_player(ctx.guild.id, cls=Player, context=ctx) - - if player.context: - if player.context.channel != ctx.channel: - await ctx.send(f"{ctx.author.mention}, vous devez être dans {player.context.channel.mention} pour cette session.") - raise IncorrectChannelError - - if ctx.command.name == "connect" and not player.context: - return - elif self.is_privileged(ctx): - return - - if not player.channel_id: - return - - channel = self.bot.get_channel(int(player.channel_id)) - if not channel: - return - - if player.is_connected: - if ctx.author not in channel.members: - await ctx.send(f'{ctx.author.mention}, vous devez être dans `{channel.name}` pour utiliser les commandes de musique.') - raise IncorrectChannelError - - def required(self, ctx: commands.Context): - """Method which returns required votes based on amount of members in a channel.""" - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - channel = self.bot.get_channel(int(player.channel_id)) - required = math.ceil((len(channel.members) - 1) / 2.5) - - if ctx.command.name == 'stop': - if len(channel.members) == 3: - required = 2 - - return required - - def is_privileged(self, ctx: commands.Context): - """Check whether the user is an Admin or DJ.""" - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - - return player.dj == ctx.author or ctx.author.guild_permissions.kick_members - - @commands.command(aliases=["join", "j"]) - async def connect(self, ctx: commands.Context, *, channel: typing.Union[discord.VoiceChannel, discord.StageChannel] = None, fromSlash = None): - """Se connecte au salon vocal.\n ➡ Syntaxe: {PREFIX}connect/join/j [salon]""" - _, fromSlash, _ = isSlash(multipleArgsToTuple(_getframe(0))) - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - - if player.is_connected: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Déjà connecté.") - - channel = getattr(ctx.author.voice, 'channel', channel) - if channel is None: - if fromSlash != True: - await addReaction(ctx.message, 2) - raise NoChannelProvided - - await player.connect(channel.id, self_deaf = True) - await ctx.send(f":thumbsup: **Connecté à __{channel.name}__**") - if fromSlash != True: - return await addReaction(ctx.message, 0) - @cog_ext.cog_slash(name="connect", description = "Se connecte au salon vocal.") - async def _connect(self, ctx, channel = None): - """Slash command""" - return await self.connect(ctx = ctx, channel = channel, fromSlash = True) - - @commands.command(aliases=["p"]) - async def play(self, ctx: commands.Context, *, query: str = None, fromSlash = None): - """Joue ou met dans la file d'attente le son demandé.\n ➡ Syntaxe: {PREFIX}play/p """ - _, fromSlash, _ = isSlash(multipleArgsToTuple(_getframe(0))) - noSoundFound = "Aucun son n'a été trouvée avec cette requête." - if query == None: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, noSoundFound, delete_after=15) - - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - - if not player.is_connected: - try: - channel = getattr(ctx.author.voice, 'channel') - except: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Tu n'es connecté à aucun salon vocal.") - if channel is None: - if fromSlash != True: - await addReaction(ctx.message, 2) - raise NoChannelProvided - await self.connect(ctx = ctx, channel = channel, fromSlash = True) - - query = query.strip('<>') - if not URL_REG.match(query): - query = f'ytsearch:{query}' - - tracks = await self.bot.wavelink.get_tracks(query) - if not tracks: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, noSoundFound, delete_after=15) - - if isinstance(tracks, wavelink.TrackPlaylist): - for track in tracks.tracks: - track = Track(track.id, track.info, requester=ctx.author) - await player.queue.put(track) - - lenghtTracks = len(tracks.tracks) - await ctx.send(f"```ini\nAjout de la playlist {tracks.data['playlistInfo']['name']}" - f" ({lenghtTracks} son{'s' if lenghtTracks > 1 else ''}) à la file d'attente.```", delete_after=15) - else: - track = Track(tracks[0].id, tracks[0].info, requester=ctx.author) - await ctx.send(f"```ini\nAjout de {track.title} à la file d'attente```", delete_after=15) - await player.queue.put(track) - - if not player.is_playing: - await player.do_next() - - if fromSlash != True: - return await addReaction(ctx.message, 0) - @cog_ext.cog_slash(name="play", description = "Joue ou met dans la file d'attente le son demandé.") - async def _play(self, ctx, son): - """Slash command""" - return await self.play(ctx = ctx, query = son, fromSlash = True) - - @commands.command() - async def pause(self, ctx: commands.Context, *, fromSlash = None): - """Met en pause du son en cours.""" - _, fromSlash, _ = isSlash(multipleArgsToTuple(_getframe(0))) - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - - if player.is_paused or not player.is_connected: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Aucune musique n'est en cours.") - - if fromSlash != True: - await addReaction(ctx.message, 0) - - if self.is_privileged(ctx): - await ctx.send(f"{ctx.author.mention} a mis la musique en pause.", delete_after=10) - player.pause_votes.clear() - - return await player.set_pause(True) - - required = self.required(ctx) - player.pause_votes.add(ctx.author) - - if len(player.pause_votes) >= required: - await ctx.send("Mise en pause votée.", delete_after=10) - player.pause_votes.clear() - await player.set_pause(True) - else: - await mySendHidden(ctx, fromSlash, "Tu as voté pour mettre la musique en pause.", delete_after=15) - @cog_ext.cog_slash(name="pause", description = "Met en pause du son en cours.") - async def _pause(self, ctx): - """Slash command""" - return await self.pause(ctx = ctx, fromSlash = True) - - @commands.command() - async def resume(self, ctx: commands.Context, *, fromSlash = None): - """Reprend la musique en pause.""" - _, fromSlash, _ = isSlash(multipleArgsToTuple(_getframe(0))) - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - - if not player.is_paused or not player.is_connected: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Aucune musique en pause n'est en cours.") - - if fromSlash != True: - await addReaction(ctx.message, 0) - - if self.is_privileged(ctx): - await ctx.send(f"{ctx.author.mention} a relancé la musique.", delete_after=10) - player.resume_votes.clear() - - return await player.set_pause(False) - - required = self.required(ctx) - player.resume_votes.add(ctx.author) - - if len(player.resume_votes) >= required: - await ctx.send("Relance de la musique votée.", delete_after=10) - player.resume_votes.clear() - await player.set_pause(False) - else: - await mySendHidden(ctx, fromSlash, "Tu as voté pour relancer la musique.", delete_after=15) - @cog_ext.cog_slash(name="resume", description = "Reprend la musique en pause.") - async def _resume(self, ctx): - """Slash command""" - return await self.resume(ctx = ctx, fromSlash = True) - - @commands.command(aliases=["s"]) - async def skip(self, ctx: commands.Context, *, fromSlash = None): - """Passe le son en cours.\n ➡ Syntaxe: {PREFIX}skip/s""" - _, fromSlash, _ = isSlash(multipleArgsToTuple(_getframe(0))) - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - - if not player.is_connected: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Aucune musique n'est en cours.") - - if fromSlash != True: - await addReaction(ctx.message, 0) - - if self.is_privileged(ctx) or ctx.author == player.current.requester: - await ctx.send(f"{ctx.author.mention} passe le son.", delete_after=10) - player.skip_votes.clear() - - return await player.stop() - - required = self.required(ctx) - player.skip_votes.add(ctx.author) - - if len(player.skip_votes) >= required: - await ctx.send("Passe le son à cause du vote.", delete_after=10) - player.skip_votes.clear() - await player.stop() - else: - await mySendHidden(ctx, fromSlash, f"Tu as voté pour passer le son ({len(player.skip_votes)}/{required}).", delete_after=15) - @cog_ext.cog_slash(name="skip", description = "Passe le son en cours.") - async def _skip(self, ctx): - """Slash command""" - return await self.skip(ctx = ctx, fromSlash = True) - - @commands.command(aliases=["disconnect", "dc"]) - async def stop(self, ctx: commands.Context, *, fromSlash = None): - """Stop le son en cours de lecture, supprime la file d'attente et quitte le vocal.\n ➡ Syntaxe: {PREFIX}stop/disconnect/dc""" - _, fromSlash, _ = isSlash(multipleArgsToTuple(_getframe(0))) - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - - if not player.is_connected: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Aucune musique n'est en cours.") - - if fromSlash != True: - await addReaction(ctx.message, 0) - - if self.is_privileged(ctx): - await ctx.send(f"{ctx.author.mention} a déconnecté le bot et a supprimé la file d'attente.", delete_after=10) - return await player.teardown() - - required = self.required(ctx) - player.stop_votes.add(ctx.author) - - if len(player.stop_votes) >= required: - await ctx.send("Arrêt et suppression de la file d'attente voté.", delete_after=10) - await player.teardown() - else: - await mySendHidden(ctx, fromSlash, "Tu as voté pour arrêter d'écouter de la musique.", delete_after=15) - @cog_ext.cog_slash(name="stop", description = "Stop le son en cours de lecture, supprime la file d'attente et quitte le vocal.") - async def _stop(self, ctx): - """Slash command""" - return await self.stop(ctx = ctx, fromSlash = True) - - @commands.command(aliases=['v', 'vol']) - async def volume(self, ctx: commands.Context, *, vol: int = None, fromSlash = None): - """Modifie le volume du bot.\n ➡ Syntaxe: {PREFIX}volume/vol/v (1-100)""" - _, fromSlash, _ = isSlash(multipleArgsToTuple(_getframe(0))) - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - - if not player.is_connected: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Aucune musique n'est en cours.") - - if not self.is_privileged(ctx): - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Seuls le DJ ou les admins peuvent modifier le volume.") - - if vol == None: - await mySendHidden(ctx, fromSlash, f"Le volume est actuellement réglé à **{player.volume}%**.") - if fromSlash != True: - return await addReaction(ctx.message, 0) - else: - if not 0 < vol < 101: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Veuillez saisir une valeur comprise entre 1 et 100.") - - if fromSlash != True: - await addReaction(ctx.message, 0) - - await player.set_volume(vol) - await ctx.send(f"Volume réglé sur **{vol}%** par {ctx.author.mention}", delete_after=7) - @cog_ext.cog_slash(name="volume", description = "Modifie le volume du bot.") - async def _volume(self, ctx, vol: int = None): - """Slash command""" - return await self.volume(ctx = ctx, vol = vol, fromSlash = True) - - @commands.command(aliases=['mix']) - async def shuffle(self, ctx: commands.Context, *, fromSlash = None): - """Mélange les sons en file d'attente. (minimum 3 sons en file d'attente)\n ➡ Syntaxe: {PREFIX}shuffle/mix""" - _, fromSlash, _ = isSlash(multipleArgsToTuple(_getframe(0))) - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - - if not player.is_connected: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Aucune musique n'est en cours.") - - if player.queue.qsize() < 3: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Ajoutez d'autres sons à la file d'attente avant de les mélanger (3 minimum).", delete_after=15) - - if fromSlash != True: - await addReaction(ctx.message, 0) - - if self.is_privileged(ctx): - await ctx.send(f"{ctx.author.mention} a mélangé la liste de lecture.", delete_after=10) - player.shuffle_votes.clear() - return random.shuffle(player.queue._queue) - - required = self.required(ctx) - player.shuffle_votes.add(ctx.author) - - if len(player.shuffle_votes) >= required: - await ctx.send("Le vote pour la lecture aléatoire a été voté.", delete_after=10) - player.shuffle_votes.clear() - random.shuffle(player.queue._queue) - else: - await mySendHidden(ctx, fromSlash, "Tu as voté pour mélanger la liste de lecture.", delete_after=15) - @cog_ext.cog_slash(name="shuffle", description = "Mélange les sons en file d'attente. (minimum 3 sons en file d'attente)") - async def _shuffle(self, ctx): - """Slash command""" - return await self.shuffle(ctx = ctx, fromSlash = True) - - @commands.command(hidden = True) - async def vol_up(self, ctx: commands.Context): - """Command used for volume up button.""" - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - - if not player.is_connected or not self.is_privileged(ctx): - return - - vol = int(math.ceil((player.volume + 10) / 10)) * 10 - - if vol > 100: - vol = 100 - await ctx.send("Le volume maximal est atteint.", delete_after=5) - - await player.set_volume(vol) - - @commands.command(hidden = True) - async def vol_down(self, ctx: commands.Context): - """Command used for volume down button.""" - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - - if not player.is_connected or not self.is_privileged(ctx): - return - - vol = int(math.ceil((player.volume - 10) / 10)) * 10 - - if vol < 0: - vol = 0 - await ctx.send("Le son est mute.", delete_after=7) - - await player.set_volume(vol) - - @commands.command(aliases=['eq']) - async def equalizer(self, ctx: commands.Context, *, equalizer: str = "", fromSlash = None): - """Change l'égaliseur de musique.\n ➡ Syntaxe: {PREFIX}equalizer/eq (flat/boost/metal/piano)""" - _, fromSlash, _ = isSlash(multipleArgsToTuple(_getframe(0))) - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - - if not player.is_connected: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Aucune musique n'est en cours.") - - if not self.is_privileged(ctx): - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Seul le DJ ou les admins peuvent modifier l'égaliseur.") - - eqs = {'flat': wavelink.Equalizer.flat(), - 'boost': wavelink.Equalizer.boost(), - 'metal': wavelink.Equalizer.metal(), - 'piano': wavelink.Equalizer.piano()} - - eq = eqs.get(equalizer.lower(), None) - - if not eq: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, f"EQ non valide fourni.\nEQs valides : {', '.join(eqs.keys())}") - - await ctx.send(f"Changement d'état de l'égaliseur en `{equalizer}` par {ctx.author.mention}.", delete_after=15) - await player.set_eq(eq) - if fromSlash != True: - return await addReaction(ctx.message, 0) - @cog_ext.cog_slash(name="equalizer", description = "Change l'égaliseur de musique. (flat/boost/metal/piano)") - async def _equalizer(self, ctx, equalizer: str): - """Slash command""" - return await self.equalizer(ctx = ctx, equalizer = equalizer, fromSlash = True) - - @commands.command(aliases=['q', 'que']) - async def queue(self, ctx: commands.Context, *, fromSlash = None): - """Affiche les sons en attente.\n ➡ Syntaxe: {PREFIX}queue/que/q""" - _, fromSlash, _ = isSlash(multipleArgsToTuple(_getframe(0))) - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - - if not player.is_connected: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Aucune musique n'est en cours.") - - if player.queue.qsize() == 0: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Il n'y a plus de sons dans la file d'attente.", delete_after=15) - - entries = [track.title for track in player.queue._queue] - pages = PaginatorSource(entries=entries) - paginator = menus.MenuPages(source=pages, timeout=None, delete_message_after=True) - - if fromSlash: - await ctx.send("Affichage de la liste ci-dessous.", hidden = True) - else: - return await addReaction(ctx.message, 0) - await paginator.start(ctx) - @cog_ext.cog_slash(name="queue", description = "Affiche les sons en attente.") - async def _queue(self, ctx,): - """Slash command""" - return await self.queue(ctx = ctx, fromSlash = True) - - @commands.command(aliases=['np', 'now_playing', 'current']) - async def nowplaying(self, ctx: commands.Context, *, fromSlash = None): - """Met à jour l'embed qui affiche les informations sur la chanson en cours si besoin.\n ➡ Syntaxe: {PREFIX}nowplaying/now_playing/current/np""" - _, fromSlash, _ = isSlash(multipleArgsToTuple(_getframe(0))) - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - - if not player.is_connected: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Aucune musique n'est en cours.") - - await player.invoke_controller() - if fromSlash: - await mySendHidden(ctx, fromSlash, "Mise à jour effectuée.") - else: - return await addReaction(ctx.message, 0) - @cog_ext.cog_slash(name="nowplaying", description = "Met à jour l'embed qui affiche les informations sur la chanson en cours si besoin.") - async def _nowplaying(self, ctx,): - """Slash command""" - return await self.nowplaying(ctx = ctx, fromSlash = True) - - @commands.command(aliases=['swap']) - async def swap_dj(self, ctx: commands.Context, *, member: discord.Member = None, fromSlash = None): - """Donne le rôle de DJ à un autre membre dans le salon vocal.\n ➡ Syntaxe: {PREFIX}swap_dj/swap (membre)""" - _, fromSlash, _ = isSlash(multipleArgsToTuple(_getframe(0))) - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - - if not player.is_connected: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Aucune musique n'est en cours.") - - if not self.is_privileged(ctx): - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Seuls les admins et le DJ peuvent utiliser cette commande.", delete_after=15) - - members = self.bot.get_channel(int(player.channel_id)).members - - if member and member not in members: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, f"{member} n'est actuellement pas dans le salon vocal, et ne peut donc pas devenir DJ.", delete_after=15) - - if member and member == player.dj: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Cette personne est déjà DJ.", delete_after=15) - - if len(members) <= 2: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await mySendHidden(ctx, fromSlash, "Aucun autre membre à qui donner le rôle de DJ.", delete_after=15) - - if fromSlash != True: - await addReaction(ctx.message, 0) - - if member: - player.dj = member - return await ctx.send(f"{member.mention} est maintenant le DJ.") - - for m in members: - if m == player.dj or m.bot: - continue - else: - player.dj = m - return await ctx.send(f"{member.mention} est maintenant le DJ.") - @cog_ext.cog_slash(name="swap_dj", description = "Donne le rôle de DJ à un autre membre dans le salon vocal.") - async def _swap_dj(self, ctx, member: discord.Member = None): - """Slash command""" - return await self.swap_dj(ctx = ctx, member = member, fromSlash = True) - - @commands.command(name='lyrics', aliases = ['l', 'lyric']) - async def _lyrics(self, ctx, *, song: str = None, fromSlash = None): - """Affiche les paroles de la musique en cours, ou de la chanson spécifiée.\n ➡ Syntaxe: {PREFIX}lyrics/lyric/l (musique)⁢⁢⁢⁢⁢⁢⁢⁢⁢⁢""" - _, fromSlash, _ = isSlash(multipleArgsToTuple(_getframe(0))) - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - if song or player.is_playing: - if not song: - song = re.sub(r"(\ )?\(.*\)", "", player.current.title) - if " romanized" in song: - message = await ctx.send(f":mag: **Cherche les paroles romanisées de** `{song.replace(' romanized', '')}`") - else: - message = await ctx.send(f":mag: **Cherche les paroles de** `{song}`") - temps_requete = int(round(nowCustom() * 1000)) - song_genius = genius.search_song(song) - couleur_embed = discord.Colour.random() - try: - paroles = song_genius.lyrics - except: - if fromSlash != True: - await addReaction(ctx.message, 2) - return await message.edit(content = f"Pas de résultats trouvés pour `{song}`.") - paroles = re.sub(r"3?EmbedShare URLCopyEmbedCopy", "", paroles) # Fix temporaire bug Genius - paroles = re.sub(r"\*\*\*\**", "\*\*\*", paroles) # Fix des *** dans les chansons Genius - lignetotal = "" - premierembed = True - if len(paroles) > 7500: - if fromSlash != True: - await addReaction(ctx.message, 3) - return await message.edit(content = f"Les paroles sont trop longues pour être affichés (lien vers la page des paroles : {song_genius.url}).") - title_first_embed = f"Paroles de {song_genius.title} par {song_genius.artist}." - desc_first_embed = f"[Lien vers les paroles sur le site]({song_genius.url})" - type_de_comptage = "\n\n" if paroles.count("\n\n") > 2 else "\n" - for ligne in paroles.split(type_de_comptage): - if len(ligne) >= 2048: - type_de_comptage = "\n" - for ligne in paroles.split(type_de_comptage): - if len(f"{lignetotal}{type_de_comptage}{ligne}") < 1900: - lignetotal = f"{lignetotal}{type_de_comptage}{ligneFormatage(ligne)}" - else: - if premierembed == True: - premierembed = False - embed = discord.Embed(title = title_first_embed, description = f"{desc_first_embed}{lignetotal}", color = couleur_embed) - embed.set_thumbnail(url = song_genius.song_art_image_url) - await message.edit(embed = embed) - else: - embed = discord.Embed(description = lignetotal, color = couleur_embed) - await ctx.send(embed = embed) - lignetotal = f"{ligneFormatage(ligne)}" - - temps_requete = int(round(nowCustom() * 1000)) - temps_requete - footer_embed = f"Pour {userOrNick(ctx.author)} par Genius en {round(temps_requete / 1000, 2)} s." - await addReaction(ctx.message, 0) - if premierembed == True: - premierembed = False - embed = discord.Embed(title = title_first_embed, description = f"{desc_first_embed}{lignetotal}", color = couleur_embed) - embed.set_footer(icon_url = ctx.author.avatar_url, text = footer_embed) - return await message.edit(embed = embed) - else: - embed = discord.Embed(description = lignetotal, color = couleur_embed) - embed.set_footer(icon_url = ctx.author.avatar_url, text = footer_embed) - return await ctx.send(embed = embed) - else: - if fromSlash != True: - await addReaction(ctx.message, 2) - await mySendHidden(ctx, fromSlash, f"Aucune musique demandé... `{ctx.prefix}lyrics/l/lyrics `.") - @cog_ext.cog_slash(name="lyrics", description = "Affiche les paroles de la musique en cours, ou de la chanson spécifiée.") - async def __lyrics(self, ctx, song: str = None): - """Slash command""" - ctx.prefix = "/" - return await self._lyrics(ctx = ctx, song = song, fromSlash = True) - - @commands.command(name='lyricsromanized', aliases = ['lr', 'lyricromanized'], hidden = True) - async def _lyricsromanized(self, ctx, *, song: str = None): - """Affiche les paroles de la musique en cours ou de la chanson spécifié mais rajouter un 'romanized' à la fin""" - player: Player = self.bot.wavelink.get_player(guild_id=ctx.guild.id, cls=Player, context=ctx) - if not song and player.is_playing: - song = re.sub(r"(\ )?\(.*\)", "", player.current.title) - await ctx.invoke(self.bot.get_command("lyrics"), song = f"{song} romanized" if song else song) + # @commands.command(aliases=["connect"]) + # async def join(self, ctx: commands.Context, *, channel: discord.VoiceChannel = None) -> None: + # if not channel: + # channel = getattr(ctx.author.voice, "channel", None) + # if not channel: + # raise commands.CheckFailure( + # "Vous devez être dans un salon vocal pour utiliser cette " + # "commande sans spécifier en argument le salon." + # ) + + # # With the release of discord.py 1.7, you can now add a compatible + # # VoiceProtocol class as an argument in VoiceChannel.connect(). + # # This library takes advantage of that and is how you initialize a player. + # await ctx.author.voice.channel.connect(cls = pomice.Player) + # await ctx.send(f"Rejoint le salon vocal `{channel}`") + + # @commands.command(aliases=["dc", "disconnect"]) + # async def leave(self, ctx: commands.Context): + # if not ctx.voice_client: + # raise commands.CommandError("Je ne suis pas connecté.") + + # player: pomice.Player = ctx.voice_client + + # await player.destroy() + # await ctx.send("Salon quitté.") + + # @commands.command(aliases=["p"]) + # async def play(self, ctx: commands.Context, *, search: str) -> None: + # # Checks if the player is in the channel before we play anything + # if not ctx.voice_client: + # await ctx.invoke(self.join) + + # player: pomice.Player = ctx.voice_client + + # # If you search a keyword, Pomice will automagically search the result using YouTube + # # You can pass in "search_type=" as an argument to change the search type + # # i.e: player.get_tracks("query", search_type=SearchType.ytmsearch) + # # will search up any keyword results on YouTube Music + # results = await player.get_tracks(search) + + # if not results: + # raise commands.CommandError("No results were found for that search term.") + + # if isinstance(results, pomice.Playlist): + # await player.play(track=results.tracks[0]) + # else: + # await player.play(track=results[0]) + + # @commands.command() + # async def pause(self, ctx: commands.Context): + # if not ctx.voice_client: + # raise commands.CommandError("No player detected") + + # player: pomice.Player = ctx.voice_client + + # if player.is_paused: + # return await ctx.send("Player is already paused!") + + # await player.set_pause(pause=True) + # await ctx.send("Player has been paused") + + # @commands.command() + # async def resume(self, ctx: commands.Context): + # if not ctx.voice_client: + # raise commands.CommandError("No player detected") + + # player: pomice.Player = ctx.voice_client + + # if not player.is_paused: + # return await ctx.send("Player is already playing!") + + # await player.set_pause(pause=False) + # await ctx.send("Player has been resumed") + + # @commands.command() + # async def stop(self, ctx: commands.Context): + # if not ctx.voice_client: + # raise commands.CommandError("No player detected") + + # player: pomice.Player = ctx.voice_client + + # if not player.is_playing: + # return await ctx.send("Player is already stopped!") + + # await player.stop() + # await ctx.send("Player has been stopped") + + # @commands.command(name='lyrics', aliases = ['l', 'lyric']) + # async def _lyrics(self, ctx, *, song: str = None, fromSlash = None): + # """Affiche les paroles de la musique en cours, ou de la chanson spécifiée.\n ➡ Syntaxe: {PREFIX}lyrics/lyric/l (musique)⁢⁢⁢⁢⁢⁢⁢⁢⁢⁢""" + # _, fromSlash, _ = isSlash(multipleArgsToTuple(_getframe(0))) + # player: pomice.Player = self.bot.pomice.get_player(guild_id=ctx.guild.id, cls=pomice.Player, context=ctx) + # if song or player.is_playing: + # if not song: + # song = re.sub(r"(\ )?\(.*\)", "", player.current.title) + # if " romanized" in song: + # message = await ctx.send(f":mag: **Cherche les paroles romanisées de** `{song.replace(' romanized', '')}`") + # else: + # message = await ctx.send(f":mag: **Cherche les paroles de** `{song}`") + # temps_requete = int(round(nowCustom() * 1000)) + # song_genius = genius.search_song(song) + # couleur_embed = discord.Colour.random() + # try: + # paroles = song_genius.lyrics + # except: + # if fromSlash != True: + # await addReaction(ctx.message, 2) + # return await message.edit(content = f"Pas de résultats trouvés pour `{song}`.") + # paroles = re.sub(r"3?EmbedShare URLCopyEmbedCopy", "", paroles) # Fix temporaire bug Genius + # paroles = re.sub(r"\*\*\*\**", "\*\*\*", paroles) # Fix des *** dans les chansons Genius + # lignetotal = "" + # premierembed = True + # if len(paroles) > 7500: + # if fromSlash != True: + # await addReaction(ctx.message, 3) + # return await message.edit(content = f"Les paroles sont trop longues pour être affichés (lien vers la page des paroles : {song_genius.url}).") + # title_first_embed = f"Paroles de {song_genius.title} par {song_genius.artist}." + # desc_first_embed = f"[Lien vers les paroles sur le site]({song_genius.url})" + # type_de_comptage = "\n\n" if paroles.count("\n\n") > 2 else "\n" + # for ligne in paroles.split(type_de_comptage): + # if len(ligne) >= 2048: + # type_de_comptage = "\n" + # for ligne in paroles.split(type_de_comptage): + # if len(f"{lignetotal}{type_de_comptage}{ligne}") < 1900: + # lignetotal = f"{lignetotal}{type_de_comptage}{ligneFormatage(ligne)}" + # else: + # if premierembed == True: + # premierembed = False + # embed = discord.Embed(title = title_first_embed, description = f"{desc_first_embed}{lignetotal}", color = couleur_embed) + # embed.set_thumbnail(url = song_genius.song_art_image_url) + # await message.edit(embed = embed) + # else: + # embed = discord.Embed(description = lignetotal, color = couleur_embed) + # await ctx.send(embed = embed) + # lignetotal = f"{ligneFormatage(ligne)}" + + # temps_requete = int(round(nowCustom() * 1000)) - temps_requete + # footer_embed = f"Pour {userOrNick(ctx.author)} par Genius en {round(temps_requete / 1000, 2)} s." + # await addReaction(ctx.message, 0) + # if premierembed == True: + # premierembed = False + # embed = discord.Embed(title = title_first_embed, description = f"{desc_first_embed}{lignetotal}", color = couleur_embed) + # embed.set_footer(icon_url = ctx.author.avatar_url, text = footer_embed) + # return await message.edit(embed = embed) + # else: + # embed = discord.Embed(description = lignetotal, color = couleur_embed) + # embed.set_footer(icon_url = ctx.author.avatar_url, text = footer_embed) + # return await ctx.send(embed = embed) + # else: + # if fromSlash != True: + # await addReaction(ctx.message, 2) + # await mySendHidden(ctx, fromSlash, f"Aucune musique demandé... `{ctx.prefix}lyrics/l/lyrics `.") + + # @commands.command(name='lyricsromanized', aliases = ['lr', 'lyricromanized'], hidden = True) + # async def _lyricsromanized(self, ctx, *, song: str = None): + # """Affiche les paroles de la musique en cours ou de la chanson spécifié mais rajouter un 'romanized' à la fin""" + # player: pomice.Player = self.bot.pomice.get_player(guild_id=ctx.guild.id, cls=pomice.Player, context=ctx) + # if not song and player.is_playing: + # song = re.sub(r"(\ )?\(.*\)", "", player.current.title) + # await ctx.invoke(self.bot.get_command("lyrics"), song = f"{song} romanized" if song else song) diff --git a/src/main.py b/src/main.py index 4264150..241d2b6 100644 --- a/src/main.py +++ b/src/main.py @@ -2,15 +2,13 @@ print("Chargement des extensions & librairie...", end = " ") import discord from os import listdir, rename, getcwd -from discord_slash import SlashCommand from discord.ext import commands from utils.core import load, addReaction from utils.page import listReaction keys = load(["PREFIX", "TOKEN_DISCORD", "DEACTIVATE"]) customPrefix = keys["PREFIX"] -client = commands.Bot(command_prefix = customPrefix, case_insensitive = True, intents = discord.Intents.all()) -slash = SlashCommand(client, sync_commands = True) +client = commands.Bot(command_prefix = customPrefix, case_insensitive = True, intents = discord.Intents.all(), slash_commands = True) client.cogs_folder = "cogs" @@ -23,9 +21,10 @@ if keys["DEACTIVATE"] != "None": print(f"No file {file} found, check your \"DEACTIVATE\" variable.") exit(1) -for file in listdir(client.cogs_folder): +""" for file in listdir(client.cogs_folder): if file.endswith(".py") and file.startswith("-") == False: - client.load_extension(f"{client.cogs_folder}.{file[:-3]}") + client.load_extension(f"{client.cogs_folder}.{file[:-3]}") """ +client.load_extension(f"cogs.music") print("Terminé !") @client.event