From 7c034ea20db94f079241d76fabe12f4cc133bcc2 Mon Sep 17 00:00:00 2001 From: strNophix Date: Sun, 31 Oct 2021 11:49:56 +0100 Subject: [PATCH 1/9] Moved slash_command_guilds to the config --- bot.py | 8 ++++++-- config.json.sample | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index 3b0a779..60ece9c 100644 --- a/bot.py +++ b/bot.py @@ -25,14 +25,18 @@ class ChristmasBot(commands.Bot): self.invite_link: str = "" + slash_guilds = None + if len(self.config["slash_command_guilds"]) > 0: + slash_guilds = self.config["slash_command_guilds"] + super().__init__( command_prefix=self.prefix_callable, - description=self.config["info"].get("description", ""), + description=self.config["info"]["description"], case_insensitive=False, fetch_offline_members=False, intents=intents, slash_commands=True, - slash_command_guilds=[227431704426446848], + slash_command_guilds=slash_guilds, ) self.loop.create_task(self.async_init()) diff --git a/config.json.sample b/config.json.sample index 285e92c..e543b6e 100644 --- a/config.json.sample +++ b/config.json.sample @@ -23,5 +23,6 @@ "name": "CloudKid Radio", "description": "A sample bot description" }, - "cogs": ["cogs.owner", "cogs.settings", "cogs.information", "cogs.music"] + "cogs": ["cogs.owner", "cogs.settings", "cogs.information", "cogs.music"], + "slash_command_guilds": [] } From 66fc0924e74ab7a0560cd69a99904d0c50887159 Mon Sep 17 00:00:00 2001 From: strNophix Date: Sun, 31 Oct 2021 11:50:23 +0100 Subject: [PATCH 2/9] Removed unused database namespace from config --- config.json.sample | 7 ------- 1 file changed, 7 deletions(-) diff --git a/config.json.sample b/config.json.sample index e543b6e..10e26bd 100644 --- a/config.json.sample +++ b/config.json.sample @@ -2,13 +2,6 @@ "token": "", "owner_ids": [194545408960102400, 190875175460405249], "prefixes": ["ck!"], - "database": { - "host": "", - "port": 3306, - "user": "", - "password": "", - "db": "" - }, "lavalink": { "host": "", "port": 2333, From 3a165d846c29eec0f8be2a03450a14a74c621413 Mon Sep 17 00:00:00 2001 From: strNophix Date: Sun, 31 Oct 2021 11:51:10 +0100 Subject: [PATCH 3/9] Use the initially generated invite link in /invite --- cogs/information.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cogs/information.py b/cogs/information.py index 1d45160..ae9236c 100644 --- a/cogs/information.py +++ b/cogs/information.py @@ -6,12 +6,13 @@ from discord.ext.commands import Context import humanize import datetime import lavalink +from bot import ChristmasBot from utils.EmbedGenerator import EmbedGenerator from utils.paginator import HelpPaginator class InformationCog(commands.Cog, name="Information"): - def __init__(self, bot: commands.Bot): + def __init__(self, bot: ChristmasBot): self.bot = bot self.is_help_msg = True self.update_status.start() @@ -53,8 +54,9 @@ class InformationCog(commands.Cog, name="Information"): @commands.cooldown(rate=1, per=5, type=commands.BucketType.user) async def invite(self, ctx: Context): """Gets the invite link!""" - link = f"https://discord.com/oauth2/authorize?client_id=643555373814382593&permissions=3230720&scope=bot%20applications.commands" - embed = await EmbedGenerator.Message(ctx, "Add our bot to your server:", link) + embed = await EmbedGenerator.Message( + ctx, "Add our bot to your server:", self.bot.invite_link + ) await ctx.send(embed=embed) @commands.command( @@ -129,6 +131,6 @@ class InformationCog(commands.Cog, name="Information"): await ctx.send(embed=embed) -def setup(bot: commands.Bot): +def setup(bot: ChristmasBot): bot.remove_command("help") bot.add_cog(InformationCog(bot)) From ba51aba4e92fd520227e10edc70299209228a5c6 Mon Sep 17 00:00:00 2001 From: strNophix Date: Sun, 31 Oct 2021 12:05:27 +0100 Subject: [PATCH 4/9] Moved rpc loop to bot.py --- bot.py | 15 ++++++++++++++- cogs/information.py | 11 ----------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/bot.py b/bot.py index 3b0a779..bbae001 100644 --- a/bot.py +++ b/bot.py @@ -1,6 +1,6 @@ import discord from discord.colour import Color -from discord.ext import commands +from discord.ext import commands, tasks import sys import json from typing import Any, Dict, List, Sequence @@ -19,6 +19,9 @@ class ChristmasBot(commands.Bot): voice_states=True, guild_messages=True, guilds=True, messages=True ) + self.rpc_is_help_message = True + self.update_status.start() + self.config = config self.initial_cog_names: List[str] = self.config.get("cogs", []) self.colors: Dict[str, Color] = self.process_colours(config.get("colors", [])) @@ -69,6 +72,16 @@ class ChristmasBot(commands.Bot): colour_dict[name] = Color(int(color, 16)) return colour_dict + @tasks.loop(seconds=20) + async def update_status(self): + await self.wait_until_ready() + title = "ck!connect | ck!help" + if self.rpc_is_help_message: + title = "Tfoe broer" + self.rpc_is_help_message = not self.rpc_is_help_message + + await self.change_presence(activity=discord.Game(title)) + if __name__ == "__main__": try: diff --git a/cogs/information.py b/cogs/information.py index 1d45160..e8336f3 100644 --- a/cogs/information.py +++ b/cogs/information.py @@ -13,17 +13,6 @@ from utils.paginator import HelpPaginator class InformationCog(commands.Cog, name="Information"): def __init__(self, bot: commands.Bot): self.bot = bot - self.is_help_msg = True - self.update_status.start() - - @tasks.loop(seconds=30.0) - async def update_status(self): - await self.bot.wait_until_ready() - title = "ck!connect | ck!help" - if self.is_help_msg: - title = "Tfoe broer" - self.is_help_msg = not self.is_help_msg - await self.bot.change_presence(activity=discord.Game(title)) @commands.cooldown(rate=1, per=5, type=commands.BucketType.user) @commands.command(description="PONG!", aliases=["pong"]) From 0179e98135f1bc112a2a47c78a12574e3e6dbb87 Mon Sep 17 00:00:00 2001 From: strNophix Date: Sun, 31 Oct 2021 12:26:00 +0100 Subject: [PATCH 5/9] Using prefix from config for presence --- bot.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/bot.py b/bot.py index bbae001..11e334f 100644 --- a/bot.py +++ b/bot.py @@ -1,4 +1,5 @@ import discord +from discord import ActivityType from discord.colour import Color from discord.ext import commands, tasks import sys @@ -72,15 +73,19 @@ class ChristmasBot(commands.Bot): colour_dict[name] = Color(int(color, 16)) return colour_dict - @tasks.loop(seconds=20) + @tasks.loop(seconds=30) async def update_status(self): await self.wait_until_ready() - title = "ck!connect | ck!help" - if self.rpc_is_help_message: - title = "Tfoe broer" - self.rpc_is_help_message = not self.rpc_is_help_message - await self.change_presence(activity=discord.Game(title)) + bot_prefix = self.config["prefixes"][0] + if self.rpc_is_help_message: + title = f"for {bot_prefix}connect | {bot_prefix}help" + activity = discord.Activity(name=title, type=ActivityType.watching) + else: + activity = discord.Activity(name="Some song", type=ActivityType.playing) + + self.rpc_is_help_message = not self.rpc_is_help_message + await self.change_presence(activity=activity) if __name__ == "__main__": From 8391e6928e34ee78c1de27a343255e4c2acce936 Mon Sep 17 00:00:00 2001 From: strNophix Date: Sun, 31 Oct 2021 13:38:20 +0100 Subject: [PATCH 6/9] Updated information.py --- cogs/information.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/information.py b/cogs/information.py index ae9236c..45385ba 100644 --- a/cogs/information.py +++ b/cogs/information.py @@ -55,7 +55,7 @@ class InformationCog(commands.Cog, name="Information"): async def invite(self, ctx: Context): """Gets the invite link!""" embed = await EmbedGenerator.Message( - ctx, "Add our bot to your server:", self.bot.invite_link + ctx, "Add Christmas Music to your server:", self.bot.invite_link ) await ctx.send(embed=embed) From f6c05d03deb74aa265b82608f60413c36aeecb8a Mon Sep 17 00:00:00 2001 From: Matthww Date: Sun, 31 Oct 2021 14:28:27 +0100 Subject: [PATCH 7/9] fix invite command --- cogs/information.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cogs/information.py b/cogs/information.py index 1d45160..a415669 100644 --- a/cogs/information.py +++ b/cogs/information.py @@ -54,8 +54,7 @@ class InformationCog(commands.Cog, name="Information"): async def invite(self, ctx: Context): """Gets the invite link!""" link = f"https://discord.com/oauth2/authorize?client_id=643555373814382593&permissions=3230720&scope=bot%20applications.commands" - embed = await EmbedGenerator.Message(ctx, "Add our bot to your server:", link) - await ctx.send(embed=embed) + await EmbedGenerator.Message(ctx, "Add our bot to your server:", link) @commands.command( name="wlinfo", From b3a6a3e13ca947709c331c48afed90cbb34d2e52 Mon Sep 17 00:00:00 2001 From: strNophix Date: Sun, 31 Oct 2021 14:43:53 +0100 Subject: [PATCH 8/9] Added aioredis + custom context --- bot.py | 13 +++++++++++++ config.json.sample | 8 +------- context.py | 7 +++++++ poetry.lock | 23 +++++++++++++++++++++-- pyproject.toml | 1 + 5 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 context.py diff --git a/bot.py b/bot.py index 3b0a779..a8e7262 100644 --- a/bot.py +++ b/bot.py @@ -1,7 +1,10 @@ +import asyncio +from aioredis.client import Redis import discord from discord.colour import Color from discord.ext import commands import sys +from signal import SIGINT, SIGTERM import json from typing import Any, Dict, List, Sequence from discord import Message @@ -11,6 +14,10 @@ from discord.ext.commands.errors import ( ExtensionNotFound, NoEntryPointError, ) +import aioredis +from aioredis import Redis + +from context import CustomContext class ChristmasBot(commands.Bot): @@ -23,6 +30,9 @@ class ChristmasBot(commands.Bot): self.initial_cog_names: List[str] = self.config.get("cogs", []) self.colors: Dict[str, Color] = self.process_colours(config.get("colors", [])) + self._redis_client: Redis = aioredis.from_url( + self.config["redis_url"], encoding="utf-8", decode_responses=True + ) self.invite_link: str = "" super().__init__( @@ -69,6 +79,9 @@ class ChristmasBot(commands.Bot): colour_dict[name] = Color(int(color, 16)) return colour_dict + async def get_context(self, message: Message, *, cls=CustomContext): + return await super().get_context(message, cls=cls) + if __name__ == "__main__": try: diff --git a/config.json.sample b/config.json.sample index 285e92c..f3ab845 100644 --- a/config.json.sample +++ b/config.json.sample @@ -2,13 +2,7 @@ "token": "", "owner_ids": [194545408960102400, 190875175460405249], "prefixes": ["ck!"], - "database": { - "host": "", - "port": 3306, - "user": "", - "password": "", - "db": "" - }, + "redis_url": "", "lavalink": { "host": "", "port": 2333, diff --git a/context.py b/context.py new file mode 100644 index 0000000..c518260 --- /dev/null +++ b/context.py @@ -0,0 +1,7 @@ +from aioredis.client import Redis +from discord.ext import commands + + +class CustomContext(commands.Context): + def get_redis(self) -> Redis: + return self.bot._redis_client diff --git a/poetry.lock b/poetry.lock index 9095636..f08b96d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -16,6 +16,21 @@ yarl = ">=1.0,<1.6.0" [package.extras] speedups = ["aiodns", "brotlipy", "cchardet"] +[[package]] +name = "aioredis" +version = "2.0.0" +description = "asyncio (PEP 3156) Redis support" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +async-timeout = "*" +typing-extensions = "*" + +[package.extras] +hiredis = ["hiredis (>=1.0)"] + [[package]] name = "async-timeout" version = "3.0.1" @@ -258,7 +273,7 @@ python-versions = ">=3.6" name = "typing-extensions" version = "3.10.0.2" description = "Backported and Experimental Type Hints for Python 3.5+" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -290,7 +305,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "9ecfdbfb13478abd79ab060516a2001a0706019f52c946f63e5ebb308e01622f" +content-hash = "3e8f3b5171be334aaa606cd70df03f9b0ac49cb2f78b69723ff49233ce9c1cd3" [metadata.files] aiohttp = [ @@ -308,6 +323,10 @@ aiohttp = [ {file = "aiohttp-3.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:687461cd974722110d1763b45c5db4d2cdee8d50f57b00c43c7590d1dd77fc5c"}, {file = "aiohttp-3.6.3.tar.gz", hash = "sha256:698cd7bc3c7d1b82bb728bae835724a486a8c376647aec336aa21a60113c3645"}, ] +aioredis = [ + {file = "aioredis-2.0.0-py3-none-any.whl", hash = "sha256:9921d68a3df5c5cdb0d5b49ad4fc88a4cfdd60c108325df4f0066e8410c55ffb"}, + {file = "aioredis-2.0.0.tar.gz", hash = "sha256:3a2de4b614e6a5f8e104238924294dc4e811aefbe17ddf52c04a93cbf06e67db"}, +] async-timeout = [ {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, diff --git a/pyproject.toml b/pyproject.toml index c560149..1291457 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ lavalink = "^3.1.4" humanize = "^3.12.0" "discord.py" = { git = "https://github.com/iDevision/enhanced-discord.py", branch = "2.0", extras = ["voice", "speed"] } uvloop = {version = "^0.16.0", optional = true} +aioredis = "^2.0.0" [tool.poetry.dev-dependencies] black = {version = "^21.9b0", allow-prereleases = true} From 75feac1e0e86d2b569362de98ae574f5fe5399ea Mon Sep 17 00:00:00 2001 From: strNophix Date: Sun, 31 Oct 2021 16:32:56 +0100 Subject: [PATCH 9/9] Implemented lavalink for bot + cog --- bot.py | 6 +- cogs/music.py | 211 ++++++++++++++++++++++++++++++++++---------------- 2 files changed, 147 insertions(+), 70 deletions(-) diff --git a/bot.py b/bot.py index 3b0a779..c03e31c 100644 --- a/bot.py +++ b/bot.py @@ -11,9 +11,13 @@ from discord.ext.commands.errors import ( ExtensionNotFound, NoEntryPointError, ) +import lavalink class ChristmasBot(commands.Bot): + lavalink: lavalink.Client + invite_link: str + def __init__(self, config: Dict[Any, Any]): intents = discord.Intents( voice_states=True, guild_messages=True, guilds=True, messages=True @@ -23,8 +27,6 @@ class ChristmasBot(commands.Bot): self.initial_cog_names: List[str] = self.config.get("cogs", []) self.colors: Dict[str, Color] = self.process_colours(config.get("colors", [])) - self.invite_link: str = "" - super().__init__( command_prefix=self.prefix_callable, description=self.config["info"].get("description", ""), diff --git a/cogs/music.py b/cogs/music.py index 855a346..f16309e 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -1,45 +1,91 @@ -import discord -from discord import channel -from discord.ext import commands +import re +import discord +from discord.ext.commands.context import Context import lavalink -import asyncio +from discord.ext import commands +from lavalink.models import DefaultPlayer + from bot import ChristmasBot from utils.EmbedGenerator import EmbedGenerator +url_rx = re.compile(r"https?://(?:www\.)?.+") -class MusicCog(commands.Cog, name="Music"): - def __init__(self, bot: commands.Bot): + +class LavalinkVoiceClient(discord.VoiceClient): + def __init__(self, client: discord.Client, channel: discord.abc.Connectable): + self.client = client + self.channel = channel + # ensure there exists a client already + if hasattr(self.client, "lavalink"): + self.lavalink = self.client.lavalink + else: + self.client.lavalink = lavalink.Client(client.user.id) + self.client.lavalink.add_node( + "localhost", 2333, "youshallnotpass", "us", "default-node" + ) + self.lavalink = self.client.lavalink + + async def on_voice_server_update(self, data): + # the data needs to be transformed before being handed down to + # voice_update_handler + lavalink_data = {"t": "VOICE_SERVER_UPDATE", "d": data} + await self.lavalink.voice_update_handler(lavalink_data) + + async def on_voice_state_update(self, data): + # the data needs to be transformed before being handed down to + # voice_update_handler + lavalink_data = {"t": "VOICE_STATE_UPDATE", "d": data} + await self.lavalink.voice_update_handler(lavalink_data) + + async def connect(self, *, timeout: float, reconnect: bool) -> None: + """ + Connect the bot to the voice channel and create a player_manager + if it doesn't exist yet. + """ + # ensure there is a player_manager when creating a new voice_client + self.lavalink.player_manager.create(guild_id=self.channel.guild.id) + await self.channel.guild.change_voice_state(channel=self.channel) + + async def disconnect(self, *, force: bool) -> None: + """ + Handles the disconnect. + Cleans up running player and leaves the voice client. + """ + player = self.lavalink.player_manager.get(self.channel.guild.id) + + # no need to disconnect if we are not connected + if not force and not player.is_connected: + return + + # None means disconnect + await self.channel.guild.change_voice_state(channel=None) + + # update the channel_id of the player to None + # this must be done because the on_voice_state_update that + # would set channel_id to None doesn't get dispatched after the + # disconnect + player.channel_id = None + self.cleanup() + + +class Music(commands.Cog): + def __init__(self, bot: ChristmasBot): self.bot = bot - self.stream = "https://azuracast.exobot.site/radio/8000/radio.opus" - if not hasattr(bot, "lavalink"): - bot.lavalink = lavalink.Client(bot.user.id) - bot.lavalink.add_node("de-1.rivalmc.net", 2333, "12345", "eu", "poggers") - bot.add_listener( - self.bot.lavalink.voice_update_handler, "on_socket_response" + @commands.Cog.listener() + async def on_ready(self): + if not hasattr( + self.bot, "lavalink" + ): # This ensures the client isn't overwritten during cog reloads. + self.bot.lavalink = lavalink.Client(self.bot.user.id) + + ll = self.bot.config["lavalink"] + self.bot.lavalink.add_node( + ll["host"], ll["port"], ll["password"], ll["region"], ll["name"] ) - lavalink.add_event_hook(self.track_hook) - - bot.loop.create_task(self.async_init()) - - async def async_init(self): - await self.bot.wait_until_ready() - - channels = [] - - # We startup to fast #NOTPOGGERS - await asyncio.sleep(5) - for channel in channels: - guild = self.bot.get_guild(channel[0]) - player = self.bot.lavalink.player_manager.create( - channel[0], endpoint=str(guild.region) - ) - track = await player.node.get_tracks(self.stream) - if not player.is_playing: - await player.play(track["tracks"][0]) - await self.connect_to(channel[0], channel[1]) + self.bot.lavalink.add_event_hook(self.track_hook) def cog_unload(self): """Cog unload handler. This removes any event hooks that were registered.""" @@ -48,18 +94,42 @@ class MusicCog(commands.Cog, name="Music"): async def cog_before_invoke(self, ctx): """Command before-invoke handler.""" guild_check = ctx.guild is not None + # This is essentially the same as `@commands.guild_only()` + # except it saves us repeating ourselves (and also a few lines). + if guild_check: await self.ensure_voice(ctx) + # Ensure that the bot and command author share a mutual voicechannel. + return guild_check + async def cog_command_error(self, ctx, error): + if isinstance(error, commands.CommandInvokeError): + await ctx.send(error.original) + # The above handles errors thrown in this cog and shows them to the user. + # This shouldn't be a problem as the only errors thrown in this cog are from `ensure_voice` + # which contain a reason string, such as "Join a voicechannel" etc. You can modify the above + # if you want to do things differently. + async def ensure_voice(self, ctx): """This check ensures that the bot and command author are in the same voicechannel.""" player = self.bot.lavalink.player_manager.create( ctx.guild.id, endpoint=str(ctx.guild.region) ) - should_connect = ctx.command.name in ("connect",) + # Create returns a player if one exists, otherwise creates. + # This line is important because it ensures that a player always exists for a guild. + + # Most people might consider this a waste of resources for guilds that aren't playing, but this is + # the easiest and simplest way of ensuring players are created. + + # These are commands that require the bot to join a voicechannel (i.e. initiating playback). + # Commands such as volume/skip etc don't require the bot to be in a voicechannel so don't need listing here. + should_connect = ctx.command.name in ("play",) if not ctx.author.voice or not ctx.author.voice.channel: + # Our cog_command_error handler catches this and sends it to the voicechannel. + # Exceptions allow us to "short-circuit" command invocation via checks so the + # execution state of the command goes no further. raise commands.CommandInvokeError("Join a voicechannel first.") if not player.is_connected: @@ -76,69 +146,74 @@ class MusicCog(commands.Cog, name="Music"): ) player.store("channel", ctx.channel.id) - await self.connect_to(ctx.guild.id, str(ctx.author.voice.channel.id)) + await ctx.author.voice.channel.connect(cls=LavalinkVoiceClient) else: if int(player.channel_id) != ctx.author.voice.channel.id: raise commands.CommandInvokeError("You need to be in my voicechannel.") async def track_hook(self, event): if isinstance(event, lavalink.events.QueueEndEvent): + # When this track_hook receives a "QueueEndEvent" from lavalink.py + # it indicates that there are no tracks left in the player's queue. + # To save on resources, we can tell the bot to disconnect from the voicechannel. guild_id = int(event.player.guild_id) - await self.connect_to(guild_id, None) + guild = self.bot.get_guild(guild_id) + await guild.voice_client.disconnect(force=True) - async def connect_to(self, guild_id: int, channel_id: str): - """Connects to the given voicechannel ID. A channel_id of `None` means disconnect.""" - ws = self.bot._connection._get_websocket(guild_id) - await ws.voice_state(str(guild_id), channel_id) + @commands.command(name="play", aliases=["p", "connect", "join"]) + @commands.guild_only() + async def play(self, ctx: Context): + """Searches and plays a song from a given query.""" + # Get the player for this guild from cache. + player: DefaultPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + query = "https://youtu.be/VcIt_AcOPjs" - @commands.command(name="connect") - async def connect(self, ctx): - """Starts vibing.""" - player = self.bot.lavalink.player_manager.get(ctx.guild.id) - results = await player.node.get_tracks(self.stream) + # Get the results for the query from Lavalink. + results = await player.node.get_tracks(query) + # Results could be None if Lavalink returns an invalid response (non-JSON/non-200 (OK)). + # ALternatively, resullts['tracks'] could be an empty array if the query yielded no tracks. if not results or not results["tracks"]: - return await ctx.send("Nothing found!") + return await EmbedGenerator.Title(ctx, "Nothing found!") - if results["loadType"] == "PLAYLIST_LOADED": - tracks = results["tracks"] + # Theoretically songs will always be TRACK_LOADED + track = results["tracks"][0] + await EmbedGenerator.Message( + ctx, "Track Enqueued", f'[{track["info"]["title"]}]({track["info"]["uri"]})' + ) - for track in tracks: - player.add(requester=ctx.author.id, track=track) - else: - track = results["tracks"][0] - track = lavalink.models.AudioTrack(track, ctx.author.id, recommended=True) - player.add(requester=ctx.author.id, track=track) + track = lavalink.models.AudioTrack(track, ctx.author.id, recommended=True) + player.add(requester=ctx.author.id, track=track) if not player.is_playing: await player.play() - @commands.command(aliases=["dc"]) - async def disconnect(self, ctx): + @commands.command(name="disconnect", aliases=["dc", "stop"]) + @commands.guild_only() + async def disconnect(self, ctx: Context): """Disconnects the player from the voice channel and clears its queue.""" - player = self.bot.lavalink.player_manager.get(ctx.guild.id) + player: DefaultPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) if not player.is_connected: - return await ctx.send("Not connected.") + return await EmbedGenerator.Title(ctx, "Not connected.") if not ctx.author.voice or ( player.is_connected and ctx.author.voice.channel.id != int(player.channel_id) ): - return await ctx.send("You're not in my voicechannel!") + # Abuse prevention. Users not in voice channels, or not in the same voice channel as the bot + # may not disconnect the bot. + return await EmbedGenerator.Title(ctx, "You're not in my voicechannel!") + # Clear the queue to ensure old tracks don't start playing + # when someone else queues something. player.queue.clear() + # Stop the current track so Lavalink consumes less resources. await player.stop() - await self.connect_to(ctx.guild.id, None) - - @commands.command(name="now", aliases=["playing"]) - async def now_playing(self, ctx): - """Stop and disconnect the player and controller.""" - em = discord.Embed(color=self.bot.colors["embed"]) - # em.set_thumbnail(url=np["thumbnail"]) - em.add_field(name="Currently playing:", value="Some song") - await EmbedGenerator.SendWithFooter(ctx, em) + # Disconnect from the voice channel. + await ctx.voice_client.disconnect(force=True) + await EmbedGenerator.Title(ctx, "*⃣ | Disconnected.") def setup(bot: ChristmasBot): - bot.add_cog(MusicCog(bot)) + bot.add_cog(Music(bot))