diff --git a/app/auto_download_scheduler.py b/app/auto_download_scheduler.py
index 76ed5dd..eda4fd0 100644
--- a/app/auto_download_scheduler.py
+++ b/app/auto_download_scheduler.py
@@ -1,4 +1,5 @@
"""Scheduler for automatic episode checking and downloading"""
+import asyncio
import logging
from datetime import datetime
from typing import Optional
diff --git a/app/watchlist.py b/app/watchlist.py
index 37c2277..bf2c2af 100644
--- a/app/watchlist.py
+++ b/app/watchlist.py
@@ -155,14 +155,24 @@ class WatchlistManager:
logger.info(f"Added anime to watchlist: {watchlist_item.anime_title} (ID: {item_id})")
return watchlist_item
- def update(self, item_id: str, update_data: WatchlistItemUpdate) -> Optional[WatchlistItem]:
- """Update a watchlist item"""
+ def update(self, item_id: str, update_data) -> Optional[WatchlistItem]:
+ """Update a watchlist item
+
+ Args:
+ item_id: Item ID to update
+ update_data: WatchlistItemUpdate object or dict with fields to update
+ """
item = self.watchlist.get(item_id)
if not 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)
+
# Update fields
- update_dict = update_data.model_dump(exclude_unset=True)
for field, value in update_dict.items():
if value is not None:
setattr(item, field, value)
diff --git a/config/watchlist.json b/config/watchlist.json
new file mode 100644
index 0000000..c79695f
--- /dev/null
+++ b/config/watchlist.json
@@ -0,0 +1,22 @@
+{
+ "2293bca2-c1c2-4e4f-8862-c4a6601f2b6f": {
+ "id": "2293bca2-c1c2-4e4f-8862-c4a6601f2b6f",
+ "user_id": "test_user_1",
+ "anime_title": "Test Anime",
+ "anime_url": "https://anime-sama.si/catalogue/test/vostfr/",
+ "provider_id": "animesama",
+ "lang": "vostfr",
+ "last_checked": null,
+ "last_episode_downloaded": 0,
+ "total_episodes": null,
+ "auto_download": true,
+ "quality_preference": "auto",
+ "status": "active",
+ "poster_image": null,
+ "cover_image": null,
+ "synopsis": null,
+ "genres": [],
+ "added_at": "2026-01-29T21:53:38.078765",
+ "updated_at": "2026-01-29T21:53:38.078765"
+ }
+}
\ No newline at end of file
diff --git a/config/watchlist_settings.json b/config/watchlist_settings.json
new file mode 100644
index 0000000..caa0063
--- /dev/null
+++ b/config/watchlist_settings.json
@@ -0,0 +1,7 @@
+{
+ "check_interval_hours": 6,
+ "auto_download_enabled": true,
+ "max_concurrent_auto_downloads": 2,
+ "notify_on_new_episodes": false,
+ "include_completed_anime": false
+}
\ No newline at end of file
diff --git a/main.py b/main.py
index fca0f9a..d4e6921 100644
--- a/main.py
+++ b/main.py
@@ -346,6 +346,12 @@ async def login_page(request: Request):
return templates.TemplateResponse("login.html", {"request": request})
+@app.get("/watchlist")
+async def watchlist_page(request: Request):
+ """Watchlist management page"""
+ return templates.TemplateResponse("watchlist.html", {"request": request})
+
+
# API Endpoints
@app.post("/api/download")
async def create_download(request: DownloadRequest, background_tasks: BackgroundTasks):
diff --git a/static/js/anime.js b/static/js/anime.js
index 647138e..a27c576 100644
--- a/static/js/anime.js
+++ b/static/js/anime.js
@@ -93,6 +93,16 @@ async function renderAnimeCard(anime, providerId, providerInfo, lang) {
Toute la saison
+
`;
diff --git a/static/js/tabs.js b/static/js/tabs.js
index 80b8181..a720422 100644
--- a/static/js/tabs.js
+++ b/static/js/tabs.js
@@ -385,6 +385,9 @@ document.addEventListener('DOMContentLoaded', () => {
loadProvidersGrid();
window.providersTabLoaded = true;
}
+ } else if (tabName === 'watchlist') {
+ // Watchlist is handled by its own page
+ window.location.href = '/watchlist';
}
}, 100);
};
diff --git a/static/js/watchlist-ui.js b/static/js/watchlist-ui.js
new file mode 100644
index 0000000..a36848b
--- /dev/null
+++ b/static/js/watchlist-ui.js
@@ -0,0 +1,331 @@
+/**
+ * Watchlist UI functions
+ */
+
+/**
+ * Display watchlist items
+ */
+async function displayWatchlist(status = null) {
+ const container = document.getElementById('watchlistContainer');
+ if (!container) return;
+
+ try {
+ container.innerHTML = '
Chargement de la watchlist...
';
+
+ const items = await getWatchlist(status);
+ const stats = await getWatchlistStats();
+
+ if (items.length === 0) {
+ container.innerHTML = `
+
+
+
+
Aucun anime dans votre watchlist
+
Ajoutez des animes depuis la recherche pour commencer le suivi automatique
+
+
+ `;
+ return;
+ }
+
+ // Render stats
+ let statsHtml = '';
+ if (stats && stats.total > 0) {
+ statsHtml = `
+
+
+
${stats.total}
+
Total
+
+
+
${stats.active}
+
Actifs
+
+
+
${stats.paused}
+
En pause
+
+
+
${stats.completed}
+
Terminés
+
+
+ `;
+ }
+
+ // Render items
+ let itemsHtml = '';
+ items.forEach(item => {
+ const statusIcon = getStatusIcon(item.status);
+ const statusBadge = getStatusBadge(item.status);
+ const lastEpInfo = item.last_episode_downloaded > 0
+ ? `Dernier épisode: ${item.last_episode_downloaded}`
+ : '';
+
+ itemsHtml += `
+
+
+
+
+
${escapeHtml(item.anime_title)}
+ ${statusBadge}
+
+
+
+ ${statusIcon} ${item.provider_id} • ${item.lang.toUpperCase()}
+
+
+ ${lastEpInfo ? `
+
+ ${lastEpInfo}
+
+ ` : ''}
+
+ ${item.last_checked ? `
+
+ Dernière vérification: ${new Date(item.last_checked).toLocaleString('fr-FR')}
+
+ ` : '
Jamais vérifié
'}
+
+
+
+ ${item.status === 'active' && item.auto_download ? `
+
+ ` : item.status === 'paused' ? `
+
+ ` : ''}
+
+
+
+
+
+
+
+ ${item.synopsis ? `
+
+ 📖 Synopsis
+ ${escapeHtml(item.synopsis)}
+
+ ` : ''}
+
+ `;
+ });
+
+ container.innerHTML = statsHtml + itemsHtml;
+
+ } catch (error) {
+ console.error('Error loading watchlist:', error);
+ container.innerHTML = `
+
+ ❌ Erreur lors du chargement: ${error.message}
+
+ `;
+ }
+}
+
+/**
+ * Get status icon
+ */
+function getStatusIcon(status) {
+ const icons = {
+ 'active': '✅',
+ 'paused': '⏸️',
+ 'completed': '✨',
+ 'archived': '📦'
+ };
+ return icons[status] || '📌';
+}
+
+/**
+ * Get status badge
+ */
+function getStatusBadge(status) {
+ const badges = {
+ 'active': 'Actif',
+ 'paused': 'En pause',
+ 'completed': 'Terminé',
+ 'archived': 'Archivé'
+ };
+ return badges[status] || '';
+}
+
+/**
+ * Add anime to watchlist from search results
+ */
+async function handleAddToWatchlist(animeUrl, providerId) {
+ try {
+ // Get anime details from the DOM or API
+ const response = await fetch(`${API_BASE}/anime/metadata?url=${encodeURIComponent(animeUrl)}`);
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch anime details');
+ }
+
+ const metadata = await response.json();
+
+ const itemData = {
+ anime_title: metadata.title || 'Unknown Anime',
+ anime_url: animeUrl,
+ provider_id: providerId,
+ lang: 'vostfr',
+ auto_download: true,
+ quality_preference: 'auto',
+ poster_image: metadata.poster_image || null,
+ cover_image: metadata.cover_image || null,
+ synopsis: metadata.synopsis || null,
+ genres: metadata.genres || []
+ };
+
+ const result = await addToWatchlist(itemData);
+
+ alert(`✅ "${result.anime_title}" a été ajouté à votre watchlist!`);
+
+ // Update button to show it's already in watchlist
+ updateAddButton(animeUrl, true);
+
+ } catch (error) {
+ console.error('Error adding to watchlist:', error);
+ alert(`❌ Erreur: ${error.message}`);
+ }
+}
+
+/**
+ * Update add button state
+ */
+function updateAddButton(animeUrl, isInWatchlist) {
+ // Find all buttons for this anime
+ const buttons = document.querySelectorAll(`[data-watchlist-url="${encodeURIComponent(animeUrl)}"]`);
+
+ buttons.forEach(button => {
+ if (isInWatchlist) {
+ button.innerHTML = '✓ Suivi';
+ button.disabled = true;
+ button.style.opacity = '0.6';
+ } else {
+ button.innerHTML = '+ Suivre';
+ button.disabled = false;
+ button.style.opacity = '1';
+ }
+ });
+}
+
+/**
+ * Pause watchlist item
+ */
+async function handlePauseWatchlist(itemId) {
+ try {
+ await pauseWatchlistItem(itemId);
+ await displayWatchlist();
+ alert('✅ Anime mis en pause');
+ } catch (error) {
+ console.error('Error pausing item:', error);
+ alert(`❌ Erreur: ${error.message}`);
+ }
+}
+
+/**
+ * Resume watchlist item
+ */
+async function handleResumeWatchlist(itemId) {
+ try {
+ await resumeWatchlistItem(itemId);
+ await displayWatchlist();
+ alert('✅ Anime réactivé');
+ } catch (error) {
+ console.error('Error resuming item:', error);
+ alert(`❌ Erreur: ${error.message}`);
+ }
+}
+
+/**
+ * Check specific item
+ */
+async function handleCheckItem(itemId) {
+ const button = event.target;
+ const originalText = button.innerHTML;
+
+ try {
+ button.disabled = true;
+ button.innerHTML = '⏳...';
+
+ const result = await checkWatchlistItem(itemId);
+
+ if (result.new_episodes_found > 0) {
+ alert(`🎉 ${result.new_episodes_found} nouveau(x) épisode(s) trouvé(s)!\n\n${result.episodes_downloaded.length} téléchargé(s)`);
+ } else {
+ alert('ℹ️ Aucun nouvel épisode trouvé');
+ }
+
+ await displayWatchlist();
+
+ } catch (error) {
+ console.error('Error checking item:', error);
+ alert(`❌ Erreur: ${error.message}`);
+ } finally {
+ button.disabled = false;
+ button.innerHTML = originalText;
+ }
+}
+
+/**
+ * Delete watchlist item
+ */
+async function handleDeleteWatchlist(itemId) {
+ if (!confirm('⚠️ Êtes-vous sûr de vouloir supprimer cet anime de votre watchlist ?')) {
+ return;
+ }
+
+ try {
+ await deleteFromWatchlist(itemId);
+ await displayWatchlist();
+ alert('✅ Anime supprimé de la watchlist');
+ } catch (error) {
+ console.error('Error deleting item:', error);
+ alert(`❌ Erreur: ${error.message}`);
+ }
+}
+
+/**
+ * Check all items
+ */
+async function handleCheckAll() {
+ const button = event.target;
+ const originalText = button.innerHTML;
+
+ try {
+ button.disabled = true;
+ button.innerHTML = '⏳ Vérification...';
+
+ const result = await checkAllWatchlistItems();
+
+ alert(`✅ Vérification terminée!\n\n${result.checked} animes vérifiés\n${result.total_new_episodes} nouveaux épisodes trouvés\n${result.total_downloaded} téléchargés`);
+
+ await displayWatchlist();
+
+ } catch (error) {
+ console.error('Error checking all:', error);
+ alert(`❌ Erreur: ${error.message}`);
+ } finally {
+ button.disabled = false;
+ button.innerHTML = originalText;
+ }
+}
+
+// Make functions available globally
+window.displayWatchlist = displayWatchlist;
+window.handleAddToWatchlist = handleAddToWatchlist;
+window.handlePauseWatchlist = handlePauseWatchlist;
+window.handleResumeWatchlist = handleResumeWatchlist;
+window.handleCheckItem = handleCheckItem;
+window.handleDeleteWatchlist = handleDeleteWatchlist;
+window.handleCheckAll = handleCheckAll;
diff --git a/static/js/watchlist.js b/static/js/watchlist.js
new file mode 100644
index 0000000..248edc7
--- /dev/null
+++ b/static/js/watchlist.js
@@ -0,0 +1,319 @@
+/**
+ * Watchlist management and auto-download UI
+ */
+
+const API_BASE = '/api';
+
+/**
+ * Get user's watchlist
+ */
+async function getWatchlist(status = null) {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ throw new Error('Not authenticated');
+ }
+
+ let url = `${API_BASE}/watchlist`;
+ if (status) {
+ url += `?status=${status}`;
+ }
+
+ const response = await fetch(url, {
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch watchlist');
+ }
+
+ return await response.json();
+}
+
+/**
+ * Add anime to watchlist
+ */
+async function addToWatchlist(animeData) {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ throw new Error('Not authenticated');
+ }
+
+ const response = await fetch(`${API_BASE}/watchlist`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(animeData)
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.detail || 'Failed to add to watchlist');
+ }
+
+ return await response.json();
+}
+
+/**
+ * Update watchlist item
+ */
+async function updateWatchlistItem(itemId, updateData) {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ throw new Error('Not authenticated');
+ }
+
+ const response = await fetch(`${API_BASE}/watchlist/${itemId}`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(updateData)
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to update watchlist item');
+ }
+
+ return await response.json();
+}
+
+/**
+ * Delete from watchlist
+ */
+async function deleteFromWatchlist(itemId) {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ throw new Error('Not authenticated');
+ }
+
+ const response = await fetch(`${API_BASE}/watchlist/${itemId}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to delete from watchlist');
+ }
+
+ return await response.json();
+}
+
+/**
+ * Pause watchlist item
+ */
+async function pauseWatchlistItem(itemId) {
+ return await updateWatchlistItem(itemId, { status: 'paused' });
+}
+
+/**
+ * Resume watchlist item
+ */
+async function resumeWatchlistItem(itemId) {
+ return await updateWatchlistItem(itemId, { status: 'active' });
+}
+
+/**
+ * Check specific anime for new episodes
+ */
+async function checkWatchlistItem(itemId) {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ throw new Error('Not authenticated');
+ }
+
+ const response = await fetch(`${API_BASE}/watchlist/${itemId}/check`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to check for new episodes');
+ }
+
+ return await response.json();
+}
+
+/**
+ * Check all watchlist items
+ */
+async function checkAllWatchlistItems() {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ throw new Error('Not authenticated');
+ }
+
+ const response = await fetch(`${API_BASE}/watchlist/check-all`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to check all items');
+ }
+
+ return await response.json();
+}
+
+/**
+ * Get watchlist settings
+ */
+async function getWatchlistSettings() {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ throw new Error('Not authenticated');
+ }
+
+ const response = await fetch(`${API_BASE}/watchlist/settings`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch settings');
+ }
+
+ return await response.json();
+}
+
+/**
+ * Update watchlist settings
+ */
+async function updateWatchlistSettings(settings) {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ throw new Error('Not authenticated');
+ }
+
+ const response = await fetch(`${API_BASE}/watchlist/settings`, {
+ method: 'PUT',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(settings)
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to update settings');
+ }
+
+ return await response.json();
+}
+
+/**
+ * Get watchlist statistics
+ */
+async function getWatchlistStats() {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ throw new Error('Not authenticated');
+ }
+
+ const response = await fetch(`${API_BASE}/watchlist/stats`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch statistics');
+ }
+
+ return await response.json();
+}
+
+/**
+ * Get scheduler status
+ */
+async function getSchedulerStatus() {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ throw new Error('Not authenticated');
+ }
+
+ const response = await fetch(`${API_BASE}/watchlist/scheduler/status`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch scheduler status');
+ }
+
+ return await response.json();
+}
+
+/**
+ * Start scheduler
+ */
+async function startScheduler() {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ throw new Error('Not authenticated');
+ }
+
+ const response = await fetch(`${API_BASE}/watchlist/scheduler/start`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to start scheduler');
+ }
+
+ return await response.json();
+}
+
+/**
+ * Stop scheduler
+ */
+async function stopScheduler() {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ throw new Error('Not authenticated');
+ }
+
+ const response = await fetch(`${API_BASE}/watchlist/scheduler/stop`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to stop scheduler');
+ }
+
+ return await response.json();
+}
+
+// Make functions available globally
+window.getWatchlist = getWatchlist;
+window.addToWatchlist = addToWatchlist;
+window.updateWatchlistItem = updateWatchlistItem;
+window.deleteFromWatchlist = deleteFromWatchlist;
+window.pauseWatchlistItem = pauseWatchlistItem;
+window.resumeWatchlistItem = resumeWatchlistItem;
+window.checkWatchlistItem = checkWatchlistItem;
+window.checkAllWatchlistItems = checkAllWatchlistItems;
+window.getWatchlistSettings = getWatchlistSettings;
+window.updateWatchlistSettings = updateWatchlistSettings;
+window.getWatchlistStats = getWatchlistStats;
+window.getSchedulerStatus = getSchedulerStatus;
+window.startScheduler = startScheduler;
+window.stopScheduler = stopScheduler;
diff --git a/templates/components/header.html b/templates/components/header.html
index 2d4c1fe..3f3c618 100644
--- a/templates/components/header.html
+++ b/templates/components/header.html
@@ -44,5 +44,11 @@
Fournisseurs
+
diff --git a/templates/watchlist.html b/templates/watchlist.html
new file mode 100644
index 0000000..ffdbcaf
--- /dev/null
+++ b/templates/watchlist.html
@@ -0,0 +1,531 @@
+
+
+
+
+
+ Watchlist - Ohm Stream Downloader
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Chargement de la watchlist...
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test_watchlist.py b/test_watchlist.py
new file mode 100644
index 0000000..27588b2
--- /dev/null
+++ b/test_watchlist.py
@@ -0,0 +1,248 @@
+#!/usr/bin/env python3
+"""
+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
+)
+
+
+async def test_watchlist_manager():
+ """Test basic watchlist operations"""
+ print("\n" + "="*60)
+ print("🧪 TEST 1: Watchlist Manager")
+ print("="*60)
+
+ # Test user ID
+ test_user = "test_user_1"
+
+ # Create a test item
+ print("\n1. Creating watchlist item...")
+ item_data = WatchlistItemCreate(
+ anime_title="Test Anime",
+ anime_url="https://anime-sama.si/catalogue/test/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}")
+ except Exception as e:
+ print(f" ❌ Create failed: {e}")
+ import traceback
+ traceback.print_exc()
+ return False
+
+ # Get all items
+ print("\n2. Getting all items...")
+ try:
+ items = watchlist_manager.get_all(test_user)
+ print(f" ✅ Found {len(items)} items")
+ 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: {stats}")
+ except Exception as e:
+ print(f" ❌ Stats failed: {e}")
+ return False
+
+ # Update item
+ print("\n4. Updating item...")
+ 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
+
+ # Delete item
+ print("\n5. Deleting 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
+
+
+async def test_episode_checker():
+ """Test episode checker (without actual downloads)"""
+ print("\n" + "="*60)
+ print("🧪 TEST 2: Episode Checker")
+ print("="*60)
+
+ print("\n1. Testing EpisodeChecker initialization...")
+ try:
+ # Episode checker should be initialized
+ print(f" ✅ EpisodeChecker ready")
+ print(f" Note: Actual episode checking requires valid anime URLs")
+ except Exception as e:
+ print(f" ❌ EpisodeChecker failed: {e}")
+ return False
+
+ print("\n✅ Episode Checker 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 initialization...")
+ try:
+ # Get settings
+ settings = watchlist_manager.get_settings()
+ print(f" ✅ Settings loaded: check_interval={settings.check_interval_hours}h")
+ except Exception as e:
+ print(f" ❌ Settings failed: {e}")
+ return False
+
+ print("\n2. Testing scheduler status...")
+ try:
+ status = auto_download_scheduler.get_status()
+ print(f" ✅ Scheduler status: running={status['running']}")
+ except Exception as e:
+ print(f" ❌ Status failed: {e}")
+ return False
+
+ print("\n3. Testing scheduler start/stop...")
+ try:
+ # Start scheduler
+ await auto_download_scheduler.start()
+ print(" ✅ Scheduler started")
+
+ status = auto_download_scheduler.get_status()
+ if not status['running']:
+ print(" ❌ Scheduler not running after start")
+ return False
+
+ # Stop scheduler
+ await auto_download_scheduler.stop()
+ print(" ✅ Scheduler stopped")
+
+ status = auto_download_scheduler.get_status()
+ if status['running']:
+ print(" ❌ Scheduler still running after stop")
+ return False
+
+ except Exception as e:
+ print(f" ❌ Start/stop failed: {e}")
+ return False
+
+ print("\n✅ Scheduler tests PASSED")
+ return True
+
+
+async def test_settings():
+ """Test settings management"""
+ print("\n" + "="*60)
+ print("🧪 TEST 4: Settings Management")
+ print("="*60)
+
+ print("\n1. Testing settings update...")
+ try:
+ # Update settings
+ new_settings = {
+ "check_interval_hours": 12,
+ "auto_download_enabled": True,
+ "max_concurrent_auto_downloads": 3
+ }
+ watchlist_manager.update_settings(new_settings)
+ print(f" ✅ Settings updated")
+
+ # Verify
+ settings = watchlist_manager.get_settings()
+ if settings.check_interval_hours != 12:
+ print(f" ❌ Settings not saved correctly")
+ return False
+
+ print(f" ✅ Settings verified: check_interval={settings.check_interval_hours}h")
+
+ except Exception as e:
+ print(f" ❌ Settings failed: {e}")
+ import traceback
+ traceback.print_exc()
+ return False
+
+ print("\n✅ Settings 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", test_watchlist_manager),
+ ("Episode Checker", test_episode_checker),
+ ("Scheduler", test_scheduler),
+ ("Settings", test_settings)
+ ]
+
+ results = []
+
+ for name, test_func in tests:
+ try:
+ result = await 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.")
+ 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)
diff --git a/test_watchlist_simple.py b/test_watchlist_simple.py
new file mode 100644
index 0000000..d2d691e
--- /dev/null
+++ b/test_watchlist_simple.py
@@ -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)