- 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
225 lines
5.9 KiB
Python
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())
|
|
]
|