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>
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple test script for the watchlist & auto-download system
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add project to path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from app.watchlist import watchlist_manager
|
||||
from app.episode_checker import episode_checker
|
||||
from app.auto_download_scheduler import auto_download_scheduler
|
||||
from app.models.watchlist import (
|
||||
WatchlistItemCreate,
|
||||
WatchlistStatus,
|
||||
QualityPreference,
|
||||
WatchlistSettings
|
||||
)
|
||||
|
||||
|
||||
def test_watchlist_basics():
|
||||
"""Test basic watchlist operations"""
|
||||
print("\n" + "="*60)
|
||||
print("🧪 TEST 1: Watchlist Manager Basics")
|
||||
print("="*60)
|
||||
|
||||
# Test user ID
|
||||
test_user = "test_user_simple"
|
||||
|
||||
# Clean up any existing test items first
|
||||
print("\n0. Cleaning up any existing test items...")
|
||||
all_items = watchlist_manager.get_all()
|
||||
for item in all_items:
|
||||
if item.user_id == test_user:
|
||||
watchlist_manager.delete(item.id)
|
||||
print(f" ✓ Deleted old test item: {item.id}")
|
||||
|
||||
# Create a test item
|
||||
print("\n1. Creating watchlist item...")
|
||||
item_data = WatchlistItemCreate(
|
||||
anime_title="Test Anime Simple",
|
||||
anime_url="https://anime-sama.si/catalogue/test-simple/vostfr/",
|
||||
provider_id="animesama",
|
||||
lang="vostfr",
|
||||
auto_download=True,
|
||||
quality_preference=QualityPreference.AUTO
|
||||
)
|
||||
|
||||
try:
|
||||
item = watchlist_manager.create(test_user, item_data)
|
||||
print(f" ✅ Item created: {item.id}")
|
||||
print(f" Title: {item.anime_title}")
|
||||
print(f" Status: {item.status}")
|
||||
print(f" Auto-download: {item.auto_download}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Create failed: {e}")
|
||||
return False
|
||||
|
||||
# Get all items for user
|
||||
print("\n2. Getting user's items...")
|
||||
try:
|
||||
items = watchlist_manager.get_all(test_user)
|
||||
print(f" ✅ Found {len(items)} items for user")
|
||||
if len(items) > 0:
|
||||
print(f" First item: {items[0].anime_title}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Get all failed: {e}")
|
||||
return False
|
||||
|
||||
# Get stats
|
||||
print("\n3. Getting statistics...")
|
||||
try:
|
||||
stats = watchlist_manager.get_stats(test_user)
|
||||
print(f" ✅ Stats: total={stats['total']}, active={stats['active']}, paused={stats['paused']}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Stats failed: {e}")
|
||||
return False
|
||||
|
||||
# Update item
|
||||
print("\n4. Updating item to paused...")
|
||||
try:
|
||||
updated = watchlist_manager.update(item.id, {"status": WatchlistStatus.PAUSED})
|
||||
print(f" ✅ Item updated to status: {updated.status}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Update failed: {e}")
|
||||
return False
|
||||
|
||||
# Update check time
|
||||
print("\n5. Updating check time...")
|
||||
try:
|
||||
updated = watchlist_manager.update_check_time(item.id, 5)
|
||||
print(f" ✅ Check time updated")
|
||||
print(f" Last episode: {updated.last_episode_downloaded}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Update check time failed: {e}")
|
||||
return False
|
||||
|
||||
# Delete item (cleanup)
|
||||
print("\n6. Cleaning up - deleting test item...")
|
||||
try:
|
||||
result = watchlist_manager.delete(item.id)
|
||||
print(f" ✅ Item deleted: {result}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Delete failed: {e}")
|
||||
return False
|
||||
|
||||
print("\n✅ Watchlist Manager tests PASSED")
|
||||
return True
|
||||
|
||||
|
||||
def test_settings():
|
||||
"""Test settings management"""
|
||||
print("\n" + "="*60)
|
||||
print("🧪 TEST 2: Settings Management")
|
||||
print("="*60)
|
||||
|
||||
print("\n1. Getting current settings...")
|
||||
try:
|
||||
settings = watchlist_manager.get_settings()
|
||||
print(f" ✅ Current settings:")
|
||||
print(f" - Check interval: {settings.check_interval_hours}h")
|
||||
print(f" - Auto-download: {settings.auto_download_enabled}")
|
||||
print(f" - Max concurrent: {settings.max_concurrent_auto_downloads}")
|
||||
print(f" - Notifications: {settings.notify_on_new_episodes}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Get settings failed: {e}")
|
||||
return False
|
||||
|
||||
print("\n2. Updating settings...")
|
||||
try:
|
||||
new_settings = WatchlistSettings(
|
||||
check_interval_hours=12,
|
||||
auto_download_enabled=True,
|
||||
max_concurrent_auto_downloads=3,
|
||||
notify_on_new_episodes=False
|
||||
)
|
||||
watchlist_manager.update_settings(new_settings)
|
||||
print(f" ✅ Settings updated")
|
||||
except Exception as e:
|
||||
print(f" ❌ Update settings failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
print("\n3. Verifying settings...")
|
||||
try:
|
||||
settings = watchlist_manager.get_settings()
|
||||
if settings.check_interval_hours != 12:
|
||||
print(f" ❌ Settings not saved correctly: check_interval is {settings.check_interval_hours}, expected 12")
|
||||
return False
|
||||
|
||||
print(f" ✅ Settings verified:")
|
||||
print(f" - Check interval: {settings.check_interval_hours}h ✓")
|
||||
except Exception as e:
|
||||
print(f" ❌ Verify settings failed: {e}")
|
||||
return False
|
||||
|
||||
# Reset to defaults
|
||||
print("\n4. Resetting to default settings...")
|
||||
try:
|
||||
default_settings = WatchlistSettings()
|
||||
watchlist_manager.update_settings(default_settings)
|
||||
print(f" ✅ Settings reset to defaults")
|
||||
except Exception as e:
|
||||
print(f" ❌ Reset settings failed: {e}")
|
||||
return False
|
||||
|
||||
print("\n✅ Settings tests PASSED")
|
||||
return True
|
||||
|
||||
|
||||
async def test_scheduler():
|
||||
"""Test scheduler controls"""
|
||||
print("\n" + "="*60)
|
||||
print("🧪 TEST 3: Auto-Download Scheduler")
|
||||
print("="*60)
|
||||
|
||||
print("\n1. Testing scheduler start (async)...")
|
||||
try:
|
||||
auto_download_scheduler.start()
|
||||
print(f" ✅ Scheduler started")
|
||||
print(f" Status: running={auto_download_scheduler.is_running()}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Start failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
if not auto_download_scheduler.is_running():
|
||||
print(f" ❌ Scheduler not running after start")
|
||||
return False
|
||||
|
||||
print("\n2. Testing scheduler stop...")
|
||||
try:
|
||||
auto_download_scheduler.stop()
|
||||
print(f" ✅ Scheduler stopped")
|
||||
print(f" Status: running={auto_download_scheduler.is_running()}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Stop failed: {e}")
|
||||
return False
|
||||
|
||||
if auto_download_scheduler.is_running():
|
||||
print(f" ❌ Scheduler still running after stop")
|
||||
return False
|
||||
|
||||
print("\n3. Testing scheduler restart...")
|
||||
try:
|
||||
auto_download_scheduler.start()
|
||||
print(f" ✅ Scheduler restarted")
|
||||
|
||||
# Get next run time
|
||||
next_run = auto_download_scheduler.get_next_run_time()
|
||||
if next_run:
|
||||
print(f" Next run: {next_run}")
|
||||
|
||||
auto_download_scheduler.stop()
|
||||
print(f" ✅ Scheduler stopped again")
|
||||
except Exception as e:
|
||||
print(f" ❌ Restart failed: {e}")
|
||||
return False
|
||||
|
||||
print("\n✅ Scheduler tests PASSED")
|
||||
return True
|
||||
|
||||
|
||||
async def run_all_tests():
|
||||
"""Run all tests"""
|
||||
print("\n" + "="*60)
|
||||
print("🚀 WATCHLIST SYSTEM TEST SUITE")
|
||||
print("="*60)
|
||||
|
||||
tests = [
|
||||
("Watchlist Manager", lambda: test_watchlist_basics()),
|
||||
("Settings", lambda: test_settings()),
|
||||
("Scheduler", test_scheduler) # This one is async
|
||||
]
|
||||
|
||||
results = []
|
||||
|
||||
for name, test_func in tests:
|
||||
try:
|
||||
# Check if it's a coroutine function
|
||||
import asyncio
|
||||
if asyncio.iscoroutinefunction(test_func):
|
||||
result = await test_func()
|
||||
else:
|
||||
result = test_func()
|
||||
results.append((name, result))
|
||||
except Exception as e:
|
||||
print(f"\n❌ {name} test crashed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
results.append((name, False))
|
||||
|
||||
# Summary
|
||||
print("\n" + "="*60)
|
||||
print("📊 TEST SUMMARY")
|
||||
print("="*60)
|
||||
|
||||
passed = sum(1 for _, result in results if result)
|
||||
total = len(results)
|
||||
|
||||
for name, result in results:
|
||||
status = "✅ PASSED" if result else "❌ FAILED"
|
||||
print(f"{status}: {name}")
|
||||
|
||||
print(f"\nTotal: {passed}/{total} tests passed")
|
||||
|
||||
if passed == total:
|
||||
print("\n🎉 ALL TESTS PASSED! The watchlist system is ready to use.")
|
||||
print("\n📝 Next steps:")
|
||||
print(" 1. Start the server: uvicorn main:app --reload")
|
||||
print(" 2. Open http://localhost:3000/watchlist")
|
||||
print(" 3. Add anime to your watchlist")
|
||||
print(" 4. Start the scheduler")
|
||||
return True
|
||||
else:
|
||||
print("\n⚠️ Some tests failed. Please review the errors above.")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = asyncio.run(run_all_tests())
|
||||
sys.exit(0 if success else 1)
|
||||
Reference in New Issue
Block a user