""" Song validation utilities for checking if songs should be downloaded. Centralizes song validation logic to eliminate code duplication. """ from pathlib import Path from typing import Any, Dict, List, Optional, Tuple from karaoke_downloader.file_utils import check_file_exists_with_patterns from karaoke_downloader.tracking_manager import TrackingManager class SongValidator: """ Centralized song validation logic for checking if songs should be downloaded. """ def __init__(self, tracker: TrackingManager, downloads_dir: Path): """ Initialize the song validator. Args: tracker: Tracking manager instance downloads_dir: Base downloads directory """ self.tracker = tracker self.downloads_dir = downloads_dir def should_skip_song( self, artist: str, title: str, channel_name: str, video_id: Optional[str] = None, video_title: Optional[str] = None, server_songs: Optional[Dict[str, Any]] = None, server_duplicates_tracking: Optional[Dict[str, Any]] = None, force_download: bool = False, ) -> Tuple[bool, Optional[str], int]: """ Check if a song should be skipped based on multiple criteria. Performs checks in order: 1. Already downloaded (tracking) 2. File exists on filesystem 3. Already on server 4. Previously failed download (bad file) Args: artist: Song artist name title: Song title channel_name: Channel name video_id: YouTube video ID (optional) video_title: YouTube video title (optional) server_songs: Server songs data (optional) server_duplicates_tracking: Server duplicates tracking (optional) force_download: If True, bypass all validation checks and force download Returns: Tuple of (should_skip, reason, total_filtered) """ # If force download is enabled, skip all validation checks if force_download: return False, None, 0 total_filtered = 0 # Check 1: Already downloaded by this system if self.tracker.is_song_downloaded(artist, title, channel_name, video_id): return True, "already downloaded", total_filtered # Check 2: File already exists on filesystem file_exists, _ = check_file_exists_with_patterns( self.downloads_dir, channel_name, artist, title ) if file_exists: return True, "file exists", total_filtered # Check 3: Already on server (if server data provided) if server_songs is not None and server_duplicates_tracking is not None: from karaoke_downloader.server_manager import ( check_and_mark_server_duplicate, ) if check_and_mark_server_duplicate( server_songs, server_duplicates_tracking, artist, title, video_title, channel_name, ): total_filtered += 1 return True, "on server", total_filtered # Check 4: Previously failed download (bad file) if self.tracker.is_song_failed(artist, title, channel_name, video_id): return True, "previously failed", total_filtered return False, None, total_filtered def mark_song_failed( self, artist: str, title: str, video_id: Optional[str], channel_name: str, error_message: str, ) -> None: """ Mark a song as failed in tracking. Args: artist: Song artist name title: Song title video_id: YouTube video ID (optional) channel_name: Channel name error_message: Error message to record """ self.tracker.mark_song_failed( artist, title, video_id, channel_name, error_message ) print(f"🏷️ Marked song as failed: {artist} - {title}") def handle_download_failure( self, artist: str, title: str, video_id: Optional[str], channel_name: str, error_type: str, error_details: str = "", ) -> None: """ Handle download failures with consistent error formatting. Args: artist: Song artist name title: Song title video_id: YouTube video ID (optional) channel_name: Channel name error_type: Type of error (e.g., "yt-dlp failed", "file verification failed") error_details: Additional error details """ error_msg = f"{error_type}" if error_details: error_msg += f": {error_details}" self.mark_song_failed(artist, title, video_id, channel_name, error_msg) def create_song_validator( tracker: TrackingManager, downloads_dir: Path ) -> SongValidator: """ Factory function to create a song validator instance. Args: tracker: Tracking manager instance downloads_dir: Base downloads directory Returns: SongValidator instance """ return SongValidator(tracker, downloads_dir)