summaryrefslogtreecommitdiff
path: root/tests/test_download_duplicate_prevention.py
diff options
context:
space:
mode:
authorWarry <FunWarry@users.noreply.github.com>2025-12-03 11:41:18 +0100
committerWarry <FunWarry@users.noreply.github.com>2025-12-03 11:41:18 +0100
commitc1d40619aa2aa44f5d967af9b5f47d040d6dc7dd (patch)
treead05969f0bae4cfe68e3e5baf9ea6a8954fa626a /tests/test_download_duplicate_prevention.py
parent85f7e33b28985afd62a99d64b3e7a6883d62b072 (diff)
parente89be804fd8fdbee1638f0cdc055eb095206ef8c (diff)
downloadtidal-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.py394
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"])