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:
+86
-8
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user