Aniworld/src/server/api/anime.py
2025-10-19 20:49:42 +02:00

121 lines
4.3 KiB
Python

from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from src.server.utils.dependencies import get_series_app, require_auth
router = APIRouter(prefix="/api/v1/anime", tags=["anime"])
class AnimeSummary(BaseModel):
id: str
title: str
missing_episodes: int
class AnimeDetail(BaseModel):
id: str
title: str
episodes: List[str]
description: Optional[str] = None
@router.get("/", response_model=List[AnimeSummary])
async def list_anime(
_auth: dict = Depends(require_auth),
series_app=Depends(get_series_app)
):
"""List series with missing episodes using the core SeriesApp."""
try:
series = series_app.List.GetMissingEpisode()
result = []
for s in series:
missing = 0
try:
missing = len(s.episodeDict) if getattr(s, "episodeDict", None) is not None else 0
except Exception:
missing = 0
result.append(AnimeSummary(id=getattr(s, "key", getattr(s, "folder", "")), title=getattr(s, "name", ""), missing_episodes=missing))
return result
except HTTPException:
raise
except Exception:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve anime list")
@router.post("/rescan")
async def trigger_rescan(series_app=Depends(get_series_app)):
"""Trigger a rescan of local series data using SeriesApp.ReScan."""
try:
# SeriesApp.ReScan expects a callback; pass a no-op
if hasattr(series_app, "ReScan"):
series_app.ReScan(lambda *args, **kwargs: None)
return {"success": True, "message": "Rescan started"}
else:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Rescan not available")
except HTTPException:
raise
except Exception:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to start rescan")
class SearchRequest(BaseModel):
query: str
@router.post("/search", response_model=List[AnimeSummary])
async def search_anime(request: SearchRequest, series_app=Depends(get_series_app)):
"""Search for new anime by query text using the SeriesApp loader."""
try:
matches = []
if hasattr(series_app, "search"):
# SeriesApp.search is synchronous in core; call directly
matches = series_app.search(request.query)
result = []
for m in matches:
# matches may be dicts or objects
if isinstance(m, dict):
mid = m.get("key") or m.get("id") or ""
title = m.get("title") or m.get("name") or ""
missing = int(m.get("missing", 0)) if m.get("missing") is not None else 0
else:
mid = getattr(m, "key", getattr(m, "id", ""))
title = getattr(m, "title", getattr(m, "name", ""))
missing = int(getattr(m, "missing", 0))
result.append(AnimeSummary(id=mid, title=title, missing_episodes=missing))
return result
except HTTPException:
raise
except Exception:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Search failed")
@router.get("/{anime_id}", response_model=AnimeDetail)
async def get_anime(anime_id: str, series_app=Depends(get_series_app)):
"""Return detailed info about a series from SeriesApp.List."""
try:
series = series_app.List.GetList()
found = None
for s in series:
if getattr(s, "key", None) == anime_id or getattr(s, "folder", None) == anime_id:
found = s
break
if not found:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Series not found")
episodes = []
epdict = getattr(found, "episodeDict", {}) or {}
for season, eps in epdict.items():
for e in eps:
episodes.append(f"{season}-{e}")
return AnimeDetail(id=getattr(found, "key", getattr(found, "folder", "")), title=getattr(found, "name", ""), episodes=episodes, description=getattr(found, "description", None))
except HTTPException:
raise
except Exception:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve series details")