Phase 2 Complete: SQL migration with SQLModel and Alembic
This commit is contained in:
+73
-79
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user