"""Size and a still frame of a clip, read straight from Zipline with ffprobe/ffmpeg, for Discord's link preview.""" import json import subprocess import threading from functools import lru_cache # /v/ and /_site/thumb need no login: cap how many ffmpeg/ffprobe processes they can start at once SLOTS = threading.BoundedSemaphore(2) @lru_cache(maxsize=256) def video_size(url: str, length: str) -> tuple[int, int] | None: """(width, height) of the first video stream; `length` is only part of the cache key (a reused name).""" cmd = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of", "json", url] try: with SLOTS: result = subprocess.run(cmd, capture_output=True, timeout=10) stream = json.loads(result.stdout)["streams"][0] return int(stream["width"]), int(stream["height"]) except (OSError, subprocess.TimeoutExpired, ValueError, KeyError, IndexError): return None @lru_cache(maxsize=64) def still_jpeg(url: str, length: str) -> bytes | None: """One frame (1 s in, or the first one for very short clips) as a JPEG at most 720 px tall.""" for seek in ("1", "0"): cmd = ["ffmpeg", "-v", "error", "-ss", seek, "-i", url, "-frames:v", "1", "-vf", "scale=-2:'min(720,ih)'", "-f", "image2", "-c:v", "mjpeg", "pipe:1"] try: with SLOTS: result = subprocess.run(cmd, capture_output=True, timeout=30) except (OSError, subprocess.TimeoutExpired): return None if result.returncode == 0 and result.stdout: return result.stdout return None