holy shit esef still hasnt forgotten about this bot

This commit is contained in:
esef committed 2026-10-03 17:59:11 +03:00
1 parent 4fa7d5ca50
commit 2691806453
6 files changed
+322 -101

No files matched your search

+65 -79
View File
@@ -1,9 +1,9 @@
"""FFmpeg media conversion cog.
Commands operate on a message attachment, or the attachment of a replied-to
message: !togif, !caption <text>, !reverse, !speed <x>, !tomp4, !tomp3,
!toopus. All ffmpeg invocations use argument lists (no shell) and captions
are passed via a textfile to avoid drawtext injection.
Slash commands that take an uploaded file: /togif, /caption <text>,
/reverse, /speed <x>, /tomp4, /tomp3, /toopus. All ffmpeg invocations use
argument lists (no shell) and captions are passed via a textfile to avoid
drawtext injection.
"""
import asyncio
import os
@@ -11,6 +11,7 @@ import shutil
import tempfile
import discord
from discord import app_commands
from discord.ext import commands
MAX_UPLOAD = 50 * 1024 * 1024 # 50 MB input cap
@@ -29,36 +30,33 @@ class FfmpegThings(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
async def _get_attachment(self, message: discord.Message):
if message.attachments:
return message.attachments[0]
ref = message.reference
if ref is not None:
resolved = ref.resolved
if isinstance(resolved, discord.Message) and resolved.attachments:
return resolved.attachments[0]
return None
async def _transcode(self, interaction: discord.Interaction, file: discord.Attachment,
args: list[str], ext: str, caption: str | None = None):
if file.size > MAX_UPLOAD:
await interaction.response.send_message(
f"File too big ({file.size // (1024*1024)} MB). Max {MAX_UPLOAD // (1024*1024)} MB.",
ephemeral=True)
return
async def _transcode(self, ctx: commands.Context, args: list[str], ext: str):
attachment = await self._get_attachment(ctx.message)
if attachment is None:
await ctx.reply("Attach a file, or reply to a message that has one.")
return
if attachment.size > MAX_UPLOAD:
await ctx.reply(f"File too big ({attachment.size // (1024*1024)} MB). Max {MAX_UPLOAD // (1024*1024)} MB.")
return
# ffmpeg easily takes longer than the 3s interaction window
await interaction.response.defer(thinking=True)
tmpdir = tempfile.mkdtemp(prefix="ffmpeg_")
inp = os.path.join(tmpdir, "in" + os.path.splitext(attachment.filename)[1])
inp = os.path.join(tmpdir, "in" + os.path.splitext(file.filename)[1])
out = os.path.join(tmpdir, "out" + ext)
try:
await attachment.save(inp)
await file.save(inp)
if caption is not None:
caption_file = os.path.join(tmpdir, "caption.txt")
with open(caption_file, "w", encoding="utf-8") as f:
f.write(caption)
args = [a.replace("{caption_file}", caption_file) for a in args]
await self._run_ffmpeg(["-i", inp, *args, out], tmpdir)
if not os.path.exists(out) or os.path.getsize(out) == 0:
raise RuntimeError("ffmpeg produced no output")
await ctx.reply(file=discord.File(out))
await interaction.followup.send(file=discord.File(out))
except Exception as exc:
await ctx.reply(f"ffmpeg failed: {exc}"[:2000])
await interaction.followup.send(f"ffmpeg failed: {exc}"[:2000])
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
@@ -77,105 +75,93 @@ class FfmpegThings(commands.Cog):
if proc.returncode != 0:
raise RuntimeError(stderr.decode(errors="replace")[:500])
@commands.command()
async def togif(self, ctx: commands.Context):
@app_commands.command()
@app_commands.describe(file="Video to convert")
async def togif(self, interaction: discord.Interaction, file: discord.Attachment):
"""Convert a video to a GIF."""
await self._transcode(
ctx,
interaction, file,
["-vf", "fps=10,scale='min(480,iw)':-2:flags=lanczos"],
".gif",
)
@commands.command()
async def tomp4(self, ctx: commands.Context):
@app_commands.command()
@app_commands.describe(file="Video or audio to convert")
async def tomp4(self, interaction: discord.Interaction, file: discord.Attachment):
"""Convert media to MP4 (h264/aac)."""
await self._transcode(
ctx,
interaction, file,
["-c:v", "libx264", "-preset", "fast", "-crf", "23",
"-c:a", "aac", "-b:a", "128k", "-movflags", "+faststart"],
".mp4",
)
@commands.command()
async def tomp3(self, ctx: commands.Context):
@app_commands.command()
@app_commands.describe(file="Video or audio to extract from")
async def tomp3(self, interaction: discord.Interaction, file: discord.Attachment):
"""Extract audio to MP3."""
await self._transcode(
ctx,
interaction, file,
["-vn", "-c:a", "libmp3lame", "-q:a", "2"],
".mp3",
)
@commands.command()
async def toopus(self, ctx: commands.Context):
@app_commands.command()
@app_commands.describe(file="Video or audio to extract from")
async def toopus(self, interaction: discord.Interaction, file: discord.Attachment):
"""Extract audio to Opus."""
await self._transcode(
ctx,
interaction, file,
["-vn", "-c:a", "libopus", "-b:a", "128k"],
".opus",
)
@commands.command()
async def reverse(self, ctx: commands.Context):
@app_commands.command()
@app_commands.describe(file="Video to reverse")
async def reverse(self, interaction: discord.Interaction, file: discord.Attachment):
"""Reverse a video/audio clip."""
await self._transcode(
ctx,
interaction, file,
["-vf", "reverse", "-af", "areverse"],
".mp4",
)
@commands.command()
async def speed(self, ctx: commands.Context, factor: float = 1.0):
@app_commands.command()
@app_commands.describe(file="Video to speed up or slow down",
factor="Playback speed, 0.5 to 2.0")
async def speed(self, interaction: discord.Interaction, file: discord.Attachment,
factor: app_commands.Range[float, 0.5, 2.0] = 1.0):
"""Change playback speed (0.5 to 2.0)."""
if not 0.5 <= factor <= 2.0:
await ctx.reply("Speed must be between 0.5 and 2.0.")
return
await self._transcode(
ctx,
interaction, file,
["-filter_complex",
f"[0:v]setpts=PTS/{factor}[v];[0:a]atempo={factor}[a]",
"-map", "[v]", "-map", "[a]"],
".mp4",
)
@commands.command()
async def caption(self, ctx: commands.Context, *, text: str):
@app_commands.command()
@app_commands.describe(file="Video to caption", text="Text to put on top")
async def caption(self, interaction: discord.Interaction, file: discord.Attachment,
text: str):
"""Add a caption to the top of a video."""
text = text.strip()
if not text:
await ctx.reply("Provide a caption, e.g. `!caption hello world`.")
return
attachment = await self._get_attachment(ctx.message)
if attachment is None:
await ctx.reply("Attach a file, or reply to a message that has one.")
return
if attachment.size > MAX_UPLOAD:
await ctx.reply(f"File too big ({attachment.size // (1024*1024)} MB).")
await interaction.response.send_message("Provide a caption.", ephemeral=True)
return
font = next((f for f in FONT_CANDIDATES if os.path.exists(f)), None)
tmpdir = tempfile.mkdtemp(prefix="ffmpeg_cap_")
inp = os.path.join(tmpdir, "in" + os.path.splitext(attachment.filename)[1])
caption_file = os.path.join(tmpdir, "caption.txt")
out = os.path.join(tmpdir, "out.mp4")
try:
await attachment.save(inp)
with open(caption_file, "w", encoding="utf-8") as f:
f.write(text)
font_arg = f":fontfile={font}" if font else ""
vf = (
f"drawtext=textfile={caption_file}:fontsize=40:fontcolor=white"
f":box=1:[email protected]:boxborderw=10:x=(w-text_w)/2:y=20{font_arg}"
)
await self._run_ffmpeg(
["-i", inp, "-vf", vf, "-c:a", "copy", out],
tmpdir,
)
await ctx.reply(file=discord.File(out))
except Exception as exc:
await ctx.reply(f"ffmpeg failed: {exc}"[:2000])
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
font_arg = f":fontfile={font}" if font else ""
vf = (
"drawtext=textfile={caption_file}:fontsize=40:fontcolor=white"
f":box=1:[email protected]:boxborderw=10:x=(w-text_w)/2:y=20{font_arg}"
)
await self._transcode(
interaction, file,
["-vf", vf, "-c:a", "copy"],
".mp4",
caption=text,
)
async def setup(bot: commands.Bot) -> None: