diff --git a/src/extractors/filename_extractor.py b/src/extractors/filename_extractor.py index c416ec6..32dfa38 100644 --- a/src/extractors/filename_extractor.py +++ b/src/extractors/filename_extractor.py @@ -64,19 +64,26 @@ class FilenameExtractor: if dot_match: year_pos = dot_match.start() else: - # Last resort: any 4-digit number - any_match = re.search(r'\b(\d{4})\b', self.file_name) - if any_match: - year = int(any_match.group(1)) - # Basic sanity check using constants - if is_valid_year(year): - year_pos = any_match.start() # Cut before the year for plain years - + # Try year between mixed separators (like .1967_ or _1967.) + sep_match = re.search(r'(?<=[.\-_\s])(\d{4})(?=[.\-_\s])', self.file_name) + if sep_match: + year_val = int(sep_match.group(1)) + if is_valid_year(year_val): + year_pos = sep_match.start(1) + else: + # Last resort: any 4-digit number + any_match = re.search(r'\b(\d{4})\b', self.file_name) + if any_match: + year = int(any_match.group(1)) + # Basic sanity check using constants + if is_valid_year(year): + year_pos = any_match.start() # Cut before the year for plain years + # Find source position source = self.extract_source() if source: for alias in SOURCE_DICT[source]: - match = re.search(r'\b' + re.escape(alias) + r'\b', self.file_name, re.IGNORECASE) + match = re.search(r'(? 1 or '.' in order: - title = re.sub(r'^\s*(\d+(?:\.\d+)?)\.(?=\s|_)', '', title) - - # Remove order like 1.9 where 1 is order, 9 is title + # Remove order prefix (order followed by dot or space) order = self.extract_order() if order: - match = re.match(r'^' + re.escape(order) + r'\.(.+)', title) + match = re.match(r'^' + re.escape(order) + r'[.\s]+(.+)', title) if match: title = match.group(1) # Clean up any remaining leading separators title = title.lstrip('_ \t') - # Clean up title: remove leading/trailing brackets and dots - title = title.strip('[](). ') + # Clean up title: remove leading/trailing brackets and orphaned dots + title = title.strip('[]. ') + # Only strip unmatched leading/trailing parens + if title.endswith(')') and title.count('(') < title.count(')'): + title = title.rstrip(')') + if title.startswith('(') and title.count('(') > title.count(')'): + title = title.lstrip('(') # Replace dots with spaces if they appear to be word separators # Only replace dots that are surrounded by letters/digits (not at edges) @@ -150,7 +154,14 @@ class FilenameExtractor: dot_match = re.search(r'\.(\d{4})\.', self.file_name) if dot_match: return dot_match.group(1) - + + # Try year between mixed separators (like .1967_ or _1967.) + sep_match = re.search(r'(?<=[.\-_\s])(\d{4})(?=[.\-_\s])', self.file_name) + if sep_match: + year = int(sep_match.group(1)) + if is_valid_year(year): + return str(year) + # Last resort: any 4-digit number (but this is less reliable) any_match = re.search(r'\b(\d{4})\b', self.file_name) if any_match: @@ -212,6 +223,14 @@ class FilenameExtractor: return frame_class # Fallback to height-based if not in constants return self._get_frame_class_from_height(height) + + # Check for bare resolution numbers inside brackets (e.g., [720,ukr,eng]) + bare_match = re.search(r'[\[,](\d{3,4})(?=[,\]])', normalized_name, re.IGNORECASE) + if bare_match: + height = int(bare_match.group(1)) + frame_class = self._get_frame_class_from_height(height) + if frame_class: + return frame_class # If no specific resolution found, check for non-standard quality indicators for indicator in NON_STANDARD_QUALITY_INDICATORS: @@ -334,8 +353,17 @@ class FilenameExtractor: # Remove bracketed content first text_without_brackets = re.sub(r'\[([^\]]+)\]', '', self.file_name) - # Split on dots, spaces, and underscores - parts = re.split(r'[.\s_]+', text_without_brackets) + # Find start of metadata section (after title) to avoid title words being + # misdetected as language codes (e.g. "War" from "The.War.Wagon") + metadata_start = 0 + year_m = (re.search(r'\(\d{4}\)', text_without_brackets) or + re.search(r'\.\d{4}\.', text_without_brackets) or + re.search(r'(?<=[.\-_\s])\d{4}(?=[.\-_\s])', text_without_brackets)) + if year_m: + metadata_start = year_m.start() + + # Split on dots, spaces, and underscores (only in the post-title portion) + parts = re.split(r'[.\s_]+', text_without_brackets[metadata_start:]) for part in parts: part = part.strip() diff --git a/src/test/datasets/filenames/filename_patterns.json b/src/test/datasets/filenames/filename_patterns.json index 2ec13c5..180a36f 100644 --- a/src/test/datasets/filenames/filename_patterns.json +++ b/src/test/datasets/filenames/filename_patterns.json @@ -2,6 +2,24 @@ "description": "Comprehensive test dataset for filename metadata extraction", "version": "2.0", "test_cases": [ + { + "filename": "The.War.Wagon.1967_BDRip Ukr_Eng[Hurtom].mkv", + "expected": { + "order": null, + "title": "The War Wagon", + "year": "1967", + "source": "BDRip", + "frame_class": null, + "hdr": null, + "movie_db": null, + "special_info": null, + "audio_langs": "ukr,eng", + "extension": "mkv" + }, + "testname": "edge-multi-lang-001", + "category": "edge_cases", + "description": "Multiple languages without brackets" + }, { "filename": "Le Jaguar.(1996).[1080i,3ukr,fra].mkv", "expected": { @@ -799,7 +817,7 @@ "filename": "Movie.Title (2020) BDRip [1080p,ukr,eng].mkv", "expected": { "order": null, - "title": "Movie.Title", + "title": "Movie Title", "year": "2020", "source": "BDRip", "frame_class": "1080p", @@ -810,7 +828,7 @@ "extension": "mkv" }, "category": "edge_cases", - "description": "Title with dots" + "description": "Title with dot separator (dot replaced with space by extractor)" }, { "testname": "edge-no-brackets-001", diff --git a/src/test/test_filename_extractor.py b/src/test/test_filename_extractor.py index 8c6bbef..4456d54 100644 --- a/src/test/test_filename_extractor.py +++ b/src/test/test_filename_extractor.py @@ -15,6 +15,22 @@ def load_test_filenames(): return [] +def load_test_cases(): + """Load full test cases (testname, filename, expected) from dataset""" + dataset_file = Path(__file__).parent / "datasets" / "filenames" / "filename_patterns.json" + if dataset_file.exists(): + with open(dataset_file, 'r', encoding='utf-8') as f: + data = json.load(f) + return [ + pytest.param( + case['filename'], + case['expected'], + id=case.get('testname', case['filename']) + ) + for case in data['test_cases'] + ] + return [] + @pytest.mark.parametrize("filename", load_test_filenames()) def test_extract_title(filename): """Test title extraction from filename""" @@ -128,4 +144,50 @@ def test_extract_audio_tracks(filename): assert isinstance(audio_tracks, list) for track in audio_tracks: assert isinstance(track, dict) - assert 'language' in track \ No newline at end of file + assert 'language' in track + + +# --------------------------------------------------------------------------- +# Dataset-based value-checking tests (check against filename_patterns.json) +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("filename,expected", load_test_cases()) +def test_expected_title(filename, expected): + """Test that extracted title matches the expected value from dataset.""" + extractor = FilenameExtractor(Path(filename), use_cache=False) + assert extractor.extract_title() == expected['title'] + + +@pytest.mark.parametrize("filename,expected", load_test_cases()) +def test_expected_year(filename, expected): + """Test that extracted year matches the expected value from dataset.""" + extractor = FilenameExtractor(Path(filename), use_cache=False) + assert extractor.extract_year() == expected['year'] + + +@pytest.mark.parametrize("filename,expected", load_test_cases()) +def test_expected_source(filename, expected): + """Test that extracted source matches the expected value from dataset.""" + extractor = FilenameExtractor(Path(filename), use_cache=False) + assert extractor.extract_source() == expected['source'] + + +@pytest.mark.parametrize("filename,expected", load_test_cases()) +def test_expected_frame_class(filename, expected): + """Test that extracted frame_class matches the expected value from dataset.""" + extractor = FilenameExtractor(Path(filename), use_cache=False) + assert extractor.extract_frame_class() == expected['frame_class'] + + +@pytest.mark.parametrize("filename,expected", load_test_cases()) +def test_expected_audio_langs(filename, expected): + """Test that extracted audio_langs matches the expected value from dataset.""" + extractor = FilenameExtractor(Path(filename), use_cache=False) + assert extractor.extract_audio_langs() == expected['audio_langs'] + + +@pytest.mark.parametrize("filename,expected", load_test_cases()) +def test_expected_movie_db(filename, expected): + """Test that extracted movie_db matches the expected value from dataset.""" + extractor = FilenameExtractor(Path(filename), use_cache=False) + assert extractor.extract_movie_db() == expected['movie_db']