Files
Aniworld/src/server/utils/template_helpers.py
2026-05-14 17:30:13 +02:00

223 lines
5.9 KiB
Python

"""
Template integration utilities for FastAPI application.
This module provides utilities for template rendering with common context
and helper functions.
Series Identifier Convention:
- `key`: Primary identifier for all series operations
(URL-safe, e.g., "attack-on-titan")
- `folder`: Metadata only for filesystem operations and display
(e.g., "Attack on Titan (2013)")
All template helpers that handle series data use `key` for identification and
provide `folder` as display metadata only.
"""
import logging
import time
from pathlib import Path
from typing import Any, Dict, List, Optional
from fastapi import Request
from fastapi.templating import Jinja2Templates
logger = logging.getLogger(__name__)
# Configure templates directory
TEMPLATES_DIR = Path(__file__).parent.parent / "web" / "templates"
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
# Version token for static asset cache-busting; changes on every server start.
STATIC_VERSION: str = str(int(time.time()))
def get_base_context(
request: Request, title: str = "Aniworld"
) -> Dict[str, Any]:
"""
Get base context for all templates.
Args:
request: FastAPI request object
title: Page title
Returns:
Dictionary with base context variables
"""
return {
"request": request,
"title": title,
"app_name": "Aniworld Download Manager",
"version": "1.0.1",
"static_v": STATIC_VERSION,
}
def render_template(
template_name: str,
request: Request,
context: Optional[Dict[str, Any]] = None,
title: Optional[str] = None
):
"""
Render a template with base context.
Args:
template_name: Name of the template file
request: FastAPI request object
context: Additional context variables
title: Page title (optional)
Returns:
TemplateResponse object
"""
base_context = get_base_context(
request,
title or template_name.replace('.html', '').replace('_', ' ').title()
)
if context:
base_context.update(context)
return templates.TemplateResponse(template_name, base_context)
def validate_template_exists(template_name: str) -> bool:
"""
Check if a template file exists.
Args:
template_name: Name of the template file
Returns:
True if template exists, False otherwise
"""
template_path = TEMPLATES_DIR / template_name
return template_path.exists()
def list_available_templates() -> list[str]:
"""
Get list of all available template files.
Returns:
List of template file names
"""
if not TEMPLATES_DIR.exists():
return []
return [
f.name
for f in TEMPLATES_DIR.glob("*.html")
if f.is_file()
]
def prepare_series_context(
series_data: List[Dict[str, Any]],
sort_by: str = "name"
) -> List[Dict[str, Any]]:
"""
Prepare series data for template rendering.
This function ensures series data follows the identifier convention:
- `key` is used as the primary identifier for all operations
- `folder` is included as metadata for display purposes
Args:
series_data: List of series dictionaries from the API
sort_by: Field to sort by ("name", "key", or "folder")
Returns:
List of series dictionaries prepared for template use
Raises:
ValueError: If series_data contains items without required 'key' field
Example:
>>> series = [
... {"key": "attack-on-titan", "name": "Attack on Titan",
... "folder": "Attack on Titan (2013)"},
... {"key": "one-piece", "name": "One Piece",
... "folder": "One Piece (1999)"}
... ]
>>> prepared = prepare_series_context(series, sort_by="name")
"""
if not series_data:
return []
prepared = []
for series in series_data:
if "key" not in series:
logger.warning(
"Series data missing 'key' field: %s",
series.get("name", "unknown")
)
continue
prepared_item = {
"key": series["key"],
"name": series.get("name", series["key"]),
"folder": series.get("folder", ""),
**{k: v for k, v in series.items()
if k not in ("key", "name", "folder")}
}
prepared.append(prepared_item)
# Sort by specified field
if sort_by in ("name", "key", "folder"):
prepared.sort(key=lambda x: x.get(sort_by, "").lower())
return prepared
def get_series_by_key(
series_data: List[Dict[str, Any]],
key: str
) -> Optional[Dict[str, Any]]:
"""
Find a series in the data by its key.
Uses `key` as the identifier (not `folder`) following the project
identifier convention.
Args:
series_data: List of series dictionaries
key: The unique series key to search for
Returns:
The series dictionary if found, None otherwise
Example:
>>> series = [{"key": "attack-on-titan", "name": "Attack on Titan"}]
>>> result = get_series_by_key(series, "attack-on-titan")
>>> result["name"]
'Attack on Titan'
"""
for series in series_data:
if series.get("key") == key:
return series
return None
def filter_series_by_missing_episodes(
series_data: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""
Filter series to only include those with missing episodes.
Args:
series_data: List of series dictionaries with 'missing_episodes' field
Returns:
Filtered list containing only series with missing episodes
Note:
Identification uses `key`, not `folder`.
"""
return [
series for series in series_data
if series.get("missing_episodes")
and any(episodes for episodes in series["missing_episodes"].values())
]