feat: Complete watchlist & auto-download system with UI

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 <noreply@anthropic.com>
This commit is contained in:
root
2026-02-24 09:13:22 +00:00
parent c6be191699
commit da5403a307
17 changed files with 1733 additions and 259 deletions
+21 -20
View File
@@ -62,26 +62,27 @@ async function searchAnimeDetails(query, malId = null) {
const providersData = await getProvidersInfo();
// Build results HTML
streamingHtml = `
<div class="streaming-results-header">
const streamingParts = [
`<div class="streaming-results-header">
<h3>🎬 Résultats de streaming</h3>
</div>
<div class="search-results" style="margin-top: 20px;">
`;
<div class="search-results" style="margin-top: 20px;">`
];
// Display results from each provider
// Display results from each provider - render all cards in parallel
for (const [providerId, results] of Object.entries(streamingData.value.results)) {
if (results && results.length > 0) {
const provider = providersData.anime_providers[providerId];
results.forEach((anime) => {
// Use the same renderAnimeCard function from anime.js for consistency
streamingHtml += renderAnimeCard(anime, providerId, provider, 'vostfr');
});
// Render all cards for this provider
const cardPromises = results.map((anime) => renderAnimeCard(anime, providerId, provider, 'vostfr'));
const cards = await Promise.all(cardPromises);
streamingParts.push(...cards);
}
}
streamingHtml += '</div>';
streamingParts.push('</div>');
streamingHtml = streamingParts.join('');
}
// Display results
@@ -149,12 +150,12 @@ async function getProviderSearchResults(query) {
}
// Build results HTML
let html = `
<div class="streaming-results-header">
const htmlParts = [
`<div class="streaming-results-header">
<h3>🎬 Résultats de streaming</h3>
</div>
<div class="search-results" style="margin-top: 20px;">
`;
<div class="search-results" style="margin-top: 20px;">`
];
// Display results from each provider
for (const [providerId, results] of Object.entries(data.results)) {
@@ -162,16 +163,16 @@ async function getProviderSearchResults(query) {
const providersData = await getProvidersInfo();
const provider = providersData.anime_providers[providerId];
results.forEach((anime, index) => {
// Use the same renderAnimeCard function from anime.js for consistency
html += renderAnimeCard(anime, providerId, provider, 'vostfr');
});
// Render all cards for this provider in parallel
const cardPromises = results.map((anime) => renderAnimeCard(anime, providerId, provider, 'vostfr'));
const cards = await Promise.all(cardPromises);
htmlParts.push(...cards);
}
}
html += '</div>';
htmlParts.push('</div>');
return html;
return htmlParts.join('');
} catch (error) {
console.error('Error getting provider search results:', error);