Compare commits
1
Commits
6d0daddea5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b1d460d3d |
+4
-8
@@ -1,8 +1,4 @@
|
|||||||
ff,2
|
git-honney333-work-cred,794
|
||||||
simple_ffprobe_script,1
|
ff,53
|
||||||
show_path,2
|
deappend,8
|
||||||
append,15
|
rename_to_first_n,1
|
||||||
deappend,17
|
|
||||||
rename_to_first_n,3
|
|
||||||
count_files,4
|
|
||||||
remove_fname_last_n,2
|
|
||||||
|
|||||||
|
@@ -264,6 +264,12 @@ class audio_lines:
|
|||||||
|
|
||||||
if self.language:
|
if self.language:
|
||||||
string += f" [{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:
|
if self.duration:
|
||||||
string += f" {self.duration}s"
|
string += f" {self.duration}s"
|
||||||
@@ -318,6 +324,13 @@ class subtitles:
|
|||||||
if self.language:
|
if self.language:
|
||||||
parts.append(f"[{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:
|
if self.duration:
|
||||||
parts.append(f"{self.duration}s")
|
parts.append(f"{self.duration}s")
|
||||||
|
|
||||||
@@ -384,6 +397,212 @@ class subtitles:
|
|||||||
|
|
||||||
# return duration, video_streams, audio_streams, subtitle_streams
|
# 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):
|
def get_media_info(file):
|
||||||
cmd = [
|
cmd = [
|
||||||
"ffprobe",
|
"ffprobe",
|
||||||
@@ -391,12 +610,12 @@ def get_media_info(file):
|
|||||||
"-show_entries",
|
"-show_entries",
|
||||||
(
|
(
|
||||||
"format=duration:format_tags=title:"
|
"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,"
|
"width,height,r_frame_rate,bit_rate,duration,nb_frames,"
|
||||||
"pix_fmt,field_order,time_base,display_aspect_ratio,"
|
"pix_fmt,field_order,time_base,display_aspect_ratio,"
|
||||||
"color_space,color_transfer,color_primaries,bits_per_raw_sample,"
|
"color_space,color_transfer,color_primaries,bits_per_raw_sample,"
|
||||||
"sample_rate,channels,bits_per_sample,"
|
"sample_rate,channels,bits_per_sample,"
|
||||||
"stream_disposition=forced,default:"
|
"stream_disposition=forced,default,attached_pic:"
|
||||||
"stream_tags=language,title"
|
"stream_tags=language,title"
|
||||||
),
|
),
|
||||||
"-of", "json",
|
"-of", "json",
|
||||||
@@ -408,14 +627,14 @@ def get_media_info(file):
|
|||||||
info = json.loads(result.stdout)
|
info = json.loads(result.stdout)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print(f"Error running ffprobe: {e.stderr}")
|
print(f"Error running ffprobe: {e.stderr}")
|
||||||
return None, [], [], [], None, "UNKNOWN"
|
return None, [], [], [], None, []
|
||||||
|
|
||||||
# Duration
|
# Duration
|
||||||
duration = None
|
duration = None
|
||||||
if "format" in info:
|
if "format" in info:
|
||||||
duration = info["format"].get("duration")
|
duration = info["format"].get("duration")
|
||||||
|
|
||||||
# ✅ Extract container title
|
# ✅ Extract container title
|
||||||
title = None
|
title = None
|
||||||
if "format" in info:
|
if "format" in info:
|
||||||
title = info["format"].get("tags", {}).get("title")
|
title = info["format"].get("tags", {}).get("title")
|
||||||
@@ -433,27 +652,28 @@ def get_media_info(file):
|
|||||||
elif stream_type == "subtitle":
|
elif stream_type == "subtitle":
|
||||||
subtitle_streams.append(stream)
|
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 = []
|
hdr_types = []
|
||||||
|
|
||||||
if video_streams:
|
if video_streams:
|
||||||
for video in video_streams:
|
trace_cache = {} # file-level Tier 2 result, run at most once per file
|
||||||
color_transfer = video.get("color_transfer")
|
for v_pos, video in enumerate(video_streams):
|
||||||
color_primaries = video.get("color_primaries")
|
label, needs_deep = _classify_fast(video)
|
||||||
|
if not needs_deep:
|
||||||
if color_transfer == "smpte2084":
|
hdr_types.append(label)
|
||||||
hdr_types.append("HDR10")
|
continue
|
||||||
|
# Tier 1: 1-frame + stream dump (cheap, no decode)
|
||||||
elif color_transfer == "arib-std-b67":
|
deep = _probe_frame_tier1(file, v_pos)
|
||||||
hdr_types.append("HLG")
|
if deep:
|
||||||
|
hdr_types.append(deep)
|
||||||
elif (
|
continue
|
||||||
color_transfer == "bt709"
|
# Tier 2: bitstream trace_headers (expensive, no decode)
|
||||||
and color_primaries == "bt709"
|
if "trace" not in trace_cache:
|
||||||
):
|
trace_cache["trace"] = _probe_trace_tier2(file)
|
||||||
hdr_types.append("SDR")
|
if trace_cache["trace"]:
|
||||||
else:
|
hdr_types.append(trace_cache["trace"])
|
||||||
hdr_types.append("Unknown HDR")
|
continue
|
||||||
|
hdr_types.append("UNKNOWN")
|
||||||
|
|
||||||
if duration is not None:
|
if duration is not None:
|
||||||
duration = float(duration)
|
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)
|
np(f"\t{self.name} ({self.size}, {self.duration}, {self.bitrate} MB/s):", NORMAL_STYLE)
|
||||||
if self.title:
|
if self.title:
|
||||||
np(f"\t\tTitle: \"{self.title}\"", NORMAL_STYLE)
|
np(f"\t\tTitle: \"{self.title}\"", NORMAL_STYLE)
|
||||||
|
if self.videos:
|
||||||
|
np(f"\t Videos:", INFO_STYLE)
|
||||||
for video in self.videos:
|
for video in self.videos:
|
||||||
np(f"\t\t{video}", NORMAL_STYLE)
|
np(f"\t\t{video}", NORMAL_STYLE)
|
||||||
|
if self.audios:
|
||||||
|
np(f"\t Audios:", INFO_STYLE)
|
||||||
for audio in self.audios:
|
for audio in self.audios:
|
||||||
np(f"\t\t{audio}", NORMAL_STYLE)
|
np(f"\t\t{audio}", NORMAL_STYLE)
|
||||||
|
if self.subtitles:
|
||||||
|
np(f"\t Subtitles:", INFO_STYLE)
|
||||||
for subtitle in self.subtitles:
|
for subtitle in self.subtitles:
|
||||||
np(f"\t\t{subtitle}", NORMAL_STYLE)
|
np(f"\t\t{subtitle}", NORMAL_STYLE)
|
||||||
print()
|
print()
|
||||||
|
|||||||
Reference in New Issue
Block a user