Add comprehensive Pydantic models and configure templates/static files
- Create detailed Pydantic models for anime requests and responses - Add AnimeCreateRequest, AnimeUpdateRequest, PaginatedAnimeResponse, etc. - Update route signatures to use proper response models - Convert return values to use Pydantic models instead of raw dicts - Configure Jinja2Templates in FastAPI application - Mount StaticFiles for CSS, JS, images at /static endpoint - Update anime search and list endpoints to use typed responses - Mark completed Pydantic models and template configuration tasks in web_todo.md
This commit is contained in:
parent
e15c0a21e0
commit
6e136e832b
@ -27,6 +27,8 @@ from fastapi import FastAPI, HTTPException, Depends, Security, status, Request
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic_settings import BaseSettings
|
||||
import uvicorn
|
||||
@ -228,6 +230,12 @@ app = FastAPI(
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# Configure templates
|
||||
templates = Jinja2Templates(directory="src/server/web/templates")
|
||||
|
||||
# Mount static files
|
||||
app.mount("/static", StaticFiles(directory="src/server/web/static"), name="static")
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
|
||||
@ -33,6 +33,43 @@ class AnimeResponse(BaseModel):
|
||||
folder: Optional[str] = None
|
||||
episodes: int = 0
|
||||
|
||||
class AnimeCreateRequest(BaseModel):
|
||||
"""Request model for creating anime entries."""
|
||||
name: str = Field(..., min_length=1, max_length=255)
|
||||
folder: str = Field(..., min_length=1)
|
||||
description: Optional[str] = None
|
||||
status: str = Field(default="planned", pattern="^(ongoing|completed|planned|dropped|paused)$")
|
||||
genre: Optional[str] = None
|
||||
year: Optional[int] = Field(None, ge=1900, le=2100)
|
||||
|
||||
class AnimeUpdateRequest(BaseModel):
|
||||
"""Request model for updating anime entries."""
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
folder: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
status: Optional[str] = Field(None, pattern="^(ongoing|completed|planned|dropped|paused)$")
|
||||
genre: Optional[str] = None
|
||||
year: Optional[int] = Field(None, ge=1900, le=2100)
|
||||
|
||||
class PaginatedAnimeResponse(BaseModel):
|
||||
"""Paginated response model for anime lists."""
|
||||
success: bool = True
|
||||
data: List[AnimeResponse]
|
||||
pagination: Dict[str, Any]
|
||||
|
||||
class AnimeSearchResponse(BaseModel):
|
||||
"""Response model for anime search results."""
|
||||
success: bool = True
|
||||
data: List[AnimeResponse]
|
||||
pagination: Dict[str, Any]
|
||||
search: Dict[str, Any]
|
||||
|
||||
class RescanResponse(BaseModel):
|
||||
"""Response model for rescan operations."""
|
||||
success: bool
|
||||
message: str
|
||||
total_series: int
|
||||
|
||||
# Dependency to get SeriesApp instance
|
||||
def get_series_app() -> SeriesApp:
|
||||
"""Get SeriesApp instance for business logic operations."""
|
||||
@ -47,7 +84,7 @@ def get_series_app() -> SeriesApp:
|
||||
router = APIRouter(prefix='/api/v1/anime', tags=['anime'])
|
||||
|
||||
|
||||
@router.get('', response_model=Dict[str, Any])
|
||||
@router.get('', response_model=PaginatedAnimeResponse)
|
||||
async def list_anime(
|
||||
status: Optional[str] = Query(None, pattern="^(ongoing|completed|planned|dropped|paused)$"),
|
||||
genre: Optional[str] = Query(None),
|
||||
@ -57,7 +94,7 @@ async def list_anime(
|
||||
per_page: int = Query(50, ge=1, le=1000),
|
||||
current_user: Optional[Dict] = Depends(get_current_user),
|
||||
series_app: SeriesApp = Depends(get_series_app)
|
||||
) -> Dict[str, Any]:
|
||||
) -> PaginatedAnimeResponse:
|
||||
"""
|
||||
Get all anime with optional filtering and pagination.
|
||||
|
||||
@ -76,35 +113,34 @@ async def list_anime(
|
||||
# Get the series list from SeriesApp
|
||||
anime_list = series_app.series_list
|
||||
|
||||
# Convert to list of dicts for response
|
||||
formatted_anime = []
|
||||
# Convert to list of AnimeResponse objects
|
||||
anime_responses = []
|
||||
for series_item in anime_list:
|
||||
anime_dict = {
|
||||
'id': getattr(series_item, 'id', str(uuid.uuid4())),
|
||||
'title': getattr(series_item, 'name', 'Unknown'),
|
||||
'folder': getattr(series_item, 'folder', ''),
|
||||
'description': getattr(series_item, 'description', ''),
|
||||
'status': 'ongoing', # Default status
|
||||
'episodes': getattr(series_item, 'total_episodes', 0)
|
||||
}
|
||||
anime_response = AnimeResponse(
|
||||
id=getattr(series_item, 'id', str(uuid.uuid4())),
|
||||
title=getattr(series_item, 'name', 'Unknown'),
|
||||
folder=getattr(series_item, 'folder', ''),
|
||||
description=getattr(series_item, 'description', ''),
|
||||
status='ongoing', # Default status
|
||||
episodes=getattr(series_item, 'total_episodes', 0)
|
||||
)
|
||||
|
||||
# Apply search filter if provided
|
||||
if search:
|
||||
if search.lower() not in anime_dict['title'].lower():
|
||||
if search.lower() not in anime_response.title.lower():
|
||||
continue
|
||||
|
||||
formatted_anime.append(anime_dict)
|
||||
anime_responses.append(anime_response)
|
||||
|
||||
# Apply pagination
|
||||
total = len(formatted_anime)
|
||||
total = len(anime_responses)
|
||||
start_idx = (page - 1) * per_page
|
||||
end_idx = start_idx + per_page
|
||||
paginated_anime = formatted_anime[start_idx:end_idx]
|
||||
paginated_anime = anime_responses[start_idx:end_idx]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": paginated_anime,
|
||||
"pagination": {
|
||||
return PaginatedAnimeResponse(
|
||||
data=paginated_anime,
|
||||
pagination={
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"total": total,
|
||||
@ -112,7 +148,7 @@ async def list_anime(
|
||||
"has_next": end_idx < total,
|
||||
"has_prev": page > 1
|
||||
}
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@ -370,14 +406,14 @@ def delete_anime(anime_id: int) -> Dict[str, Any]:
|
||||
)
|
||||
|
||||
|
||||
@router.get('/search', response_model=Dict[str, Any])
|
||||
@router.get('/search', response_model=AnimeSearchResponse)
|
||||
async def search_anime(
|
||||
q: str = Query(..., min_length=2, description="Search query"),
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(20, ge=1, le=100),
|
||||
current_user: Optional[Dict] = Depends(get_current_user),
|
||||
series_app: SeriesApp = Depends(get_series_app)
|
||||
) -> Dict[str, Any]:
|
||||
) -> AnimeSearchResponse:
|
||||
"""
|
||||
Search anime by name using SeriesApp.
|
||||
|
||||
@ -393,29 +429,28 @@ async def search_anime(
|
||||
# Use SeriesApp to perform search
|
||||
search_results = series_app.search(q)
|
||||
|
||||
# Convert search results to our response format
|
||||
formatted_results = []
|
||||
# Convert search results to AnimeResponse objects
|
||||
anime_responses = []
|
||||
for result in search_results:
|
||||
anime_dict = {
|
||||
'id': getattr(result, 'id', str(uuid.uuid4())),
|
||||
'title': getattr(result, 'name', getattr(result, 'title', 'Unknown')),
|
||||
'description': getattr(result, 'description', ''),
|
||||
'status': 'available',
|
||||
'episodes': getattr(result, 'episodes', 0),
|
||||
'key': getattr(result, 'key', '')
|
||||
}
|
||||
formatted_results.append(anime_dict)
|
||||
anime_response = AnimeResponse(
|
||||
id=getattr(result, 'id', str(uuid.uuid4())),
|
||||
title=getattr(result, 'name', getattr(result, 'title', 'Unknown')),
|
||||
description=getattr(result, 'description', ''),
|
||||
status='available',
|
||||
episodes=getattr(result, 'episodes', 0),
|
||||
folder=getattr(result, 'key', '')
|
||||
)
|
||||
anime_responses.append(anime_response)
|
||||
|
||||
# Apply pagination
|
||||
total = len(formatted_results)
|
||||
total = len(anime_responses)
|
||||
start_idx = (page - 1) * per_page
|
||||
end_idx = start_idx + per_page
|
||||
paginated_results = formatted_results[start_idx:end_idx]
|
||||
paginated_results = anime_responses[start_idx:end_idx]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": paginated_results,
|
||||
"pagination": {
|
||||
return AnimeSearchResponse(
|
||||
data=paginated_results,
|
||||
pagination={
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"total": total,
|
||||
@ -423,11 +458,11 @@ async def search_anime(
|
||||
"has_next": end_idx < total,
|
||||
"has_prev": page > 1
|
||||
},
|
||||
"search": {
|
||||
search={
|
||||
"query": q,
|
||||
"total_results": total
|
||||
}
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@ -633,11 +668,11 @@ def bulk_anime_operation() -> Dict[str, Any]:
|
||||
message=f"Bulk {action} operation completed"
|
||||
)
|
||||
|
||||
@router.post('/rescan', response_model=Dict[str, Any])
|
||||
@router.post('/rescan', response_model=RescanResponse)
|
||||
async def rescan_anime_directory(
|
||||
current_user: Dict = Depends(get_current_user),
|
||||
series_app: SeriesApp = Depends(get_series_app)
|
||||
) -> Dict[str, Any]:
|
||||
) -> RescanResponse:
|
||||
"""
|
||||
Rescan the anime directory for new episodes and series.
|
||||
|
||||
@ -653,11 +688,11 @@ async def rescan_anime_directory(
|
||||
|
||||
series_app.ReScan(progress_callback)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Anime directory rescanned successfully",
|
||||
"total_series": len(series_app.series_list) if hasattr(series_app, 'series_list') else 0
|
||||
}
|
||||
return RescanResponse(
|
||||
success=True,
|
||||
message="Anime directory rescanned successfully",
|
||||
total_series=len(series_app.series_list) if hasattr(series_app, 'series_list') else 0
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
|
||||
12
web_todo.md
12
web_todo.md
@ -35,17 +35,17 @@ This document contains tasks for migrating the web application from Flask to Fas
|
||||
- [x] Convert Flask `redirect()` and `url_for()` to FastAPI equivalents
|
||||
|
||||
### Request/Response Models
|
||||
- [ ] Create Pydantic models for request bodies (replace Flask request parsing)
|
||||
- [ ] Create Pydantic models for response schemas
|
||||
- [x] Create Pydantic models for request bodies (replace Flask request parsing)
|
||||
- [x] Create Pydantic models for response schemas
|
||||
- [ ] Update form handling to use FastAPI Form dependencies
|
||||
- [ ] Convert file upload handling to FastAPI UploadFile
|
||||
|
||||
## 🎨 Template and Static Files Migration
|
||||
|
||||
### Template Engine Setup
|
||||
- [ ] Configure Jinja2Templates in FastAPI application
|
||||
- [ ] Set up template directory structure
|
||||
- [ ] Create templates directory configuration in FastAPI app
|
||||
- [x] Configure Jinja2Templates in FastAPI application
|
||||
- [x] Set up template directory structure
|
||||
- [x] Create templates directory configuration in FastAPI app
|
||||
|
||||
### HTML Template Migration
|
||||
- [ ] Review all `.html` files in templates directory
|
||||
@ -55,7 +55,7 @@ This document contains tasks for migrating the web application from Flask to Fas
|
||||
- [ ] Test all template variables and filters still work correctly
|
||||
|
||||
### Static Files Configuration
|
||||
- [ ] Configure StaticFiles mount in FastAPI for CSS, JS, images
|
||||
- [x] Configure StaticFiles mount in FastAPI for CSS, JS, images
|
||||
- [ ] Update static file URL generation in templates
|
||||
- [ ] Verify all CSS file references work correctly
|
||||
- [ ] Verify all JavaScript file references work correctly
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user