diff --git a/data/tracker.csv b/data/tracker.csv index 78288ba..b55db01 100644 --- a/data/tracker.csv +++ b/data/tracker.csv @@ -1,8 +1,4 @@ -ff,2 -simple_ffprobe_script,1 -show_path,2 -append,15 -deappend,17 -rename_to_first_n,3 -count_files,4 -remove_fname_last_n,2 +git-honney333-work-cred,794 +ff,53 +deappend,8 +rename_to_first_n,1 diff --git a/ff b/ff index 15db2fd..28f98e9 100755 --- a/ff +++ b/ff @@ -264,6 +264,12 @@ class audio_lines: if self.language: string += f" [{self.language}]" + if len(self.language) >= 14: + pass + elif len(self.language) >= 8: + string += "\t" + else: + string += "\t\t" if self.duration: string += f" {self.duration}s" @@ -317,7 +323,14 @@ class subtitles: if self.language: parts.append(f"[{self.language}]") - + + if len(self.language) >= 14: + pass + elif len(self.language) >= 7: + parts.append("\t") + else: + parts.append("\t\t") + if self.duration: parts.append(f"{self.duration}s") @@ -384,6 +397,212 @@ class subtitles: # return duration, video_streams, audio_streams, subtitle_streams +def _side_data_hints(side_data_list): + """Return set of hints from ffprobe side_data_list (case-insensitive).""" + hints = set() + for sd in side_data_list or []: + try: + t = str(sd.get("side_data_type", "")).lower() + except Exception: + continue + if "dovi" in t or "dolby" in t or "rpu" in t: + hints.add("DV") + if "2094-40" in t or "hdr dynamic" in t or "hdr10+" in t: + hints.add("HDR10+") + if "mastering" in t or "content light" in t or "content_light" in t or "cll" in t: + hints.add("HDR-static") + return hints + + +def _classify_fast(video): + """Tier 0: pure-python, zero extra subprocess. + + Returns (label, needs_deep). Final labels need no further probing. + UNKNOWN with needs_deep=True means Tier 1/2 should run. + Uses SDR? for highly-probable-but-untagged SDR, HDR? for probable HDR. + """ + transfer = str(video.get("color_transfer") or "").lower() + primaries = str(video.get("color_primaries") or "").lower() + space = str(video.get("color_space") or "").lower() + pix_fmt = str(video.get("pix_fmt") or "").lower() + codec = str(video.get("codec_name") or "").lower() + profile = str(video.get("profile") or "").lower() + bits = video.get("bits_per_raw_sample") + try: + bits = int(bits) if bits is not None else 0 + except Exception: + bits = 0 + + hints = _side_data_hints(video.get("side_data_list")) + + # Cover art / thumbnails are not real video tracks — never deep-probe them. + dispo = video.get("disposition") or {} + try: + if dispo.get("attached_pic") == 1 or codec in ("mjpeg", "png", "gif", "bmp", "tiff"): + return ("SDR", False) + except Exception: + pass + + # 1. Definitive transfer function + if transfer == "smpte2084": + if "DV" in hints: + return ("DV", False) + if "HDR10+" in hints: + return ("HDR10+", False) + return ("HDR10", False) + if transfer == "arib-std-b67": + return ("HLG", False) + sdr_transfers = { + "bt709", "bt470m", "bt470bg", "smpte170m", "smpte240m", + "gamma22", "gamma28", "iec61966-2-1", "iec61966-2-4", "srgb", + } + if transfer in sdr_transfers: + return ("SDR", False) + + # 2. Primaries / matrix when transfer was stripped + is_bt2020 = primaries == "bt2020" or space in ("bt2020nc", "bt2020c", "bt2020_ncl", "bt2020_cl") + is_bt709 = primaries == "bt709" or space == "bt709" + if is_bt2020: + if "DV" in hints: + return ("DV", False) + if "HDR10+" in hints: + return ("HDR10+", False) + # Transfer missing so HDR10 vs HLG unknown — probable HDR. + return ("HDR?", False) + if is_bt709: + return ("SDR", False) + if "HDR-static" in hints or "DV" in hints or "HDR10+" in hints: + if "DV" in hints: + return ("DV", False) + if "HDR10+" in hints: + return ("HDR10+", False) + return ("HDR?", False) + + # 3. All color tags empty — fall back to bit-depth / codec heuristics. + is_10bit_plus = ("p10" in pix_fmt or "p12" in pix_fmt or "p14" in pix_fmt + or "p16" in pix_fmt or bits >= 10 or "10" in profile + or "12" in profile) + if not transfer and not primaries and not space: + if is_10bit_plus: + # 10-bit SDR (anime etc.) and stripped HDR look identical here. + return ("UNKNOWN", True) + # 8-bit + common SDR codecs = highly probable untagged SDR. + return ("SDR?", False) + + # Partial tags, unknown values — probable SDR, no expensive probe. + if not is_10bit_plus: + return ("SDR?", False) + return ("UNKNOWN", True) + + +def _classify_colors(transfer, primaries, space, hints): + """Shared helper for Tier 1/2 dicts. Returns label or None.""" + transfer = str(transfer or "").lower() + primaries = str(primaries or "").lower() + space = str(space or "").lower() + if transfer == "smpte2084": + if "DV" in hints: + return "DV" + if "HDR10+" in hints: + return "HDR10+" + return "HDR10" + if transfer == "arib-std-b67": + return "HLG" + if transfer in ("bt709", "smpte170m", "smpte240m", "gamma22", "gamma28", + "iec61966-2-1", "iec61966-2-4", "srgb", "bt470m", "bt470bg"): + return "SDR" + if primaries == "bt2020" or space in ("bt2020nc", "bt2020c", "bt2020_ncl", "bt2020_cl"): + if "DV" in hints: + return "DV" + if "HDR10+" in hints: + return "HDR10+" + return "HDR?" + if primaries == "bt709" or space == "bt709": + return "SDR" + if "DV" in hints: + return "DV" + if "HDR10+" in hints: + return "HDR10+" + if "HDR-static" in hints: + return "HDR?" + return None + + +def _probe_frame_tier1(path, v_idx): + """Tier 1 (cheap): 1-frame + full stream dump for one v stream. No decode.""" + try: + cmd = [ + "ffprobe", "-v", "error", + "-select_streams", f"v:{v_idx}", + "-show_streams", + "-show_frames", "-read_intervals", "%+#1", + "-show_entries", "frame=color_space,color_transfer,color_primaries,pix_fmt,side_data_list", + "-of", "json", + path, + ] + r = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + if r.returncode != 0: + return None + info = json.loads(r.stdout or "{}") + streams = info.get("streams") or [] + frames = info.get("frames") or [] + s0 = streams[0] if streams else {} + f0 = frames[0] if frames else {} + hints = _side_data_hints(s0.get("side_data_list")) | _side_data_hints(f0.get("side_data_list")) + # Prefer stream tags, fall back to frame tags per field. + label = _classify_colors( + s0.get("color_transfer") or f0.get("color_transfer"), + s0.get("color_primaries") or f0.get("color_primaries"), + s0.get("color_space") or f0.get("color_space"), + hints, + ) + return label + except Exception: + return None + + +def _probe_trace_tier2(path): + """Tier 2 (expensive): bitstream VUI/SEI via trace_headers, 1 frame, no decode.""" + try: + import shutil + if shutil.which("ffmpeg") is None: + return None + cmd = [ + "ffmpeg", "-hide_banner", "-v", "info", + "-i", path, + "-c:v", "copy", "-bsf:v", "trace_headers", + "-frames:v", "1", "-f", "null", "-", + ] + r = subprocess.run(cmd, capture_output=True, text=True, timeout=15) + out = ((r.stderr or "") + "\n" + (r.stdout or "")).lower() + if not out.strip(): + return None + has_dv = "dovi" in out or "rpu" in out + has_hdr10plus = "2094-40" in out or "hdr10+" in out or "dhdr10" in out + has_static = "mastering" in out or "content_light" in out or "max_cll" in out + # VUI transfer_characteristics: 16=PQ(ST2084), 18=HLG; primaries 9=BT.2020 + import re + m = re.search(r"transfer_characteristics\s*[:=]\s*(\d+)", out) + trc = m.group(1) if m else "" + m2 = re.search(r"colour_primaries\s*[:=]\s*(\d+)|color_primaries\s*[:=]\s*(\d+)", out) + prim = (m2.group(1) or m2.group(2)) if m2 else "" + if has_dv: + return "DV" + if has_hdr10plus: + return "HDR10+" + if trc == "16": + return "HDR10" + if trc == "18": + return "HLG" + if prim == "9" or has_static: + return "HDR?" + if trc in ("1", "6", "7"): + return "SDR" + return None + except Exception: + return None + + def get_media_info(file): cmd = [ "ffprobe", @@ -391,12 +610,12 @@ def get_media_info(file): "-show_entries", ( "format=duration:format_tags=title:" - "stream=index,codec_type,codec_name," + "stream=index,codec_type,codec_name,profile," "width,height,r_frame_rate,bit_rate,duration,nb_frames," "pix_fmt,field_order,time_base,display_aspect_ratio," "color_space,color_transfer,color_primaries,bits_per_raw_sample," "sample_rate,channels,bits_per_sample," - "stream_disposition=forced,default:" + "stream_disposition=forced,default,attached_pic:" "stream_tags=language,title" ), "-of", "json", @@ -408,14 +627,14 @@ def get_media_info(file): info = json.loads(result.stdout) except subprocess.CalledProcessError as e: print(f"Error running ffprobe: {e.stderr}") - return None, [], [], [], None, "UNKNOWN" + return None, [], [], [], None, [] # Duration duration = None if "format" in info: duration = info["format"].get("duration") - # ✅ Extract container title + # ✅ Extract container title title = None if "format" in info: title = info["format"].get("tags", {}).get("title") @@ -433,27 +652,28 @@ def get_media_info(file): elif stream_type == "subtitle": subtitle_streams.append(stream) - # Detect HDR type from the first video stream + # Tiered HDR/SDR detection: Tier 0 fast path first, expensive only on UNKNOWN. hdr_types = [] if video_streams: - for video in video_streams: - color_transfer = video.get("color_transfer") - color_primaries = video.get("color_primaries") - - if color_transfer == "smpte2084": - hdr_types.append("HDR10") - - elif color_transfer == "arib-std-b67": - hdr_types.append("HLG") - - elif ( - color_transfer == "bt709" - and color_primaries == "bt709" - ): - hdr_types.append("SDR") - else: - hdr_types.append("Unknown HDR") + trace_cache = {} # file-level Tier 2 result, run at most once per file + for v_pos, video in enumerate(video_streams): + label, needs_deep = _classify_fast(video) + if not needs_deep: + hdr_types.append(label) + continue + # Tier 1: 1-frame + stream dump (cheap, no decode) + deep = _probe_frame_tier1(file, v_pos) + if deep: + hdr_types.append(deep) + continue + # Tier 2: bitstream trace_headers (expensive, no decode) + if "trace" not in trace_cache: + trace_cache["trace"] = _probe_trace_tier2(file) + if trace_cache["trace"]: + hdr_types.append(trace_cache["trace"]) + continue + hdr_types.append("UNKNOWN") if duration is not None: duration = float(duration) @@ -531,10 +751,16 @@ class video_file: np(f"\t{self.name} ({self.size}, {self.duration}, {self.bitrate} MB/s):", NORMAL_STYLE) if self.title: np(f"\t\tTitle: \"{self.title}\"", NORMAL_STYLE) + if self.videos: + np(f"\t Videos:", INFO_STYLE) for video in self.videos: np(f"\t\t{video}", NORMAL_STYLE) + if self.audios: + np(f"\t Audios:", INFO_STYLE) for audio in self.audios: np(f"\t\t{audio}", NORMAL_STYLE) + if self.subtitles: + np(f"\t Subtitles:", INFO_STYLE) for subtitle in self.subtitles: np(f"\t\t{subtitle}", NORMAL_STYLE) print()