Initial version commit.
This commit is contained in:
parent
cfd2e0dadb
commit
e6f3864571
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
|
||||||
32
boywife_bot.py
Executable file
32
boywife_bot.py
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
#!/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')
|
||||||
|
|
||||||
|
@commands.command(name='boywife', aliases=['bb', 'bw', 'bot'], description="Command for chatting with chatbot.")
|
||||||
|
async def chat(self, ctx):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# player = music_player.setup(client)
|
||||||
|
|
||||||
|
client.run(TOKEN)
|
||||||
526
cogs/music_player.py
Normal file
526
cogs/music_player.py
Normal file
@ -0,0 +1,526 @@
|
|||||||
|
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
|
||||||
|
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)
|
||||||
|
with yt_dlp.YoutubeDL({}) as ydl:
|
||||||
|
self.boywife_tracks = ydl.extract_info('https://soundcloud.com/djboywife', download=False)['entries']
|
||||||
|
|
||||||
|
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, 'https://soundcloud.com/djboywife/youre-listening-to-dj-boywife', loop=self.bot.loop,
|
||||||
|
download=True, silent=True, artist='DJ Boywife', 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.boywife_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="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 = f"\n__Now Playing__:\n[{vc.source.title}]({vc.source.web_url}) | ` {duration} Requested by: {vc.source.requester}`\n\n__Up Next:__\n" + fmt + f"\n**{len(upcoming)} songs in queue**"
|
||||||
|
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))
|
||||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
discord
|
||||||
|
discord[voice]
|
||||||
|
ffmpeg
|
||||||
|
python-dotenv
|
||||||
|
yt-dlp
|
||||||
|
async_timeout
|
||||||
|
validators
|
||||||
Loading…
x
Reference in New Issue
Block a user