diff --git a/fastanime/cli/services/integration/__init__.py b/fastanime/cli/services/integration/__init__.py deleted file mode 100644 index 52c2a60..0000000 --- a/fastanime/cli/services/integration/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Integration services for synchronizing watch history and download tracking. -""" - -from .sync import HistoryDownloadSync - -__all__ = ["HistoryDownloadSync"] diff --git a/fastanime/cli/services/integration/sync.py b/fastanime/cli/services/integration/sync.py deleted file mode 100644 index c93512b..0000000 --- a/fastanime/cli/services/integration/sync.py +++ /dev/null @@ -1,301 +0,0 @@ -""" -Synchronization service between watch history and download tracking. - -This module provides functionality to keep watch history and download status -in sync, enabling features like offline availability markers and smart -download suggestions based on viewing patterns. -""" - -from __future__ import annotations - -import logging -from typing import List, Optional - -from ....libs.api.types import MediaItem -from ..downloads.manager import DownloadManager -from ..watch_history.manager import WatchHistoryManager -from ..watch_history.types import WatchHistoryEntry - -logger = logging.getLogger(__name__) - - -class HistoryDownloadSync: - """ - Service to synchronize watch history and download tracking. - - Provides bidirectional synchronization between viewing history and - download status, enabling features like offline availability and - smart download recommendations. - """ - - def __init__(self, watch_manager: WatchHistoryManager, download_manager: DownloadManager): - self.watch_manager = watch_manager - self.download_manager = download_manager - - def sync_download_status(self, media_id: int) -> bool: - """ - Update watch history with download availability status. - - Args: - media_id: The media ID to sync - - Returns: - True if sync was successful - """ - try: - # Get download record - download_record = self.download_manager.get_download_record(media_id) - if not download_record: - return False - - # Get watch history entry - watch_entry = self.watch_manager.get_entry_by_media_id(media_id) - if not watch_entry: - return False - - # Check if any episodes are downloaded - has_downloads = any( - ep.is_completed for ep in download_record.episodes.values() - ) - - # Check if current/next episode is available offline - current_episode = watch_entry.last_watched_episode - next_episode = current_episode + 1 - - offline_available = ( - current_episode in download_record.episodes and - download_record.episodes[current_episode].is_completed - ) or ( - next_episode in download_record.episodes and - download_record.episodes[next_episode].is_completed - ) - - # Update watch history entry - updated_entry = watch_entry.model_copy(update={ - "has_downloads": has_downloads, - "offline_available": offline_available - }) - - return self.watch_manager.save_entry(updated_entry) - - except Exception as e: - logger.error(f"Failed to sync download status for media {media_id}: {e}") - return False - - def mark_episodes_offline_available(self, media_id: int, episodes: List[int]) -> bool: - """ - Mark specific episodes as available offline in watch history. - - Args: - media_id: The media ID - episodes: List of episode numbers that are available offline - - Returns: - True if successful - """ - try: - watch_entry = self.watch_manager.get_entry_by_media_id(media_id) - if not watch_entry: - return False - - # Check if current or next episode is in the available episodes - current_episode = watch_entry.last_watched_episode - next_episode = current_episode + 1 - - offline_available = ( - current_episode in episodes or - next_episode in episodes or - len(episodes) > 0 # Any episodes available - ) - - updated_entry = watch_entry.model_copy(update={ - "has_downloads": len(episodes) > 0, - "offline_available": offline_available - }) - - return self.watch_manager.save_entry(updated_entry) - - except Exception as e: - logger.error(f"Failed to mark episodes offline available for media {media_id}: {e}") - return False - - def suggest_downloads_for_watching(self, media_id: int, lookahead: int = 3) -> List[int]: - """ - Suggest episodes to download based on watch history. - - Args: - media_id: The media ID - lookahead: Number of episodes ahead to suggest - - Returns: - List of episode numbers to download - """ - try: - watch_entry = self.watch_manager.get_entry_by_media_id(media_id) - if not watch_entry or watch_entry.status != "watching": - return [] - - download_record = self.download_manager.get_download_record(media_id) - if not download_record: - return [] - - # Get currently downloaded episodes - downloaded_episodes = set( - ep_num for ep_num, ep in download_record.episodes.items() - if ep.is_completed - ) - - # Suggest next episodes - current_episode = watch_entry.last_watched_episode - total_episodes = watch_entry.media_item.episodes or 999 - - suggestions = [] - for i in range(1, lookahead + 1): - next_episode = current_episode + i - if (next_episode <= total_episodes and - next_episode not in downloaded_episodes): - suggestions.append(next_episode) - - return suggestions - - except Exception as e: - logger.error(f"Failed to suggest downloads for media {media_id}: {e}") - return [] - - def suggest_downloads_for_completed(self, limit: int = 5) -> List[MediaItem]: - """ - Suggest anime to download based on completed watch history. - - Args: - limit: Maximum number of suggestions - - Returns: - List of MediaItems to consider for download - """ - try: - # Get completed anime from watch history - completed_entries = self.watch_manager.get_entries_by_status("completed") - - suggestions = [] - for entry in completed_entries[:limit]: - # Check if not already fully downloaded - download_record = self.download_manager.get_download_record(entry.media_item.id) - - if not download_record: - suggestions.append(entry.media_item) - elif download_record.completion_percentage < 100: - suggestions.append(entry.media_item) - - return suggestions - - except Exception as e: - logger.error(f"Failed to suggest downloads for completed anime: {e}") - return [] - - def sync_all_entries(self) -> int: - """ - Sync download status for all watch history entries. - - Returns: - Number of entries successfully synced - """ - try: - watch_entries = self.watch_manager.get_all_entries() - synced_count = 0 - - for entry in watch_entries: - if self.sync_download_status(entry.media_item.id): - synced_count += 1 - - logger.info(f"Synced download status for {synced_count}/{len(watch_entries)} entries") - return synced_count - - except Exception as e: - logger.error(f"Failed to sync all entries: {e}") - return 0 - - def update_watch_progress_from_downloads(self, media_id: int) -> bool: - """ - Update watch progress based on downloaded episodes. - - Useful when episodes are watched outside the app but files exist. - - Args: - media_id: The media ID to update - - Returns: - True if successful - """ - try: - download_record = self.download_manager.get_download_record(media_id) - if not download_record: - return False - - watch_entry = self.watch_manager.get_entry_by_media_id(media_id) - if not watch_entry: - # Create new watch entry if none exists - watch_entry = WatchHistoryEntry( - media_item=download_record.media_item, - status="watching" - ) - - # Find highest downloaded episode - downloaded_episodes = [ - ep_num for ep_num, ep in download_record.episodes.items() - if ep.is_completed - ] - - if downloaded_episodes: - max_downloaded = max(downloaded_episodes) - - # Only update if we have more episodes downloaded than watched - if max_downloaded > watch_entry.last_watched_episode: - updated_entry = watch_entry.model_copy(update={ - "last_watched_episode": max_downloaded, - "watch_progress": 1.0, # Assume completed if downloaded - "has_downloads": True, - "offline_available": True - }) - - return self.watch_manager.save_entry(updated_entry) - - return True - - except Exception as e: - logger.error(f"Failed to update watch progress from downloads for media {media_id}: {e}") - return False - - def get_offline_watchable_anime(self) -> List[WatchHistoryEntry]: - """ - Get list of anime that can be watched offline. - - Returns: - List of watch history entries with offline episodes available - """ - try: - watch_entries = self.watch_manager.get_all_entries() - offline_entries = [] - - for entry in watch_entries: - if entry.offline_available: - offline_entries.append(entry) - else: - # Double-check by looking at downloads - download_record = self.download_manager.get_download_record(entry.media_item.id) - if download_record: - next_episode = entry.last_watched_episode + 1 - if (next_episode in download_record.episodes and - download_record.episodes[next_episode].is_completed): - offline_entries.append(entry) - - return offline_entries - - except Exception as e: - logger.error(f"Failed to get offline watchable anime: {e}") - return [] - - -def create_sync_service(watch_manager: WatchHistoryManager, - download_manager: DownloadManager) -> HistoryDownloadSync: - """Factory function to create a synchronization service.""" - return HistoryDownloadSync(watch_manager, download_manager) diff --git a/fastanime/cli/services/media_registry/__init__.py b/fastanime/cli/services/registry/__init__.py similarity index 100% rename from fastanime/cli/services/media_registry/__init__.py rename to fastanime/cli/services/registry/__init__.py diff --git a/fastanime/cli/services/media_registry/manager.py b/fastanime/cli/services/registry/manager.py similarity index 100% rename from fastanime/cli/services/media_registry/manager.py rename to fastanime/cli/services/registry/manager.py diff --git a/fastanime/cli/services/media_registry/models.py b/fastanime/cli/services/registry/models.py similarity index 100% rename from fastanime/cli/services/media_registry/models.py rename to fastanime/cli/services/registry/models.py diff --git a/fastanime/cli/services/media_registry/tracker.py b/fastanime/cli/services/registry/tracker.py similarity index 100% rename from fastanime/cli/services/media_registry/tracker.py rename to fastanime/cli/services/registry/tracker.py