"""Watchlist management system for automatic episode tracking and downloading with SQLModel support""" import json import os import uuid import logging from datetime import datetime, timedelta from typing import List, Optional, Dict from pathlib import Path from sqlmodel import Session, select from app.database import engine from app.models.watchlist import ( WatchlistItem, WatchlistItemTable, WatchlistItemCreate, WatchlistItemUpdate, WatchlistStatus, WatchlistSettings, NewEpisodeInfo, AutoDownloadResult ) logger = logging.getLogger(__name__) # Settings file remains JSON for simplicity for now WATCHLIST_SETTINGS_FILE = "config/watchlist_settings.json" class WatchlistManager: """Manages user watchlist for automatic episode downloads using SQL database""" def __init__(self): self.settings_file = WATCHLIST_SETTINGS_FILE self.settings: Optional[WatchlistSettings] = None self._load_settings() def _load_settings(self): """Load watchlist settings from JSON file""" try: if os.path.exists(self.settings_file): with open(self.settings_file, 'r', encoding='utf-8') as f: data = json.load(f) self.settings = WatchlistSettings(**data) logger.info(f"Loaded watchlist settings") else: self.settings = WatchlistSettings() self._save_settings() logger.info("Settings file not found, using defaults") except Exception as e: logger.error(f"Error loading settings: {e}") self.settings = WatchlistSettings() def _save_settings(self): try: os.makedirs(os.path.dirname(self.settings_file), exist_ok=True) temp_file = f"{self.settings_file}.tmp" with open(temp_file, 'w', encoding='utf-8') as f: json.dump(self.settings.model_dump(mode='json'), f, indent=2, ensure_ascii=False) os.replace(temp_file, self.settings_file) logger.debug("Saved watchlist settings") except Exception as e: logger.error(f"Error saving settings: {e}") def _to_api_model(self, db_item: WatchlistItemTable) -> WatchlistItem: """Convert database table model to API response model""" data = db_item.model_dump() data["genres"] = db_item.genres return WatchlistItem(**data) def get_all(self, user_id: Optional[str] = None, status: Optional[WatchlistStatus] = None) -> List[WatchlistItem]: """Get all watchlist items, optionally filtered by user and status""" with Session(engine) as session: statement = select(WatchlistItemTable) if user_id: statement = statement.where(WatchlistItemTable.user_id == user_id) if status: statement = statement.where(WatchlistItemTable.status == status) # Sort by added_at descending statement = statement.order_by(WatchlistItemTable.added_at.desc()) db_items = session.exec(statement).all() return [self._to_api_model(item) for item in db_items] def get_by_id(self, item_id: str) -> Optional[WatchlistItem]: """Get a specific watchlist item by ID""" with Session(engine) as session: db_item = session.get(WatchlistItemTable, item_id) if db_item: return self._to_api_model(db_item) return None def get_by_anime_url(self, anime_url: str, user_id: str) -> Optional[WatchlistItem]: """Get a watchlist item by anime URL and user ID""" with Session(engine) as session: statement = select(WatchlistItemTable).where( WatchlistItemTable.anime_url == anime_url, WatchlistItemTable.user_id == user_id ) db_item = session.exec(statement).first() if db_item: return self._to_api_model(db_item) return None def add(self, user_id: str, item_create: WatchlistItemCreate) -> WatchlistItem: """Add a new anime to the watchlist""" # Check if already in watchlist for this user existing = self.get_by_anime_url(item_create.anime_url, user_id) if existing: return existing with Session(engine) as session: # Create new item db_item = WatchlistItemTable( user_id=user_id, anime_title=item_create.anime_title, anime_url=item_create.anime_url, provider_id=item_create.provider_id, lang=item_create.lang, auto_download=item_create.auto_download, quality_preference=item_create.quality_preference, poster_image=item_create.poster_image, cover_image=item_create.cover_image, synopsis=item_create.synopsis, status=WatchlistStatus.ACTIVE, added_at=datetime.now(), updated_at=datetime.now(), last_episode_downloaded=0 ) db_item.genres = item_create.genres session.add(db_item) session.commit() session.refresh(db_item) logger.info(f"Added {db_item.anime_title} to watchlist for user {user_id}") return self._to_api_model(db_item) # Alias for backward compatibility if needed add_item = add def update(self, item_id: str, update_data) -> Optional[WatchlistItem]: """Update a watchlist item""" with Session(engine) as session: db_item = session.get(WatchlistItemTable, item_id) if not db_item: return None # Handle both dict and WatchlistItemUpdate if isinstance(update_data, dict): update_dict = update_data else: update_dict = update_data.model_dump(exclude_unset=True) for key, value in update_dict.items(): if hasattr(db_item, key): setattr(db_item, key, value) db_item.updated_at = datetime.now() session.add(db_item) session.commit() session.refresh(db_item) logger.info(f"Updated watchlist item: {item_id}") return self._to_api_model(db_item) # Alias for backward compatibility update_item = update def delete(self, item_id: str) -> bool: """Remove an item from the watchlist""" with Session(engine) as session: db_item = session.get(WatchlistItemTable, item_id) if not db_item: return False session.delete(db_item) session.commit() logger.info(f"Deleted item {item_id} from watchlist") return True def update_last_checked(self, item_id: str, last_episode: Optional[int] = None): """Update the last_checked timestamp and optionally last episode for an item""" with Session(engine) as session: db_item = session.get(WatchlistItemTable, item_id) if db_item: db_item.last_checked = datetime.now() if last_episode is not None: db_item.last_episode_downloaded = last_episode session.add(db_item) session.commit() # Alias for backward compatibility update_check_time = update_last_checked def get_due_items(self) -> List[WatchlistItem]: """Get all items that are due for a check based on settings""" interval = timedelta(hours=self.settings.check_interval_hours) now = datetime.now() with Session(engine) as session: statement = select(WatchlistItemTable).where( (WatchlistItemTable.status == WatchlistStatus.ACTIVE) ) db_items = session.exec(statement).all() due_items = [] for item in db_items: if not item.last_checked or (item.last_checked + interval) < now: due_items.append(self._to_api_model(item)) return due_items def update_settings(self, settings: WatchlistSettings) -> WatchlistSettings: """Update global watchlist settings""" self.settings = settings self._save_settings() logger.info("Updated watchlist settings") return self.settings def get_stats(self, user_id: str) -> Dict: """Get statistics for a user's watchlist""" items = self.get_all(user_id=user_id) stats = { "total_items": len(items), "active_items": len([i for i in items if i.status == WatchlistStatus.ACTIVE]), "paused_items": len([i for i in items if i.status == WatchlistStatus.PAUSED]), "completed_items": len([i for i in items if i.status == WatchlistStatus.COMPLETED]), "total_episodes_downloaded": sum(i.last_episode_downloaded for i in items), "providers": {} } # Count by provider for item in items: provider = item.provider_id stats["providers"][provider] = stats["providers"].get(provider, 0) + 1 return stats # Global watchlist manager instance watchlist_manager = WatchlistManager()