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' : ''}>