diff --git a/README.md b/README.md index 6fa91da..c57771f 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,5 @@ Credits: [Vert Cogs](https://github.com/vertyco/vrt-cogs) [Vex Cogs](https://github.com/Vexed01/Vex-Cogs) [x26 Cogs](https://github.com/Twentysix26/x26-Cogs) -[Toxic Cogs](https://github.com/NeuroAssassin/Toxic-Cogs) \ No newline at end of file +[Toxic Cogs](https://github.com/NeuroAssassin/Toxic-Cogs) +[Karlo Cogs](https://github.com/karlsbjorn/karlo-cogs) \ No newline at end of file diff --git a/autoplay/__init__.py b/autoplay/__init__.py new file mode 100644 index 0000000..2554ba9 --- /dev/null +++ b/autoplay/__init__.py @@ -0,0 +1,13 @@ +import json +from pathlib import Path + +from pylav.extension.red.utils.required_methods import pylav_auto_setup + +from .autoplay import AutoPlay + +with open(Path(__file__).parent / "info.json") as fp: + __red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"] + + +async def setup(bot): + await pylav_auto_setup(bot, AutoPlay) diff --git a/autoplay/autoplay.py b/autoplay/autoplay.py new file mode 100644 index 0000000..c475370 --- /dev/null +++ b/autoplay/autoplay.py @@ -0,0 +1,240 @@ +from typing import Optional + +import discord +from discord import AppCommandType +from redbot.core import Config, commands +from redbot.core.i18n import Translator, cog_i18n + +from pylav.events.player import PlayerDisconnectedEvent +from pylav.logging import getLogger +from pylav.players.player import Player +from pylav.players.query.obj import Query +from pylav.type_hints.bot import DISCORD_BOT_TYPE, DISCORD_INTERACTION_TYPE + +log = getLogger("PyLav.3rdpt.karlo-cogs.autoplay") +_ = Translator("AutoPlay", __file__) + + +@cog_i18n(_) +class AutoPlay(commands.Cog): + """Automatically play music that a guild member is listening to on Spotify.""" + + def __init__(self, bot: DISCORD_BOT_TYPE): + self.bot: DISCORD_BOT_TYPE = bot + self.config = Config.get_conf(self, identifier=87446677010550784, force_registration=True) + default_guild = {"tracked_member": None, "autoplaying": False, "paused_track": None} + self.config.register_guild(**default_guild) + self.context_user_autoplay = discord.app_commands.ContextMenu( + name=_("Start AutoPlay"), + callback=self._context_user_autoplay, + type=AppCommandType.user, + ) + self.bot.tree.add_command(self.context_user_autoplay) + + @commands.hybrid_command() + @commands.guild_only() + async def autoplay(self, ctx, member: discord.Member = None): + """Toggle autoplay for a member. + + This will cause the bot to automatically play music that + the member is listening to on Spotify. + + To stop it, use `[p]autoplay` without a member, or + use a player command like `[p]stop` or `[p]play`. + """ + if ctx.interaction: + await ctx.defer(ephemeral=True) + + if member is None: + await self.config.guild(ctx.guild).tracked_member.set(None) + return + else: + await self.config.guild(ctx.guild).tracked_member.set(member.id) + msg = await self._prepare_autoplay(ctx.guild, ctx.author) + await ctx.send(embed=await self.pylav.construct_embed(description=msg, messageable=ctx)) + + async def _context_user_autoplay( + self, interaction: DISCORD_INTERACTION_TYPE, member: discord.Member + ): + await interaction.response.defer(ephemeral=True) + + if not interaction.guild: + await interaction.followup.send( + embed=await self.pylav.construct_embed( + description=_("This can only be used in a guild."), messageable=interaction + ) + ) + return + + await self.config.guild(interaction.guild).tracked_member.set(member.id) + + msg = await self._prepare_autoplay(interaction.guild, interaction.user) + await interaction.followup.send( + embed=await self.pylav.construct_embed( + description=msg.format(member=member.mention), + messageable=interaction, + ) + ) + + async def _prepare_autoplay(self, guild, author) -> str: + player: Player = self.bot.pylav.get_player(guild.id) + + if not player and author.voice: + await self.bot.pylav.connect_player(author, author.voice.channel) + player: Player = self.bot.pylav.get_player(guild.id) + elif not player: + return _( + "I am not in a voice channel. Please connect to a voice channel and then use this command." + ) + elif player and player.channel.id != author.voice.channel.id: + await player.move_to(author, author.voice.channel) + + if player.is_playing or player.paused: + await player.stop(author) + if player.queue.size: + player.queue.clear() + return _( + "I'll now play whatever {member} is listening to.\n" + "To stop autoplay, use a player command like `stop`" + ) + + @commands.Cog.listener() + async def on_presence_update( + self, member_before: discord.Member, member_after: discord.Member + ): + if await self._member_checks(member_after): + return + + player: Player = self.bot.lavalink.get_player(member_after.guild.id) + if player is None: + log.verbose("No player found.") + return + + current_activity = self._get_spotify_activity(member_after) + past_activity = self._get_spotify_activity(member_before) + if not current_activity: + # Member is no longer listening to Spotify. + autoplaying = await self.config.guild(member_after.guild).autoplaying() + if autoplaying and player.is_playing: + await player.set_pause(True, member_after) + await self.config.guild(member_after.guild).paused_track.set( + past_activity.track_id + ) + return + log.verbose(f"Presence update detected. {current_activity.track_url}") + if past_activity and past_activity.track_id == current_activity.track_id: + # Same track, no need to do anything. + return + if current_activity.track_id == await self.config.guild(member_after.guild).paused_track(): + # If the track is the same as when the activity stopped, it was probably paused, + # so we'll resume it. + log.verbose("Resuming track.") + await player.set_pause(False, member_after) + return + + log.verbose(f"Querying {current_activity.track_url}") + query = await Query.from_string(current_activity.track_url) + response = await self.bot.lavalink.search_query(query=query) + if response.loadType == "error": + log.verbose(f"No tracks found. Response: {response}") + return + + log.verbose(f"Query successful: {response.data}") + + if player.paused: + # To prevent overlapping tracks, we'll stop the player first to clear the paused track. + await player.stop(member_after) + if player.queue.size(): + log.verbose("Queue is not empty, clearing.") + player.queue.clear() + log.verbose(f"Playing {response.data.info.title}.") + await player.play( + query=query, + track=response.data, + requester=member_after, + ) + await self.config.guild(member_after.guild).paused_track.set(None) + await self.config.guild(member_after.guild).autoplaying.set(True) + + async def _member_checks(self, member: discord.Member) -> bool: + """Check if the member is valid for autoplay.""" + return ( + member.id != tracked_member + if (tracked_member := await self.config.guild(member.guild).tracked_member()) + else True + ) + + @staticmethod + def _get_spotify_activity(member: discord.Member) -> Optional[discord.Spotify]: + """Get the Spotify activity of a member.""" + activity = next( + ( + activity + for activity in member.activities + if activity.type == discord.ActivityType.listening + and hasattr(activity, "track_id") + and hasattr(activity, "track_url") + ), + None, + ) + return activity if activity and isinstance(activity, discord.Spotify) else None + + @commands.Cog.listener() + async def on_command(self, ctx: commands.Context): + """Stop autoplay when a player command is used.""" + log.verbose(f"Command {ctx.command.name}, {ctx.command.qualified_name} used.") + player_commands = [ + "play", + "skip", + "stop", + "playlist play", + "dc", + "prev", + "repeat", + "shuffle", + "pause", + "resume", + ] + if ctx.command.name in player_commands and ctx.guild: + await self._stop_autoplay(ctx.guild) + + @commands.Cog.listener() + async def on_interaction(self, interaction: discord.Interaction): + """Stop autoplay when a player interaction is used.""" + if interaction.type != discord.InteractionType.application_command: + return + log.verbose( + f"Interaction {interaction.type}, " + f"{interaction.command.name if interaction.command else None} used." + ) + player_commands = [ + "play", + "skip", + "stop", + "playlist play", + "dc", + "prev", + "repeat", + "shuffle", + "pause", + "resume", + ] + if interaction.command.name in player_commands and interaction.guild: + await self._stop_autoplay(interaction.guild) + + @commands.Cog.listener() + async def on_pylav_player_disconnected_event(self, event: PlayerDisconnectedEvent): + """Stop autoplay when the player is disconnected.""" + guild = event.player.channel.guild + log.verbose(f"Player in {guild} disconnected.") + if await self.config.guild(guild).autoplaying(): + await self.pylav.player_state_db_manager.delete_player(guild.id) + await self._stop_autoplay(guild) + + async def _stop_autoplay(self, guild: discord.Guild): + if not await self.config.guild(guild).autoplaying(): + return + log.verbose("Stopping autoplay.") + await self.config.guild(guild).autoplaying.set(False) + await self.config.guild(guild).tracked_member.set(None) + await self.config.guild(guild).paused_track.set(None) diff --git a/autoplay/info.json b/autoplay/info.json new file mode 100644 index 0000000..b0b5e35 --- /dev/null +++ b/autoplay/info.json @@ -0,0 +1,12 @@ +{ + "author": ["Karlo"], + "description": "Automatically play music that a guild member is listening to on Spotify.\nThis cog REQUIRES a fully set up PyLav and will not work without it.", + "short": "Automatically play music that a guild member is listening to on Spotify.", + "tags": ["music", "spotify"], + "min_python_version": [3, 11, 0], + "requirements": ["Py-Lav>=1.10.10"], + "min_bot_version": "3.5.0.dev306", + "max_bot_version": "3.5.99", + "type": "COG", + "end_user_data_statement": "This cog does not persistently store data or metadata about users." +} \ No newline at end of file diff --git a/autoplay/locales/hr-HR.po b/autoplay/locales/hr-HR.po new file mode 100644 index 0000000..8987a69 --- /dev/null +++ b/autoplay/locales/hr-HR.po @@ -0,0 +1,42 @@ +msgid "" +msgstr "" +"Project-Id-Version: karlo-cogs\n" +"POT-Creation-Date: 2025-01-06 18:00+0000\n" +"PO-Revision-Date: 2025-02-12 11:32\n" +"Last-Translator: \n" +"Language-Team: Croatian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Crowdin-Project: karlo-cogs\n" +"X-Crowdin-Project-ID: 523580\n" +"X-Crowdin-Language: hr\n" +"X-Crowdin-File: /autoplay/locales/messages.pot\n" +"X-Crowdin-File-ID: 37\n" +"Language: hr_HR\n" + +#: autoplay/autoplay.py:20 +#, docstring +msgid "Automatically play music that a guild member is listening to on Spotify." +msgstr "Automatski reproduciraj glazbu koju neki član guilde sluša na Spotifyu." + +#: autoplay/autoplay.py:28 +msgid "Start AutoPlay" +msgstr "Pokreni AutoPlay" + +#: autoplay/autoplay.py:64 +msgid "This can only be used in a guild." +msgstr "Ovo se može koristiti samo u guildu." + +#: autoplay/autoplay.py:86 +msgid "I am not in a voice channel. Please connect to a voice channel and then use this command." +msgstr "" + +#: autoplay/autoplay.py:96 +msgid "I'll now play whatever {member} is listening to.\n" +"To stop autoplay, use a player command like `stop`" +msgstr "Sada ću pustiti što god {member} sluša.\n" +"Za zaustavljanje automatske reprodukcije upotrijebite naredbu playera poput `stop`" + diff --git a/autoplay/locales/sr-SP.po b/autoplay/locales/sr-SP.po new file mode 100644 index 0000000..ca7230b --- /dev/null +++ b/autoplay/locales/sr-SP.po @@ -0,0 +1,41 @@ +msgid "" +msgstr "" +"Project-Id-Version: karlo-cogs\n" +"POT-Creation-Date: 2025-01-06 18:00+0000\n" +"PO-Revision-Date: 2025-01-06 18:00\n" +"Last-Translator: \n" +"Language-Team: Serbian (Cyrillic)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Crowdin-Project: karlo-cogs\n" +"X-Crowdin-Project-ID: 523580\n" +"X-Crowdin-Language: sr\n" +"X-Crowdin-File: /autoplay/locales/messages.pot\n" +"X-Crowdin-File-ID: 37\n" +"Language: sr_SP\n" + +#: autoplay/autoplay.py:20 +#, docstring +msgid "Automatically play music that a guild member is listening to on Spotify." +msgstr "" + +#: autoplay/autoplay.py:28 +msgid "Start AutoPlay" +msgstr "" + +#: autoplay/autoplay.py:64 +msgid "This can only be used in a guild." +msgstr "" + +#: autoplay/autoplay.py:86 +msgid "I am not in a voice channel. Please connect to a voice channel and then use this command." +msgstr "" + +#: autoplay/autoplay.py:96 +msgid "I'll now play whatever {member} is listening to.\n" +"To stop autoplay, use a player command like `stop`" +msgstr "" + diff --git a/info.json b/info.json index fdb4a1f..f81592b 100644 --- a/info.json +++ b/info.json @@ -1,6 +1,7 @@ { "author": [ - "Valerie (1050531216589332581)" + "Valerie", + "Rose Haven Network" ], "install_msg": "Thanks for adding Ruby Cogs! Join our Discord @ https://discord.gg/5CA8sewarU", "name": "Ruby Cogs", diff --git a/modrinthtracker/info.json b/modrinthtracker/info.json index ccc245f..b62bb3b 100644 --- a/modrinthtracker/info.json +++ b/modrinthtracker/info.json @@ -3,10 +3,10 @@ "Valerie" ], "install_msg": "Thank you for installing Modrinth Tracker", - "name": "Modrinth tracker", + "name": "Modrinth Tracker", "disabled": false, - "short": "Track Modrinth Projects", - "description": "Track Modrinth Projects..", + "short": "Modrinth Projects Tracker", + "description": "A cog for tracking Modrinth Projects via their project ID, using Modrinth's API", "tags": [ "modrinth" ], diff --git a/silentreplier/__init__.py b/silentreplier/__init__.py new file mode 100644 index 0000000..11f7bdb --- /dev/null +++ b/silentreplier/__init__.py @@ -0,0 +1,33 @@ +""" +A large majority of the code here is from Zephyrkul's cmdreplier +https://github.com/Zephyrkul/FluffyCogs/blob/master/cmdreplier/__init__.py +Thanks Zeph +""" + +import contextlib +from functools import partial + +from redbot.core import commands +from redbot.core.bot import Red + + +async def silent_send(__sender, /, *args, **kwargs): + ctx: commands.Context = __sender.__self__ + if not ctx.command_failed and "silent" not in kwargs: + kwargs["silent"] = True + return await __sender(*args, **kwargs) + + +async def before_hook(ctx: commands.Context): + if not ctx.message.edited_at and ctx.message.flags.silent: + with contextlib.suppress(AttributeError): + del ctx.send + ctx.send = partial(silent_send, ctx.send) + + +async def setup(bot: Red): + bot.before_invoke(before_hook) + + +async def teardown(bot: Red): + bot.remove_before_invoke_hook(before_hook) diff --git a/silentreplier/info.json b/silentreplier/info.json new file mode 100644 index 0000000..21b754f --- /dev/null +++ b/silentreplier/info.json @@ -0,0 +1,11 @@ +{ + "author": ["Karlo"], + "description": "Have the bot silently respond to commands which have been prefixed with @silent", + "short": "Have the bot silently respond to commands which have been prefixed with @silent", + "install_msg": "This cog has no commands. It will silently respond to commands which have been prefixed with @silent", + "tags": ["utility"], + "min_bot_version": "3.5.0.dev329", + "max_bot_version": "3.6.0.dev0", + "type": "COG", + "end_user_data_statement": "This cog does not persistently store any data or metadata about users." +} \ No newline at end of file diff --git a/silentreplier/locales/hr-HR.po b/silentreplier/locales/hr-HR.po new file mode 100644 index 0000000..34acc21 --- /dev/null +++ b/silentreplier/locales/hr-HR.po @@ -0,0 +1,19 @@ +msgid "" +msgstr "" +"Project-Id-Version: karlo-cogs\n" +"POT-Creation-Date: 2023-03-29 12:09+0000\n" +"PO-Revision-Date: 2025-02-12 11:32\n" +"Last-Translator: \n" +"Language-Team: Croatian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Crowdin-Project: karlo-cogs\n" +"X-Crowdin-Project-ID: 523580\n" +"X-Crowdin-Language: hr\n" +"X-Crowdin-File: /silentreplier/locales/messages.pot\n" +"X-Crowdin-File-ID: 43\n" +"Language: hr_HR\n" + diff --git a/silentreplier/locales/sr-SP.po b/silentreplier/locales/sr-SP.po new file mode 100644 index 0000000..f464f1f --- /dev/null +++ b/silentreplier/locales/sr-SP.po @@ -0,0 +1,19 @@ +msgid "" +msgstr "" +"Project-Id-Version: karlo-cogs\n" +"POT-Creation-Date: 2023-03-29 12:09+0000\n" +"PO-Revision-Date: 2023-03-29 12:10\n" +"Last-Translator: \n" +"Language-Team: Serbian (Cyrillic)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: redgettext 3.4.2\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Crowdin-Project: karlo-cogs\n" +"X-Crowdin-Project-ID: 523580\n" +"X-Crowdin-Language: sr\n" +"X-Crowdin-File: /silentreplier/locales/messages.pot\n" +"X-Crowdin-File-ID: 43\n" +"Language: sr_SP\n" +