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))