"""Ad-hoc clips: a marked range of a YouTube video or a Plex item, cut into a new per-user asset. A clip never needs the whole source first. A YouTube clip fetches only its range (yt-dlp's `download_ranges`; measured: 3-7 s whether the video runs four minutes or an hour, and the fetched range starts on the marked frame), and a Plex clip reads the library file in place. Either way the range then goes through ONE re-encode built here, so every clip has the same shape whatever its source: frame-exact at both ends, H.264 at most 1080p (an HDR source tonemapped to SDR — kept as 10-bit PQ it played washed out), stereo AAC, in an .mp4 that plays on any phone. The spec is stored on the job (`DownloadJob.edit_spec`) and is the clip's whole identity: its signature is the asset's cache key, so marking the same range again is an instant cache hit, and a different range or audio track is a different clip. """ import hashlib import json import math import re from pathlib import Path from app.downloads.edit import AAC_ARGS, EVEN_FRAME, INPUT_GUARD, X264_ARGS SOURCES = ("youtube", "plex") # Owner's cap: a clip is a moment, not a way to export a whole film to a public link. MAX_CLIP_S = 600 MIN_CLIP_S = 1 MAX_HEIGHT = 1080 # What yt-dlp fetches the range in (formats.normalize fills the rest): the clip's own ceiling, so the # re-encode never upscales and never throws away a better source than it keeps. YOUTUBE_FETCH_SPEC = {"mode": "av", "max_height": MAX_HEIGHT, "container": "mp4"} # Bump when the spec → output-bytes mapping changes, so a cached clip re-derives. _SIG_VERSION = 1 _YOUTUBE_ID = re.compile(r"[A-Za-z0-9_-]{11}") _RATING_KEY = re.compile(r"[0-9]{1,32}") _MAX_AUDIO_TRACK = 63 # The transfer characteristics of HDR video (PQ, HLG): an 8-bit SDR encode must tonemap them. _HDR_TRANSFERS = frozenset({"smpte2084", "arib-std-b67"}) # HDR → SDR as measured on the owner's 4K HDR files (2026-10-10): linearise, tonemap (hable), # convert the primaries and transfer to BT.709 and back to 8-bit 4:2:0. _TONEMAP = ( "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable:desat=0," "zscale=t=bt709:m=bt709:r=tv,format=yuv420p" ) _SCALE = f"scale=-2:'min({MAX_HEIGHT},ih)'" class ClipError(Exception): """A clip request that cannot be cut; `reason` is an i18n-mappable key.""" def __init__(self, reason: str): self.reason = reason super().__init__(reason) def normalize( source: str, ref: str, start_s: float, end_s: float, audio_track: int | None = None ) -> dict: """The validated, canonical clip spec. Times are rounded to the millisecond so the same marks always give the same cache key. Raises ClipError.""" if source not in SOURCES: raise ClipError("bad_source") pattern = _YOUTUBE_ID if source == "youtube" else _RATING_KEY if not isinstance(ref, str) or not pattern.fullmatch(ref): raise ClipError("bad_source") if not (math.isfinite(start_s) and math.isfinite(end_s)) or start_s < 0: raise ClipError("bad_range") start, end = round(start_s, 3), round(end_s, 3) if end - start < MIN_CLIP_S: raise ClipError("too_short") if end - start > MAX_CLIP_S: raise ClipError("too_long") spec: dict = {"source": source, "ref": ref, "start_s": start, "end_s": end} if source == "plex" and audio_track is not None: if not 0 <= audio_track <= _MAX_AUDIO_TRACK: raise ClipError("bad_audio") spec["audio_track"] = audio_track return spec def duration(spec: dict) -> float: return spec["end_s"] - spec["start_s"] def sig(spec: dict) -> str: """The clip's cache signature: the spec itself, versioned.""" body = json.dumps({**spec, "_v": _SIG_VERSION}, sort_keys=True, separators=(",", ":")) return hashlib.sha256(body.encode()).hexdigest()[:24] def asset_ref(user_id: int, spec: dict) -> str: """The asset's `source_ref`. The user id makes a clip private to whoever cut it, like an edit: never dedup-shared, never cache-served to someone else.""" return f"{user_id}:{spec['source']}:{spec['ref']}" def reference_url(spec: dict) -> str | None: """Where a YouTube clip came from: its video at the clip's start. A Plex clip has no public page.""" if spec.get("source") != "youtube" or not spec.get("ref"): return None return f"https://www.youtube.com/watch?v={spec['ref']}&t={int(spec.get('start_s') or 0)}s" def is_hdr(probe: dict) -> bool: """Whether the first video stream of an ffprobe `-show_streams` JSON carries an HDR transfer.""" for s in probe.get("streams") or []: if s.get("codec_type") == "video": return s.get("color_transfer") in _HDR_TRANSFERS return False def audio_track_count(probe: dict) -> int: return sum(1 for s in probe.get("streams") or [] if s.get("codec_type") == "audio") def build_cmd( src: Path, dest: Path, start_s: float, dur_s: float, *, audio_track: int | None, hdr: bool, threads: int, ) -> list[str]: """ffmpeg for one clip: an input seek (decodes from the keyframe before `start_s` and drops the frames up to it, so the clip starts on the marked frame), the first video stream, the chosen audio track (the first when none was chosen; none at all is fine), no subtitles, data or chapters.""" vf = ",".join([*([_TONEMAP] if hdr else []), _SCALE, EVEN_FRAME]) audio = f"0:a:{audio_track}" if audio_track is not None else "0:a:0?" return [ "ffmpeg", "-y", "-hide_banner", "-loglevel", "error", "-nostdin", "-progress", "pipe:1", "-nostats", "-ss", f"{start_s:.3f}", *INPUT_GUARD, "-i", str(src), "-t", f"{dur_s:.3f}", "-map", "0:v:0", "-map", audio, "-sn", "-dn", # A source's chapters would ride into the .mp4 as a data track of their own (an mkv's # chapter list came out as `bin_data`); a clip of a moment has no chapters to keep. "-map_chapters", "-1", "-vf", vf, *X264_ARGS, "-threads", str(threads), *AAC_ARGS, "-ac", "2", "-movflags", "+faststart", str(dest), ]