Files
ohm_streaming/app/favorites.py
T
root a684237725
CI / Test (Python 3.11) (push) Has been cancelled
CI / Test (Python 3.12) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Type Check (push) Has been cancelled
CI / Summary (push) Has been cancelled
Phase 2 Complete: SQL migration with SQLModel and Alembic
2026-03-25 13:46:15 +00:00

198 lines
6.8 KiB
Python

"""
Favorites management system for Ohm Stream Downloader
Stores user's favorite anime with metadata using SQLModel
"""
import logging
from typing import List, Dict, Optional
from datetime import datetime
from sqlmodel import Session, select
from app.database import engine
from app.models.favorites import FavoriteTable
logger = logging.getLogger(__name__)
class FavoritesManager:
"""Manages user's favorite anime list using SQL database"""
def __init__(self, storage_path: str = None):
# Database connection is managed via engine and sessions
pass
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"""
with Session(engine) as session:
statement = select(FavoriteTable).where(FavoriteTable.anime_id == anime_id)
existing = session.exec(statement).first()
if existing:
# Update existing favorite
existing.updated_at = datetime.now()
if metadata:
existing.anime_metadata = metadata
if poster_url:
existing.poster_url = poster_url
session.add(existing)
session.commit()
session.refresh(existing)
return self._to_dict(existing)
else:
# Add new favorite
fav = FavoriteTable(
anime_id=anime_id,
title=title,
url=url,
provider=provider,
anime_metadata=metadata or {},
poster_url=poster_url
)
session.add(fav)
session.commit()
session.refresh(fav)
return self._to_dict(fav)
async def remove_favorite(self, anime_id: str) -> bool:
"""Remove an anime from favorites"""
with Session(engine) as session:
statement = select(FavoriteTable).where(FavoriteTable.anime_id == anime_id)
existing = session.exec(statement).first()
if existing:
session.delete(existing)
session.commit()
return True
return False
async def get_favorite(self, anime_id: str) -> Optional[Dict]:
"""Get a specific favorite by ID"""
with Session(engine) as session:
statement = select(FavoriteTable).where(FavoriteTable.anime_id == anime_id)
existing = session.exec(statement).first()
if existing:
return self._to_dict(existing)
return None
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"""
with Session(engine) as session:
statement = select(FavoriteTable)
if filter_provider:
statement = statement.where(FavoriteTable.provider == filter_provider)
# SQLite JSON filtering for genres is complex, handle it in Python
results = session.exec(statement).all()
favorites = [self._to_dict(fav) for fav in results]
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"""
with Session(engine) as session:
statement = select(FavoriteTable).where(FavoriteTable.anime_id == anime_id)
return session.exec(statement).first() is not None
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"""
favorites = await self.list_favorites()
total = len(favorites)
# Count by provider
by_provider = {}
for fav in favorites:
provider = fav["provider"]
by_provider[provider] = by_provider.get(provider, 0) + 1
# Count by genre
by_genre = {}
for fav in favorites:
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
}
def _to_dict(self, fav: FavoriteTable) -> Dict:
"""Convert a FavoriteTable instance to a dictionary for API compatibility"""
return {
"id": fav.anime_id,
"title": fav.title,
"url": fav.url,
"provider": fav.provider,
"metadata": fav.anime_metadata,
"poster_url": fav.poster_url,
"created_at": fav.created_at.isoformat() if fav.created_at else None,
"updated_at": fav.updated_at.isoformat() if fav.updated_at else None
}
# 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