Add Reddit video support

Reddit links now download and post like the other platforms.

Two changes were needed, not just the URL pattern:

- Regex: yt-dlp's Reddit extractor only matches /comments/ permalinks, so
  the app's /r/<sub>/s/<code> share links and redd.it / v.redd.it
  shortlinks are listed explicitly — they redirect to the canonical URL,
  which yt-dlp follows. Without them the link is silently dropped.

- Format selector: Reddit publishes video and audio as separate DASH/HLS
  streams with no muxed rendition, so every `best...` term failed and
  yt-dlp aborted with "Requested format is not available". Added a
  bv*+ba merge tail (and a bare bv* for silent videos) to the normal and
  /force selectors. Muxed hosts still resolve at the earlier terms, so
  YouTube/IG/TikTok/X selection is unchanged.

Posts with no video report "No media found", already in the silent list,
so ordinary Reddit links shared in chat draw no reply.
This commit is contained in:
2026-08-14 05:00:16 -04:00
parent 94e8d7bf84
commit 3e3aba4157
2 changed files with 29 additions and 7 deletions
+5 -3
View File
@@ -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
+24 -4
View File
@@ -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/<sub>/comments/<id>/<slug>, but the mobile share
# sheet emits /r/<sub>/s/<code> and older shares use redd.it/<id>; both are plain
# redirects to the canonical URL, which yt-dlp follows. v.redd.it/<id> 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.