From 155c5712c2db2cbe56d7cf261a83192a546d7e55 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 22 May 2026 16:44:08 +0000 Subject: [PATCH] Prefer ffmpeg-derived resolution and normalize to SD/HD/FHD/4K/8K --- mediahive/hivescan/indexer.py | 4 +-- mediahive/hivescan/parsing.py | 3 +- mediahive/hivescan/showreel.py | 6 +++- mediahive/hivescan/utils.py | 66 ++++++++++++++++++++++++++++++++-- 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/mediahive/hivescan/indexer.py b/mediahive/hivescan/indexer.py index 241c237..2963675 100644 --- a/mediahive/hivescan/indexer.py +++ b/mediahive/hivescan/indexer.py @@ -67,7 +67,7 @@ async def _build_torrent_info( return Torrent( title=item.title, playable_file=make_relative_path(playable_file, media_root), - resolution=item.resolution or (probe_info.resolution if probe_info else None), + resolution=(probe_info.resolution if probe_info else None) or item.resolution, quality=item.quality, codec=item.codec, audio=item.audio, @@ -252,7 +252,7 @@ def _build_episodes_data( torrents[relpath] = Torrent( title=f["torrent_title"], playable_file=make_relative_path(f["path"], media_root), - resolution=f.get("resolution") or f.get("probed_resolution"), + resolution=f.get("probed_resolution") or f.get("resolution"), quality=f.get("quality"), codec=f.get("codec"), audio=f.get("audio"), diff --git a/mediahive/hivescan/parsing.py b/mediahive/hivescan/parsing.py index 817f86a..952efba 100644 --- a/mediahive/hivescan/parsing.py +++ b/mediahive/hivescan/parsing.py @@ -8,6 +8,7 @@ import PTN from aiopathlib import AsyncPath from mediahive.hivescan.models import ContentHash, ContentType, ParsedContent +from mediahive.hivescan.utils import normalize_resolution_label def determine_content_type(parsed: dict) -> ContentType: @@ -36,7 +37,7 @@ async def parse_download(path: Path) -> ParsedContent: content_type=content_type, title=parsed.get("title", name), year=parsed.get("year"), - resolution=parsed.get("resolution"), + resolution=normalize_resolution_label(parsed.get("resolution")), quality=parsed.get("quality"), codec=parsed.get("codec"), audio=parsed.get("audio"), diff --git a/mediahive/hivescan/showreel.py b/mediahive/hivescan/showreel.py index f4ddae4..711978b 100644 --- a/mediahive/hivescan/showreel.py +++ b/mediahive/hivescan/showreel.py @@ -19,6 +19,8 @@ from typing import Optional from aiopathlib import AsyncPath +from mediahive.hivescan.utils import classify_resolution_from_dimensions + logger = logging.getLogger("hivescan.showreel") @@ -388,7 +390,9 @@ async def probe_media_info(video_path: str) -> MediaProbeInfo: if dim_match: info.width = int(dim_match.group(1)) info.height = int(dim_match.group(2)) - info.resolution = f"{info.width}x{info.height}" + info.resolution = classify_resolution_from_dimensions( + info.width, info.height + ) info.is_hdr = ( "smpte2084" in lower_text diff --git a/mediahive/hivescan/utils.py b/mediahive/hivescan/utils.py index a7cf6a2..397a4ce 100644 --- a/mediahive/hivescan/utils.py +++ b/mediahive/hivescan/utils.py @@ -2,11 +2,10 @@ import time from pathlib import Path -from typing import Optional, List +from typing import List, Optional from aiopathlib import AsyncPath - # Default output folder name (created at common root of scanned paths) DEFAULT_OUTPUT_FOLDER = ".mediahive" @@ -15,15 +14,76 @@ _ATIME_FRESHNESS_THRESHOLD = 3600 # Resolution priority for quality sorting (higher = better) RESOLUTION_PRIORITY = { - "2160p": 4, + "8K": 5, "4K": 4, + "FHD": 3, + "HD": 2, + "SD": 1, + # Backward compatibility for existing snapshot data + "4320p": 5, + "2160p": 4, + "UHD": 4, "1080p": 3, "1080i": 3, "720p": 2, + "576p": 1, "480p": 1, } +def classify_resolution_from_dimensions( + width: int | None, height: int | None +) -> str | None: + """Map raw frame dimensions to SD/HD/FHD/4K/8K buckets. + + Uses the smallest standard frame bucket that can contain the source frame, + which keeps cropped cinematic encodes in their expected class. + """ + if not width or not height or width <= 0 or height <= 0: + return None + + long_edge = max(width, height) + short_edge = min(width, height) + + buckets = [ + (1024, 576, "SD"), + (1280, 720, "HD"), + (1920, 1080, "FHD"), + (4096, 2160, "4K"), + (8192, 4320, "8K"), + ] + + for max_w, max_h, label in buckets: + if long_edge <= max_w and short_edge <= max_h: + return label + + return "8K" + + +def normalize_resolution_label(value: str | None) -> str | None: + """Normalize PTN/legacy resolution text into SD/HD/FHD/4K/8K labels.""" + if not value: + return None + + normalized = str(value).strip().upper() + mapping = { + "SD": "SD", + "HD": "HD", + "FHD": "FHD", + "4K": "4K", + "8K": "8K", + "4320P": "8K", + "2160P": "4K", + "UHD": "4K", + "1080P": "FHD", + "1080I": "FHD", + "720P": "HD", + "576P": "SD", + "480P": "SD", + } + return mapping.get(normalized) + + async def get_added_timestamp(path: Path) -> Optional[int]: """ Get the timestamp when a torrent was added to the collection.