Initial version commit.
This commit is contained in:
parent
cfd2e0dadb
commit
a63658c13d
160
.gitignore
vendored
Normal file
160
.gitignore
vendored
Normal file
@ -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/
|
||||
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@ -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
|
||||
28
[REDACTED].py
Executable file
28
[REDACTED].py
Executable file
@ -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)
|
||||
84
cogs/chatbot.py
Normal file
84
cogs/chatbot.py
Normal file
@ -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))
|
||||
543
cogs/music_player.py
Normal file
543
cogs/music_player.py
Normal file
@ -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))
|
||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@ -0,0 +1,8 @@
|
||||
discord
|
||||
discord[voice]
|
||||
ffmpeg
|
||||
python-dotenv
|
||||
yt-dlp
|
||||
async_timeout
|
||||
validators
|
||||
openai
|
||||
Loading…
x
Reference in New Issue
Block a user