diff options
| author | Warry <FunWarry@users.noreply.github.com> | 2025-12-03 11:41:18 +0100 |
|---|---|---|
| committer | Warry <FunWarry@users.noreply.github.com> | 2025-12-03 11:41:18 +0100 |
| commit | c1d40619aa2aa44f5d967af9b5f47d040d6dc7dd (patch) | |
| tree | ad05969f0bae4cfe68e3e5baf9ea6a8954fa626a /tests/test_download_duplicate_prevention.py | |
| parent | 85f7e33b28985afd62a99d64b3e7a6883d62b072 (diff) | |
| parent | e89be804fd8fdbee1638f0cdc055eb095206ef8c (diff) | |
| download | tidal-dl-ng-c1d40619aa2aa44f5d967af9b5f47d040d6dc7dd.tar.gz | |
Merge branch 'feature/json-downloaded' into fusion-refac-&-info
# Conflicts:
# .gitignore
# README.md
# tidal_dl_ng/gui.py
Diffstat (limited to 'tests/test_download_duplicate_prevention.py')
| -rw-r--r-- | tests/test_download_duplicate_prevention.py | 394 |
1 files changed, 394 insertions, 0 deletions
diff --git a/tests/test_download_duplicate_prevention.py b/tests/test_download_duplicate_prevention.py new file mode 100644 index 0000000..d6caee0 --- /dev/null +++ b/tests/test_download_duplicate_prevention.py @@ -0,0 +1,394 @@ +""" +test_download_duplicate_prevention.py + +Test suite for download duplicate prevention functionality. + +Tests cover: +- Duplicate prevention during downloads +- Integration with HistoryService +- Skipped item logging +- Settings toggle behavior +""" + +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest +from tidalapi import Quality, Track + +from tidal_dl_ng.download import Download +from tidal_dl_ng.history import HistoryService + + +@pytest.fixture +def mock_tidal(): + """Create a mock Tidal object for testing. + + Returns: + Mock Tidal object with session. + """ + mock = MagicMock() + mock.session = MagicMock() + mock.session.audio_quality = Quality.high_lossless + mock.switch_to_atmos_session = MagicMock(return_value=True) + mock.restore_normal_session = MagicMock(return_value=True) + mock.stream_lock = MagicMock() + mock.stream_lock.__enter__ = MagicMock(return_value=None) + mock.stream_lock.__exit__ = MagicMock(return_value=None) + mock.settings = MagicMock() + mock.settings.data = MagicMock() + return mock + + +@pytest.fixture +def mock_logger(): + """Create a mock logger for testing. + + Returns: + Mock logger object. + """ + logger = MagicMock() + logger.info = MagicMock() + logger.debug = MagicMock() + logger.warning = MagicMock() + logger.error = MagicMock() + return logger + + +@pytest.fixture +def temp_download_path(tmp_path: Path) -> Path: + """Create a temporary download directory. + + Args: + tmp_path: pytest fixture providing temporary directory. + + Returns: + Path to temporary download directory. + """ + download_dir = tmp_path / "downloads" + download_dir.mkdir() + return download_dir + + +@pytest.fixture +def history_service_with_temp(tmp_path: Path, monkeypatch) -> HistoryService: + """Create a HistoryService with temporary storage. + + Args: + tmp_path: pytest fixture providing temporary directory. + monkeypatch: pytest monkeypatch fixture. + + Returns: + Configured HistoryService instance. + """ + config_path = tmp_path / "config" + config_path.mkdir(parents=True, exist_ok=True) + + monkeypatch.setattr("tidal_dl_ng.history.path_config_base", lambda: str(config_path)) + + # Reset singleton + if hasattr(HistoryService, "_instances"): + HistoryService._instances = {} + + return HistoryService() + + +@pytest.fixture +def download_instance(mock_tidal, mock_logger, temp_download_path, history_service_with_temp): + """Create a Download instance for testing. + + Args: + mock_tidal: Mock Tidal object. + mock_logger: Mock logger. + temp_download_path: Temporary download directory. + history_service_with_temp: HistoryService with temp storage. + + Returns: + Configured Download instance. + """ + with patch("tidal_dl_ng.download.HistoryService", return_value=history_service_with_temp): + download = Download( + tidal_obj=mock_tidal, path_base=str(temp_download_path), fn_logger=mock_logger, skip_existing=False + ) + return download + + +class TestDuplicatePreventionInDownload: + """Test duplicate prevention during download operations.""" + + def test_skip_download_when_track_in_history_and_prevention_enabled(self, download_instance: Download, mock_logger): + """Test that download is skipped when track is in history and prevention is enabled.""" + # Add track to history + track_id = "123456" + download_instance.history_service.add_track_to_history(track_id) + download_instance.history_service.update_settings(preventDuplicates=True) + + # Create mock track + mock_track = MagicMock(spec=Track) + mock_track.id = int(track_id) + mock_track.name = "Test Track" + mock_track.available = True + mock_track.album = MagicMock() + mock_track.album.name = "Test Album" + + # Attempt download + with patch.object(download_instance.session, "track", return_value=mock_track): + result, path = download_instance.item(file_template="{artist_name} - {track_title}", media=mock_track) + + # Should be skipped (result False means skipped) + assert result is False + # Logger should have logged the skip message + assert mock_logger.info.called + skip_call_found = any( + "Skipped item" in str(call) or "already in history" in str(call) for call in mock_logger.info.call_args_list + ) + assert skip_call_found + + def test_download_proceeds_when_prevention_disabled(self, download_instance: Download, mock_logger): + """Test that download proceeds when prevention is disabled.""" + # Add track to history but disable prevention + track_id = "789012" + download_instance.history_service.add_track_to_history(track_id) + download_instance.history_service.update_settings(preventDuplicates=False) + + # Create mock track + mock_track = MagicMock(spec=Track) + mock_track.id = int(track_id) + mock_track.name = "Test Track 2" + mock_track.available = True + + # Mock the download process to avoid actual download + with ( + patch.object(download_instance, "_validate_and_prepare_media", return_value=mock_track), + patch.object(download_instance, "_prepare_file_paths_and_skip_logic") as mock_paths, + ): + + mock_paths.return_value = (Path("/fake/path.flac"), ".flac", False, False) + + # The download should not be skipped by duplicate prevention + # (it will still fail later in the chain, but that's okay for this test) + result = download_instance.history_service.should_skip_download(track_id) + + # Should NOT be skipped + assert result is False + + def test_download_proceeds_when_track_not_in_history(self, download_instance: Download): + """Test that download proceeds when track is not in history.""" + track_id = "999999" + download_instance.history_service.update_settings(preventDuplicates=True) + + # Track not in history + result = download_instance.history_service.should_skip_download(track_id) + + assert result is False + + +class TestHistoryLoggingMessages: + """Test logging messages for skipped downloads.""" + + def test_skip_message_format(self, download_instance: Download, mock_logger): + """Test that skip message follows the correct format.""" + track_id = "111222" + download_instance.history_service.add_track_to_history(track_id) + download_instance.history_service.update_settings(preventDuplicates=True) + + mock_track = MagicMock(spec=Track) + mock_track.id = int(track_id) + mock_track.name = "Amazing Song" + mock_track.artist = MagicMock() + mock_track.artist.name = "Great Artist" + mock_track.available = True + mock_track.album = MagicMock() + + with patch.object(download_instance.session, "track", return_value=mock_track): + download_instance.item(file_template="{artist_name} - {track_title}", media=mock_track) + + # Check that info logger was called + assert mock_logger.info.called + + # Verify message contains expected parts + log_calls = [str(call) for call in mock_logger.info.call_args_list] + skip_message_found = any("Skipped item" in msg and "already in history" in msg for msg in log_calls) + assert skip_message_found + + +class TestHistoryAfterDownload: + """Test that successful downloads are added to history.""" + + @patch("tidal_dl_ng.download.Download._download_and_process_media") + @patch("tidal_dl_ng.download.Download._adjust_quality_settings") + @patch("tidal_dl_ng.download.Download._perform_post_processing") + def test_track_added_to_history_after_successful_download( + self, mock_post_process, mock_quality, mock_download_process, download_instance: Download + ): + """Test that a track is added to history after successful download.""" + # Mock successful download + mock_download_process.return_value = True + mock_quality.return_value = (None, None) + + track_id = "333444" + mock_track = MagicMock(spec=Track) + mock_track.id = int(track_id) + mock_track.name = "New Track" + mock_track.available = True + mock_track.album = MagicMock() + + with ( + patch.object(download_instance, "_validate_and_prepare_media", return_value=mock_track), + patch.object(download_instance, "_prepare_file_paths_and_skip_logic") as mock_paths, + ): + + mock_paths.return_value = (Path("/fake/path.flac"), ".flac", False, False) + + download_instance.item( + file_template="{artist_name} - {track_title}", + media=mock_track, + source_type="playlist", + source_id="pl-123", + source_name="Test Playlist", + ) + + # Track should be in history + assert download_instance.history_service.is_downloaded(track_id) + + # Check metadata + track_info = download_instance.history_service.get_track_info(track_id) + assert track_info is not None + assert track_info["sourceType"] == "playlist" + assert track_info["sourceId"] == "pl-123" + + @patch("tidal_dl_ng.download.Download._download_and_process_media") + def test_track_not_added_on_failed_download(self, mock_download_process, download_instance: Download): + """Test that track is NOT added to history if download fails.""" + # Mock failed download + mock_download_process.return_value = False + + track_id = "555666" + mock_track = MagicMock(spec=Track) + mock_track.id = int(track_id) + mock_track.available = True + mock_track.album = MagicMock() + + with ( + patch.object(download_instance, "_validate_and_prepare_media", return_value=mock_track), + patch.object(download_instance, "_prepare_file_paths_and_skip_logic") as mock_paths, + patch.object(download_instance, "_adjust_quality_settings") as mock_quality, + ): + + mock_paths.return_value = (Path("/fake/path.flac"), ".flac", False, False) + mock_quality.return_value = (None, None) + + download_instance.item(file_template="{artist_name} - {track_title}", media=mock_track) + + # Track should NOT be in history + assert not download_instance.history_service.is_downloaded(track_id) + + +class TestSettingsIntegration: + """Test integration with settings toggle.""" + + def test_settings_toggle_affects_duplicate_check(self, download_instance: Download): + """Test that toggling settings immediately affects duplicate checking.""" + track_id = "777888" + download_instance.history_service.add_track_to_history(track_id) + + # Enable prevention + download_instance.history_service.update_settings(preventDuplicates=True) + assert download_instance.history_service.should_skip_download(track_id) is True + + # Disable prevention + download_instance.history_service.update_settings(preventDuplicates=False) + assert download_instance.history_service.should_skip_download(track_id) is False + + # Re-enable + download_instance.history_service.update_settings(preventDuplicates=True) + assert download_instance.history_service.should_skip_download(track_id) is True + + def test_settings_persist_across_service_instances(self, tmp_path: Path, monkeypatch): + """Test that settings persist across service instances.""" + config_path = tmp_path / "config" + config_path.mkdir(parents=True, exist_ok=True) + + monkeypatch.setattr("tidal_dl_ng.history.path_config_base", lambda: str(config_path)) + + # Create first instance and disable prevention + if hasattr(HistoryService, "_instances"): + HistoryService._instances = {} + + service1 = HistoryService() + service1.update_settings(preventDuplicates=False) + + # Create second instance (simulating app restart) + HistoryService._instances = {} + service2 = HistoryService() + + # Settings should persist + assert service2.get_settings()["preventDuplicates"] is False + + +class TestVideoNotAffectedByHistory: + """Test that video downloads are not affected by history (only tracks).""" + + def test_video_download_not_checked_against_history(self, download_instance: Download): + """Test that videos are not checked against download history.""" + from tidalapi import Video + + # This test verifies the type check in the duplicate prevention code + # Videos should bypass the history check entirely + + mock_video = MagicMock(spec=Video) + mock_video.id = 123456 + mock_video.name = "Test Video" + mock_video.available = True + + # Even if we somehow had a video ID in history, it shouldn't matter + # because the check is `isinstance(media, Track)` + download_instance.history_service.add_track_to_history("123456") + + with ( + patch.object(download_instance, "_validate_and_prepare_media", return_value=mock_video), + patch.object(download_instance, "_prepare_file_paths_and_skip_logic") as mock_paths, + patch.object(download_instance.history_service, "should_skip_download") as mock_skip, + ): + mock_paths.return_value = (Path("/fake.mp4"), ".mp4", False, False) + + # The key is that should_skip_download is never called for videos + # Expected to fail, we're just checking the skip wasn't called + from contextlib import suppress + + with suppress(Exception): + download_instance.item(file_template="{artist_name} - {track_title}", media=mock_video) + + # should_skip_download should NOT have been called for video + assert not mock_skip.called + + +class TestBulkDownloadWithHistory: + """Test history tracking in bulk download scenarios.""" + + @patch("tidal_dl_ng.download.Download.item") + def test_multiple_tracks_added_to_history(self, mock_item_download, download_instance: Download): + """Test that multiple tracks in a bulk download are tracked.""" + # Simulate successful downloads + mock_item_download.return_value = (True, Path("/fake/path.flac")) + + track_ids = ["t1", "t2", "t3", "t4", "t5"] + + # Manually add tracks to history (simulating what item() would do) + for track_id in track_ids: + download_instance.history_service.add_track_to_history( + track_id=track_id, source_type="album", source_id="al-123", source_name="Test Album" + ) + + # All tracks should be in history + for track_id in track_ids: + assert download_instance.history_service.is_downloaded(track_id) + + # Should be grouped under same album + grouped = download_instance.history_service.get_history_by_source() + assert "album_al-123" in grouped + assert len(grouped["album_al-123"]) == 5 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) |
