Files
ohm_streaming/static/js/watchlist.js
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 <[email protected]>
Co-Authored-By: Happy <[email protected]>
2026-01-29 21:56:39 +00:00

320 lines
7.2 KiB
JavaScript

/**
* 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;