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:
@@ -1,8 +1,8 @@
|
|||||||
# signal-bot
|
# signal-bot
|
||||||
|
|
||||||
A Signal group-chat bot that grabs videos from links people post (X/Twitter,
|
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
|
Instagram, YouTube, TikTok, Reddit) and posts them back into the chat, plus a
|
||||||
toys for messing with the last video.
|
couple of toys for messing with the last video.
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
@@ -16,7 +16,9 @@ toys for messing with the last video.
|
|||||||
to refresh Instagram auth without touching the server.
|
to refresh Instagram auth without touching the server.
|
||||||
|
|
||||||
Supported links: `x.com` / `twitter.com` (and `fxtwitter`/`vxtwitter`/`fixupx`
|
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
|
## How it's wired
|
||||||
|
|
||||||
|
|||||||
@@ -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-]+"
|
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-]+"
|
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+)"
|
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
|
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100 MB
|
||||||
CLIP_DURATION = 60 # default 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
|
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
|
# 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
|
# re-encoding when a smaller variant exists (e.g. a 4K rendition
|
||||||
# >100 MB alongside a 1080p ~50 MB).
|
# >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 += [
|
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 += [
|
cmd += [
|
||||||
"--merge-output-format", "mp4",
|
"--merge-output-format", "mp4",
|
||||||
@@ -704,7 +723,8 @@ def _force_download_and_publish(url: str, token: str) -> tuple[str | None, int |
|
|||||||
cmd = [
|
cmd = [
|
||||||
*_NICE,
|
*_NICE,
|
||||||
YTDLP, "--no-playlist", "--no-warnings", "--js-runtimes", JS_RUNTIME,
|
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,
|
"--max-filesize", FORCE_MAX_FILESIZE,
|
||||||
*_cookie_args(url),
|
*_cookie_args(url),
|
||||||
"-o", os.path.join(tokdir, "video.%(ext)s"),
|
"-o", os.path.join(tokdir, "video.%(ext)s"),
|
||||||
@@ -1109,7 +1129,7 @@ class ForceCommand(Command):
|
|||||||
|
|
||||||
HELP_TEXT = f"""🎬 Video bot — what I can do
|
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
|
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.
|
A YouTube link with a timestamp → I post a {CLIP_DURATION}s clip starting at that moment.
|
||||||
|
|||||||
Reference in New Issue
Block a user