diff --git a/README.md b/README.md index c615b29..6172b88 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # signal-bot A Signal group-chat bot that grabs videos from links people post (X/Twitter, -Instagram, YouTube, TikTok) and posts them back into the chat, plus a couple of -toys for messing with the last video. +Instagram, YouTube, TikTok, Reddit) and posts them back into the chat, plus a +couple of toys for messing with the last video. ## What it does @@ -16,7 +16,9 @@ toys for messing with the last video. to refresh Instagram auth without touching the server. Supported links: `x.com` / `twitter.com` (and `fxtwitter`/`vxtwitter`/`fixupx` -wrappers), `instagram.com/reel|p`, `youtube.com` / `youtu.be` / Shorts, and TikTok. +wrappers), `instagram.com/reel|p`, `youtube.com` / `youtu.be` / Shorts, TikTok, +and Reddit (`/comments/` permalinks, `/s/` app share links, `redd.it` and +`v.redd.it` shortlinks). Reddit posts with no video stay silent. ## How it's wired diff --git a/bot.py b/bot.py index d15945f..6671af9 100644 --- a/bot.py +++ b/bot.py @@ -30,7 +30,19 @@ TWITTER_URL_PATTERN = r"https?://(?:www\.)?(?:twitter\.com|x\.com|fxtwitter\.com INSTAGRAM_URL_PATTERN = r"https?://(?:www\.)?instagram\.com/(?:share/)?(?:reels?|p|tv)/[\w-]+" YOUTUBE_URL_PATTERN = r"https?://(?:www\.)?(?:youtube\.com/(?:watch\?v=|shorts/)|youtu\.be/)[\w-]+" 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})" +# Reddit's canonical form is /r//comments//, but the mobile share +# sheet emits /r//s/ and older shares use redd.it/; both are plain +# redirects to the canonical URL, which yt-dlp follows. v.redd.it/ is the bare +# media host. Any subdomain (www/old/new/np/m) is valid. yt-dlp's own extractor +# only matches the /comments/ form, so the redirecting shapes must be listed here +# explicitly or the link is silently dropped (regex never matches -> no download, +# no error reply). Most Reddit links carry no video at all; the extractor reports +# "No media found" for those, which _NO_MEDIA_ERROR_PATTERNS already keeps quiet. +REDDIT_URL_PATTERN = ( + r"https?://(?:(?:\w+\.)?reddit\.com/(?:(?:r|user|u)/[^/\s]+/)?(?:comments|s)/[\w-]+" + r"|(?:v\.)?redd\.it/[\w-]+)" +) +VIDEO_URL_PATTERN = rf"(?:{TWITTER_URL_PATTERN}|{INSTAGRAM_URL_PATTERN}|{YOUTUBE_URL_PATTERN}|{TIKTOK_URL_PATTERN}|{REDDIT_URL_PATTERN})" MAX_FILE_SIZE = 100 * 1024 * 1024 # 100 MB CLIP_DURATION = 60 # default seconds to grab around a shared ?t= timestamp MAX_CLIP_DURATION = 600 # ceiling for a user-supplied /clip override @@ -543,8 +555,15 @@ def _run_ytdlp(url: str, outpath: str, tmpdir: str, # Prefer the largest mp4 that already fits under 95 MB, so we avoid # re-encoding when a smaller variant exists (e.g. a 4K rendition # >100 MB alongside a 1080p ~50 MB). + # The bv*+ba tail is required for hosts that never publish a muxed + # rendition — Reddit serves video and audio as separate DASH/HLS + # streams, so every `best...` term above fails and yt-dlp aborts with + # "Requested format is not available" unless we merge them ourselves. + # The bare bv* last resort covers silent videos (no audio to merge). cmd += [ - "-f", "best[ext=mp4][filesize<95M]/best[ext=mp4][filesize_approx<95M]/best[ext=mp4]/best", + "-f", + "best[ext=mp4][filesize<95M]/best[ext=mp4][filesize_approx<95M]/best[ext=mp4]/best" + "/bv*[filesize<95M]+ba/bv*[filesize_approx<95M]+ba/bv*+ba/bv*", ] cmd += [ "--merge-output-format", "mp4", @@ -704,7 +723,8 @@ def _force_download_and_publish(url: str, token: str) -> tuple[str | None, int | cmd = [ *_NICE, YTDLP, "--no-playlist", "--no-warnings", "--js-runtimes", JS_RUNTIME, - "-f", "best[ext=mp4]/best", "--merge-output-format", "mp4", + # bv*+ba tail: split-stream hosts (Reddit) have no muxed rendition. + "-f", "best[ext=mp4]/best/bv*+ba/bv*", "--merge-output-format", "mp4", "--max-filesize", FORCE_MAX_FILESIZE, *_cookie_args(url), "-o", os.path.join(tokdir, "video.%(ext)s"), @@ -1109,7 +1129,7 @@ class ForceCommand(Command): 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. +Share a video link (X/Twitter, Instagram, YouTube, TikTok, Reddit) 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.