"""Episode checker for detecting and downloading new episodes automatically""" import logging from typing import List, Optional, Dict from datetime import datetime from app.watchlist import watchlist_manager, WatchlistManager from app.models import DownloadRequest, DownloadTask, DownloadStatus from app.models.watchlist import ( WatchlistItem, WatchlistSettings, NewEpisodeInfo, AutoDownloadResult ) logger = logging.getLogger(__name__) class EpisodeChecker: """Checks for new episodes and downloads them automatically""" def __init__(self, wlm: Optional[WatchlistManager] = None): self.wlm = wlm or watchlist_manager self.download_manager = None # Will be set by main.py def set_download_manager(self, download_manager): """Set the download manager (called by main.py to avoid circular import)""" self.download_manager = download_manager async def check_anime(self, item: WatchlistItem) -> List[NewEpisodeInfo]: """ Check for new episodes of a specific anime Args: item: WatchlistItem to check Returns: List of NewEpisodeInfo objects """ try: logger.info(f"Checking for new episodes: {item.anime_title}") # Import here to avoid circular imports from app.downloaders import get_downloader from urllib.parse import unquote # Decode URL if it's encoded (handles double-encoded URLs) anime_url = item.anime_url try: # Try to decode - if already decoded, this will be a no-op decoded_url = unquote(anime_url) # Handle double encoding if '%' in decoded_url: decoded_url = unquote(decoded_url) anime_url = decoded_url except Exception as e: logger.warning(f"Could not decode URL: {e}, using original") # Get the appropriate downloader downloader = get_downloader(anime_url) if not downloader: logger.error(f"No downloader found for URL: {anime_url}") return [] # Get episodes list episodes = await downloader.get_episodes(anime_url, item.lang) if not episodes: logger.warning(f"No episodes found for {item.anime_title}") return [] # Filter new episodes new_episodes = [] for ep in episodes: # Handle both 'episode' (from anime-sama) and 'episode_number' keys ep_num_raw = ep.get('episode_number') or ep.get('episode') # Convert to int (handles string episode numbers like "01", "02") try: ep_num = int(str(ep_num_raw).lstrip('0') or '0') except (ValueError, TypeError): ep_num = 0 if ep_num > item.last_episode_downloaded: new_episodes.append(NewEpisodeInfo( episode_number=ep_num, episode_title=ep.get('title'), episode_url=ep['url'], season_number=ep.get('season'), anime_title=item.anime_title, provider_id=item.provider_id )) if new_episodes: logger.info(f"Found {len(new_episodes)} new episodes for {item.anime_title}") else: logger.info(f"No new episodes for {item.anime_title}") return new_episodes except Exception as e: logger.error(f"Error checking anime {item.anime_title}: {e}", exc_info=True) return [] async def download_new_episodes( self, item: WatchlistItem, episodes: List[NewEpisodeInfo] ) -> AutoDownloadResult: """ Download new episodes for a watchlist item Args: item: WatchlistItem episodes: List of new episodes to download Returns: AutoDownloadResult with download status """ result = AutoDownloadResult( watchlist_item_id=item.id, anime_title=item.anime_title, new_episodes_found=len(episodes), checked_at=datetime.now() ) if not episodes: return result # Get settings settings = self.wlm.get_settings() if not settings.auto_download_enabled: logger.info(f"Auto-download disabled, skipping {len(episodes)} episodes") return result try: # Import here to avoid circular imports from app.downloaders import get_downloader from urllib.parse import unquote # Decode URL if it's encoded anime_url = item.anime_url try: decoded_url = unquote(anime_url) if '%' in decoded_url: decoded_url = unquote(decoded_url) anime_url = decoded_url except Exception: pass downloader = get_downloader(anime_url) # Download each new episode for ep_info in episodes: try: logger.info(f"Downloading {item.anime_title} Episode {ep_info.episode_number}") # Get download link - episode_url may be pipe-separated with multiple sources download_link, filename = await downloader.get_download_link(ep_info.episode_url) # Create download task request = DownloadRequest(url=download_link, filename=filename) task = self.download_manager.create_task(request) if task: await self.download_manager.start_download(task.id) result.episodes_downloaded.append(ep_info.episode_number) logger.info(f"Started download: {filename}") else: result.episodes_failed.append((ep_info.episode_number, "Failed to create download task")) except Exception as e: error_msg = str(e) logger.error(f"Error downloading episode {ep_info.episode_number}: {error_msg}") result.episodes_failed.append((ep_info.episode_number, error_msg)) # Update watchlist with last episode downloaded if result.episodes_downloaded: last_ep = max(result.episodes_downloaded) self.wlm.update_check_time(item.id, last_ep) except Exception as e: logger.error(f"Error in download_new_episodes: {e}", exc_info=True) return result async def check_and_download(self, item: WatchlistItem) -> AutoDownloadResult: """ Check for new episodes and download them if auto_download is enabled Args: item: WatchlistItem to check Returns: AutoDownloadResult """ # Check for new episodes new_episodes = await self.check_anime(item) result = AutoDownloadResult( watchlist_item_id=item.id, anime_title=item.anime_title, new_episodes_found=len(new_episodes), checked_at=datetime.now() ) # Download if auto_download is enabled if item.auto_download and new_episodes: settings = self.wlm.get_settings() if settings.auto_download_enabled: download_result = await self.download_new_episodes(item, new_episodes) result = download_result else: logger.info(f"Auto-download globally disabled, skipping {len(new_episodes)} episodes") # Update check time even if no downloads self.wlm.update_check_time(item.id, item.last_episode_downloaded) return result async def check_all_due(self) -> List[AutoDownloadResult]: """ Check all watchlist items that are due for checking Returns: List of AutoDownloadResult objects """ settings = self.wlm.get_settings() due_items = self.wlm.get_due_for_check(settings.check_interval_hours) logger.info(f"Checking {len(due_items)} due watchlist items") results = [] for item in due_items: try: result = await self.check_and_download(item) results.append(result) except Exception as e: logger.error(f"Error processing {item.anime_title}: {e}", exc_info=True) # Still add a result to track the failure results.append(AutoDownloadResult( watchlist_item_id=item.id, anime_title=item.anime_title, new_episodes_found=0, checked_at=datetime.now() )) # Log summary total_new = sum(r.new_episodes_found for r in results) total_downloaded = sum(len(r.episodes_downloaded) for r in results) total_failed = sum(len(r.episodes_failed) for r in results) logger.info( f"Check complete: {total_new} new episodes found, " f"{total_downloaded} downloaded, {total_failed} failed" ) return results async def manual_check(self, item_id: str) -> Optional[AutoDownloadResult]: """ Manually trigger a check for a specific watchlist item Args: item_id: Watchlist item ID Returns: AutoDownloadResult or None if item not found """ item = self.wlm.get_by_id(item_id) if not item: logger.error(f"Watchlist item not found: {item_id}") return None return await self.check_and_download(item) # Global episode checker instance episode_checker = EpisodeChecker()