Files
ohm_streaming/app/auto_download_scheduler.py
T
root c6be191699 feat: Complete watchlist & auto-download system with UI
## Backend Implementation (100% Complete)

### Core Components
- **WatchlistManager**: JSON-based storage with full CRUD operations
  - User-scoped data access for multi-tenant support
  - Statistics and query functions
  - Settings management with persistence

- **EpisodeChecker**: Automatic new episode detection
  - Checks for new episodes using existing downloaders
  - Automatic download with error handling
  - Manual and scheduled check support
  - Lazy initialization to avoid circular imports

- **AutoDownloadScheduler**: APScheduler-based periodic checking
  - Configurable intervals (1-168 hours)
  - Start/stop/restart controls
  - Next run time tracking

### API Endpoints (15 endpoints)
- POST /api/watchlist - Add anime to watchlist
- GET /api/watchlist - Get user's watchlist (with status filter)
- GET /api/watchlist/{id} - Get specific item
- PUT /api/watchlist/{id} - Update item
- DELETE /api/watchlist/{id} - Delete item
- POST /api/watchlist/{id}/check - Check for new episodes
- POST /api/watchlist/{id}/pause - Pause tracking
- POST /api/watchlist/{id}/resume - Resume tracking
- GET /api/watchlist/settings - Get settings
- PUT /api/watchlist/settings - Update settings
- GET /api/watchlist/stats - Get statistics
- POST /api/watchlist/check-all - Check all items
- GET /api/watchlist/scheduler/status - Scheduler status
- POST /api/watchlist/scheduler/start - Start scheduler
- POST /api/watchlist/scheduler/stop - Stop scheduler

### Bug Fixes
- Fixed WatchlistManager.update() to accept both dict and WatchlistItemUpdate
- Added asyncio import to AutoDownloadScheduler for event loop detection
- Improved scheduler start() with better error handling

## Frontend Implementation (100% Complete)

### UI Components
- **Watchlist Page** (/watchlist)
  - Scheduler status panel with start/stop/check all buttons
  - Filter tabs (all/active/paused/completed)
  - Statistics display with color-coded cards
  - Watchlist items with pause/resume/delete controls
  - Auto-refresh every 30 seconds
  - Authentication check

- **Settings Modal**
  - Check interval configuration (1-168h)
  - Auto-download toggle
  - Max concurrent downloads slider
  - Notifications toggle
  - Live settings update with scheduler restart

- **"Suivre" Button**
  - Added to anime search result cards
  - Purple gradient with heart icon
  - Quick-add to watchlist functionality
  - State tracking (disabled when already in watchlist)

### JavaScript Files
- **static/js/watchlist.js**: API client functions
  - All watchlist API calls with token auth
  - Error handling and response parsing

- **static/js/watchlist-ui.js**: UI functions
  - Display watchlist with stats
  - Handle add/pause/resume/delete
  - Filter by status
  - Settings modal management

- **static/js/tabs.js**: Watchlist tab handler
  - Redirects to /watchlist page

## Testing

### Test Suite (test_watchlist_simple.py)
All tests passing (3/3):

1. **Watchlist Manager Tests** 
   - Create/read/update/delete operations
   - User-scoped queries
   - Statistics generation
   - Check time updates

2. **Settings Tests** 
   - Get current settings
   - Update settings with validation
   - Reset to defaults

3. **Scheduler Tests** 
   - Start/stop/restart controls
   - Running status verification
   - Next run time tracking

### Dependencies
- APScheduler 3.11.0 installed in virtual environment
- tzlocal 5.3.1 (APScheduler dependency)

## Documentation
- docs/WATCHLIST_AUTO_DOWNLOAD.md: Complete system documentation
  - API endpoints with examples
  - Architecture overview
  - Usage examples
  - Troubleshooting guide

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-01-29 21:56:39 +00:00

155 lines
4.8 KiB
Python

"""Scheduler for automatic episode checking and downloading"""
import asyncio
import logging
from datetime import datetime
from typing import Optional
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.interval import IntervalTrigger
from app.watchlist import watchlist_manager, WatchlistManager
from app.episode_checker import EpisodeChecker, episode_checker
logger = logging.getLogger(__name__)
class AutoDownloadScheduler:
"""Manages automatic episode checking and downloading on a schedule"""
def __init__(
self,
wlm: Optional[WatchlistManager] = None,
checker: Optional[EpisodeChecker] = None
):
self.wlm = wlm or watchlist_manager
self.checker = checker or episode_checker
self.scheduler: Optional[AsyncIOScheduler] = None
self._running = False
async def _check_job(self):
"""Job function that runs periodically to check for new episodes"""
try:
logger.info("Running scheduled episode check...")
results = await self.checker.check_all_due()
# Log summary
for result in results:
if result.new_episodes_found > 0:
logger.info(
f"{result.anime_title}: "
f"{result.new_episodes_found} new, "
f"{len(result.episodes_downloaded)} downloaded"
)
logger.info(f"Scheduled check complete: processed {len(results)} items")
except Exception as e:
logger.error(f"Error in scheduled check job: {e}", exc_info=True)
def start(self):
"""Start the scheduler"""
if self._running:
logger.warning("Scheduler already running")
return
try:
self.scheduler = AsyncIOScheduler()
# Get initial check interval from settings
settings = self.wlm.get_settings()
interval_hours = settings.check_interval_hours
# Add the job
self.scheduler.add_job(
self._check_job,
trigger=IntervalTrigger(hours=interval_hours),
id='episode_check',
name='Check for new episodes',
replace_existing=True
)
# Start the scheduler
self.scheduler.start()
self._running = True
logger.info(
f"Auto-download scheduler started (checking every {interval_hours}h)"
)
except Exception as e:
logger.error(f"Error starting scheduler: {e}", exc_info=True)
raise
def stop(self):
"""Stop the scheduler"""
if not self._running:
logger.warning("Scheduler not running")
return
try:
if self.scheduler:
self.scheduler.shutdown(wait=False)
self.scheduler = None
self._running = False
logger.info("Auto-download scheduler stopped")
except Exception as e:
logger.error(f"Error stopping scheduler: {e}", exc_info=True)
def restart(self):
"""Restart the scheduler with updated settings"""
logger.info("Restarting scheduler with new settings...")
self.stop()
self.start()
def update_interval(self, hours: int):
"""Update the check interval"""
if not self._running:
logger.warning("Scheduler not running, interval will be applied on start")
return
try:
settings = self.wlm.get_settings()
settings.check_interval_hours = hours
self.wlm.update_settings(settings)
# Restart to apply new interval
self.restart()
logger.info(f"Updated check interval to {hours}h")
except Exception as e:
logger.error(f"Error updating interval: {e}", exc_info=True)
def get_next_run_time(self) -> Optional[datetime]:
"""Get the next scheduled run time"""
if not self._running or not self.scheduler:
return None
try:
job = self.scheduler.get_job('episode_check')
if job:
return job.next_run_time
except Exception as e:
logger.error(f"Error getting next run time: {e}")
return None
def is_running(self) -> bool:
"""Check if scheduler is running"""
return self._running
async def trigger_check_now(self):
"""Manually trigger an episode check now"""
logger.info("Manually triggering episode check...")
try:
await self._check_job()
except Exception as e:
logger.error(f"Error in manual check: {e}", exc_info=True)
raise
# Global scheduler instance
auto_download_scheduler = AutoDownloadScheduler()