"""Unit tests for the analysis engine. Tests series completeness analysis, duplicate detection, and quality comparison. """ import pytest from pathlib import Path from datetime import datetime from vlm.models import SeriesIdentity, SeasonCompleteness, MovieIdentity, VideoFile, DuplicateGroup from vlm.analysis import analyze_series_completeness, detect_duplicates, compare_quality class TestSeriesCompletenessAnalysis: """Test series completeness analysis functionality.""" def test_detect_single_gap(self): """Test detection of a single missing episode.""" episodes = [ SeriesIdentity("Show Name", 1, [1], 0.9, False, "Show.Name.S01E01.mkv"), SeriesIdentity("Show Name", 1, [2], 0.9, False, "Show.Name.S01E02.mkv"), SeriesIdentity("Show Name", 1, [4], 0.9, False, "Show.Name.S01E04.mkv"), SeriesIdentity("Show Name", 1, [5], 0.9, False, "Show.Name.S01E05.mkv"), ] result = analyze_series_completeness(episodes) assert len(result) == 1 assert result[0].series_title == "Show Name" assert result[0].season == 1 assert result[0].episodes_found == [1, 2, 4, 5] assert result[0].episodes_missing == [3] def test_detect_multiple_gaps(self): """Test detection of multiple missing episodes.""" episodes = [ SeriesIdentity("Show Name", 1, [1], 0.9, False, "Show.Name.S01E01.mkv"), SeriesIdentity("Show Name", 1, [3], 0.9, False, "Show.Name.S01E03.mkv"), SeriesIdentity("Show Name", 1, [5], 0.9, False, "Show.Name.S01E05.mkv"), SeriesIdentity("Show Name", 1, [7], 0.9, False, "Show.Name.S01E07.mkv"), ] result = analyze_series_completeness(episodes) assert len(result) == 1 assert result[0].episodes_found == [1, 3, 5, 7] assert result[0].episodes_missing == [2, 4, 6] def test_no_gaps_returns_empty(self): """Test that complete seasons are not included in results.""" episodes = [ SeriesIdentity("Show Name", 1, [1], 0.9, False, "Show.Name.S01E01.mkv"), SeriesIdentity("Show Name", 1, [2], 0.9, False, "Show.Name.S01E02.mkv"), SeriesIdentity("Show Name", 1, [3], 0.9, False, "Show.Name.S01E03.mkv"), ] result = analyze_series_completeness(episodes) assert len(result) == 0 def test_multi_season_independence(self): """Test that gap detection for one season doesn't affect others.""" episodes = [ # Season 1 - has gap at episode 2 SeriesIdentity("Show Name", 1, [1], 0.9, False, "Show.Name.S01E01.mkv"), SeriesIdentity("Show Name", 1, [3], 0.9, False, "Show.Name.S01E03.mkv"), # Season 2 - complete SeriesIdentity("Show Name", 2, [1], 0.9, False, "Show.Name.S02E01.mkv"), SeriesIdentity("Show Name", 2, [2], 0.9, False, "Show.Name.S02E02.mkv"), # Season 3 - has gap at episode 5 SeriesIdentity("Show Name", 3, [4], 0.9, False, "Show.Name.S03E04.mkv"), SeriesIdentity("Show Name", 3, [6], 0.9, False, "Show.Name.S03E06.mkv"), ] result = analyze_series_completeness(episodes) # Should have 2 results (seasons 1 and 3 with gaps) assert len(result) == 2 # Find season 1 result season1 = next(r for r in result if r.season == 1) assert season1.episodes_found == [1, 3] assert season1.episodes_missing == [2] # Find season 3 result season3 = next(r for r in result if r.season == 3) assert season3.episodes_found == [4, 6] assert season3.episodes_missing == [5] def test_multi_episode_files(self): """Test handling of multi-episode files.""" episodes = [ SeriesIdentity("Show Name", 1, [1, 2], 0.9, False, "Show.Name.S01E01-E02.mkv"), SeriesIdentity("Show Name", 1, [4], 0.9, False, "Show.Name.S01E04.mkv"), ] result = analyze_series_completeness(episodes) assert len(result) == 1 assert result[0].episodes_found == [1, 2, 4] assert result[0].episodes_missing == [3] def test_different_series_separate_analysis(self): """Test that different series are analyzed separately.""" episodes = [ # Series A - has gap SeriesIdentity("Series A", 1, [1], 0.9, False, "Series.A.S01E01.mkv"), SeriesIdentity("Series A", 1, [3], 0.9, False, "Series.A.S01E03.mkv"), # Series B - complete SeriesIdentity("Series B", 1, [1], 0.9, False, "Series.B.S01E01.mkv"), SeriesIdentity("Series B", 1, [2], 0.9, False, "Series.B.S01E02.mkv"), ] result = analyze_series_completeness(episodes) # Only Series A should be in results assert len(result) == 1 assert result[0].series_title == "Series A" assert result[0].episodes_missing == [2] def test_skip_episodes_without_season(self): """Test that episodes with season=None are excluded from analysis.""" episodes = [ SeriesIdentity("Show Name", 1, [1], 0.9, False, "Show.Name.S01E01.mkv"), SeriesIdentity("Show Name", 1, [2], 0.9, False, "Show.Name.S01E02.mkv"), SeriesIdentity("Show Name", None, [1], 0.3, True, "Show.Name.Episode.1.mkv"), ] result = analyze_series_completeness(episodes) # Should only analyze season 1, which is complete assert len(result) == 0 def test_skip_episodes_with_empty_episode_list(self): """Test that episodes with empty episode list are excluded from analysis.""" episodes = [ SeriesIdentity("Show Name", 1, [1], 0.9, False, "Show.Name.S01E01.mkv"), SeriesIdentity("Show Name", 1, [3], 0.9, False, "Show.Name.S01E03.mkv"), SeriesIdentity("Show Name", 1, [], 0.3, True, "Show.Name.S01.mkv"), ] result = analyze_series_completeness(episodes) # Should detect gap at episode 2 assert len(result) == 1 assert result[0].episodes_missing == [2] def test_non_sequential_start(self): """Test gap detection when episodes don't start at 1.""" episodes = [ SeriesIdentity("Show Name", 1, [5], 0.9, False, "Show.Name.S01E05.mkv"), SeriesIdentity("Show Name", 1, [6], 0.9, False, "Show.Name.S01E06.mkv"), SeriesIdentity("Show Name", 1, [8], 0.9, False, "Show.Name.S01E08.mkv"), ] result = analyze_series_completeness(episodes) # Should detect gap at episode 7 in range [5, 8] assert len(result) == 1 assert result[0].episodes_found == [5, 6, 8] assert result[0].episodes_missing == [7] def test_empty_input(self): """Test handling of empty episode list.""" result = analyze_series_completeness([]) assert len(result) == 0 def test_single_episode_no_gap(self): """Test that a single episode has no gaps.""" episodes = [ SeriesIdentity("Show Name", 1, [1], 0.9, False, "Show.Name.S01E01.mkv"), ] result = analyze_series_completeness(episodes) # Single episode has no gaps assert len(result) == 0 class TestDuplicateDetection: """Test duplicate detection functionality.""" def test_detect_movie_duplicates(self): """Test detection of duplicate movies with identical title and year.""" identities = [ MovieIdentity("The Matrix", 1999, 0.9, False, "The.Matrix.1999.1080p.mkv"), MovieIdentity("The Matrix", 1999, 0.9, False, "The.Matrix.1999.720p.mkv"), MovieIdentity("Inception", 2010, 0.9, False, "Inception.2010.mkv"), ] files = [ VideoFile( Path("/movies/The.Matrix.1999.1080p.mkv"), "The.Matrix.1999.1080p.mkv", 2000000000, datetime.now(), "movie", resolution="1920x1080", codec="h264" ), VideoFile( Path("/movies/The.Matrix.1999.720p.mkv"), "The.Matrix.1999.720p.mkv", 1000000000, datetime.now(), "movie", resolution="1280x720", codec="h264" ), VideoFile( Path("/movies/Inception.2010.mkv"), "Inception.2010.mkv", 1500000000, datetime.now(), "movie" ), ] result = detect_duplicates(identities, files) # Should find one duplicate group (The Matrix) assert len(result) == 1 assert isinstance(result[0].identity, MovieIdentity) assert result[0].identity.title == "The Matrix" assert result[0].identity.year == 1999 assert len(result[0].files) == 2 assert len(result[0].quality_comparison) == 2 def test_detect_series_duplicates(self): """Test detection of duplicate series episodes.""" identities = [ SeriesIdentity("Breaking Bad", 1, [1], 0.9, False, "Breaking.Bad.S01E01.1080p.mkv"), SeriesIdentity("Breaking Bad", 1, [1], 0.9, False, "Breaking.Bad.S01E01.720p.mkv"), SeriesIdentity("Breaking Bad", 1, [2], 0.9, False, "Breaking.Bad.S01E02.mkv"), ] files = [ VideoFile( Path("/series/Breaking.Bad.S01E01.1080p.mkv"), "Breaking.Bad.S01E01.1080p.mkv", 1500000000, datetime.now(), "series", resolution="1920x1080" ), VideoFile( Path("/series/Breaking.Bad.S01E01.720p.mkv"), "Breaking.Bad.S01E01.720p.mkv", 800000000, datetime.now(), "series", resolution="1280x720" ), VideoFile( Path("/series/Breaking.Bad.S01E02.mkv"), "Breaking.Bad.S01E02.mkv", 1200000000, datetime.now(), "series" ), ] result = detect_duplicates(identities, files) # Should find one duplicate group (S01E01) assert len(result) == 1 assert isinstance(result[0].identity, SeriesIdentity) assert result[0].identity.title == "Breaking Bad" assert result[0].identity.season == 1 assert 1 in result[0].identity.episodes assert len(result[0].files) == 2 def test_no_duplicates(self): """Test that unique files are not flagged as duplicates.""" identities = [ MovieIdentity("Movie A", 2020, 0.9, False, "Movie.A.2020.mkv"), MovieIdentity("Movie B", 2021, 0.9, False, "Movie.B.2021.mkv"), ] files = [ VideoFile(Path("/movies/Movie.A.2020.mkv"), "Movie.A.2020.mkv", 1000000000, datetime.now(), "movie"), VideoFile(Path("/movies/Movie.B.2021.mkv"), "Movie.B.2021.mkv", 1000000000, datetime.now(), "movie"), ] result = detect_duplicates(identities, files) assert len(result) == 0 def test_skip_movies_without_year(self): """Test that movies without year are excluded from duplicate detection.""" identities = [ MovieIdentity("Unknown Movie", None, 0.3, True, "Unknown.Movie.mkv"), MovieIdentity("Unknown Movie", None, 0.3, True, "Unknown.Movie.2.mkv"), ] files = [ VideoFile(Path("/movies/Unknown.Movie.mkv"), "Unknown.Movie.mkv", 1000000000, datetime.now(), "movie"), VideoFile(Path("/movies/Unknown.Movie.2.mkv"), "Unknown.Movie.2.mkv", 1000000000, datetime.now(), "movie"), ] result = detect_duplicates(identities, files) # Should not detect duplicates for files needing review assert len(result) == 0 def test_skip_series_without_season(self): """Test that series without season are excluded from duplicate detection.""" identities = [ SeriesIdentity("Unknown Show", None, [1], 0.3, True, "Unknown.Show.E01.mkv"), SeriesIdentity("Unknown Show", None, [1], 0.3, True, "Unknown.Show.Episode.1.mkv"), ] files = [ VideoFile(Path("/series/Unknown.Show.E01.mkv"), "Unknown.Show.E01.mkv", 1000000000, datetime.now(), "series"), VideoFile(Path("/series/Unknown.Show.Episode.1.mkv"), "Unknown.Show.Episode.1.mkv", 1000000000, datetime.now(), "series"), ] result = detect_duplicates(identities, files) assert len(result) == 0 def test_skip_series_with_empty_episodes(self): """Test that series with empty episode list are excluded.""" identities = [ SeriesIdentity("Show Name", 1, [], 0.3, True, "Show.Name.S01.mkv"), SeriesIdentity("Show Name", 1, [], 0.3, True, "Show.Name.Season.1.mkv"), ] files = [ VideoFile(Path("/series/Show.Name.S01.mkv"), "Show.Name.S01.mkv", 1000000000, datetime.now(), "series"), VideoFile(Path("/series/Show.Name.Season.1.mkv"), "Show.Name.Season.1.mkv", 1000000000, datetime.now(), "series"), ] result = detect_duplicates(identities, files) assert len(result) == 0 def test_quality_comparison_includes_all_metadata(self): """Test that quality comparison includes all available metadata.""" identities = [ MovieIdentity("Test Movie", 2020, 0.9, False, "Test.Movie.2020.1080p.mkv"), MovieIdentity("Test Movie", 2020, 0.9, False, "Test.Movie.2020.720p.mkv"), ] files = [ VideoFile( Path("/movies/Test.Movie.2020.1080p.mkv"), "Test.Movie.2020.1080p.mkv", 2000000000, datetime.now(), "movie", resolution="1920x1080", codec="h264", duration_seconds=7200.0, bitrate_kbps=5000 ), VideoFile( Path("/movies/Test.Movie.2020.720p.mkv"), "Test.Movie.2020.720p.mkv", 1000000000, datetime.now(), "movie", resolution="1280x720", codec="h264", duration_seconds=7200.0, bitrate_kbps=2500 ), ] result = detect_duplicates(identities, files) assert len(result) == 1 comparison = result[0].quality_comparison assert len(comparison) == 2 # Check first file comparison data assert comparison[0]['filename'] == "Test.Movie.2020.1080p.mkv" assert comparison[0]['size_bytes'] == 2000000000 assert comparison[0]['resolution'] == "1920x1080" assert comparison[0]['codec'] == "h264" assert comparison[0]['duration_seconds'] == 7200.0 assert comparison[0]['bitrate_kbps'] == 5000 # Check second file comparison data assert comparison[1]['filename'] == "Test.Movie.2020.720p.mkv" assert comparison[1]['size_bytes'] == 1000000000 assert comparison[1]['resolution'] == "1280x720" def test_multi_episode_file_duplicates(self): """Test duplicate detection for multi-episode files.""" identities = [ SeriesIdentity("Show", 1, [1, 2], 0.9, False, "Show.S01E01-E02.mkv"), SeriesIdentity("Show", 1, [1], 0.9, False, "Show.S01E01.mkv"), SeriesIdentity("Show", 1, [2], 0.9, False, "Show.S01E02.mkv"), ] files = [ VideoFile(Path("/series/Show.S01E01-E02.mkv"), "Show.S01E01-E02.mkv", 2000000000, datetime.now(), "series"), VideoFile(Path("/series/Show.S01E01.mkv"), "Show.S01E01.mkv", 1000000000, datetime.now(), "series"), VideoFile(Path("/series/Show.S01E02.mkv"), "Show.S01E02.mkv", 1000000000, datetime.now(), "series"), ] result = detect_duplicates(identities, files) # Should find duplicates for both E01 and E02 assert len(result) == 2 def test_different_years_not_duplicates(self): """Test that same title with different years are not duplicates.""" identities = [ MovieIdentity("The Thing", 1982, 0.9, False, "The.Thing.1982.mkv"), MovieIdentity("The Thing", 2011, 0.9, False, "The.Thing.2011.mkv"), ] files = [ VideoFile(Path("/movies/The.Thing.1982.mkv"), "The.Thing.1982.mkv", 1000000000, datetime.now(), "movie"), VideoFile(Path("/movies/The.Thing.2011.mkv"), "The.Thing.2011.mkv", 1000000000, datetime.now(), "movie"), ] result = detect_duplicates(identities, files) assert len(result) == 0 def test_different_seasons_not_duplicates(self): """Test that same series/episode in different seasons are not duplicates.""" identities = [ SeriesIdentity("Show", 1, [1], 0.9, False, "Show.S01E01.mkv"), SeriesIdentity("Show", 2, [1], 0.9, False, "Show.S02E01.mkv"), ] files = [ VideoFile(Path("/series/Show.S01E01.mkv"), "Show.S01E01.mkv", 1000000000, datetime.now(), "series"), VideoFile(Path("/series/Show.S02E01.mkv"), "Show.S02E01.mkv", 1000000000, datetime.now(), "series"), ] result = detect_duplicates(identities, files) assert len(result) == 0 class TestQualityComparison: """Test quality comparison functionality.""" def test_compare_quality_with_all_metadata(self): """Test quality comparison with all metadata available.""" files = [ VideoFile( Path("/test/file1.mkv"), "file1.mkv", 2000000000, datetime.now(), "movie", resolution="1920x1080", codec="h264", duration_seconds=7200.0, bitrate_kbps=5000 ), VideoFile( Path("/test/file2.mkv"), "file2.mkv", 1000000000, datetime.now(), "movie", resolution="1280x720", codec="h265", duration_seconds=7200.0, bitrate_kbps=2500 ), ] result = compare_quality(files) assert len(result) == 2 assert result[0]['filename'] == "file1.mkv" assert result[0]['size_bytes'] == 2000000000 assert result[0]['resolution'] == "1920x1080" assert result[0]['codec'] == "h264" assert result[0]['duration_seconds'] == 7200.0 assert result[0]['bitrate_kbps'] == 5000 assert result[1]['filename'] == "file2.mkv" assert result[1]['size_bytes'] == 1000000000 assert result[1]['resolution'] == "1280x720" assert result[1]['codec'] == "h265" def test_compare_quality_with_partial_metadata(self): """Test quality comparison when some metadata is missing.""" files = [ VideoFile( Path("/test/file1.mkv"), "file1.mkv", 2000000000, datetime.now(), "movie", resolution="1920x1080" # codec, duration, bitrate not available ), VideoFile( Path("/test/file2.mkv"), "file2.mkv", 1000000000, datetime.now(), "movie" # No optional metadata ), ] result = compare_quality(files) assert len(result) == 2 assert result[0]['filename'] == "file1.mkv" assert result[0]['size_bytes'] == 2000000000 assert result[0]['resolution'] == "1920x1080" assert 'codec' not in result[0] assert 'duration_seconds' not in result[0] assert 'bitrate_kbps' not in result[0] assert result[1]['filename'] == "file2.mkv" assert result[1]['size_bytes'] == 1000000000 assert 'resolution' not in result[1] assert 'codec' not in result[1] def test_compare_quality_empty_list(self): """Test quality comparison with empty file list.""" result = compare_quality([]) assert len(result) == 0 def test_compare_quality_single_file(self): """Test quality comparison with single file.""" files = [ VideoFile( Path("/test/file.mkv"), "file.mkv", 1500000000, datetime.now(), "movie", resolution="1920x1080", codec="h264" ), ] result = compare_quality(files) assert len(result) == 1 assert result[0]['filename'] == "file.mkv" assert result[0]['size_bytes'] == 1500000000