feat: Add anime metadata extraction and fix episode selection bug

Features:
- Added rich metadata extraction for all anime providers (Anime-Sama, Neko-Sama, Anime-Ultime, Vostfree)
- New AnimeMetadata model with synopsis, genres, rating, release year, studio, poster/banner images, episode count, and status
- New /api/anime/metadata endpoint for fetching metadata of specific anime
- Enhanced /api/anime/search endpoint with optional include_metadata parameter
- Updated web interface with metadata display (expandable synopsis, genres, rating, year)
- Added metadata toggle checkbox in search UI (disabled by default for performance)

Bug Fixes:
- Fixed episode selection bug where select would reset to default after any change
- Removed onchange event from select element that was causing unwanted reloads
- Fixed download button disappearing after episode download
- Episodes can now be downloaded multiple times without page refresh

Enhancements:
- Metadata displayed with icons (📅 year,  rating, 🏷️ genres, 📺 episodes, 📡 status)
- Expandable synopsis section for detailed descriptions
- Better visual organization of anime information
- Maintains backward compatibility (metadata is optional)

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:
root
2026-01-23 09:36:59 +00:00
parent 40977438ff
commit 20cad0b4fe
7 changed files with 693 additions and 29 deletions
+86 -8
View File
@@ -377,6 +377,46 @@
font-size: 12px;
}
.anime-metadata {
font-size: 12px;
color: #aaa;
margin-bottom: 10px;
padding: 8px 12px;
background: rgba(0, 0, 0, 0.2);
border-radius: 6px;
line-height: 1.6;
}
.anime-synopsis {
margin-bottom: 10px;
padding: 10px 12px;
background: rgba(0, 217, 255, 0.05);
border-left: 3px solid #00d9ff;
border-radius: 6px;
}
.anime-synopsis summary {
cursor: pointer;
font-size: 13px;
font-weight: 600;
color: #00d9ff;
margin-bottom: 8px;
user-select: none;
}
.anime-synopsis summary:hover {
color: #00ff88;
}
.anime-synopsis p {
font-size: 12px;
color: #ccc;
line-height: 1.5;
margin: 0;
max-height: 200px;
overflow-y: auto;
}
.loading-spinner {
text-align: center;
padding: 40px;
@@ -537,6 +577,12 @@
Rechercher
</button>
</div>
<div style="display: flex; align-items: center; gap: 10px; margin-top: 10px; font-size: 13px; color: #888;">
<input type="checkbox" id="includeMetadata" style="width: auto; margin: 0;">
<label for="includeMetadata" style="cursor: pointer; user-select: none;">
📊 Inclure les métadonnées (synopsis, genres, note) • Plus lent mais plus complet
</label>
</div>
<div style="margin-top: 10px; padding: 10px; background: rgba(0, 217, 255, 0.1); border-radius: 8px; font-size: 12px; color: #88;">
💡 <strong>Astuce:</strong> Pour de meilleurs résultats, essayez le nom en anglais ou japonais (ex: "One Piece", "Naruto"). Certains sites n'ont pas tous les animes.
</div>
@@ -594,6 +640,7 @@
async function searchAnime() {
const query = document.getElementById('searchInput').value.trim();
const lang = document.getElementById('langSelect').value;
const includeMetadata = document.getElementById('includeMetadata').checked;
if (!query) {
alert('Veuillez entrer un nom d\'anime');
@@ -604,7 +651,7 @@
resultsContainer.innerHTML = '<div class="loading-spinner">Recherche en cours...</div>';
try {
const response = await fetch(`${API_BASE}/anime/search?q=${encodeURIComponent(query)}&lang=${lang}`);
const response = await fetch(`${API_BASE}/anime/search?q=${encodeURIComponent(query)}&lang=${lang}&include_metadata=${includeMetadata}`);
const data = await response.json();
displaySearchResults(data, lang);
@@ -627,14 +674,47 @@
results.forEach(anime => {
const providerInfo = providers.anime_providers[providerId];
// Build metadata HTML if available
let metadataHtml = '';
if (anime.metadata) {
const meta = anime.metadata;
let metaParts = [];
if (meta.release_year) metaParts.push(`📅 ${meta.release_year}`);
if (meta.rating) metaParts.push(`${meta.rating}`);
if (meta.genres && meta.genres.length > 0) metaParts.push(`🏷️ ${meta.genres.slice(0, 3).join(', ')}`);
if (meta.total_episodes) metaParts.push(`📺 ${meta.total_episodes} épisodes`);
if (meta.status) metaParts.push(`📡 ${meta.status === 'Ongoing' ? 'En cours' : 'Terminé'}`);
if (metaParts.length > 0) {
metadataHtml = `
<div class="anime-metadata">
${metaParts.join(' • ')}
</div>
`;
}
// Add synopsis if available (expandable)
if (meta.synopsis) {
metadataHtml += `
<details class="anime-synopsis">
<summary>📖 Synopsis</summary>
<p>${escapeHtml(meta.synopsis)}</p>
</details>
`;
}
}
html += `
<div class="anime-card" id="anime-${providerId}-${encodeURIComponent(anime.url)}">
<div class="anime-card-header">
<div class="anime-card-title">${escapeHtml(anime.title)}</div>
<div class="anime-card-provider">${providerInfo?.icon || ''} ${providerInfo?.name || providerId}</div>
</div>
${metadataHtml}
<div class="anime-card-actions">
<select id="episodes-${providerId}-${encodeURIComponent(anime.url)}" onchange="loadEpisodesForAnime('${providerId}', '${encodeURIComponent(anime.url)}', '${lang}', this)">
<select id="episodes-${providerId}-${encodeURIComponent(anime.url)}">
<option value="">Charger les épisodes...</option>
</select>
</div>
@@ -739,14 +819,12 @@
});
if (response.ok) {
// Reset selection
selectElement.value = '';
const actionsId = `actions-${providerId}-${encodedUrl}`;
document.getElementById(actionsId).style.display = 'none';
// Show success message
// Show success message and refresh downloads
loadDownloads();
alert('Téléchargement démarré!');
// Keep the select available for more downloads, just reset the selection
selectElement.value = '';
}
} catch (error) {
console.error('Error:', error);