mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-27 13:13:25 -08:00
302 lines
11 KiB
Python
302 lines
11 KiB
Python
"""
|
|
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)
|