da5403a307
Implement comprehensive watchlist system with automatic episode detection
and downloading. Features include per-user watchlists, scheduler-based
periodic checks, and a modern web UI.
**Backend Components:**
- WatchlistManager: JSON-based storage with multi-tenant support
- EpisodeChecker: Detects and downloads new episodes automatically
- AutoDownloadScheduler: APScheduler-based periodic task execution
- Complete REST API for CRUD operations and scheduler control
**Frontend Components:**
- Modern watchlist page with dark theme and animations
- Real-time status updates and progress tracking
- Scheduler controls with next-run display
- Add anime directly from search results
**Models & Configuration:**
- WatchlistItem with status, quality, and auto-download settings
- WatchlistSettings for global configuration
- Per-user statistics and provider tracking
**API Endpoints:**
- GET/POST /api/watchlist - List and add items
- PUT/DELETE /api/watchlist/{id} - Update and delete
- POST /api/watchlist/{id}/check - Manual check trigger
- POST /api/watchlist/check-all - Check all due items
- GET/PUT /api/watchlist/settings - Global settings
- GET /api/watchlist/stats - Statistics
- GET/POST /api/watchlist/scheduler/* - Scheduler control
**Configuration Files:**
- config/watchlist.json - User watchlist data
- config/watchlist_settings.json - Global settings
Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <[email protected]>
319 lines
7.3 KiB
JavaScript
319 lines
7.3 KiB
JavaScript
/**
|
|
* Watchlist management and auto-download UI
|
|
* Note: API_BASE is defined in api.js (loaded before this file)
|
|
*/
|
|
|
|
/**
|
|
* Get user's watchlist
|
|
*/
|
|
async function getWatchlist(status = null) {
|
|
const token = localStorage.getItem('auth_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('auth_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('auth_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('auth_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('auth_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('auth_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('auth_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('auth_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('auth_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('auth_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('auth_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('auth_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;
|