"""Tests for the identity parser module.""" import pytest from vlm.parser import ( parse_movie, parse_series, normalize_title, remove_quality_tags, remove_release_groups, ) class TestMovieParser: """Tests for movie identity parsing.""" def test_parse_movie_with_parentheses_year(self): """Test parsing movie with year in parentheses.""" result = parse_movie("The Matrix (1999).mkv") assert result.title == "The Matrix" assert result.year == 1999 assert result.confidence == 0.9 assert result.needs_review is False assert result.original_filename == "The Matrix (1999).mkv" def test_parse_movie_with_dot_year(self): """Test parsing movie with dot-separated year.""" result = parse_movie("Inception.2010.1080p.BluRay.mkv") assert result.title == "Inception" assert result.year == 2010 assert result.confidence == 0.9 assert result.needs_review is False def test_parse_movie_with_dash_year(self): """Test parsing movie with dash-separated year.""" result = parse_movie("The Godfather - 1972.mp4") assert result.title == "The Godfather" assert result.year == 1972 assert result.confidence == 0.7 assert result.needs_review is False def test_parse_movie_with_space_year(self): """Test parsing movie with space-separated year.""" result = parse_movie("Pulp Fiction 1994.avi") assert result.title == "Pulp Fiction" assert result.year == 1994 assert result.confidence == 0.7 assert result.needs_review is False def test_parse_movie_with_quality_tags(self): """Test parsing movie with quality tags removed.""" result = parse_movie("Interstellar.2014.1080p.BluRay.x264.mkv") assert result.title == "Interstellar" assert result.year == 2014 assert "1080p" not in result.title assert "BluRay" not in result.title assert "x264" not in result.title def test_parse_movie_with_release_group(self): """Test parsing movie with release group tags removed.""" result = parse_movie("The Shawshank Redemption (1994) [RARBG].mkv") assert result.title == "The Shawshank Redemption" assert result.year == 1994 assert "RARBG" not in result.title def test_parse_movie_with_yts_release_group(self): """Test parsing movie with YTS release group.""" result = parse_movie("Fight Club (1999) (YTS).mp4") assert result.title == "Fight Club" assert result.year == 1999 # YTS should be removed def test_parse_movie_without_year(self): """Test parsing movie without extractable year.""" result = parse_movie("Some Random Movie.mkv") assert result.title == "Some Random Movie" assert result.year is None assert result.needs_review is True assert result.confidence == 0.3 def test_parse_movie_with_multiple_quality_tags(self): """Test parsing movie with multiple quality indicators.""" result = parse_movie("Avatar.2009.2160p.4K.UHD.BluRay.x265.10bit.mkv") assert result.title == "Avatar" assert result.year == 2009 assert "2160p" not in result.title assert "4K" not in result.title assert "UHD" not in result.title def test_parse_movie_with_web_dl(self): """Test parsing movie with WEB-DL tag.""" result = parse_movie("The Dark Knight (2008) WEB-DL 1080p.mkv") assert result.title == "The Dark Knight" assert result.year == 2008 assert "WEB-DL" not in result.title def test_parse_movie_with_hdtv(self): """Test parsing movie with HDTV tag.""" result = parse_movie("Movie Name 2015 HDTV 720p.avi") assert result.title == "Movie Name" assert result.year == 2015 assert "HDTV" not in result.title def test_parse_movie_with_dots_in_title(self): """Test parsing movie with dots in title.""" result = parse_movie("The.Lord.of.the.Rings.2001.mkv") assert result.title == "The Lord Of The Rings" assert result.year == 2001 def test_parse_movie_with_underscores(self): """Test parsing movie with underscores in title.""" result = parse_movie("Star_Wars_Episode_IV (1977).mp4") assert result.title == "Star Wars Episode Iv" assert result.year == 1977 def test_parse_movie_preserves_original_filename(self): """Test that original filename is preserved.""" original = "Complex.Movie.Name.2020.1080p.BluRay.x264.[RARBG].mkv" result = parse_movie(original) assert result.original_filename == original class TestNormalizeTitle: """Tests for title normalization.""" def test_normalize_removes_dots(self): """Test that dots are replaced with spaces.""" assert normalize_title("The.Matrix") == "The Matrix" def test_normalize_removes_underscores(self): """Test that underscores are replaced with spaces.""" assert normalize_title("Star_Wars") == "Star Wars" def test_normalize_removes_extra_whitespace(self): """Test that extra whitespace is removed.""" assert normalize_title("The Matrix Reloaded") == "The Matrix Reloaded" def test_normalize_applies_title_case(self): """Test that title case is applied.""" assert normalize_title("the matrix") == "The Matrix" assert normalize_title("THE MATRIX") == "The Matrix" def test_normalize_idempotence(self): """Test that normalizing multiple times produces same result.""" title = "The.Matrix.Reloaded" normalized_once = normalize_title(title) normalized_twice = normalize_title(normalized_once) assert normalized_once == normalized_twice class TestRemoveQualityTags: """Tests for quality tag removal.""" def test_remove_resolution_tags(self): """Test removal of resolution tags.""" assert "1080p" not in remove_quality_tags("Movie 1080p") assert "720p" not in remove_quality_tags("Movie 720p") assert "4K" not in remove_quality_tags("Movie 4K") def test_remove_source_tags(self): """Test removal of source tags.""" assert "BluRay" not in remove_quality_tags("Movie BluRay") assert "WEB-DL" not in remove_quality_tags("Movie WEB-DL") assert "HDTV" not in remove_quality_tags("Movie HDTV") def test_remove_codec_tags(self): """Test removal of codec tags.""" assert "x264" not in remove_quality_tags("Movie x264") assert "x265" not in remove_quality_tags("Movie x265") assert "HEVC" not in remove_quality_tags("Movie HEVC") def test_case_insensitive_removal(self): """Test that removal is case-insensitive.""" assert "bluray" not in remove_quality_tags("Movie bluray").lower() assert "BLURAY" not in remove_quality_tags("Movie BLURAY").upper() class TestRemoveReleaseGroups: """Tests for release group removal.""" def test_remove_bracketed_groups(self): """Test removal of bracketed release groups.""" assert "[RARBG]" not in remove_release_groups("Movie [RARBG]") assert "[YTS]" not in remove_release_groups("Movie [YTS]") def test_preserve_year_in_parentheses(self): """Test that years in parentheses are preserved.""" result = remove_release_groups("Movie (2020)") # Years in parentheses should be preserved - they're handled by the parser assert "(2020)" in result class TestSeriesParser: """Tests for series identity parsing.""" def test_parse_series_sxxeyy_format(self): """Test parsing series with SXXEYY format.""" result = parse_series("Breaking Bad S01E01.mkv") assert result.title == "Breaking Bad" assert result.season == 1 assert result.episodes == [1] assert result.confidence == 0.9 assert result.needs_review is False assert result.original_filename == "Breaking Bad S01E01.mkv" def test_parse_series_sxxeyy_lowercase(self): """Test parsing series with lowercase sxxeyy format.""" result = parse_series("Game of Thrones s02e05.mkv") assert result.title == "Game Of Thrones" assert result.season == 2 assert result.episodes == [5] assert result.confidence == 0.9 assert result.needs_review is False def test_parse_series_xxxyy_format(self): """Test parsing series with XXxYY format.""" result = parse_series("The Office 1x01.mp4") assert result.title == "The Office" assert result.season == 1 assert result.episodes == [1] assert result.confidence == 0.9 assert result.needs_review is False def test_parse_series_season_episode_format(self): """Test parsing series with Season X Episode Y format.""" result = parse_series("Friends Season 1 Episode 1.avi") assert result.title == "Friends" assert result.season == 1 assert result.episodes == [1] assert result.confidence == 0.7 assert result.needs_review is False def test_parse_series_multi_episode_dash(self): """Test parsing multi-episode file with dash separator.""" result = parse_series("The Wire S01E01-E02.mkv") assert result.title == "The Wire" assert result.season == 1 assert result.episodes == [1, 2] assert result.confidence == 0.9 assert result.needs_review is False def test_parse_series_multi_episode_no_dash(self): """Test parsing multi-episode file without dash.""" result = parse_series("Stranger Things S01E01E02.mkv") assert result.title == "Stranger Things" assert result.season == 1 assert result.episodes == [1, 2] assert result.confidence == 0.9 assert result.needs_review is False def test_parse_series_multi_episode_three_episodes(self): """Test parsing file with three episodes.""" result = parse_series("Show Name S02E01E02E03.mkv") assert result.title == "Show Name" assert result.season == 2 assert result.episodes == [1, 2, 3] assert result.confidence == 0.9 assert result.needs_review is False def test_parse_series_with_quality_tags(self): """Test parsing series with quality tags removed.""" result = parse_series("The Mandalorian S01E01 1080p WEB-DL x264.mkv") assert result.title == "The Mandalorian" assert result.season == 1 assert result.episodes == [1] assert "1080p" not in result.title assert "WEB-DL" not in result.title assert "x264" not in result.title def test_parse_series_with_release_group(self): """Test parsing series with release group removed.""" result = parse_series("Westworld S01E01 [RARBG].mkv") assert result.title == "Westworld" assert result.season == 1 assert result.episodes == [1] assert "RARBG" not in result.title def test_parse_series_with_dots_in_title(self): """Test parsing series with dots in title.""" result = parse_series("The.Walking.Dead.S05E10.mkv") assert result.title == "The Walking Dead" assert result.season == 5 assert result.episodes == [10] def test_parse_series_with_underscores(self): """Test parsing series with underscores in title.""" result = parse_series("Better_Call_Saul_S02E03.mp4") assert result.title == "Better Call Saul" assert result.season == 2 assert result.episodes == [3] def test_parse_series_without_season(self): """Test parsing series without extractable season.""" result = parse_series("Random Show Episode.mkv") assert result.title == "Random Show Episode" assert result.season is None assert result.episodes == [] assert result.needs_review is True assert result.confidence == 0.3 def test_parse_series_without_episode(self): """Test parsing series without extractable episode.""" result = parse_series("Some Series Name.mkv") assert result.title == "Some Series Name" assert result.season is None assert result.episodes == [] assert result.needs_review is True def test_parse_series_double_digit_season_episode(self): """Test parsing series with double-digit season and episode.""" result = parse_series("Doctor Who S12E10.mkv") assert result.title == "Doctor Who" assert result.season == 12 assert result.episodes == [10] assert result.needs_review is False def test_parse_series_complex_filename(self): """Test parsing series with complex filename.""" result = parse_series("The.Expanse.S03E05.1080p.BluRay.x264.[YTS].mkv") assert result.title == "The Expanse" assert result.season == 3 assert result.episodes == [5] assert "1080p" not in result.title assert "BluRay" not in result.title assert "YTS" not in result.title def test_parse_series_preserves_original_filename(self): """Test that original filename is preserved.""" original = "Complex.Series.Name.S01E01.1080p.WEB-DL.[RARBG].mkv" result = parse_series(original) assert result.original_filename == original def test_parse_series_with_multiple_quality_tags(self): """Test parsing series with multiple quality indicators.""" result = parse_series("Series.Name.S01E01.2160p.4K.UHD.WEB-DL.x265.10bit.mkv") assert result.title == "Series Name" assert result.season == 1 assert result.episodes == [1] assert "2160p" not in result.title assert "4K" not in result.title assert "UHD" not in result.title def test_parse_series_xxxyy_double_digits(self): """Test parsing series with XXxYY format and double digits.""" result = parse_series("Show Name 10x15.mp4") assert result.title == "Show Name" assert result.season == 10 assert result.episodes == [15] assert result.confidence == 0.9 class TestEpisodeGrouping: """Tests for episode grouping functionality.""" def test_group_episodes_by_title_and_season(self): """Test grouping episodes by normalized title and season.""" from vlm.parser import group_episodes episodes = [ parse_series("Breaking Bad S01E01.mkv"), parse_series("Breaking Bad S01E02.mkv"), parse_series("Breaking Bad S02E01.mkv"), parse_series("Game of Thrones S01E01.mkv"), ] groups = group_episodes(episodes) # Should have 3 groups: Breaking Bad S01, Breaking Bad S02, Game of Thrones S01 assert len(groups) == 3 assert ("Breaking Bad", 1) in groups assert ("Breaking Bad", 2) in groups assert ("Game Of Thrones", 1) in groups # Breaking Bad S01 should have 2 episodes assert len(groups[("Breaking Bad", 1)]) == 2 # Breaking Bad S02 should have 1 episode assert len(groups[("Breaking Bad", 2)]) == 1 # Game of Thrones S01 should have 1 episode assert len(groups[("Game Of Thrones", 1)]) == 1 def test_group_episodes_excludes_none_season(self): """Test that episodes with season=None are excluded from grouping.""" from vlm.parser import group_episodes episodes = [ parse_series("Breaking Bad S01E01.mkv"), parse_series("Random Show Episode.mkv"), # No season parse_series("Breaking Bad S01E02.mkv"), ] groups = group_episodes(episodes) # Should only have 1 group (Breaking Bad S01) assert len(groups) == 1 assert ("Breaking Bad", 1) in groups assert len(groups[("Breaking Bad", 1)]) == 2 def test_group_episodes_empty_list(self): """Test grouping with empty episode list.""" from vlm.parser import group_episodes groups = group_episodes([]) assert len(groups) == 0 assert groups == {} def test_group_episodes_single_episode(self): """Test grouping with single episode.""" from vlm.parser import group_episodes episodes = [parse_series("The Office S01E01.mkv")] groups = group_episodes(episodes) assert len(groups) == 1 assert ("The Office", 1) in groups assert len(groups[("The Office", 1)]) == 1 def test_group_episodes_same_title_different_seasons(self): """Test grouping episodes from same series but different seasons.""" from vlm.parser import group_episodes episodes = [ parse_series("Friends S01E01.mkv"), parse_series("Friends S01E02.mkv"), parse_series("Friends S02E01.mkv"), parse_series("Friends S02E02.mkv"), parse_series("Friends S03E01.mkv"), ] groups = group_episodes(episodes) # Should have 3 groups (one per season) assert len(groups) == 3 assert ("Friends", 1) in groups assert ("Friends", 2) in groups assert ("Friends", 3) in groups # Check episode counts per season assert len(groups[("Friends", 1)]) == 2 assert len(groups[("Friends", 2)]) == 2 assert len(groups[("Friends", 3)]) == 1 def test_group_episodes_normalized_titles(self): """Test that grouping uses normalized titles.""" from vlm.parser import group_episodes episodes = [ parse_series("The.Walking.Dead.S01E01.mkv"), # Dots parse_series("The_Walking_Dead_S01E02.mkv"), # Underscores parse_series("The Walking Dead S01E03.mkv"), # Spaces ] groups = group_episodes(episodes) # All should be grouped together under normalized title assert len(groups) == 1 assert ("The Walking Dead", 1) in groups assert len(groups[("The Walking Dead", 1)]) == 3 def test_group_episodes_multi_episode_files(self): """Test grouping with multi-episode files.""" from vlm.parser import group_episodes episodes = [ parse_series("Show Name S01E01.mkv"), parse_series("Show Name S01E02E03.mkv"), # Multi-episode parse_series("Show Name S01E04.mkv"), ] groups = group_episodes(episodes) # All should be in same group assert len(groups) == 1 assert ("Show Name", 1) in groups assert len(groups[("Show Name", 1)]) == 3 def test_group_episodes_preserves_original_objects(self): """Test that grouping preserves original SeriesIdentity objects.""" from vlm.parser import group_episodes episodes = [ parse_series("Breaking Bad S01E01.mkv"), parse_series("Breaking Bad S01E02.mkv"), ] groups = group_episodes(episodes) # Check that original objects are preserved grouped_episodes = groups[("Breaking Bad", 1)] assert grouped_episodes[0].original_filename == "Breaking Bad S01E01.mkv" assert grouped_episodes[1].original_filename == "Breaking Bad S01E02.mkv" assert grouped_episodes[0].episodes == [1] assert grouped_episodes[1].episodes == [2] # ============================================================================ # Property-Based Tests # ============================================================================ from hypothesis import given, strategies as st, assume, settings from vlm.parser import group_episodes import logging # Custom strategies for generating test data @st.composite def movie_filename_strategy(draw): """Generate movie filenames matching common patterns.""" # Generate a title (1-5 words) title_words = draw(st.lists( st.text( alphabet=st.characters(whitelist_categories=('Lu', 'Ll'), min_codepoint=65, max_codepoint=122), min_size=3, max_size=10 ), min_size=1, max_size=5 )) title = ' '.join(title_words) # Generate a year (1900-2030) year = draw(st.integers(min_value=1900, max_value=2030)) # Choose a pattern pattern = draw(st.sampled_from([ 'parentheses', # Title (Year) 'dot', # Title.Year 'dash', # Title - Year 'space' # Title Year ])) # Choose optional quality tags quality_tags = draw(st.lists( st.sampled_from(['1080p', '720p', '4K', 'BluRay', 'WEB-DL', 'HDTV', 'x264', 'x265']), max_size=3 )) # Choose optional release group release_group = draw(st.one_of( st.none(), st.sampled_from(['[RARBG]', '[YTS]', '[YIFY]']) )) # Choose extension ext = draw(st.sampled_from(['.mp4', '.mkv', '.avi', '.mov'])) # Build filename based on pattern if pattern == 'parentheses': filename = f"{title} ({year})" elif pattern == 'dot': filename = title.replace(' ', '.') + f".{year}" elif pattern == 'dash': filename = f"{title} - {year}" else: # space filename = f"{title} {year}" # Add quality tags if quality_tags: filename += ' ' + ' '.join(quality_tags) # Add release group if release_group: filename += ' ' + release_group # Add extension filename += ext return filename, title, year @st.composite def series_filename_strategy(draw): """Generate series filenames matching common patterns.""" # Generate a title (1-5 words) title_words = draw(st.lists( st.text( alphabet=st.characters(whitelist_categories=('Lu', 'Ll'), min_codepoint=65, max_codepoint=122), min_size=3, max_size=10 ), min_size=1, max_size=5 )) title = ' '.join(title_words) # Generate season and episode season = draw(st.integers(min_value=1, max_value=20)) episode = draw(st.integers(min_value=1, max_value=30)) # Choose a pattern pattern = draw(st.sampled_from([ 'SXXEYY', # S01E01 'sxxeyy', # s01e01 'XXxYY', # 1x01 'season_episode' # Season 1 Episode 1 ])) # Choose optional quality tags quality_tags = draw(st.lists( st.sampled_from(['1080p', '720p', '4K', 'WEB-DL', 'BluRay', 'x264']), max_size=2 )) # Choose extension ext = draw(st.sampled_from(['.mp4', '.mkv', '.avi'])) # Build filename based on pattern if pattern == 'SXXEYY': episode_part = f"S{season:02d}E{episode:02d}" elif pattern == 'sxxeyy': episode_part = f"s{season:02d}e{episode:02d}" elif pattern == 'XXxYY': episode_part = f"{season}x{episode:02d}" else: # season_episode episode_part = f"Season {season} Episode {episode}" # Build filename filename = f"{title} {episode_part}" # Add quality tags if quality_tags: filename += ' ' + ' '.join(quality_tags) # Add extension filename += ext return filename, title, season, episode @st.composite def ambiguous_filename_strategy(draw): """Generate filenames without clear season/episode patterns.""" # Generate random text without season/episode patterns words = draw(st.lists( st.text( alphabet=st.characters(whitelist_categories=('Lu', 'Ll'), min_codepoint=65, max_codepoint=122), min_size=3, max_size=10 ), min_size=1, max_size=5 )) filename = ' '.join(words) # Add extension ext = draw(st.sampled_from(['.mp4', '.mkv', '.avi'])) filename += ext return filename class TestParserProperties: """Property-based tests for the Identity Parser.""" # Feature: video-library-manager, Property 5: Movie parsing @settings(max_examples=100) @given(movie_filename_strategy()) def test_property_5_movie_parsing(self, movie_data): """For any filename matching common movie patterns, parser SHALL extract title and year. Validates: Requirements 2.1, 2.2 """ filename, expected_title, expected_year = movie_data result = parse_movie(filename) # Parser should extract a title assert result.title is not None assert len(result.title) > 0 # Parser should extract the year assert result.year == expected_year # Title should be normalized (no dots, underscores, proper case) assert '.' not in result.title assert '_' not in result.title # Quality tags should be removed from title quality_indicators = ['1080p', '720p', '4K', 'BluRay', 'WEB-DL', 'HDTV', 'x264', 'x265'] for tag in quality_indicators: assert tag not in result.title # Release groups should be removed from title assert '[RARBG]' not in result.title assert '[YTS]' not in result.title assert '[YIFY]' not in result.title # Original filename should be preserved assert result.original_filename == filename # Feature: video-library-manager, Property 6: Series parsing @settings(max_examples=100) @given(series_filename_strategy()) def test_property_6_series_parsing(self, series_data): """For any filename matching common series patterns, parser SHALL extract series title, season, and episodes. Validates: Requirements 3.1, 3.2 """ filename, expected_title, expected_season, expected_episode = series_data result = parse_series(filename) # Parser should extract a title assert result.title is not None assert len(result.title) > 0 # Parser should extract season and episode assert result.season == expected_season assert expected_episode in result.episodes # Title should be normalized assert '.' not in result.title assert '_' not in result.title # Quality tags should be removed from title quality_indicators = ['1080p', '720p', '4K', 'WEB-DL', 'BluRay', 'x264'] for tag in quality_indicators: assert tag not in result.title # Original filename should be preserved assert result.original_filename == filename # Feature: video-library-manager, Property 7: Title normalization idempotence @settings(max_examples=100) @given(st.text( alphabet=st.characters(whitelist_categories=('Lu', 'Ll', 'Nd', 'Zs'), min_codepoint=32, max_codepoint=126), min_size=1, max_size=100 )) def test_property_7_title_normalization_idempotence(self, title): """For any title string, normalizing multiple times SHALL produce same result. Validates: Requirements 2.6, 3.6 """ # Filter out empty strings after normalization assume(len(title.strip()) > 0) normalized_once = normalize_title(title) normalized_twice = normalize_title(normalized_once) normalized_thrice = normalize_title(normalized_twice) # All normalizations should produce the same result assert normalized_once == normalized_twice assert normalized_twice == normalized_thrice # Feature: video-library-manager, Property 8: Ambiguous filename flagging @settings(max_examples=100) @given(ambiguous_filename_strategy()) def test_property_8_ambiguous_filename_flagging(self, filename): """For any filename where season/episode cannot be extracted, parser SHALL mark as needs_review. Validates: Requirements 2.5, 3.5 """ # Ensure the filename doesn't accidentally match a pattern # by checking it doesn't contain common episode markers assume('S' not in filename.upper() or 'E' not in filename.upper()) assume('x' not in filename.lower()) assume('season' not in filename.lower()) assume('episode' not in filename.lower()) result = parse_series(filename) # If season or episodes cannot be extracted, should be marked for review if result.season is None or len(result.episodes) == 0: assert result.needs_review is True assert result.confidence <= 0.5 # Feature: video-library-manager, Property 9: Episode grouping @settings(max_examples=100) @given(st.lists(series_filename_strategy(), min_size=1, max_size=20)) def test_property_9_episode_grouping(self, series_data_list): """For any set of parsed episodes, grouping SHALL place episodes with identical normalized titles and seasons in same group. Validates: Requirements 3.7 """ # Parse all episodes episodes = [parse_series(filename) for filename, _, _, _ in series_data_list] # Group episodes groups = group_episodes(episodes) # Verify grouping correctness for (title, season), group_episodes_list in groups.items(): # All episodes in a group should have the same normalized title and season for episode in group_episodes_list: assert episode.title == title assert episode.season == season # Verify no episode is in multiple groups all_grouped_episodes = [] for group_episodes_list in groups.values(): all_grouped_episodes.extend(group_episodes_list) # Count episodes with valid season (should match grouped count) valid_episodes = [ep for ep in episodes if ep.season is not None] assert len(all_grouped_episodes) == len(valid_episodes) # Feature: video-library-manager, Property 51: Parsing error handling @settings(max_examples=100) @given(st.text( alphabet=st.characters(blacklist_categories=('Cc', 'Cs'), min_codepoint=32, max_codepoint=126), min_size=1, max_size=200 )) def test_property_51_parsing_error_handling(self, filename): """For any filename that cannot be parsed, system SHALL log error and mark file for review. Validates: Requirements 13.4 Note: This test verifies that the parser handles unparseable filenames gracefully by marking them for review, rather than crashing or producing invalid results. """ # Ensure filename has an extension if not any(filename.endswith(ext) for ext in ['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm', '.m4v']): filename += '.mkv' # Try parsing as movie movie_result = parse_movie(filename) # Parser should always return a valid MovieIdentity object assert movie_result is not None assert movie_result.title is not None assert movie_result.original_filename == filename # If year cannot be extracted, should be marked for review if movie_result.year is None: assert movie_result.needs_review is True # Try parsing as series series_result = parse_series(filename) # Parser should always return a valid SeriesIdentity object assert series_result is not None assert series_result.title is not None assert series_result.original_filename == filename # If season/episode cannot be extracted, should be marked for review if series_result.season is None or len(series_result.episodes) == 0: assert series_result.needs_review is True