Files
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

266 lines
11 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,
WatchlistSettingsTable,
NewEpisodeInfo,
AutoDownloadResult
)
logger = logging.getLogger(__name__)
class WatchlistManager:
"""Manages user watchlist for automatic episode downloads using SQL database"""
def __init__(self):
self.settings: Optional[WatchlistSettings] = None
self._load_settings()
def _load_settings(self):
"""Load watchlist settings from database"""
try:
with Session(engine) as session:
statement = select(WatchlistSettingsTable).where(WatchlistSettingsTable.user_id == "default")
db_settings = session.exec(statement).first()
if db_settings:
self.settings = WatchlistSettings(
check_interval_hours=db_settings.check_interval_hours,
auto_download_enabled=db_settings.auto_download_enabled,
max_concurrent_auto_downloads=db_settings.max_concurrent_auto_downloads,
notify_on_new_episodes=db_settings.notify_on_new_episodes,
include_completed_anime=db_settings.include_completed_anime
)
logger.info(f"Loaded watchlist settings from database")
else:
self.settings = WatchlistSettings()
self._save_settings()
logger.info("Settings not found in database, created defaults")
except Exception as e:
logger.error(f"Error loading settings from database: {e}")
self.settings = WatchlistSettings()
def _save_settings(self):
try:
with Session(engine) as session:
statement = select(WatchlistSettingsTable).where(WatchlistSettingsTable.user_id == "default")
db_settings = session.exec(statement).first()
if db_settings:
db_settings.check_interval_hours = self.settings.check_interval_hours
db_settings.auto_download_enabled = self.settings.auto_download_enabled
db_settings.max_concurrent_auto_downloads = self.settings.max_concurrent_auto_downloads
db_settings.notify_on_new_episodes = self.settings.notify_on_new_episodes
db_settings.include_completed_anime = self.settings.include_completed_anime
else:
db_settings = WatchlistSettingsTable(
user_id="default",
check_interval_hours=self.settings.check_interval_hours,
auto_download_enabled=self.settings.auto_download_enabled,
max_concurrent_auto_downloads=self.settings.max_concurrent_auto_downloads,
notify_on_new_episodes=self.settings.notify_on_new_episodes,
include_completed_anime=self.settings.include_completed_anime
)
session.add(db_settings)
session.commit()
logger.debug("Saved watchlist settings to database")
except Exception as e:
logger.error(f"Error saving settings to database: {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()