Phase 2 Complete: SQL migration with SQLModel and Alembic
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

This commit is contained in:
root
2026-03-25 13:46:15 +00:00
parent 96b12b66e2
commit a684237725
21 changed files with 1148 additions and 466 deletions
+73 -79
View File
@@ -1,52 +1,24 @@
"""
Favorites management system for Ohm Stream Downloader
Stores user's favorite anime with metadata in a local JSON file
Stores user's favorite anime with metadata using SQLModel
"""
import json
import asyncio
import logging
from pathlib import Path
from typing import List, Dict, Optional
from datetime import datetime
import aiofiles
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"""
"""Manages user's favorite anime list using SQL database"""
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}")
def __init__(self, storage_path: str = None):
# Database connection is managed via engine and sessions
pass
async def add_favorite(
self,
@@ -58,48 +30,55 @@ class FavoritesManager:
poster_url: Optional[str] = None
) -> Dict:
"""Add an anime to favorites"""
async with self._lock:
await self._load_for_operation()
with Session(engine) as session:
statement = select(FavoriteTable).where(FavoriteTable.anime_id == anime_id)
existing = session.exec(statement).first()
if anime_id in self._favorites:
if existing:
# Update existing favorite
self._favorites[anime_id]["updated_at"] = datetime.now().isoformat()
existing.updated_at = datetime.now()
if metadata:
self._favorites[anime_id]["metadata"] = metadata
existing.anime_metadata = metadata
if poster_url:
self._favorites[anime_id]["poster_url"] = poster_url
existing.poster_url = poster_url
session.add(existing)
session.commit()
session.refresh(existing)
return self._to_dict(existing)
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]
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"""
async with self._lock:
await self._load_for_operation()
if anime_id in self._favorites:
del self._favorites[anime_id]
await self._save()
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"""
await self._load()
return self._favorites.get(anime_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,
@@ -109,13 +88,15 @@ class FavoritesManager:
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]
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 = [
@@ -144,8 +125,9 @@ class FavoritesManager:
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
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,
@@ -168,19 +150,18 @@ class FavoritesManager:
async def get_stats(self) -> Dict:
"""Get statistics about favorites"""
await self._load()
total = len(self._favorites)
favorites = await self.list_favorites()
total = len(favorites)
# Count by provider
by_provider = {}
for fav in self._favorites.values():
for fav in favorites:
provider = fav["provider"]
by_provider[provider] = by_provider.get(provider, 0) + 1
# Count by genre
by_genre = {}
for fav in self._favorites.values():
for fav in favorites:
for genre in fav.get("metadata", {}).get("genres", []):
by_genre[genre] = by_genre.get(genre, 0) + 1
@@ -190,6 +171,19 @@ class FavoritesManager:
"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