diff --git a/frontend/src/components/ReleaseVersionCard.vue b/frontend/src/components/ReleaseVersionCard.vue index 8774110..64086aa 100644 --- a/frontend/src/components/ReleaseVersionCard.vue +++ b/frontend/src/components/ReleaseVersionCard.vue @@ -19,6 +19,7 @@
{{ torrent.resolution }} HDR + HDR10+ {{ displayCodecBadge }} {{ displayQualityBadge }} {{ displayAudioBadge }} @@ -317,6 +318,18 @@ const showHdrBadge = computed(() => { ) }) +const hdr10PlusPattern = /hdr10\+|hdr10plus/i + +const hasHdr10Plus = computed(() => { + if (props.torrent.hdr10plus) return true + const text = [props.torrent.title, props.torrent.quality, props.torrent.codec, props.torrent.audio] + .filter(Boolean) + .join(" ") + return hdr10PlusPattern.test(text) +}) + +const showHdr10PlusBadge = computed(() => hasHdr10Plus.value && !hasDolbyVision.value) + const isSelectable = computed(() => { if (props.selectable !== undefined) return props.selectable return Boolean(props.torrent.playable_file) @@ -525,6 +538,11 @@ html.mouse-active .version-row.version-best:hover { color: #1c1917; } +.v-badge.hdr10plus { + background: #b8860b; + color: #fff8e1; +} + .v-badge.group { background: #1c54a1; color: #f8fafc; diff --git a/mediahive/hivescan/showreel.py b/mediahive/hivescan/showreel.py index 04b143b..b36716f 100644 --- a/mediahive/hivescan/showreel.py +++ b/mediahive/hivescan/showreel.py @@ -432,6 +432,8 @@ _dovi_profile_re = re.compile( ) _audio_stream_re = re.compile(r"Stream #\d+:\d+(?:\(([^)]+)\))?:\s+Audio:") _subtitle_stream_re = re.compile(r"Stream #\d+:\d+(?:\(([^)]+)\))?:\s+Subtitle:") +# showinfo emits e.g. "side data - HDR Dynamic Metadata SMPTE2094-40 (HDR10+)" +_showinfo_hdr10plus_re = re.compile(r"SMPTE2094-40|HDR Dynamic Metadata", re.IGNORECASE) def _lang_code(raw: str | None) -> str | None: @@ -492,6 +494,35 @@ async def probe_media_info(video_path: str) -> MediaProbeInfo: or "dynamic hdr" in lower_text ) + # `ffmpeg -i` only reads container headers, not frame-level side data. + # Run a tiny showinfo decode (5 frames) to detect HDR10+ dynamic metadata + # (SMPTE ST 2094-40) when the header scan didn't already confirm it. + if info.hdr and not info.hdr10plus and not info.dovi: + showinfo_cmd = [ + "ffmpeg", + "-hide_banner", + "-ss", + "0", + "-i", + video_path, + "-vf", + "showinfo", + "-frames:v", + "5", + "-an", + "-f", + "null", + "-", + ] + showinfo_result = await _run_ffmpeg( + showinfo_cmd, timeout_seconds=15, allow_nonzero_exit=True + ) + if showinfo_result is not None: + si_stdout, si_stderr = showinfo_result + si_text = (si_stderr + si_stdout).decode("utf-8", errors="replace") + if _showinfo_hdr10plus_re.search(si_text): + info.hdr10plus = True + dovi_match = _dovi_profile_re.search(text) if dovi_match: info.dovi_profile = int(dovi_match.group(1))