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:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user