import discord from discord.ext import commands import asyncio import itertools import sys import traceback from async_timeout import timeout from functools import partial from youtube_dl import YoutubeDL import lyricsgenius genius = lyricsgenius.Genius("TON TOKEN GENIUS") ytdlopts = { 'format': 'bestaudio/best', 'outtmpl': 'downloads/%(extractor)s-%(id)s-%(title)s.%(ext)s', 'restrictfilenames': True, 'noplaylist': True, 'nocheckcertificate': True, 'ignoreerrors': False, 'logtostderr': False, 'quiet': True, 'no_warnings': True, 'default_search': 'auto', 'source_address': '0.0.0.0' # Les adresses ipv6 posent parfois des problèmes } ffmpegopts = { 'before_options': '-nostdin', 'options': '-vn' } ytdl = YoutubeDL(ytdlopts) class VoiceConnectionError(commands.CommandError): """Classe d'exception personnalisée pour les erreurs de connexion.""" class InvalidVoiceChannel(VoiceConnectionError): """Exception pour les cas de canaux vocaux non valables.""" class YTDLSource(discord.PCMVolumeTransformer): def __init__(self, source, *, data, requester): super().__init__(source) self.requester = requester self.title = data.get('title') self.web_url = data.get('webpage_url') # YTDL info dicts (data) ont d'autres informations utiles que vous pourriez vouloir # https://github.com/rg3/youtube-dl/blob/master/README.md def __getitem__(self, item: str): """Nous permet d'accéder à des attributs similaires à un dict. Cette fonction n'est utile que lorsque vous n'êtes pas en train de télécharger. """ return self.__getattribute__(item) @classmethod async def create_source(cls, ctx, search: str, *, loop, download=True): loop = loop or asyncio.get_event_loop() to_run = partial(ytdl.extract_info, url=search, download=download) data = await loop.run_in_executor(None, to_run) if 'entries' in data: # prendre le premier élément d'une liste de lecture data = data['entries'][0] await ctx.send(f'```ini\n[{data["title"]} ajouté à la queue.]\n```') if download: source = ytdl.prepare_filename(data) else: return {'webpage_url': data['webpage_url'], 'requester': ctx.author, 'title': data['title']} return cls(discord.FFmpegPCMAudio(source), data=data, requester=ctx.author) @classmethod async def regather_stream(cls, data, *, loop): """Utilisé pour préparer un flux, au lieu de le télécharger. Depuis l'expiration des liens de streaming Youtube.""" loop = loop or asyncio.get_event_loop() requester = data['requester'] to_run = partial(ytdl.extract_info, url=data['webpage_url'], download=True) data = await loop.run_in_executor(None, to_run) return cls(discord.FFmpegPCMAudio(data['url']), data=data, requester=requester) class MusicPlayer: """Une classe qui est attribuée à chaque guilde à l'aide du bot pour la musique. Cette classe met en place une file d'attente et une boucle, ce qui permet aux différentes guildes d'écouter différentes listes de lecture simultanément. Lorsque le bot se déconnecte de la Voix, son instance est détruite. """ __slots__ = ('bot', '_guild', '_channel', '_cog', 'queue', 'next', 'current', 'np', 'volume') def __init__(self, ctx): self.bot = ctx.bot self._guild = ctx.guild self._channel = ctx.channel self._cog = ctx.cog self.queue = asyncio.Queue() self.next = asyncio.Event() self.np = None # Message en cours de lecture self.volume = .4 # Volume défini initalement (.5 = 50%) self.current = None ctx.bot.loop.create_task(self.player_loop()) async def player_loop(self): """Player loop principale""" await self.bot.wait_until_ready() while not self.bot.is_closed(): self.next.clear() try: # Attendez la prochaine chanson. Si nous annulons le lecteur et le déconnecter... async with timeout(300): # 5 minutes... source = await self.queue.get() except asyncio.TimeoutError: return self.destroy(self._guild) if not isinstance(source, YTDLSource): # La source était probablement un flux (non téléchargé) # Nous devrions donc nous regather pour éviter l'expiration des flux try: source = await YTDLSource.regather_stream(source, loop=self.bot.loop) except Exception as e: await self._channel.send(f'Il y a eu une erreur dans le traitement de votre chanson.\n' f'```css\n[{e}]\n```') continue source.volume = self.volume self.current = source self._guild.voice_client.play(source, after=lambda _: self.bot.loop.call_soon_threadsafe(self.next.set)) self.np = await self._channel.send(f'**Joue :** `{source.title}` demandé par ' f'`{source.requester}`') await self.next.wait() # Veillez à ce que le processus FFmpeg soit clean. source.cleanup() self.current = None try: # Nous ne jouons plus cette chanson... await self.np.delete() except discord.HTTPException: pass def destroy(self, guild): """Déconnecte et nettoie le lecteur.""" return self.bot.loop.create_task(self._cog.cleanup(guild)) class Music(commands.Cog): """Commandes liées à la musique.""" __slots__ = ('bot', 'players') def __init__(self, bot): self.bot = bot self.players = {} async def cleanup(self, guild): try: await guild.voice_client.disconnect() except AttributeError: pass try: del self.players[guild.id] except KeyError: pass async def __local_check(self, ctx): """Un contrôle local qui s'applique à tous les commandements de ce cog.""" if not ctx.guild: raise commands.NoPrivateMessage return True async def __error(self, ctx, error): """Un gestionnaire d'erreur local pour toutes les erreurs découlant des commandes de ce rouage.""" if isinstance(error, commands.NoPrivateMessage): try: return await ctx.send('Cette commande ne peut pas être utilisée dans les messages privés.') except discord.HTTPException: pass elif isinstance(error, InvalidVoiceChannel): await ctx.send('Erreur de connexion au canal vocal. ' 'Veuillez vous assurer que vous êtes dans un channel valide ou alors en spécifier un.') print('Ignorer l''exception dans la commannde {}:'.format(ctx.command), file=sys.stderr) traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr) def get_player(self, ctx): """Récupérer le joueur du serveur, ou en générer un.""" try: player = self.players[ctx.guild.id] except KeyError: player = MusicPlayer(ctx) self.players[ctx.guild.id] = player return player @commands.command(name='connect', aliases=['join']) async def connect_(self, ctx, *, channel: discord.VoiceChannel=None): """Se connecte au vocal. Paramètres ------------ channel: discord.VoiceChannel [Optionnel] Le channel auquel il faut se connecter. Si un channel n'est pas spécifié, une tentative de rejoindre le canal vocal dans lequel vous êtes sera faite. Cette commande gère également le déplacement du bot vers différents channel. """ if not channel: try: channel = ctx.author.voice.channel except AttributeError: return await ctx.send('Pas de channel à rejoindre. Veuillez soit spécifier un channel valide, soit en rejoindre un.') vc = ctx.voice_client if vc: if vc.channel.id == channel.id: return try: await vc.move_to(channel) except asyncio.TimeoutError: return await ctx.send(f'Move vers le channel : <{channel}> a expiré.') else: try: await channel.connect() except asyncio.TimeoutError: return await ctx.send(f'Connexion au channel : <{channel}> a expiré.') await ctx.send(f'Connexion à : **{channel}**') @commands.command(name='play', aliases=['p']) async def play_(self, ctx, *, search: str): """Demandez une chanson et ajoutez-la à la liste d'attente. Cette commande tente de rejoindre un canal vocal valide si le bot n'en fait pas déjà partie. Utilise YTDL pour rechercher et récupérer automatiquement une chanson. Paramètres ------------ search: str [Required] La chanson à rechercher et à récupérer en utilisant YTDL. Il peut s'agir d'une simple recherche, d'une ID ou d'une URL. """ await ctx.trigger_typing() vc = ctx.voice_client if not vc: await ctx.invoke(self.connect_) player = self.get_player(ctx) # Si le téléchargement est sur False, la source sera un dict qui sera utilisé plus tard pour reconstituer le flux. # Si le téléchargement est sur True, la source sera un discord.FFmpegPCMAudio avec un VolumeTransformer. source = await YTDLSource.create_source(ctx, search, loop=self.bot.loop, download=True) await player.queue.put(source) @commands.command(name='pause') async def pause_(self, ctx): """Met en pause la chanson en cours de lecture.""" vc = ctx.voice_client if not vc or not vc.is_playing(): return await ctx.send('Je ne joue rien en ce moment !') elif vc.is_paused(): return vc.pause() await ctx.send(f'**`{ctx.author}`**: Pause de la chanson !') @commands.command(name='resume') async def resume_(self, ctx): """Reprend la chanson actuellement en pause.""" vc = ctx.voice_client if not vc or not vc.is_connected(): return await ctx.send('Je ne joue rien en ce moment !') elif not vc.is_paused(): return vc.resume() await ctx.send(f'**`{ctx.author}`**: Reprenez la chanson !') @commands.command(name='skip', aliases=['s']) async def skip_(self, ctx): """Skip la chanson.""" vc = ctx.voice_client if not vc or not vc.is_connected(): return await ctx.send('Je ne joue rien en ce moment !') if vc.is_paused(): pass elif not vc.is_playing(): return vc.stop() await ctx.send(f'**`{ctx.author}`**: Skip la chanson !') @commands.command(name='queue', aliases=['q']) async def queue_info(self, ctx): """Affiche la playlist de musiques à venir.""" vc = ctx.voice_client if not vc or not vc.is_connected(): return await ctx.send('Je ne suis pas connecté au channel vocal actuellement !') player = self.get_player(ctx) if player.queue.empty(): return await ctx.send('Il n''y a actuellement plus de chansons en attente.') # Saisissez jusqu'à 5 entrées dans la file d'attente... upcoming = list(itertools.islice(player.queue._queue, 0, 5)) fmt = '\n'.join(f'**`{_["title"]}`**' for _ in upcoming) embed = discord.Embed(title=f"Prochainement - {len(upcoming)} restant{'s' if len(upcoming)>1 else ''}", description=fmt) await ctx.send(embed=embed) @commands.command(name='now_playing', aliases=['np']) async def now_playing_(self, ctx): """Affiche des informations sur la chanson en cours de lecture.""" vc = ctx.voice_client if not vc or not vc.is_connected(): return await ctx.send('Je ne suis pas connecté au channel vocal actuellement !') player = self.get_player(ctx) if not player.current: return await ctx.send('Je ne joue rien en ce moment !') try: # Supprimez notre message "now_playing" précédent. await player.np.delete() except discord.HTTPException: pass player.np = await ctx.send(f'**Joue :** `{vc.source.title}` ' f'demandé par `{vc.source.requester}`') @commands.command(name='volume', aliases=['vol']) async def change_volume(self, ctx, *, vol: float): """Changez le volume du lecteur. Paramètres ------------ volume: float or int [Required] Le volume à régler en pourcentage du lecteur. Celui-ci doit être compris entre 1 et 100. """ vc = ctx.voice_client if not vc or not vc.is_connected(): return await ctx.send('Je ne suis pas connecté au channel vocal actuellement !') if not 0 < vol < 101: return await ctx.send('Veuillez saisir une valeur comprise entre 1 et 100.') player = self.get_player(ctx) if vc.source: vc.source.volume = vol / 100 player.volume = vol / 100 await ctx.send(f'**`{ctx.author}`**: Volume réglé sur **{vol}%**') @commands.command(name='disconnect', aliases=['dc','stop']) async def disconnect_(self, ctx): """Arrêtez la chanson en cours de lecture et détruisez le lecteur. !Avertissement! Cela détruira le joueur affecté au serveur, en supprimant également les chansons et les paramètres en attente (c'est généralement ce qu'on veut faire). """ vc = ctx.voice_client if not vc or not vc.is_connected(): return await ctx.send('Je ne joue rien en ce moment !') await self.cleanup(ctx.guild) await ctx.send(f'Déconnexion.') @commands.command(name='lyrics', aliases=['l', 'lyric']) #caractere vide: \u200b async def lyrics_(self, ctx, *, song: str=None): vc=ctx.voice_client player=self.get_player(ctx) if song or player.current: if not song: song=f"{vc.source.title}" await ctx.send(f":mag: Cherche les paroles pour `{song}`") song=genius.search_song(song) couleur_embed=0xD3D3D3 try: paroles=str(song.lyrics) except: return await ctx.send(f"Pas de résultats trouvés pour : `{vc.source.title}`") lignetotal="" premierembed=True if len(paroles)>7500: return await ctx.send("Désolé, les paroles sont trop longues pour être affichés.") for ligne in paroles.split("\n"): if len(f"{lignetotal}\n{ligne}")<1024: lignetotal=f"{lignetotal}\n{ligne}" else: if premierembed==True: premierembed=False embed=discord.Embed(title=f'Paroles de {(str(song).split(":"))[0].replace("by", "par")}.', description=lignetotal, color=couleur_embed) else: embed=discord.Embed(description=lignetotal, color=couleur_embed) await ctx.send(embed=embed) lignetotal=f"{ligne}" if premierembed==True: premierembed=False embed=discord.Embed(description=lignetotal, color=couleur_embed) else: embed=discord.Embed(description=lignetotal, color=couleur_embed) embed.set_footer(icon_url=ctx.author.avatar_url,text=f"Demandé par {ctx.author} | Lyrics de RapGenius") return await ctx.send(embed=embed) else: await ctx.send("Aucune musique demandé... `.lyrics `") def setup(bot): bot.add_cog(Music(bot))