Files
Aniworld/src/server/utils/template_helpers.py
Lukas 246752e2fc Add dynamic version from Docker/VERSION file
- Create version.py utility to read version from Docker/VERSION
- Replace hardcoded version '1.0.1' with APP_VERSION from version.py
- Add version logging on FastAPI startup
- Use APP_VERSION in health endpoints and template context
2026-06-02 20:38:42 +02:00

225 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
from src.server.utils.version import APP_VERSION
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": APP_VERSION,
"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())
]