"""Tests for src/Matcher/file_classifier.py - FileClassifier.""" import os import sys from pathlib import Path from unittest.mock import patch, MagicMock sys.path.insert(0, str(Path(__file__).parent.parent)) import pytest from src.Matcher.file_classifier import FileClassifier, DEFAULT_VIDEO_EXTENSIONS class TestFileClassifierInit: def test_init_valid_folder(self, mock_video_files): fc = FileClassifier(str(mock_video_files)) assert fc.folder_path == mock_video_files assert len(fc.video_files) > 0 def test_init_nonexistent_folder(self, tmp_path): with pytest.raises(FileNotFoundError, match="Folder not found"): FileClassifier(str(tmp_path / "nope")) def test_init_file_not_dir(self, tmp_path): f = tmp_path / "file.txt" f.write_text("hi") fc = FileClassifier(str(f)) assert len(fc.video_files) == 0 def test_init_custom_extensions(self, tmp_path): (tmp_path / "video.mp4").write_bytes(b"\x00" * 100) (tmp_path / "video.mkv").write_bytes(b"\x00" * 100) fc = FileClassifier(str(tmp_path), video_extensions=[".mp4"]) assert len(fc.video_files) == 1 assert fc.video_files[0].name == "video.mp4" def test_init_fallback_ratio(self, mock_video_files): fc = FileClassifier(str(mock_video_files), fallback_duration_minutes_per_gb=25.0) assert fc.fallback_ratio == 25.0 def test_init_filters_macos_resource_fork(self, tmp_path): (tmp_path / "._hidden.mkv").write_bytes(b"\x00" * 100) (tmp_path / ".dot.mkv").write_bytes(b"\x00" * 100) (tmp_path / "visible.mkv").write_bytes(b"\x00" * 100) fc = FileClassifier(str(tmp_path)) assert len(fc.video_files) == 1 assert fc.video_files[0].name == "visible.mkv" def test_init_deduplicates(self, tmp_path): (tmp_path / "dup.mkv").write_bytes(b"\x00" * 100) fc = FileClassifier(str(tmp_path), video_extensions=[".mkv"]) assert len(fc.video_files) == 1 def test_init_empty_folder(self, tmp_path): fc = FileClassifier(str(tmp_path)) assert fc.video_files == [] class TestResolveExtensions: def test_resolve_default(self, mock_video_files): fc = FileClassifier(str(mock_video_files)) assert ".mkv" in fc.video_extensions def test_resolve_custom(self, mock_video_files): fc = FileClassifier(str(mock_video_files), video_extensions=["mp4", "avi"]) assert ".mp4" in fc.video_extensions assert ".avi" in fc.video_extensions assert ".mkv" not in fc.video_extensions def test_resolve_env_var(self, mock_video_files, monkeypatch): monkeypatch.setenv("VIDEO_EXTENSIONS", "webm,mp4") fc = FileClassifier(str(mock_video_files)) assert "webm" in fc.video_extensions assert "mp4" in fc.video_extensions class TestResolveFallbackRatio: def test_explicit_ratio(self, mock_video_files): fc = FileClassifier(str(mock_video_files), fallback_duration_minutes_per_gb=30.0) assert fc.fallback_ratio == 30.0 def test_env_ratio(self, mock_video_files, monkeypatch): monkeypatch.setenv("FALLBACK_DURATION_RATIO", "33.0") fc = FileClassifier(str(mock_video_files)) assert fc.fallback_ratio == 33.0 def test_env_ratio_invalid(self, mock_video_files, monkeypatch): monkeypatch.setenv("FALLBACK_DURATION_RATIO", "abc") fc = FileClassifier(str(mock_video_files)) assert fc.fallback_ratio == 45.0 class TestFileAnalysis: def test_get_file_size(self, mock_video_files): fc = FileClassifier(str(mock_video_files)) size = fc._get_file_size(mock_video_files / "Disc 1_t01.mkv") assert size == int(2 * 1024) def test_analyze_files_structure(self, mock_video_files): fc = FileClassifier(str(mock_video_files)) assert len(fc.file_info) > 0 info = fc.file_info[0] assert "path" in info assert "name" in info assert "size_bytes" in info assert "size_mb" in info assert "size_gb" in info assert "duration_minutes" in info def test_calculate_stats(self, mock_video_files): fc = FileClassifier(str(mock_video_files)) stats = fc._calculate_stats() assert stats["count"] == len(fc.file_info) assert "avg_size_bytes" in stats assert "avg_duration_minutes" in stats assert "median_size_bytes" in stats def test_calculate_stats_empty(self, tmp_path): fc = FileClassifier(str(tmp_path)) stats = fc._calculate_stats() assert stats == {} class TestClassifyFiles: def test_classify_no_files(self, tmp_path): fc = FileClassifier(str(tmp_path)) episodes, extras = fc.classify_files() assert episodes == [] assert extras == [] def test_classify_with_tvdb(self, mock_video_files, mock_tvdb_episodes): fc = FileClassifier(str(mock_video_files)) episodes, extras = fc.classify_files( expected_episode_count=6, tvdb_episodes=mock_tvdb_episodes ) assert len(episodes) <= 6 assert len(episodes) + len(extras) == len(fc.file_info) def test_classify_by_stats(self, mock_video_files): fc = FileClassifier(str(mock_video_files)) episodes, extras = fc.classify_files(expected_episode_count=6) assert len(episodes) <= 6 assert len(episodes) > 0 def test_classify_extra_small_file(self, mock_video_files): fc = FileClassifier(str(mock_video_files)) episodes, extras = fc.classify_files() extra_names = [e["name"] for e in extras] assert "extra_short.mkv" in extra_names def test_classify_expected_count(self, mock_video_files, mock_tvdb_episodes): fc = FileClassifier(str(mock_video_files)) episodes, extras = fc.classify_files( expected_episode_count=3, tvdb_episodes=mock_tvdb_episodes ) assert len(episodes) <= 3 class TestVideoDuration: @patch("src.Matcher.file_classifier.MediaInfo", None) def test_duration_fallback_no_mediainfo(self, mock_video_files): fc = FileClassifier( str(mock_video_files), fallback_duration_minutes_per_gb=45.0 ) target = mock_video_files / "Disc 1_t01.mkv" dur = fc._get_video_duration(target) expected = (target.stat().st_size / (1024**3)) * 45.0 assert abs(dur - expected) < 0.001 assert dur > 0 def test_duration_fallback_custom_ratio(self, mock_video_files): with patch("src.Matcher.file_classifier.MediaInfo", None): fc = FileClassifier( str(mock_video_files), fallback_duration_minutes_per_gb=25.0 ) target = mock_video_files / "Disc 1_t01.mkv" dur = fc._get_video_duration(target) expected = (target.stat().st_size / (1024**3)) * 25.0 assert abs(dur - expected) < 0.001 assert dur > 0 class TestPrintAnalysis: def test_print_analysis_has_files(self, mock_video_files, capsys): fc = FileClassifier(str(mock_video_files)) fc.print_analysis() out = capsys.readouterr().out assert "File Analysis" in out assert "Total files" in out def test_print_analysis_no_files(self, tmp_path, capsys): fc = FileClassifier(str(tmp_path)) fc.print_analysis() out = capsys.readouterr().out assert "No video files found" in out