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.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
import uvicorn
|
import uvicorn
|
||||||
@ -228,6 +230,12 @@ app = FastAPI(
|
|||||||
lifespan=lifespan
|
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
|
# Add CORS middleware
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
|
|||||||
@ -33,6 +33,43 @@ class AnimeResponse(BaseModel):
|
|||||||
folder: Optional[str] = None
|
folder: Optional[str] = None
|
||||||
episodes: int = 0
|
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
|
# Dependency to get SeriesApp instance
|
||||||
def get_series_app() -> SeriesApp:
|
def get_series_app() -> SeriesApp:
|
||||||
"""Get SeriesApp instance for business logic operations."""
|
"""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 = APIRouter(prefix='/api/v1/anime', tags=['anime'])
|
||||||
|
|
||||||
|
|
||||||
@router.get('', response_model=Dict[str, Any])
|
@router.get('', response_model=PaginatedAnimeResponse)
|
||||||
async def list_anime(
|
async def list_anime(
|
||||||
status: Optional[str] = Query(None, pattern="^(ongoing|completed|planned|dropped|paused)$"),
|
status: Optional[str] = Query(None, pattern="^(ongoing|completed|planned|dropped|paused)$"),
|
||||||
genre: Optional[str] = Query(None),
|
genre: Optional[str] = Query(None),
|
||||||
@ -57,7 +94,7 @@ async def list_anime(
|
|||||||
per_page: int = Query(50, ge=1, le=1000),
|
per_page: int = Query(50, ge=1, le=1000),
|
||||||
current_user: Optional[Dict] = Depends(get_current_user),
|
current_user: Optional[Dict] = Depends(get_current_user),
|
||||||
series_app: SeriesApp = Depends(get_series_app)
|
series_app: SeriesApp = Depends(get_series_app)
|
||||||
) -> Dict[str, Any]:
|
) -> PaginatedAnimeResponse:
|
||||||
"""
|
"""
|
||||||
Get all anime with optional filtering and pagination.
|
Get all anime with optional filtering and pagination.
|
||||||
|
|
||||||
@ -76,35 +113,34 @@ async def list_anime(
|
|||||||
# Get the series list from SeriesApp
|
# Get the series list from SeriesApp
|
||||||
anime_list = series_app.series_list
|
anime_list = series_app.series_list
|
||||||
|
|
||||||
# Convert to list of dicts for response
|
# Convert to list of AnimeResponse objects
|
||||||
formatted_anime = []
|
anime_responses = []
|
||||||
for series_item in anime_list:
|
for series_item in anime_list:
|
||||||
anime_dict = {
|
anime_response = AnimeResponse(
|
||||||
'id': getattr(series_item, 'id', str(uuid.uuid4())),
|
id=getattr(series_item, 'id', str(uuid.uuid4())),
|
||||||
'title': getattr(series_item, 'name', 'Unknown'),
|
title=getattr(series_item, 'name', 'Unknown'),
|
||||||
'folder': getattr(series_item, 'folder', ''),
|
folder=getattr(series_item, 'folder', ''),
|
||||||
'description': getattr(series_item, 'description', ''),
|
description=getattr(series_item, 'description', ''),
|
||||||
'status': 'ongoing', # Default status
|
status='ongoing', # Default status
|
||||||
'episodes': getattr(series_item, 'total_episodes', 0)
|
episodes=getattr(series_item, 'total_episodes', 0)
|
||||||
}
|
)
|
||||||
|
|
||||||
# Apply search filter if provided
|
# Apply search filter if provided
|
||||||
if search:
|
if search:
|
||||||
if search.lower() not in anime_dict['title'].lower():
|
if search.lower() not in anime_response.title.lower():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
formatted_anime.append(anime_dict)
|
anime_responses.append(anime_response)
|
||||||
|
|
||||||
# Apply pagination
|
# Apply pagination
|
||||||
total = len(formatted_anime)
|
total = len(anime_responses)
|
||||||
start_idx = (page - 1) * per_page
|
start_idx = (page - 1) * per_page
|
||||||
end_idx = start_idx + 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 {
|
return PaginatedAnimeResponse(
|
||||||
"success": True,
|
data=paginated_anime,
|
||||||
"data": paginated_anime,
|
pagination={
|
||||||
"pagination": {
|
|
||||||
"page": page,
|
"page": page,
|
||||||
"per_page": per_page,
|
"per_page": per_page,
|
||||||
"total": total,
|
"total": total,
|
||||||
@ -112,7 +148,7 @@ async def list_anime(
|
|||||||
"has_next": end_idx < total,
|
"has_next": end_idx < total,
|
||||||
"has_prev": page > 1
|
"has_prev": page > 1
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
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(
|
async def search_anime(
|
||||||
q: str = Query(..., min_length=2, description="Search query"),
|
q: str = Query(..., min_length=2, description="Search query"),
|
||||||
page: int = Query(1, ge=1),
|
page: int = Query(1, ge=1),
|
||||||
per_page: int = Query(20, ge=1, le=100),
|
per_page: int = Query(20, ge=1, le=100),
|
||||||
current_user: Optional[Dict] = Depends(get_current_user),
|
current_user: Optional[Dict] = Depends(get_current_user),
|
||||||
series_app: SeriesApp = Depends(get_series_app)
|
series_app: SeriesApp = Depends(get_series_app)
|
||||||
) -> Dict[str, Any]:
|
) -> AnimeSearchResponse:
|
||||||
"""
|
"""
|
||||||
Search anime by name using SeriesApp.
|
Search anime by name using SeriesApp.
|
||||||
|
|
||||||
@ -393,29 +429,28 @@ async def search_anime(
|
|||||||
# Use SeriesApp to perform search
|
# Use SeriesApp to perform search
|
||||||
search_results = series_app.search(q)
|
search_results = series_app.search(q)
|
||||||
|
|
||||||
# Convert search results to our response format
|
# Convert search results to AnimeResponse objects
|
||||||
formatted_results = []
|
anime_responses = []
|
||||||
for result in search_results:
|
for result in search_results:
|
||||||
anime_dict = {
|
anime_response = AnimeResponse(
|
||||||
'id': getattr(result, 'id', str(uuid.uuid4())),
|
id=getattr(result, 'id', str(uuid.uuid4())),
|
||||||
'title': getattr(result, 'name', getattr(result, 'title', 'Unknown')),
|
title=getattr(result, 'name', getattr(result, 'title', 'Unknown')),
|
||||||
'description': getattr(result, 'description', ''),
|
description=getattr(result, 'description', ''),
|
||||||
'status': 'available',
|
status='available',
|
||||||
'episodes': getattr(result, 'episodes', 0),
|
episodes=getattr(result, 'episodes', 0),
|
||||||
'key': getattr(result, 'key', '')
|
folder=getattr(result, 'key', '')
|
||||||
}
|
)
|
||||||
formatted_results.append(anime_dict)
|
anime_responses.append(anime_response)
|
||||||
|
|
||||||
# Apply pagination
|
# Apply pagination
|
||||||
total = len(formatted_results)
|
total = len(anime_responses)
|
||||||
start_idx = (page - 1) * per_page
|
start_idx = (page - 1) * per_page
|
||||||
end_idx = start_idx + 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 {
|
return AnimeSearchResponse(
|
||||||
"success": True,
|
data=paginated_results,
|
||||||
"data": paginated_results,
|
pagination={
|
||||||
"pagination": {
|
|
||||||
"page": page,
|
"page": page,
|
||||||
"per_page": per_page,
|
"per_page": per_page,
|
||||||
"total": total,
|
"total": total,
|
||||||
@ -423,11 +458,11 @@ async def search_anime(
|
|||||||
"has_next": end_idx < total,
|
"has_next": end_idx < total,
|
||||||
"has_prev": page > 1
|
"has_prev": page > 1
|
||||||
},
|
},
|
||||||
"search": {
|
search={
|
||||||
"query": q,
|
"query": q,
|
||||||
"total_results": total
|
"total_results": total
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
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"
|
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(
|
async def rescan_anime_directory(
|
||||||
current_user: Dict = Depends(get_current_user),
|
current_user: Dict = Depends(get_current_user),
|
||||||
series_app: SeriesApp = Depends(get_series_app)
|
series_app: SeriesApp = Depends(get_series_app)
|
||||||
) -> Dict[str, Any]:
|
) -> RescanResponse:
|
||||||
"""
|
"""
|
||||||
Rescan the anime directory for new episodes and series.
|
Rescan the anime directory for new episodes and series.
|
||||||
|
|
||||||
@ -653,11 +688,11 @@ async def rescan_anime_directory(
|
|||||||
|
|
||||||
series_app.ReScan(progress_callback)
|
series_app.ReScan(progress_callback)
|
||||||
|
|
||||||
return {
|
return RescanResponse(
|
||||||
"success": True,
|
success=True,
|
||||||
"message": "Anime directory rescanned successfully",
|
message="Anime directory rescanned successfully",
|
||||||
"total_series": len(series_app.series_list) if hasattr(series_app, 'series_list') else 0
|
total_series=len(series_app.series_list) if hasattr(series_app, 'series_list') else 0
|
||||||
}
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
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
|
- [x] Convert Flask `redirect()` and `url_for()` to FastAPI equivalents
|
||||||
|
|
||||||
### Request/Response Models
|
### Request/Response Models
|
||||||
- [ ] Create Pydantic models for request bodies (replace Flask request parsing)
|
- [x] Create Pydantic models for request bodies (replace Flask request parsing)
|
||||||
- [ ] Create Pydantic models for response schemas
|
- [x] Create Pydantic models for response schemas
|
||||||
- [ ] Update form handling to use FastAPI Form dependencies
|
- [ ] Update form handling to use FastAPI Form dependencies
|
||||||
- [ ] Convert file upload handling to FastAPI UploadFile
|
- [ ] Convert file upload handling to FastAPI UploadFile
|
||||||
|
|
||||||
## 🎨 Template and Static Files Migration
|
## 🎨 Template and Static Files Migration
|
||||||
|
|
||||||
### Template Engine Setup
|
### Template Engine Setup
|
||||||
- [ ] Configure Jinja2Templates in FastAPI application
|
- [x] Configure Jinja2Templates in FastAPI application
|
||||||
- [ ] Set up template directory structure
|
- [x] Set up template directory structure
|
||||||
- [ ] Create templates directory configuration in FastAPI app
|
- [x] Create templates directory configuration in FastAPI app
|
||||||
|
|
||||||
### HTML Template Migration
|
### HTML Template Migration
|
||||||
- [ ] Review all `.html` files in templates directory
|
- [ ] 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
|
- [ ] Test all template variables and filters still work correctly
|
||||||
|
|
||||||
### Static Files Configuration
|
### 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
|
- [ ] Update static file URL generation in templates
|
||||||
- [ ] Verify all CSS file references work correctly
|
- [ ] Verify all CSS file references work correctly
|
||||||
- [ ] Verify all JavaScript file references work correctly
|
- [ ] Verify all JavaScript file references work correctly
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user