Fetch YouTube at up to 1080p instead of 360p

YouTube's only muxed mp4 is format 18 (360p), so the muxed-first selector
was posting 360p even when 1080p was available. Split streams are now
preferred for YouTube and merged locally; every other host keeps the
muxed-first chain unchanged (verified identical selection for Instagram,
X and Reddit).

Three things the obvious version got wrong:

- A height<=1080 cap excludes portrait video: a 1080p Short or reel is
  1080x1920. Resolution is capped with -S res:1080 instead, which keys
  off the smaller dimension.

- h264 is a hard first preference, not an -S sort key. At equal
  resolution YouTube's VP9/AV1 renditions are smaller and win on size,
  but neither reliably plays in an mp4 container on older Signal
  clients. Dropping a resolution rung to stay on h264 is the better
  trade, and is usually smaller too: a 10-min video takes 480p h264
  (37 MB) over 720p AV1 (78 MB).

- The audio cap matters. YouTube's 388k track is 29 MB by itself on a
  10-min video, enough to push an otherwise-fine merge past the 100 MB
  Signal limit; abr<=160 takes the 129k track instead.

The 88 MB video cap makes yt-dlp step down automatically on long videos,
so a 3-min video lands at 1080p (80 MB) and a 10-min one at 480p.

Clips get the same treatment — they're YouTube-only, and 60s at 1080p is
only ~29 MB. /force is deliberately left muxed-first: it exists for the
biggest, slowest videos, where 1080p would risk the rsync timeout.

Measured, old -> new: 3-min 360p/11 MB -> 1080p/80 MB; 10-min 360p/27 MB
-> 480p/37 MB; portrait Short 360x640/6 MB -> 1080x1920/23 MB. Verified
end to end: real downloads decode as h264+aac, clip lands at exactly
60.000s.
This commit is contained in:
2026-08-14 14:30:11 -04:00
parent 3e3aba4157
commit 1523bdd48d
2 changed files with 66 additions and 19 deletions
+4 -1
View File
@@ -8,7 +8,10 @@ couple of toys for messing with the last video.
- **Auto-download** — watches every group message for a supported link and
replies with the video. Powered by [yt-dlp](https://github.com/yt-dlp/yt-dlp).
Videos over 100 MB are auto re-encoded with ffmpeg to fit.
Videos over 100 MB are auto re-encoded with ffmpeg to fit. YouTube is fetched
as h264 up to 1080p, stepping down a resolution rung on longer videos so the
merge lands under the Signal size limit without a re-encode; other hosts take
their muxed rendition as-is.
- **`/speed [n]`** — re-posts the last video sped up `n`× (default 2×), pitch-shifted
like tape. e.g. `/speed 4`, `/speed 0.5`.
- **`/rev`** — re-posts the last video reversed (audio too).
+62 -18
View File
@@ -68,15 +68,68 @@ JS_RUNTIME = f"node:{_NODE22}" if os.path.exists(_NODE22) else "node"
# Instagram/TikTok/X, so we withhold them per-URL rather than dropping the file.
_YOUTUBE_HOST_RE = re.compile(r"(?:^|\.)(?:youtube\.com|youtu\.be|youtube-nocookie\.com)$", re.I)
def _is_youtube(url: str) -> bool:
return bool(_YOUTUBE_HOST_RE.search((urlsplit(url).hostname or "").lower()))
def _cookie_args(url: str) -> list[str]:
"""yt-dlp --cookies args for this URL, withheld for YouTube (see note above)."""
if not os.path.exists(COOKIES):
return []
host = (urlsplit(url).hostname or "").lower()
if _YOUTUBE_HOST_RE.search(host):
if _is_youtube(url):
return []
return ["--cookies", COOKIES]
# --- format selection ---
# Muxed-first. Right for every host that publishes a complete rendition (X,
# Instagram, TikTok): their muxed stream is the best quality on offer, and taking
# it avoids a merge. The bv*+ba tail catches split-only hosts (Reddit), and the
# bare bv* catches silent videos.
FORMAT_MUXED_FIRST = (
"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*"
)
# YouTube is the one host whose muxed renditions stop at 360p (format 18 is the
# only mp4 carrying both streams), so the chain above posts 360p even when 1080p
# is sitting right there. Take split streams first and merge them instead.
#
# avc1 (h264) is taken as a hard first preference rather than left to the -S sort
# below: at equal resolution YouTube's VP9/AV1 renditions are smaller and would
# win on size, but they are the two codecs that don't reliably play in an mp4
# container on older Signal clients. Falling back a resolution rung to stay on
# h264 is the better trade for a chat bot — a 10-min video takes 480p h264
# (~27 MB) over 720p AV1 (~74 MB), which is both more compatible and smaller.
#
# The 88 MB video cap keeps the merge under MAX_FILE_SIZE without paying for a
# re-encode, and makes yt-dlp step down a rung on long videos automatically: it
# picks the best rendition that fits, so a 3-min video lands at 1080p h264
# (~81 MB) while a 10-min one (1080p ≈ 245 MB) drops to 480p. Anything past ~1 hr
# never reaches here — _process_one short-circuits it to a /force offer.
#
# abr<=160 rules out YouTube's 388k audio track, which is 29 MB by itself on a
# 10-min video — enough to push an otherwise-fine merge over the limit. The bare
# +ba term after it covers videos where yt-dlp reports no bitrate.
FORMAT_YOUTUBE = (
"bv*[vcodec^=avc1][filesize<88M]+ba[abr<=160]"
"/bv*[vcodec^=avc1][filesize_approx<88M]+ba[abr<=160]"
"/bv*[filesize<88M]+ba[abr<=160]/bv*[filesize_approx<88M]+ba[abr<=160]"
"/bv*+ba[abr<=160]/bv*+ba/" + FORMAT_MUXED_FIRST
)
# Deliberately a sort, not a `height<=1080` filter: `res` keys off the *smaller*
# dimension, so it caps portrait Shorts (1080x1920) at 1080p instead of excluding
# them outright the way a height filter would. h264/AAC play on every Signal
# client; VP9/AV1 in an mp4 container do not reliably.
FORMAT_SORT_YOUTUBE = "res:1080,vcodec:h264,acodec:aac"
def _format_args(url: str) -> list[str]:
"""yt-dlp format-selection args for this URL."""
if _is_youtube(url):
return ["-f", FORMAT_YOUTUBE, "-S", FORMAT_SORT_YOUTUBE]
return ["-f", FORMAT_MUXED_FIRST]
# --- /force: host an oversized video as a temporary download link ---
FORCE_DURATION_THRESHOLD = 3600 # offer /force for videos longer than this (1 hr)
FORCE_OFFER_TTL = 600 # a /force offer stays valid for 10 min
@@ -544,27 +597,18 @@ def _run_ytdlp(url: str, outpath: str, tmpdir: str,
# Download only the requested window instead of the whole video —
# essential for hour-long uploads shared with a ?t= timestamp.
# force-keyframes-at-cuts makes the start boundary accurate.
# The filesize filters below key off the *whole* video's size, which
# is irrelevant to a 60s slice, so drop them and just take best mp4.
# A whole-video filesize cap is meaningless for a 60s slice, so take the
# best rendition outright. Clips only ever come from YouTube (the caller
# gates timestamps on YOUTUBE_URL_PATTERN), where that means split
# streams — 1080p for a clip is only ~20 MB.
cmd += [
"--download-sections", f"*{start}-{end}",
"--force-keyframes-at-cuts",
"-f", "best[ext=mp4]/best",
"-f", "bv*+ba/b",
"-S", FORMAT_SORT_YOUTUBE,
]
else:
# 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"
"/bv*[filesize<95M]+ba/bv*[filesize_approx<95M]+ba/bv*+ba/bv*",
]
cmd += _format_args(url)
cmd += [
"--merge-output-format", "mp4",
*_cookie_args(url),