""" Favorites management system for Ohm Stream Downloader Stores user's favorite anime with metadata in a local JSON file """ import json import asyncio import logging from pathlib import Path from typing import List, Dict, Optional from datetime import datetime import aiofiles logger = logging.getLogger(__name__) class FavoritesManager: """Manages user's favorite anime list""" def __init__(self, storage_path: str = "data/favorites.json"): self.storage_path = Path(storage_path) self.storage_path.parent.mkdir(parents=True, exist_ok=True) self._favorites: Dict[str, Dict] = {} self._lock = asyncio.Lock() async def _load(self): """Load favorites from disk""" async with self._lock: await self._load_for_operation() async def _load_for_operation(self): """Load favorites from disk without acquiring lock (lock must already be held)""" if self.storage_path.exists(): try: async with aiofiles.open(self.storage_path, 'r', encoding='utf-8') as f: content = await f.read() self._favorites = json.loads(content) if content.strip() else {} except Exception as e: logger.error(f"Error loading favorites: {e}") self._favorites = {} else: self._favorites = {} async def _save(self): """Save favorites to disk (assumes lock is already held)""" try: async with aiofiles.open(self.storage_path, 'w', encoding='utf-8') as f: await f.write(json.dumps(self._favorites, indent=2, ensure_ascii=False)) except Exception as e: logger.error(f"Error saving favorites: {e}") async def add_favorite( self, anime_id: str, title: str, url: str, provider: str, metadata: Optional[Dict] = None, poster_url: Optional[str] = None ) -> Dict: """Add an anime to favorites""" async with self._lock: await self._load_for_operation() if anime_id in self._favorites: # Update existing favorite self._favorites[anime_id]["updated_at"] = datetime.now().isoformat() if metadata: self._favorites[anime_id]["metadata"] = metadata if poster_url: self._favorites[anime_id]["poster_url"] = poster_url else: # Add new favorite self._favorites[anime_id] = { "id": anime_id, "title": title, "url": url, "provider": provider, "metadata": metadata or {}, "poster_url": poster_url, "created_at": datetime.now().isoformat(), "updated_at": datetime.now().isoformat() } await self._save() return self._favorites[anime_id] async def remove_favorite(self, anime_id: str) -> bool: """Remove an anime from favorites""" async with self._lock: await self._load_for_operation() if anime_id in self._favorites: del self._favorites[anime_id] await self._save() return True return False async def get_favorite(self, anime_id: str) -> Optional[Dict]: """Get a specific favorite by ID""" await self._load() return self._favorites.get(anime_id) async def list_favorites( self, sort_by: str = "created_at", order: str = "desc", filter_provider: Optional[str] = None, filter_genre: Optional[str] = None ) -> List[Dict]: """List all favorites with optional sorting and filtering""" await self._load() favorites = list(self._favorites.values()) # Apply filters if filter_provider: favorites = [f for f in favorites if f["provider"] == filter_provider] if filter_genre: favorites = [ f for f in favorites if filter_genre in f.get("metadata", {}).get("genres", []) ] # Sort favorites reverse = order == "desc" if sort_by == "title": favorites.sort(key=lambda x: x["title"].lower(), reverse=reverse) elif sort_by == "rating": favorites.sort( key=lambda x: float(x.get("metadata", {}).get("rating", "0").split("/")[0]), reverse=reverse ) elif sort_by == "year": favorites.sort( key=lambda x: x.get("metadata", {}).get("release_year", 0), reverse=reverse ) else: # created_at, updated_at favorites.sort(key=lambda x: x.get(sort_by, ""), reverse=reverse) return favorites async def is_favorite(self, anime_id: str) -> bool: """Check if an anime is in favorites""" await self._load() return anime_id in self._favorites async def toggle_favorite( self, anime_id: str, title: str, url: str, provider: str, metadata: Optional[Dict] = None, poster_url: Optional[str] = None ) -> Dict: """Toggle an anime in favorites (add if not exists, remove if exists)""" is_fav = await self.is_favorite(anime_id) if is_fav: await self.remove_favorite(anime_id) return {"action": "removed", "anime_id": anime_id} else: fav = await self.add_favorite(anime_id, title, url, provider, metadata, poster_url) return {"action": "added", "anime_id": anime_id, "favorite": fav} async def get_stats(self) -> Dict: """Get statistics about favorites""" await self._load() total = len(self._favorites) # Count by provider by_provider = {} for fav in self._favorites.values(): provider = fav["provider"] by_provider[provider] = by_provider.get(provider, 0) + 1 # Count by genre by_genre = {} for fav in self._favorites.values(): for genre in fav.get("metadata", {}).get("genres", []): by_genre[genre] = by_genre.get(genre, 0) + 1 return { "total": total, "by_provider": by_provider, "by_genre": by_genre } # Global favorites manager instance _favorites_manager: Optional[FavoritesManager] = None def get_favorites_manager() -> FavoritesManager: """Get the global favorites manager instance""" global _favorites_manager if _favorites_manager is None: _favorites_manager = FavoritesManager() return _favorites_manager