From 26a5ecb2d074110fd354e3c83d788135bff4685c Mon Sep 17 00:00:00 2001 From: James Price Date: Mon, 15 Jun 2026 16:35:28 -0400 Subject: [PATCH] Add /clip duration override and /help command /clip in a link's message overrides the default 60s clip window (capped at 600s); with a ?t= it sets the window length, without one it clips from the start. /help lists every command with examples. Co-Authored-By: Claude Opus 4.8 (1M context) --- bot.py | 74 +++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/bot.py b/bot.py index b219511..51e5bdb 100644 --- a/bot.py +++ b/bot.py @@ -17,7 +17,8 @@ YOUTUBE_URL_PATTERN = r"https?://(?:www\.)?(?:youtube\.com/(?:watch\?v=|shorts/) TIKTOK_URL_PATTERN = r"https?://(?:(?:www|m)\.tiktok\.com/(?:@[\w.-]+/video/\d+|t/\w+|v/\d+)|(?:vm|vt)\.tiktok\.com/\w+)" VIDEO_URL_PATTERN = rf"(?:{TWITTER_URL_PATTERN}|{INSTAGRAM_URL_PATTERN}|{YOUTUBE_URL_PATTERN}|{TIKTOK_URL_PATTERN})" MAX_FILE_SIZE = 100 * 1024 * 1024 # 100 MB -CLIP_DURATION = 60 # seconds to grab around a shared ?t= timestamp +CLIP_DURATION = 60 # default seconds to grab around a shared ?t= timestamp +MAX_CLIP_DURATION = 600 # ceiling for a user-supplied /clip override YTDLP = os.path.join(os.path.dirname(os.path.abspath(__file__)), "venv", "bin", "yt-dlp") COOKIES = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cookies.txt") ADMIN_NUMBERS = {n.strip() for n in os.environ.get("BOT_ADMINS", "").split(",") if n.strip()} @@ -160,6 +161,17 @@ class VideoCommand(Command): if not matches: return + # An optional "/clip " anywhere in the message overrides the + # default window length for any clip produced from this message. + clip_len = CLIP_DURATION + mclip = re.search(r"/clip\s+(\S+)", c.message.text, re.IGNORECASE) + if mclip: + secs = _parse_timestamp(mclip.group(1)) + if secs is None or secs < 1: + await c.reply("`/clip` needs a length in seconds, e.g. `/clip 30`.") + return + clip_len = min(secs, MAX_CLIP_DURATION) + is_edit = c.message.type == MessageType.EDIT_MESSAGE for m in matches: @@ -167,13 +179,17 @@ class VideoCommand(Command): # The URL pattern stops at the video id, so any ?t=/&t= timestamp # lives in the characters that follow. Grab the whole whitespace- - # delimited token to recover it. Only YouTube uses these offsets. - token = re.match(r"\S+", c.message.text[m.start():]).group(0) - clip_start = ( - _extract_timestamp(token) - if re.match(YOUTUBE_URL_PATTERN, url) - else None - ) + # delimited token to recover it. Timestamps/clips apply to YouTube. + clip = None + if re.match(YOUTUBE_URL_PATTERN, url): + token = re.match(r"\S+", c.message.text[m.start():]).group(0) + start = _extract_timestamp(token) + if mclip: + # Explicit /clip clips even without a timestamp (from 0). + start = start or 0 + clip = (start, start + clip_len) + elif start is not None: + clip = (start, start + clip_len) # Normalize fxtwitter/vxtwitter wrappers to x.com url = re.sub( @@ -187,13 +203,11 @@ class VideoCommand(Command): continue _mark_url_handled(c.message.group, url) - await self._download_and_send(c, url, clip_start) + await self._download_and_send(c, url, clip) - async def _download_and_send(self, c: Context, url: str, clip_start: int | None = None) -> None: - clip = None - if clip_start is not None: - clip = (clip_start, clip_start + CLIP_DURATION) - log.info("Clipping %s to %d-%ds around shared timestamp", url, clip[0], clip[1]) + async def _download_and_send(self, c: Context, url: str, clip: tuple[int, int] | None = None) -> None: + if clip is not None: + log.info("Clipping %s to window %d-%ds", url, clip[0], clip[1]) with tempfile.TemporaryDirectory() as tmpdir: outpath = os.path.join(tmpdir, "video.mp4") ok, err = self._run_ytdlp(url, outpath, tmpdir, clip) @@ -530,6 +544,37 @@ class ReverseCommand(Command): await c.send("", base64_attachments=[b64_reversed]) +HELP_TEXT = f"""🎬 Video bot — what I can do + +Share a video link (X/Twitter, Instagram, YouTube, TikTok) and I'll post the video back to the group. + e.g. https://x.com/user/status/123456789 + +A YouTube link with a timestamp → I post a {CLIP_DURATION}s clip starting at that moment. + e.g. https://youtu.be/dQw4w9WgXcQ?t=90 + +/clip — set the clip length for a link in the same message (max {MAX_CLIP_DURATION}s). With a ?t= it sets the window; without one it clips from the start. + e.g. /clip 30 https://youtu.be/dQw4w9WgXcQ?t=90 + e.g. /clip 15 https://youtu.be/dQw4w9WgXcQ + +/speed [factor] — speed up the last video (default 2x). + e.g. /speed /speed 4 /speed 0.5 + +/rev — reverse the last video. + +/help — show this message. + +(In a DM, admins can run /cookies to refresh Instagram login cookies.)""" + + +class HelpCommand(Command): + async def handle(self, c: Context) -> None: + if not c.message.is_group(): + return + if (c.message.text or "").strip().lower() not in ("/help", "/commands"): + return + await c.reply(HELP_TEXT) + + def _sender_number(msg) -> str | None: for attr in ("source", "source_number", "sourceNumber"): v = getattr(msg, attr, None) @@ -609,6 +654,7 @@ def main(): bot.register(VideoCommand(), contacts=False, groups=True) bot.register(ReverseCommand(), contacts=False, groups=True) bot.register(SpeedCommand(), contacts=False, groups=True) + bot.register(HelpCommand(), contacts=False, groups=True) bot.register(CookiesCommand(), contacts=True, groups=False) log.info("Starting Signal video bot...") bot.start()