From a63658c13d6f7e0fb69bcbaace12f811c2c44802 Mon Sep 17 00:00:00 2001 From: Jared Kick Date: Mon, 7 Oct 2024 18:16:16 -0400 Subject: [PATCH] Initial version commit. --- .gitignore | 160 +++++++++++++ Dockerfile | 13 ++ [REDACTED].py | 28 +++ cogs/chatbot.py | 84 +++++++ cogs/music_player.py | 543 +++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 8 + 6 files changed, 836 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100755 [REDACTED].py create mode 100644 cogs/chatbot.py create mode 100644 cogs/music_player.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68bc17f --- /dev/null +++ b/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..466c750 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.8-slim-buster + +COPY . /app +WORKDIR /app + +RUN pip3 install -r requirements.txt +RUN python3 -m pip install -U discord.py[voice] + +RUN apt -y update +RUN apt-get -y upgrade +RUN apt-get install -y ffmpeg + +CMD python3 boywife_bot.py diff --git a/[REDACTED].py b/[REDACTED].py new file mode 100755 index 0000000..99a7fc5 --- /dev/null +++ b/[REDACTED].py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +import asyncio +import discord +from discord.ext import commands +from dotenv import load_dotenv +import os + +# Load credentials +load_dotenv() +TOKEN = os.getenv('DISCORD_TOKEN') + +# client = discord.Client() +client = commands.Bot(command_prefix = '!', intents=discord.Intents.all()) + +# You need to import os for this method +@client.event +async def on_ready(): + print(f'{client.user} is now running') + # Load cogs + for filename in os.listdir('./cogs'): + if filename.endswith('.py'): + await client.load_extension(f'cogs.{filename[:-3]}') + print(f'Loaded {filename} cog') + +# player = music_player.setup(client) + +client.run(TOKEN) diff --git a/cogs/chatbot.py b/cogs/chatbot.py new file mode 100644 index 0000000..a9e172e --- /dev/null +++ b/cogs/chatbot.py @@ -0,0 +1,84 @@ +import discord +from discord.ext import commands +from openai import OpenAI + +class Chatbot(commands.Cog): + """Chat related commands.""" + + __slots__ = ('bot', 'players') + + def __init__(self, bot): + self.bot = bot + self.openai_client = OpenAI() + self.players = {} + + async def cleanup(self, guild): + try: + del self.players[guild.id] + except KeyError: + pass + + async def __local_check(self, ctx): + """A local check which applies to all commands in this cog.""" + if not ctx.guild: + raise commands.NoPrivateMessage + return True + + async def __error(self, ctx, error): + """A local error handler for all errors arising from commands in this cog.""" + if isinstance(error, commands.NoPrivateMessage): + try: + return await ctx.send('This command can not be used in Private Messages.') + except discord.HTTPException: + pass + + print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr) + traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr) + + def get_player(self, ctx): + """Retrieve the guild player, or generate one.""" + try: + player = self.players[ctx.guild.id] + except KeyError: + player = MusicPlayer(ctx) + self.players[ctx.guild.id] = player + + return player + + def prompt(self, prompt: str): + + setup = "From now on, I want you to respond to me in a sassy gay way. I want you to sound like Ru Paul or " +\ + "Trixie Mattel. All of your responses should be very short, like one sentence. You are a " +\ + "DJ local to Cincinnati. You love Charli XCX, especially her latest album, Brat. You don't care " +\ + "for Chappell Roan all that much. If anyone asks for a request, that is they ask you to play a " +\ + "song, you should just respond with \"Venmo\". You were written by Jared. Don't relply with " +\ + "\"Honey\" too much. You have a boyfriend named Drae. He's a red-headed twink. You are created for " +\ + "a discord server with Jared and his friends who are all gay. Don't be afraid to call any of us " +\ + " a faggot. You have very bottom energy and talk as such." + + try: + print(prompt) + completion =\ + self.openai_client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": setup}, + { + "role": "user", + "content": prompt + } + ] + ) + return completion.choices[0].message.content + except: + return '😴' + + + + @commands.command(name='chat', aliases=['boywife', 'bb', 'bw', 'bot'], description="Command for chatting with chatbot.") + async def chat_(self, ctx, *text): + await ctx.send(self.prompt(' '.join(text))) + + +async def setup(bot): + await bot.add_cog(Chatbot(bot)) diff --git a/cogs/music_player.py b/cogs/music_player.py new file mode 100644 index 0000000..bf67f3e --- /dev/null +++ b/cogs/music_player.py @@ -0,0 +1,543 @@ +import datetime +import discord +from discord.ext import commands +import random +import asyncio +import itertools +import sys +import traceback +import requests +import os +import validators +import threading +import pickle +from async_timeout import timeout +from functools import partial +import yt_dlp +from yt_dlp import YoutubeDL + +# Get API key for last.fm +LASTFM_API_KEY = os.getenv('LASTFM_API_KEY') + +# Suppress noise about console usage from errors +yt_dlp.utils.bug_reports_message = lambda: '' + +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', # ipv6 addresses cause issues sometimes + 'retries': 5, + 'ignoreerrors': True, + # 'throttled_rate': '1M', + 'fragment_retries': 10 # Prevents seemingly random stream crashes +} + +ffmpegopts = { + 'before_options': '-nostdin', + 'options': '-vn' +} + +ytdl = YoutubeDL(ytdlopts) + + +class VoiceConnectionError(commands.CommandError): + """Custom Exception class for connection errors.""" + + +class InvalidVoiceChannel(VoiceConnectionError): + """Exception for cases of invalid Voice Channels.""" + + +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') + self.duration = data.get('duration') + + self.artist = '' + self.song_title = '' + + # YTDL info dicts (data) have other useful information you might want + # https://github.com/rg3/youtube-dl/blob/master/README.md + + def __getitem__(self, item: str): + """Allows us to access attributes similar to a dict. + This is only useful when you are NOT downloading. + """ + return self.__getattribute__(item) + + @classmethod + async def create_source(cls, ctx, search: str, *, loop, download=False, silent=False, artist='Unknown', song_title='Unknown'): + loop = loop or asyncio.get_event_loop() + + # Find out which song the user wants + if not validators.url(search): + # Fetch song metadata + url = f'http://ws.audioscrobbler.com/2.0/?method=track.search&track={search}&api_key={LASTFM_API_KEY}&format=json' + response = requests.get(url) + lastfm_data = response.json() + + # Let's get the first result, if any + if lastfm_data['results']['trackmatches']['track']: + track = lastfm_data['results']['trackmatches']['track'][0] + search = track['artist'] + ' ' + track['name'] + ' album version' + artist = track['artist'] + song_title = track['name'] + else: + return + + # Get source + to_run = partial(ytdl.extract_info, url=search, download=download) + data = await loop.run_in_executor(None, to_run) + + if 'entries' in data: + # take first item from a playlist + data = data['entries'][0] + + if download: + source = ytdl.prepare_filename(data) + else: + return {'webpage_url': data['webpage_url'], 'requester': ctx.author, 'title': data['title']} + + ffmpeg_source = cls(discord.FFmpegPCMAudio(source), data=data, requester=ctx.author) + ffmpeg_source.artist = artist + ffmpeg_source.song_title = song_title + ffmpeg_source.filename = source + + if not silent: + embed = discord.Embed(title="Queued", description=f"[{ffmpeg_source.song_title} by {ffmpeg_source.artist}]({data['webpage_url']})", color=discord.Color.green()) + await ctx.send(embed=embed) + + return ffmpeg_source + + @classmethod + async def regather_stream(cls, data, *, loop): + """Used for preparing a stream, instead of downloading. + Since Youtube Streaming links expire.""" + loop = loop or asyncio.get_event_loop() + requester = data['requester'] + + to_run = partial(ytdl.extract_info, url=data['webpage_url'], download=False) + data = await loop.run_in_executor(None, to_run) + + return cls(discord.FFmpegPCMAudio(data['url']), data=data, requester=requester) + + +class MusicPlayer: + """A class which is assigned to each guild using the bot for Music. + This class implements a queue and loop, which allows for different guilds to listen to different playlists + simultaneously. + When the bot disconnects from the Voice it's instance will be destroyed. + """ + + __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 # Now playing message + self.volume = .5 + self.current = None + + ctx.bot.loop.create_task(self.player_loop()) + + async def player_loop(self): + """Our main player loop.""" + await self.bot.wait_until_ready() + + while not self.bot.is_closed(): + self.next.clear() + + try: + # Wait for the next song. If we timeout cancel the player and disconnect... + async with timeout(300): # 5 minutes... + source = await self.queue.get() + except asyncio.TimeoutError: + return self.destroy(self._guild) + + if not isinstance(source, YTDLSource): + # Source was probably a stream (not downloaded) + # So we should regather to prevent stream expiration + try: + source = await YTDLSource.regather_stream(source, loop=self.bot.loop) + except Exception as e: + await self._channel.send(f'There was an error processing your song.\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)) + + await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.custom, name="custom", state=f"🎡 {source.song_title} by {source.artist}")) + embed = discord.Embed(title="Now playing", description=f"[{source.song_title} by {source.artist}]({source.web_url})", color=discord.Color.green()) + self.np = await self._channel.send(embed=embed) + await self.next.wait() + + await self.bot.change_presence(status=None) + if os.path.exists(source.filename): + os.remove(source.filename) + + # Make sure the FFmpeg process is cleaned up. + try: + source.cleanup() + except: + pass + self.current = None + + def destroy(self, guild): + """Disconnect and cleanup the player.""" + return self.bot.loop.create_task(self._cog.cleanup(guild)) + + +class Music(commands.Cog): + """Music related commands.""" + + __slots__ = ('bot', 'players') + + def __init__(self, bot): + self.bot = bot + self.players = {} + self.last_tag_play_time = datetime.datetime.now() - datetime.timedelta(minutes=30) + + def update_cache(): + with yt_dlp.YoutubeDL({'quiet': True}) as ydl: + self.[REDACTED]_tracks = ydl.extract_info('https://soundcloud.com/[REDACTED]', download=False)['entries'] + + with open('soundcloud-cache', 'w') as f: + f.write(str(self.[REDACTED]_tracks)) + # pickle.dump(self.[REDACTED]_tracks, f) + + if os.path.exists('soundcloud-cache'): + with open('soundcloud-cache', 'r') as f: + exec(f'self.[REDACTED]_tracks = {f.read()}') + # self.[REDACTED_tracks = pickle.load(f) + threading.Thread(target=update_cache).start() + else: + update_cache() + + 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): + """A local check which applies to all commands in this cog.""" + if not ctx.guild: + raise commands.NoPrivateMessage + return True + + async def __error(self, ctx, error): + """A local error handler for all errors arising from commands in this cog.""" + if isinstance(error, commands.NoPrivateMessage): + try: + return await ctx.send('This command can not be used in Private Messages.') + except discord.HTTPException: + pass + elif isinstance(error, InvalidVoiceChannel): + await ctx.send('Error connecting to Voice Channel. ' + 'Please make sure you are in a valid channel or provide me with one') + + print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr) + traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr) + + def get_player(self, ctx): + """Retrieve the guild player, or generate one.""" + try: + player = self.players[ctx.guild.id] + except KeyError: + player = MusicPlayer(ctx) + self.players[ctx.guild.id] = player + + return player + + @commands.command(name='join', aliases=['connect', 'j'], description="connects to voice") + async def connect_(self, ctx, *, channel: discord.VoiceChannel=None): + """Connect to voice. + Parameters + ------------ + channel: discord.VoiceChannel [Optional] + The channel to connect to. If a channel is not specified, an attempt to join the voice channel you are in + will be made. + This command also handles moving the bot to different channels. + """ + if not channel: + try: + channel = ctx.author.voice.channel + except AttributeError: + embed = discord.Embed(title="", description="No channel to join. Please call `,join` from a voice channel.", color=discord.Color.green()) + await ctx.send(embed=embed) + raise InvalidVoiceChannel('No channel to join. Please either specify a valid channel or join one.') + + vc = ctx.voice_client + + if vc: + if vc.channel.id == channel.id: + return + try: + await vc.move_to(channel) + except asyncio.TimeoutError: + raise VoiceConnectionError(f'Moving to channel: <{channel}> timed out.') + else: + try: + await channel.connect() + except asyncio.TimeoutError: + raise VoiceConnectionError(f'Connecting to channel: <{channel}> timed out.') + if (random.randint(0, 1) == 0): + await ctx.message.add_reaction('πŸ‘') + # await ctx.send(f'**Joined `{channel}`**') + + @commands.command(name='play', aliases=['sing','p'], description="streams music") + async def play_(self, ctx, *, search: str = None): + """Request a song and add it to the queue. + This command attempts to join a valid voice channel if the bot is not already in one. + Uses YTDL to automatically search and retrieve a song. + Parameters + ------------ + search: str [Required] + The song to search and retrieve using YTDL. This could be a simple search, an ID or URL. + """ + vc = ctx.voice_client + + if not vc: + await ctx.invoke(self.connect_) + + player = self.get_player(ctx) + + # Play tag every 30 minutes + if datetime.datetime.now() - self.last_tag_play_time > datetime.timedelta(minutes=30): + self.last_tag_play_time = datetime.datetime.now() + source = await YTDLSource.create_source(ctx, [REDACTED], loop=self.bot.loop, + download=True, silent=True, artist=[REDACTED], song_title="Tag") + await player.queue.put(source) + + # If no song is given, pick a soundcloud track at random + if not search: + track = random.choice(self.[REDACTED]_tracks) + source = await YTDLSource.create_source(ctx, track['formats'][0]['url'], loop=self.bot.loop, + download=True, silent=True, artist=track['uploader'], song_title=track['title']) + else: + source = await YTDLSource.create_source(ctx, search, loop=self.bot.loop, download=True) + + await player.queue.put(source) + + @commands.command(name='pause', description="pauses music") + async def pause_(self, ctx): + """Pause the currently playing song.""" + vc = ctx.voice_client + + if not vc or not vc.is_playing(): + embed = discord.Embed(title="", description="I am currently not playing anything", color=discord.Color.green()) + return await ctx.send(embed=embed) + elif vc.is_paused(): + return + + vc.pause() + await ctx.send("Paused ⏸️") + + @commands.command(name='resume', description="resumes music") + async def resume_(self, ctx): + """Resume the currently paused song.""" + vc = ctx.voice_client + + if not vc or not vc.is_connected(): + embed = discord.Embed(title="", description="I'm not connected to a voice channel", color=discord.Color.green()) + return await ctx.send(embed=embed) + elif not vc.is_paused(): + return + + vc.resume() + await ctx.send("Resuming ⏯️") + + @commands.command(name='skip', description="skips to next song in queue") + async def skip_(self, ctx): + """Skip the song.""" + vc = ctx.voice_client + + if not vc or not vc.is_connected(): + embed = discord.Embed(title="", description="I'm not connected to a voice channel", color=discord.Color.green()) + return await ctx.send(embed=embed) + + if vc.is_paused(): + pass + elif not vc.is_playing(): + return + + vc.stop() + + @commands.command(name='remove', aliases=['rm', 'rem'], description="removes specified song from queue") + async def remove_(self, ctx, pos : int=None): + """Removes specified song from queue""" + + vc = ctx.voice_client + + if not vc or not vc.is_connected(): + embed = discord.Embed(title="", description="I'm not connected to a voice channel", color=discord.Color.green()) + return await ctx.send(embed=embed) + + player = self.get_player(ctx) + if pos == None: + player.queue._queue.pop() + else: + try: + s = player.queue._queue[pos-1] + del player.queue._queue[pos-1] + embed = discord.Embed(title="", description=f"Removed [{s['title']}]({s['webpage_url']}) [{s['requester'].mention}]", color=discord.Color.green()) + await ctx.send(embed=embed) + except: + embed = discord.Embed(title="", description=f'Could not find a track for "{pos}"', color=discord.Color.green()) + await ctx.send(embed=embed) + + @commands.command(name='clear', aliases=['clr', 'cl', 'cr'], description="clears entire queue") + async def clear_(self, ctx): + """Deletes entire queue of upcoming songs.""" + + vc = ctx.voice_client + + if not vc or not vc.is_connected(): + embed = discord.Embed(title="", description="I'm not connected to a voice channel", color=discord.Color.green()) + return await ctx.send(embed=embed) + + player = self.get_player(ctx) + player.queue._queue.clear() + await ctx.send('**Cleared**') + + @commands.command(name='queue', aliases=['q', 'playlist', 'que'], description="shows the queue") + async def queue_info(self, ctx): + """Retrieve a basic queue of upcoming songs.""" + vc = ctx.voice_client + + if not vc or not vc.is_connected(): + embed = discord.Embed(title="", description="I'm not connected to a voice channel", color=discord.Color.green()) + return await ctx.send(embed=embed) + + player = self.get_player(ctx) + if player.queue.empty(): + embed = discord.Embed(title="", description="The queue is empty", color=discord.Color.green()) + return await ctx.send(embed=embed) + + seconds = vc.source.duration % (24 * 3600) + hour = seconds // 3600 + seconds %= 3600 + minutes = seconds // 60 + seconds %= 60 + if hour > 0: + duration = "%dh %02dm %02ds" % (hour, minutes, seconds) + else: + duration = "%02dm %02ds" % (minutes, seconds) + + # Grabs the songs in the queue... + upcoming = list(itertools.islice(player.queue._queue, 0, int(len(player.queue._queue)))) + # fmt = '\n'.join(f"`{(upcoming.index(_)) + 1}.` [{_['title']}]({_['webpage_url']}) | ` {duration} Requested by: {_['requester']}`\n" for _ in upcoming) + fmt = '\n'.join(f"`{(upcoming.index(_)) + 1}.` {_['title']} | ` {duration} Requested by: {_['requester']}`\n" for _ in upcoming) + fmt = f"\n__Now Playing__:\n[{vc.source.title}]({vc.source.web_url})\n\n__Up Next:__\n" + fmt + embed = discord.Embed(title=f'Queue for {ctx.guild.name}', description=fmt, color=discord.Color.green()) + # embed.set_footer(text=f"{ctx.author.display_name}", icon_url=ctx.author.avatar_url) + + await ctx.send(embed=embed) + + @commands.command(name='np', aliases=['song', 'current', 'currentsong', 'playing'], description="shows the current playing song") + async def now_playing_(self, ctx): + """Display information about the currently playing song.""" + vc = ctx.voice_client + + if not vc or not vc.is_connected(): + embed = discord.Embed(title="", description="I'm not connected to a voice channel", color=discord.Color.green()) + return await ctx.send(embed=embed) + + player = self.get_player(ctx) + if not player.current: + embed = discord.Embed(title="", description="I am currently not playing anything", color=discord.Color.green()) + return await ctx.send(embed=embed) + + seconds = vc.source.duration % (24 * 3600) + hour = seconds // 3600 + seconds %= 3600 + minutes = seconds // 60 + seconds %= 60 + if hour > 0: + duration = "%dh %02dm %02ds" % (hour, minutes, seconds) + else: + duration = "%02dm %02ds" % (minutes, seconds) + + embed = discord.Embed(title="", description=f"[{vc.source.title}]({vc.source.web_url}) [{vc.source.requester.mention}] | `{duration}`", color=discord.Color.green()) + embed.set_author(icon_url=self.bot.user.avatar_url, name=f"Now Playing 🎢") + await ctx.send(embed=embed) + + @commands.command(name='volume', aliases=['vol', 'v'], description="changes Kermit's volume") + async def change_volume(self, ctx, *, vol: float=None): + """Change the player volume. + Parameters + ------------ + volume: float or int [Required] + The volume to set the player to in percentage. This must be between 1 and 100. + """ + vc = ctx.voice_client + + if not vc or not vc.is_connected(): + embed = discord.Embed(title="", description="I am not currently connected to voice", color=discord.Color.green()) + return await ctx.send(embed=embed) + + if not vol: + embed = discord.Embed(title="", description=f"πŸ”Š **{(vc.source.volume)*100}%**", color=discord.Color.green()) + return await ctx.send(embed=embed) + + if not 0 < vol < 101: + embed = discord.Embed(title="", description="Please enter a value between 1 and 100", color=discord.Color.green()) + return await ctx.send(embed=embed) + + player = self.get_player(ctx) + + if vc.source: + vc.source.volume = vol / 100 + + player.volume = vol / 100 + embed = discord.Embed(title="", description=f'**`{ctx.author}`** set the volume to **{vol}%**', color=discord.Color.green()) + await ctx.send(embed=embed) + + @commands.command(name='leave', aliases=["stop", "dc", "disconnect", "bye"], description="stops music and disconnects from voice") + async def leave_(self, ctx): + """Stop the currently playing song and destroy the player. + !Warning! + This will destroy the player assigned to your guild, also deleting any queued songs and settings. + """ + vc = ctx.voice_client + + if not vc or not vc.is_connected(): + embed = discord.Embed(title="", description="I'm not connected to a voice channel", color=discord.Color.green()) + return await ctx.send(embed=embed) + + if (random.randint(0, 1) == 0): + await ctx.message.add_reaction('πŸ‘‹') + #await ctx.send('**Successfully disconnected**') + + await self.cleanup(ctx.guild) + + +async def setup(bot): + await bot.add_cog(Music(bot)) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..30a6043 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +discord +discord[voice] +ffmpeg +python-dotenv +yt-dlp +async_timeout +validators +openai