From f7f4372cf7bc22a7b9ec03fa19f3723542d5a39e Mon Sep 17 00:00:00 2001 From: neoarz Date: Sun, 5 Oct 2025 09:07:10 -0400 Subject: [PATCH] feat(img2gif): new command :) --- README.md | 2 +- cogs/media/__init__.py | 34 ++++++++-- cogs/media/img2gif.py | 150 +++++++++++++++++++++++++++++++++++++++++ requirements.txt | 4 +- 4 files changed, 181 insertions(+), 9 deletions(-) create mode 100644 cogs/media/img2gif.py diff --git a/README.md b/README.md index 41776b3..6bbc282 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ | melonx | `melonx`, `transfer`, `mods`, `gamecrash`, `requirements`, `error`, `26` | | miscellaneous | `keanu`, `labubu`, `piracy`, `tryitandsee`, `rickroll`, `dontasktoask`, `support`| | utilities | `translate`, `codepreview`, `dictionary` | -| media | `download`, `mcquote` | +| media | `download`, `mcquote`, `img2gif` | ## Download diff --git a/cogs/media/__init__.py b/cogs/media/__init__.py index 15d8cf1..11721aa 100644 --- a/cogs/media/__init__.py +++ b/cogs/media/__init__.py @@ -1,9 +1,11 @@ import discord from discord.ext import commands from discord.ext.commands import Context +from typing import Optional from .download import download_command from .mcquote import mcquote_command +from .img2gif import img2gif_command def _require_group_prefix(context: Context) -> bool: @@ -29,15 +31,20 @@ class Media(commands.GroupCog, name="media"): color=0x7289DA ) embed.set_author(name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp") - embed.add_field(name="Available", value="download, mcquote", inline=False) + embed.add_field(name="Available", value="download, mcquote, img2gif", inline=False) await context.send(embed=embed) - async def _invoke_hybrid(self, context: Context, name: str): - command = self.bot.get_command(name) - if command is not None: - await context.invoke(command) - else: - await context.send(f"Unknown media command: {name}") + async def _invoke_hybrid(self, context: Context, name: str, *args, **kwargs): + if name == "download": + await self.download(context, url=kwargs.get('url', '')) + return + if name == "mcquote": + await self.mcquote(context, text=kwargs.get('text', '')) + return + if name == "img2gif": + await self.img2gif(context, attachment=kwargs.get('attachment')) + return + await context.send(f"Unknown media command: {name}") @media_group.command(name="download") async def media_group_download(self, context: Context, *, url: str): @@ -47,6 +54,10 @@ class Media(commands.GroupCog, name="media"): async def media_group_mcquote(self, context: Context, *, text: str): await self._invoke_hybrid(context, "mcquote", text=text) + @media_group.command(name="img2gif") + async def media_group_img2gif(self, context: Context, attachment: Optional[discord.Attachment] = None): + await self._invoke_hybrid(context, "img2gif", attachment=attachment) + @commands.check(_require_group_prefix) @commands.hybrid_command( name="download", @@ -63,9 +74,18 @@ class Media(commands.GroupCog, name="media"): async def mcquote(self, context, *, text: str): return await mcquote_command()(self, context, text=text) + @commands.check(_require_group_prefix) + @commands.hybrid_command( + name="img2gif", + description="Convert an uploaded image to a GIF.", + ) + async def img2gif(self, context, attachment: Optional[discord.Attachment] = None): + return await img2gif_command()(self, context, attachment=attachment) + async def setup(bot) -> None: cog = Media(bot) await bot.add_cog(cog) bot.logger.info("Loaded extension 'media.download'") bot.logger.info("Loaded extension 'media.mcquote'") + bot.logger.info("Loaded extension 'media.img2gif'") diff --git a/cogs/media/img2gif.py b/cogs/media/img2gif.py new file mode 100644 index 0000000..df5e220 --- /dev/null +++ b/cogs/media/img2gif.py @@ -0,0 +1,150 @@ +import os +import tempfile +import discord +from discord.ext import commands +from PIL import Image +import subprocess +import shutil +from typing import Optional +try: + import pillow_heif + pillow_heif.register_heif_opener() +except Exception: + pass + + +def img2gif_command(): + @commands.hybrid_command( + name="img2gif", + description="Convert an uploaded image to a GIF.", + ) + @commands.cooldown(1, 15, commands.BucketType.user) + async def img2gif(self, context, attachment: Optional[discord.Attachment] = None): + resolved_attachment = attachment + if resolved_attachment is None: + if context.message and context.message.reference and context.message.reference.resolved: + ref_msg = context.message.reference.resolved + if isinstance(ref_msg, discord.Message) and ref_msg.attachments: + resolved_attachment = ref_msg.attachments[0] + if resolved_attachment is None and context.message and context.message.attachments: + resolved_attachment = context.message.attachments[0] + if resolved_attachment is None or not resolved_attachment.filename.lower().endswith((".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tiff", ".heic", ".heif")): + embed = discord.Embed( + title="Error", + description="Provide or reply to an image (png/jpg/jpeg/webp/bmp/tiff/heic/heif).", + color=0xE02B2B, + ) + embed.set_author(name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp") + interaction = getattr(context, "interaction", None) + if interaction is not None: + if not interaction.response.is_done(): + await interaction.response.send_message(embed=embed, ephemeral=True) + else: + await interaction.followup.send(embed=embed, ephemeral=True) + else: + await context.send(embed=embed, ephemeral=True) + return + + processing_embed = discord.Embed( + title="Image to GIF (Processing)", + description=" Converting image...", + color=0x7289DA, + ) + processing_embed.set_author(name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp") + + interaction = getattr(context, "interaction", None) + if interaction is not None: + if not interaction.response.is_done(): + await interaction.response.send_message(embed=processing_embed, ephemeral=True) + else: + await interaction.followup.send(embed=processing_embed, ephemeral=True) + else: + processing_msg = await context.send(embed=processing_embed) + + tmp_dir = tempfile.mkdtemp() + src_path = os.path.join(tmp_dir, resolved_attachment.filename) + out_path = os.path.join(tmp_dir, os.path.splitext(resolved_attachment.filename)[0] + ".gif") + + try: + await resolved_attachment.save(src_path) + src_for_pillow = src_path + try: + with Image.open(src_for_pillow) as img: + if img.mode not in ("RGB", "RGBA"): + img = img.convert("RGBA") + duration_ms = 100 + loop = 0 + img.save(out_path, format="GIF", save_all=True, optimize=True, duration=duration_ms, loop=loop) + except Exception: + if resolved_attachment.filename.lower().endswith((".heic", ".heif")) and shutil.which("ffmpeg"): + png_path = os.path.join(tmp_dir, os.path.splitext(resolved_attachment.filename)[0] + ".png") + try: + subprocess.run([ + "ffmpeg", "-y", "-i", src_path, png_path + ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + with Image.open(png_path) as img: + if img.mode not in ("RGB", "RGBA"): + img = img.convert("RGBA") + duration_ms = 100 + loop = 0 + img.save(out_path, format="GIF", save_all=True, optimize=True, duration=duration_ms, loop=loop) + except Exception as conv_err: + raise conv_err + else: + raise + + original_ext = os.path.splitext(resolved_attachment.filename)[1].lstrip('.').upper() or "IMAGE" + embed = discord.Embed( + title="Image to GIF", + description=f"Converted {original_ext} to GIF.", + color=0x7289DA, + ) + embed.set_author(name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp") + embed.set_footer(text=f"Requested by {context.author.name}", icon_url=context.author.display_avatar.url) + + with open(out_path, "rb") as f: + file = discord.File(f, filename=os.path.basename(out_path)) + if interaction is not None: + await context.channel.send(embed=embed) + await context.channel.send(file=file) + try: + await interaction.delete_original_response() + except: + pass + else: + await processing_msg.delete() + await context.channel.send(embed=embed) + await context.channel.send(file=file) + except Exception as e: + embed = discord.Embed( + title="Error", + description=f"Failed to convert image: {e}", + color=0xE02B2B, + ) + embed.set_author(name="Media", icon_url="https://yes.nighty.works/raw/y5SEZ9.webp") + if interaction is not None: + try: + await interaction.delete_original_response() + except: + pass + await interaction.followup.send(embed=embed, ephemeral=True) + else: + try: + await processing_msg.delete() + except: + pass + await context.send(embed=embed, ephemeral=True) + finally: + try: + for f in os.listdir(tmp_dir): + try: + os.remove(os.path.join(tmp_dir, f)) + except: + pass + os.rmdir(tmp_dir) + except: + pass + + return img2gif + + diff --git a/requirements.txt b/requirements.txt index 7e731e4..a9340be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,6 @@ aiohttp aiosqlite discord.py==2.6.3 python-dotenv -yt-dlp \ No newline at end of file +yt-dlp +Pillow +pillow-heif \ No newline at end of file