From 25438b18f166b45b401bf62b6d1914b40af5c8ae Mon Sep 17 00:00:00 2001 From: strNophix Date: Tue, 2 Nov 2021 20:32:50 +0100 Subject: [PATCH 1/3] Continuous radio using redis and lavalink --- cogs/information.py | 6 ++- cogs/music.py | 99 ++++++++++++++++++++++++++++------------- config.json.sample | 3 +- utils/EmbedGenerator.py | 2 +- 4 files changed, 74 insertions(+), 36 deletions(-) diff --git a/cogs/information.py b/cogs/information.py index e2820f2..5e796ce 100644 --- a/cogs/information.py +++ b/cogs/information.py @@ -43,7 +43,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!""" - await EmbedGenerator.Message(ctx, "Add our bot to your server:", self.bot.invite_link) + await EmbedGenerator.Message( + ctx, "Add our bot to your server:", self.bot.invite_link + ) @commands.command( name="wlinfo", @@ -77,7 +79,7 @@ class InformationCog(commands.Cog, name="Information"): @commands.command(name="help", aliases=["about", "info"], slash_command=True) @commands.cooldown(1, 1, commands.BucketType.user) async def about(self, ctx: Context): - """Exobot command list""" + """ChristmasBot command list""" if None: entity = self.bot.get_cog(command) or self.bot.get_command(command) diff --git a/cogs/music.py b/cogs/music.py index f16309e..e340882 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -1,13 +1,18 @@ +import asyncio import re +from aioredis.client import Redis import discord +from discord.channel import TextChannel from discord.ext.commands.context import Context import lavalink from discord.ext import commands -from lavalink.models import DefaultPlayer +from lavalink.models import AudioTrack, DefaultPlayer from bot import ChristmasBot from utils.EmbedGenerator import EmbedGenerator +from context import CustomContext +from discord import Embed url_rx = re.compile(r"https?://(?:www\.)?.+") @@ -16,15 +21,7 @@ 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 + self.lavalink = self.client.lavalink async def on_voice_server_update(self, data): # the data needs to be transformed before being handed down to @@ -94,9 +91,13 @@ class Music(commands.Cog): 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 not hasattr(self.bot, "lavalink"): + await ctx.send("Still starting please wait a moment.") + if guild_check: await self.ensure_voice(ctx) # Ensure that the bot and command author share a mutual voicechannel. @@ -124,7 +125,7 @@ class Music(commands.Cog): # 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",) + should_connect = ctx.command.name in ("connect",) if not ctx.author.voice or not ctx.author.voice.channel: # Our cog_command_error handler catches this and sends it to the voicechannel. @@ -159,39 +160,73 @@ class Music(commands.Cog): guild_id = int(event.player.guild_id) guild = self.bot.get_guild(guild_id) await guild.voice_client.disconnect(force=True) + elif isinstance(event, lavalink.events.TrackStartEvent): + channel_id = int(event.player.fetch("channel")) + channel: TextChannel = self.bot.get_channel(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.""" + color = self.bot.colors["embed"] + current_track: AudioTrack = event.player.current + embed = Embed( + title="Now playing:", + description=f"[{current_track.title}]({current_track.uri})", + color=color, + ) + await channel.send(embed=embed) + elif isinstance(event, lavalink.events.TrackEndEvent): + query = await self.bot._redis_client.randomkey() + result = await event.player.node.get_tracks(query) + if not result or not result["tracks"]: + return + + track = lavalink.models.AudioTrack( + result["tracks"][0], self.bot.user.id, recommended=False + ) + event.player.add(requester=self.bot.user.id, track=track) + + @commands.command(name="connect", aliases=["p", "play", "join"]) + async def play(self, ctx: CustomContext): + """Starts playing Christmas bangers""" # 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" + redis_client: Redis = ctx.get_redis() + pipeline = redis_client.pipeline() + for _ in range(self.bot.config["queue_buffer_size"]): + pipeline.randomkey() + + queries = await pipeline.execute() # Get the results for the query from Lavalink. - results = await player.node.get_tracks(query) + for query in queries: + result = await player.node.get_tracks(query) + if not result or not result["tracks"]: + continue - # 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 EmbedGenerator.Title(ctx, "Nothing found!") - - # Theoretically songs will always be TRACK_LOADED - track = results["tracks"][0] - await EmbedGenerator.Message( - ctx, "Track Enqueued", f'[{track["info"]["title"]}]({track["info"]["uri"]})' - ) - - track = lavalink.models.AudioTrack(track, ctx.author.id, recommended=True) - player.add(requester=ctx.author.id, track=track) + track = lavalink.models.AudioTrack( + result["tracks"][0], ctx.author.id, recommended=False + ) + player.add(requester=ctx.author.id, track=track) if not player.is_playing: await player.play() + await ctx.send("Started playing") + + @commands.command(name="skip", aliases=["next"]) + async def skip(self, ctx: Context): + """I heard this song way too often""" + player: DefaultPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + await player.skip() + await ctx.send("Skipped current song") + + @commands.command(name="queue") + async def queue(self, ctx: Context): + """Ghetto queue""" + player: DefaultPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) + await EmbedGenerator.Message(ctx, "Queue:", player.queue) + @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.""" + """Disconnects ChristmasBot""" player: DefaultPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) if not player.is_connected: diff --git a/config.json.sample b/config.json.sample index 3b0e449..022f86f 100644 --- a/config.json.sample +++ b/config.json.sample @@ -18,5 +18,6 @@ "description": "A sample bot description" }, "cogs": ["cogs.owner", "cogs.settings", "cogs.information", "cogs.music"], - "slash_command_guilds": [] + "slash_command_guilds": [], + "queue_buffer_size": 5 } diff --git a/utils/EmbedGenerator.py b/utils/EmbedGenerator.py index 0d66ff8..aebf5cd 100644 --- a/utils/EmbedGenerator.py +++ b/utils/EmbedGenerator.py @@ -39,4 +39,4 @@ class EmbedGenerator: em.set_footer(text=f"Requested by: {ctx.author}", icon_url=avatar) if kwargs.get("no_send", False): return em - return await ctx.send(embed=em, **kwargs) + return await ctx.send(embed=em, **kwargs) \ No newline at end of file From cdf848d87d2f5c730d41e4035b4483684062c65d Mon Sep 17 00:00:00 2001 From: Matthww Date: Tue, 2 Nov 2021 22:51:30 +0100 Subject: [PATCH 2/3] Add autojoin feature --- cogs/information.py | 15 ++++++++-- cogs/music.py | 71 ++++++++++++++++++++++++++++----------------- utils/database.py | 20 +++++++++++++ 3 files changed, 78 insertions(+), 28 deletions(-) create mode 100644 utils/database.py diff --git a/cogs/information.py b/cogs/information.py index 5e796ce..6ccca28 100644 --- a/cogs/information.py +++ b/cogs/information.py @@ -1,3 +1,5 @@ +from typing import Optional + import discord from discord.ext import tasks, commands @@ -76,11 +78,20 @@ class InformationCog(commands.Cog, name="Information"): ) await ctx.send(fmt) + from utils import database + database.AutoJoin.get_channels() + @commands.command(name="help", aliases=["about", "info"], slash_command=True) @commands.cooldown(1, 1, commands.BucketType.user) - async def about(self, ctx: Context): + async def about( + self, + ctx: Context, + command: Optional[str] = commands.Option( + description="Show help for a command or category" + ), + ): """ChristmasBot command list""" - if None: + if command: entity = self.bot.get_cog(command) or self.bot.get_command(command) if entity is None: diff --git a/cogs/music.py b/cogs/music.py index e340882..5a824d6 100644 --- a/cogs/music.py +++ b/cogs/music.py @@ -1,5 +1,7 @@ import asyncio import re +from typing import Optional + from aioredis.client import Redis import discord @@ -11,6 +13,7 @@ from lavalink.models import AudioTrack, DefaultPlayer from bot import ChristmasBot from utils.EmbedGenerator import EmbedGenerator +from utils.database import AutoJoin from context import CustomContext from discord import Embed @@ -83,6 +86,46 @@ class Music(commands.Cog): ) self.bot.lavalink.add_event_hook(self.track_hook) + await self.async_init() + + async def async_init(self): + redis_result = await AutoJoin.get_channels(self.bot._redis_client) + + while len(self.bot.lavalink.node_manager.available_nodes) == 0: + await asyncio.sleep(1) + + for guild_id, (voicechannel_id, textchannel_id) in redis_result.items(): + player = self.bot.lavalink.player_manager.create(guild_id) + player.store('channel', textchannel_id) + voice_channel = await self.bot.fetch_channel(voicechannel_id) + await voice_channel.connect(cls=LavalinkVoiceClient) + if not player.is_playing: + await self.fill_player_queue( + player, self.bot.config["queue_buffer_size"] + ) + await player.play() + + textchannel = await self.bot.fetch_channel(textchannel_id) + await textchannel.send("Automatically joined the voice channel") + + async def fill_player_queue(self, player: DefaultPlayer, buffer: Optional[int] = 1): + pipeline = self.bot._redis_client.pipeline() + for _ in range(buffer): + pipeline.randomkey() + + queries = await pipeline.execute() + print(queries) + # Get the results for the query from Lavalink. + for query in queries: + result = await player.node.get_tracks(query) + print(result) + if not result or not result["tracks"]: + continue + + track = lavalink.models.AudioTrack( + result["tracks"][0], self.bot.user.id, recommended=False + ) + player.add(requester=self.bot.user.id, track=track) def cog_unload(self): """Cog unload handler. This removes any event hooks that were registered.""" @@ -173,38 +216,14 @@ class Music(commands.Cog): ) await channel.send(embed=embed) elif isinstance(event, lavalink.events.TrackEndEvent): - query = await self.bot._redis_client.randomkey() - result = await event.player.node.get_tracks(query) - if not result or not result["tracks"]: - return - - track = lavalink.models.AudioTrack( - result["tracks"][0], self.bot.user.id, recommended=False - ) - event.player.add(requester=self.bot.user.id, track=track) + await self.fill_player_queue(event.player, 1) @commands.command(name="connect", aliases=["p", "play", "join"]) async def play(self, ctx: CustomContext): """Starts playing Christmas bangers""" # Get the player for this guild from cache. player: DefaultPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id) - redis_client: Redis = ctx.get_redis() - pipeline = redis_client.pipeline() - for _ in range(self.bot.config["queue_buffer_size"]): - pipeline.randomkey() - - queries = await pipeline.execute() - - # Get the results for the query from Lavalink. - for query in queries: - result = await player.node.get_tracks(query) - if not result or not result["tracks"]: - continue - - track = lavalink.models.AudioTrack( - result["tracks"][0], ctx.author.id, recommended=False - ) - player.add(requester=ctx.author.id, track=track) + await self.fill_player_queue(player, self.bot.config["queue_buffer_size"]) if not player.is_playing: await player.play() diff --git a/utils/database.py b/utils/database.py new file mode 100644 index 0000000..824020a --- /dev/null +++ b/utils/database.py @@ -0,0 +1,20 @@ +from aioredis import Redis +from typing import Sequence + + +class AutoJoin: + @staticmethod + async def get_channels(redis: Redis) -> Sequence[tuple]: + # return all channels + channels = await redis.hgetall(name="autojoin") + return {key: value.split("-") for key, value in channels.items()} + + @staticmethod + async def update_channel(redis: Redis, guild_id, voicechannel_id, textchannel_id): + await redis.hset( + name="autojoin", key=guild_id, value=f"{voicechannel_id}-{textchannel_id}" + ) + + @staticmethod + async def del_channel(redis: Redis, guild_id): + await redis.hdel("autojoin", guild_id) From 9815bf080d4ba0aa78581db0ae7ba6a911609409 Mon Sep 17 00:00:00 2001 From: Matthww Date: Tue, 2 Nov 2021 22:59:17 +0100 Subject: [PATCH 3/3] update settings cog --- cogs/settings.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/cogs/settings.py b/cogs/settings.py index eae8520..29f3285 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -1,5 +1,7 @@ +from context import CustomContext from discord.ext import commands from utils.EmbedGenerator import EmbedGenerator +from utils.database import AutoJoin from bot import ChristmasBot from discord.ext.commands import Context @@ -10,27 +12,28 @@ class SettingsCog(commands.Cog, name="Settings"): @commands.group(aliases=["aj"], invoke_without_command=True) @commands.cooldown(rate=1, per=5, type=commands.BucketType.user) - async def autojoin(self, ctx: Context): + async def autojoin(self, ctx: CustomContext): await EmbedGenerator.Message( ctx, "Autojoin", f"Usage:\n\n`{ctx.prefix}autojoin set`\n`{ctx.prefix}autojoin unset`", ) - @autojoin.command(name="set") + @autojoin.command(name="enable") @commands.has_permissions(manage_channels=True) @commands.cooldown(rate=1, per=5, type=commands.BucketType.user) - async def autojoin_set(self, ctx: Context): - vc = ctx.author.voice.channel - await AutoJoin.update_channel(self.bot, ctx.guild.id, vc.id) + async def autojoin_set(self, ctx: CustomContext): + voicechannel_id = ctx.author.voice.channel.id + textchannel_id = ctx.message.channel.id + await AutoJoin.update_channel(ctx.get_redis(), ctx.guild.id, voicechannel_id, textchannel_id) await EmbedGenerator.Message(ctx, "Autojoin", "`enabled`") - @autojoin.command(name="unset") + @autojoin.command(name="disable") @commands.has_permissions(manage_channels=True) @commands.cooldown(rate=1, per=5, type=commands.BucketType.user) - async def autojoin_del(self, ctx: Context): + async def autojoin_del(self, ctx: CustomContext): vc = ctx.author.voice.channel - await AutoJoin.del_channel(self.bot, ctx.guild.id) + await AutoJoin.del_channel(ctx.get_redis(), ctx.guild.id) await EmbedGenerator.Message(ctx, "Autojoin", "`disabled`")