""" 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()) ]