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