From 25438b18f166b45b401bf62b6d1914b40af5c8ae Mon Sep 17 00:00:00 2001 From: strNophix Date: Tue, 2 Nov 2021 20:32:50 +0100 Subject: [PATCH] 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