87f245d3fc
- Sunset Glitch color palette applied to all templates - Font Awesome icons throughout UI - Download manager with parallel queue and progress tracking - Settings page with dynamic configuration - Recommendations router enhanced with scoring - Local vendor libs (Alpine.js, HTMX) for offline support - Auto test suite with screenshots - Series releases list component - New download model
276 lines
11 KiB
Python
276 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 current settings"""
|
|
return self.get_due_for_check(self.settings.check_interval_hours if self.settings else 6)
|
|
|
|
def get_due_for_check(self, interval_hours: int) -> List[WatchlistItem]:
|
|
"""Get all items that are due for a check based on settings"""
|
|
interval = timedelta(hours=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 get_settings(self) -> WatchlistSettings:
|
|
"""Get global watchlist settings"""
|
|
if self.settings is None:
|
|
self._load_settings()
|
|
return self.settings
|
|
|
|
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()
|