Files
ohm_streaming/app/watchlist.py
T
root 29c7040b20
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
feat: migrate persistence from JSON to SQLModel (Phase 1)
- Integrated SQLModel with SQLite for robust data persistence
- Refactored UserManager and WatchlistManager to use SQL queries
- Migrated models to SQLModel with relationships and primary keys
- Updated test suite with in-memory database isolation
- Removed deprecated JSON storage files
2026-03-24 10:40:36 +00:00

246 lines
9.3 KiB
Python

"""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()