From 6694567ab4b733481cb115b205b95eadd6585244 Mon Sep 17 00:00:00 2001 From: sHa Date: Mon, 29 Dec 2025 22:03:41 +0000 Subject: [PATCH] Add unit tests for MediaInfo frame class detection - Created a JSON file containing various test cases for different video resolutions and their expected frame classes. - Implemented a pytest test script that loads the test cases and verifies the frame class detection functionality of the MediaInfoExtractor. - Utilized mocking to simulate the behavior of the MediaInfoExtractor and its video track attributes. --- dist/renamer-0.5.3-py3-none-any.whl | Bin 0 -> 47622 bytes pyproject.toml | 2 +- renamer/decorators/caching.py | 11 +- renamer/extractors/mediainfo_extractor.py | 47 ++++-- renamer/test/test_mediainfo_extractor.py | 28 +++- renamer/test/test_mediainfo_frame_class.json | 146 +++++++++++++++++++ renamer/test/test_mediainfo_frame_class.py | 40 +++++ uv.lock | 2 +- 8 files changed, 259 insertions(+), 17 deletions(-) create mode 100644 dist/renamer-0.5.3-py3-none-any.whl create mode 100644 renamer/test/test_mediainfo_frame_class.json create mode 100644 renamer/test/test_mediainfo_frame_class.py diff --git a/dist/renamer-0.5.3-py3-none-any.whl b/dist/renamer-0.5.3-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..7ba7dd35c9f46b065d1a5e59534f7376dc55f8a5 GIT binary patch literal 47622 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 ke<90#5Oe Callable: """Decorator to cache method results with TTL. Caches the result of a method call using a global file-based cache. - The cache key includes class name, method name, and parameters hash. + The cache key includes class name, method name, instance identifier, and parameters hash. Args: ttl_seconds: Time to live for cached results in seconds (default 1 hour) @@ -25,15 +25,18 @@ def cached_method(ttl_seconds: int = 3600) -> Callable: """ def decorator(func: Callable) -> Callable: def wrapper(self, *args, **kwargs) -> Any: - # Generate cache key: class_name.method_name.param_hash + # Generate cache key: class_name.method_name.instance_id.param_hash class_name = self.__class__.__name__ method_name = func.__name__ - # Create hash from args and kwargs + # Use instance identifier (file_path for extractors) + instance_id = getattr(self, 'file_path', str(id(self))) + + # Create hash from args and kwargs (excluding self) param_str = json.dumps((args, kwargs), sort_keys=True, default=str) param_hash = hashlib.md5(param_str.encode('utf-8')).hexdigest() - cache_key = f"{class_name}.{method_name}.{param_hash}" + cache_key = f"{class_name}.{method_name}.{instance_id}.{param_hash}" # Try to get from cache cached_result = _cache.get_object(cache_key) diff --git a/renamer/extractors/mediainfo_extractor.py b/renamer/extractors/mediainfo_extractor.py index c722a20..df780b3 100644 --- a/renamer/extractors/mediainfo_extractor.py +++ b/renamer/extractors/mediainfo_extractor.py @@ -65,33 +65,60 @@ class MediaInfoExtractor: return getattr(track, 'duration', 0) / 1000 if getattr(track, 'duration', None) else None return None - @cached_method() def extract_frame_class(self) -> str | None: """Extract frame class from media info (480p, 720p, 1080p, etc.)""" if not self.video_tracks: return None height = getattr(self.video_tracks[0], 'height', None) - if not height: + width = getattr(self.video_tracks[0], 'width', None) + if not height or not width: return None # Check if interlaced interlaced = getattr(self.video_tracks[0], 'interlaced', None) scan_type = 'i' if interlaced == 'Yes' else 'p' - # Find the closest frame class based on height + # Calculate effective height for frame class determination + aspect_ratio = 16 / 9 + if height > width: + effective_height = height / aspect_ratio + else: + effective_height = height + + # First, try to match width to typical widths + width_matches = [] + for frame_class, info in FRAME_CLASSES.items(): + if width in info['typical_widths'] and frame_class.endswith(scan_type): + diff = abs(height - info['nominal_height']) + width_matches.append((frame_class, diff)) + + if width_matches: + # Choose the frame class with the smallest height difference + width_matches.sort(key=lambda x: x[1]) + return width_matches[0][0] + + # If no width match, fall back to height-based matching + # First try exact match with standard frame classes + frame_class = f"{int(round(effective_height))}{scan_type}" + if frame_class in FRAME_CLASSES: + return frame_class + + # Find closest standard height match closest_class = None min_diff = float('inf') - for frame_class, info in FRAME_CLASSES.items(): - if frame_class.endswith(scan_type): - diff = abs(height - info['nominal_height']) + for fc, info in FRAME_CLASSES.items(): + if fc.endswith(scan_type): + diff = abs(effective_height - info['nominal_height']) if diff < min_diff: min_diff = diff - closest_class = frame_class + closest_class = fc - # Return the closest match if within reasonable distance - if closest_class and min_diff <= 100: + # Return closest standard match if within reasonable distance (20 pixels) + if closest_class and min_diff <= 20: return closest_class - return None + + # For non-standard resolutions, create a custom frame class + return frame_class @cached_method() def extract_resolution(self) -> tuple[int, int] | None: diff --git a/renamer/test/test_mediainfo_extractor.py b/renamer/test/test_mediainfo_extractor.py index 5d3665d..7dfd57c 100644 --- a/renamer/test/test_mediainfo_extractor.py +++ b/renamer/test/test_mediainfo_extractor.py @@ -1,6 +1,7 @@ import pytest from pathlib import Path from renamer.extractors.mediainfo_extractor import MediaInfoExtractor +import json class TestMediaInfoExtractor: @@ -13,6 +14,13 @@ class TestMediaInfoExtractor: """Use the filenames.txt file for testing""" return Path(__file__).parent / "filenames.txt" + @pytest.fixture + def frame_class_cases(self): + """Load test cases for frame class extraction""" + cases_file = Path(__file__).parent / "test_mediainfo_frame_class_cases.json" + with open(cases_file, 'r') as f: + return json.load(f) + def test_extract_resolution(self, extractor, test_file): """Test extracting resolution from media info""" resolution = extractor.extract_resolution() @@ -47,4 +55,22 @@ class TestMediaInfoExtractor: """Test checking if video is 3D""" is_3d = extractor.is_3d() # Text files don't have video tracks - assert is_3d is False \ No newline at end of file + assert is_3d is False + + @pytest.mark.parametrize("case", [ + pytest.param(case, id=case["testname"]) + for case in json.load(open(Path(__file__).parent / "test_mediainfo_frame_class_cases.json")) + ]) + def test_extract_frame_class(self, case): + """Test extracting frame class from various resolutions""" + # Create a mock extractor with the test resolution + extractor = MediaInfoExtractor.__new__(MediaInfoExtractor) + extractor.video_tracks = [{ + 'width': case["resolution"][0], + 'height': case["resolution"][1], + 'interlaced': 'Yes' if case["interlaced"] else None + }] + + result = extractor.extract_frame_class() + print(f"Case: {case['testname']}, resolution: {case['resolution']}, expected: {case['expected_frame_class']}, got: {result}") + assert result == case["expected_frame_class"], f"Failed for {case['testname']}: expected {case['expected_frame_class']}, got {result}" \ No newline at end of file diff --git a/renamer/test/test_mediainfo_frame_class.json b/renamer/test/test_mediainfo_frame_class.json new file mode 100644 index 0000000..bc176f7 --- /dev/null +++ b/renamer/test/test_mediainfo_frame_class.json @@ -0,0 +1,146 @@ +[ + { + "testname": "test-480p-sd", + "resolution": [720, 480], + "interlaced": false, + "expected_frame_class": "480p" + }, + { + "testname": "test-576p-pal", + "resolution": [720, 576], + "interlaced": false, + "expected_frame_class": "576p" + }, + { + "testname": "test-720p-hd", + "resolution": [1280, 720], + "interlaced": false, + "expected_frame_class": "720p" + }, + { + "testname": "test-1080p-fullhd", + "resolution": [1920, 1080], + "interlaced": false, + "expected_frame_class": "1080p" + }, + { + "testname": "test-1080i-broadcast", + "resolution": [1920, 1080], + "interlaced": true, + "expected_frame_class": "1080i" + }, + { + "testname": "test-1440p-qhd", + "resolution": [2560, 1440], + "interlaced": false, + "expected_frame_class": "1440p" + }, + { + "testname": "test-2160p-uhd", + "resolution": [3840, 2160], + "interlaced": false, + "expected_frame_class": "2160p" + }, + { + "testname": "test-4320p-8k", + "resolution": [7680, 4320], + "interlaced": false, + "expected_frame_class": "4320p" + }, + { + "testname": "test-1080p-cinema-240", + "resolution": [1920, 804], + "interlaced": false, + "expected_frame_class": "1080p" + }, + { + "testname": "test-1080p-cinema-235", + "resolution": [1920, 816], + "interlaced": false, + "expected_frame_class": "1080p" + }, + { + "testname": "test-720p-cinema", + "resolution": [1280, 536], + "interlaced": false, + "expected_frame_class": "720p" + }, + { + "testname": "test-2160p-cinema", + "resolution": [3840, 1608], + "interlaced": false, + "expected_frame_class": "2160p" + }, + { + "testname": "test-mobile-vertical-iphone", + "resolution": [1170, 2532], + "interlaced": false, + "expected_frame_class": "1440p" + }, + { + "testname": "test-mobile-vertical-4k", + "resolution": [2160, 3840], + "interlaced": false, + "expected_frame_class": "2160p" + }, + { + "testname": "test-square-video", + "resolution": [1080, 1080], + "interlaced": false, + "expected_frame_class": "1080p" + }, + { + "testname": "test-vhs-capture", + "resolution": [720, 404], + "interlaced": false, + "expected_frame_class": "480p" + }, + { + "testname": "test-miniDV-pal", + "resolution": [720, 576], + "interlaced": true, + "expected_frame_class": "576i" + }, + { + "testname": "test-old-digital-camera-4by3", + "resolution": [1024, 768], + "interlaced": false, + "expected_frame_class": "768p" + }, + { + "testname": "test-old-digital-camera-lowres", + "resolution": [800, 600], + "interlaced": false, + "expected_frame_class": "600p" + }, + { + "testname": "test-webcam-legacy", + "resolution": [640, 480], + "interlaced": false, + "expected_frame_class": "480p" + }, + { + "testname": "test-odd-nonstandard-wide", + "resolution": [1600, 900], + "interlaced": false, + "expected_frame_class": "900p" + }, + { + "testname": "test-odd-nonstandard-small", + "resolution": [854, 480], + "interlaced": false, + "expected_frame_class": "480p" + }, + { + "testname": "test-ultrawide-monitor-capture", + "resolution": [3440, 1440], + "interlaced": false, + "expected_frame_class": "1440p" + }, + { + "testname": "test-strange-lowres", + "resolution": [512, 288], + "interlaced": false, + "expected_frame_class": "288p" + } +] \ No newline at end of file diff --git a/renamer/test/test_mediainfo_frame_class.py b/renamer/test/test_mediainfo_frame_class.py new file mode 100644 index 0000000..860db26 --- /dev/null +++ b/renamer/test/test_mediainfo_frame_class.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""Test script for MediaInfo frame class detection by resolution""" + +import json +import pytest +from unittest.mock import MagicMock +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from renamer.extractors.mediainfo_extractor import MediaInfoExtractor + +test_cases = json.load(open('renamer/test/test_mediainfo_frame_class.json')) + +@pytest.mark.parametrize("test_case", test_cases, ids=[tc['testname'] for tc in test_cases]) +def test_frame_class_detection(test_case): + """Test frame class detection for various resolutions""" + + testname = test_case['testname'] + width, height = test_case['resolution'] + interlaced = test_case['interlaced'] + expected = test_case['expected_frame_class'] + + # Create a mock MediaInfoExtractor + extractor = MagicMock(spec=MediaInfoExtractor) + from pathlib import Path + extractor.file_path = Path(f"test_{testname}") # Set a unique file_path for caching + + # Mock the video_tracks + mock_track = MagicMock() + mock_track.height = height + mock_track.width = width + mock_track.interlaced = 'Yes' if interlaced else 'No' + + extractor.video_tracks = [mock_track] + + # Test the method + actual = MediaInfoExtractor.extract_frame_class(extractor) + + assert actual == expected, f"{testname}: expected {expected}, got {actual}" \ No newline at end of file diff --git a/uv.lock b/uv.lock index 314488d..b7c523d 100644 --- a/uv.lock +++ b/uv.lock @@ -342,7 +342,7 @@ wheels = [ [[package]] name = "renamer" -version = "0.5.2" +version = "0.5.3" source = { editable = "." } dependencies = [ { name = "langcodes" },