From 39991d9ffc6a2a45c5c774f37c04ed2dc21292c8 Mon Sep 17 00:00:00 2001 From: Lukas Date: Sun, 26 Oct 2025 19:28:23 +0100 Subject: [PATCH] fix: anime api --- src/server/api/anime.py | 56 +++++++++++++++++++++++++-------- src/server/web/static/js/app.js | 56 ++++++++++++++++++++++++--------- 2 files changed, 85 insertions(+), 27 deletions(-) diff --git a/src/server/api/anime.py b/src/server/api/anime.py index 8e0e2df..382c073 100644 --- a/src/server/api/anime.py +++ b/src/server/api/anime.py @@ -95,9 +95,24 @@ async def get_process_locks( class AnimeSummary(BaseModel): - id: str - title: str - missing_episodes: int + """Summary of an anime series with missing episodes.""" + key: str # Unique identifier (used as id in frontend) + name: str # Series name (can be empty) + site: str # Provider site + folder: str # Local folder name + missing_episodes: dict # Episode dictionary: {season: [episode_numbers]} + + class Config: + """Pydantic model configuration.""" + json_schema_extra = { + "example": { + "key": "beheneko-the-elf-girls-cat", + "name": "Beheneko", + "site": "aniworld.to", + "folder": "beheneko the elf girls cat (2025)", + "missing_episodes": {"1": [1, 2, 3, 4]} + } + } class AnimeDetail(BaseModel): @@ -203,26 +218,41 @@ async def list_anime( series = series_app.List.GetMissingEpisode() summaries: List[AnimeSummary] = [] for serie in series: - episodes_dict = getattr(serie, "episodeDict", {}) or {} - missing_episodes = len(episodes_dict) - key = getattr(serie, "key", getattr(serie, "folder", "")) - title = getattr(serie, "name", "") + # Get all properties from the serie object + key = getattr(serie, "key", "") + name = getattr(serie, "name", "") + site = getattr(serie, "site", "") + folder = getattr(serie, "folder", "") + episode_dict = getattr(serie, "episodeDict", {}) or {} + + # Convert episode dict keys to strings for JSON serialization + missing_episodes = {str(k): v for k, v in episode_dict.items()} + summaries.append( AnimeSummary( - id=key, - title=title, + key=key, + name=name, + site=site, + folder=folder, missing_episodes=missing_episodes, ) ) # Apply sorting if requested if sort_by: - if sort_by == "title": - summaries.sort(key=lambda x: x.title) + if sort_by in ["title", "name"]: + summaries.sort(key=lambda x: x.name or x.key) elif sort_by == "id": - summaries.sort(key=lambda x: x.id) + summaries.sort(key=lambda x: x.key) elif sort_by == "missing_episodes": - summaries.sort(key=lambda x: x.missing_episodes, reverse=True) + # Sort by total number of missing episodes + # (count all episodes across all seasons) + summaries.sort( + key=lambda x: sum( + len(eps) for eps in x.missing_episodes.values() + ), + reverse=True + ) return summaries except HTTPException: diff --git a/src/server/web/static/js/app.js b/src/server/web/static/js/app.js index fa3bfec..bdbe2d5 100644 --- a/src/server/web/static/js/app.js +++ b/src/server/web/static/js/app.js @@ -567,14 +567,24 @@ class AniWorldApp { // Check if response has the expected format if (Array.isArray(data)) { - // API returns array of AnimeSummary objects directly - this.seriesData = data.map(anime => ({ - id: anime.id, - name: anime.title, - title: anime.title, - missing_episodes: anime.missing_episodes || 0, - episodeDict: {} // Will be populated when needed - })); + // API returns array of AnimeSummary objects with full serie data + this.seriesData = data.map(anime => { + // Count total missing episodes from the episode dictionary + const episodeDict = anime.missing_episodes || {}; + const totalMissing = Object.values(episodeDict).reduce( + (sum, episodes) => sum + (Array.isArray(episodes) ? episodes.length : 0), + 0 + ); + + return { + key: anime.key, + name: anime.name, + site: anime.site, + folder: anime.folder, + episodeDict: episodeDict, + missing_episodes: totalMissing + }; + }); } else if (data.status === 'success') { // Legacy format support this.seriesData = data.series; @@ -633,7 +643,7 @@ class AniWorldApp { filtered.sort((a, b) => { if (this.sortAlphabetical) { // Pure alphabetical sorting when A-Z is enabled - return (a.name || a.folder).localeCompare(b.name || b.folder); + return this.getDisplayName(a).localeCompare(this.getDisplayName(b)); } else { // Default sorting: missing episodes first (descending), then by name // Always show series with missing episodes first @@ -705,7 +715,7 @@ class AniWorldApp { ${isSelected ? 'checked' : ''} ${!canBeSelected ? 'disabled' : ''}>
-

${this.escapeHtml(serie.name)}

+

${this.escapeHtml(this.getDisplayName(serie))}

${this.escapeHtml(serie.folder)}
@@ -849,8 +859,8 @@ class AniWorldApp { resultsList.innerHTML = results.map(result => `
- ${this.escapeHtml(result.name)} - @@ -1026,6 +1036,24 @@ class AniWorldApp { return div.innerHTML; } + /** + * Get display name for anime/series object. + * Returns name if available and not empty, otherwise returns key. + * @param {Object} anime - Anime/series object with name and key properties + * @returns {string} Display name + */ + getDisplayName(anime) { + if (!anime) return ''; + // Use name if it exists and is not empty (after trimming whitespace) + const name = anime.name || ''; + const trimmedName = name.trim(); + if (trimmedName) { + return trimmedName; + } + // Fallback to key + return anime.key || anime.folder || ''; + } + updateConnectionStatus() { const indicator = document.getElementById('connection-status-display'); if (indicator) { @@ -2017,7 +2045,7 @@ class AniWorldApp { // Update current downloading if (data.current_downloading) { currentDownload.classList.remove('hidden'); - document.getElementById('current-serie-name').textContent = data.current_downloading.name; + document.getElementById('current-serie-name').textContent = this.getDisplayName(data.current_downloading); document.getElementById('current-episode').textContent = `${data.current_downloading.missing_episodes} episodes remaining`; } else { currentDownload.classList.add('hidden'); @@ -2028,7 +2056,7 @@ class AniWorldApp { queueList.innerHTML = data.queue.map((serie, index) => `
${index + 1}
-
${this.escapeHtml(serie.name)}
+
${this.escapeHtml(this.getDisplayName(serie))}
Waiting
`).join('');