""" Template Manager for YouTube Summarizer Manages custom export templates with validation and rendering """ import os import json from pathlib import Path from typing import Dict, List, Optional, Any from dataclasses import dataclass from enum import Enum import jinja2 from jinja2 import Template, Environment, FileSystemLoader, TemplateError class TemplateType(Enum): """Supported template types""" MARKDOWN = "markdown" HTML = "html" TEXT = "text" @dataclass class ExportTemplate: """Export template model""" id: str name: str description: str type: TemplateType content: str variables: List[str] is_default: bool = False created_at: Optional[str] = None updated_at: Optional[str] = None class TemplateManager: """Manages custom templates for exports""" def __init__(self, templates_dir: str = "./templates"): self.templates_dir = Path(templates_dir) self.templates_dir.mkdir(parents=True, exist_ok=True) # Create subdirectories for each template type for template_type in TemplateType: (self.templates_dir / template_type.value).mkdir(exist_ok=True) # Initialize Jinja2 environment self.env = Environment( loader=FileSystemLoader(self.templates_dir), autoescape=True ) # Load default templates self._initialize_default_templates() def _initialize_default_templates(self): """Initialize default templates if they don't exist""" # Default Markdown template markdown_default = """# {{ video_metadata.title }} ## Summary {{ summary }} ## Key Points {% for point in key_points %} - {{ point }} {% endfor %} ## Main Themes {% for theme in main_themes %} - {{ theme }} {% endfor %} ## Actionable Insights {% for insight in actionable_insights %} 1. {{ insight }} {% endfor %} --- *Generated on {{ export_metadata.exported_at }}* """ # Default HTML template html_default = """ {{ video_metadata.title }} - Summary

{{ video_metadata.title }}

Channel: {{ video_metadata.channel_name }}

Duration: {{ video_metadata.duration }}

URL: {{ video_url }}

Summary

{{ summary }}

Key Points

Main Themes

Actionable Insights

    {% for insight in actionable_insights %}
  1. {{ insight }}
  2. {% endfor %}
""" # Default plain text template text_default = """{{ video_metadata.title }} {{ '=' * video_metadata.title|length }} VIDEO INFORMATION ----------------- Channel: {{ video_metadata.channel_name }} Duration: {{ video_metadata.duration }} URL: {{ video_url }} SUMMARY ------- {{ summary }} KEY POINTS ---------- {% for point in key_points %} * {{ point }} {% endfor %} MAIN THEMES ----------- {% for theme in main_themes %} * {{ theme }} {% endfor %} ACTIONABLE INSIGHTS ------------------ {% for insight in actionable_insights %} {{ loop.index }}. {{ insight }} {% endfor %} Generated on {{ export_metadata.exported_at }} """ # Save default templates self._save_template("default", TemplateType.MARKDOWN, markdown_default, is_default=True) self._save_template("default", TemplateType.HTML, html_default, is_default=True) self._save_template("default", TemplateType.TEXT, text_default, is_default=True) def _save_template(self, name: str, template_type: TemplateType, content: str, is_default: bool = False): """Save a template to disk""" template_path = self.templates_dir / template_type.value / f"{name}.j2" # Only save if doesn't exist (for default templates) if is_default and template_path.exists(): return template_path.write_text(content) def get_template(self, name: str, template_type: TemplateType) -> Optional[ExportTemplate]: """Get a specific template""" template_path = self.templates_dir / template_type.value / f"{name}.j2" if not template_path.exists(): return None content = template_path.read_text() # Extract variables from template try: ast = self.env.parse(content) variables = list(jinja2.meta.find_undeclared_variables(ast)) except Exception: variables = [] return ExportTemplate( id=f"{template_type.value}_{name}", name=name, description=f"{template_type.value.title()} template: {name}", type=template_type, content=content, variables=variables, is_default=(name == "default") ) def list_templates(self, template_type: Optional[TemplateType] = None) -> List[ExportTemplate]: """List all available templates""" templates = [] types_to_check = [template_type] if template_type else list(TemplateType) for t_type in types_to_check: template_dir = self.templates_dir / t_type.value if template_dir.exists(): for template_file in template_dir.glob("*.j2"): name = template_file.stem template = self.get_template(name, t_type) if template: templates.append(template) return templates def create_template(self, name: str, template_type: TemplateType, content: str, description: str = "") -> ExportTemplate: """Create a new custom template""" # Validate template syntax try: template = Template(content) # Try to extract variables ast = self.env.parse(content) variables = list(jinja2.meta.find_undeclared_variables(ast)) except TemplateError as e: raise ValueError(f"Invalid template syntax: {e}") # Save template self._save_template(name, template_type, content) return ExportTemplate( id=f"{template_type.value}_{name}", name=name, description=description or f"Custom {template_type.value} template", type=template_type, content=content, variables=variables, is_default=False ) def update_template(self, name: str, template_type: TemplateType, content: str) -> ExportTemplate: """Update an existing template""" # Don't allow updating default templates if name == "default": raise ValueError("Cannot modify default templates") # Validate template syntax try: template = Template(content) ast = self.env.parse(content) variables = list(jinja2.meta.find_undeclared_variables(ast)) except TemplateError as e: raise ValueError(f"Invalid template syntax: {e}") # Update template self._save_template(name, template_type, content) return self.get_template(name, template_type) def delete_template(self, name: str, template_type: TemplateType): """Delete a custom template""" # Don't allow deleting default templates if name == "default": raise ValueError("Cannot delete default templates") template_path = self.templates_dir / template_type.value / f"{name}.j2" if template_path.exists(): template_path.unlink() def render_template(self, name: str, template_type: TemplateType, data: Dict[str, Any]) -> str: """Render a template with provided data""" template = self.get_template(name, template_type) if not template: raise ValueError(f"Template not found: {name} ({template_type.value})") try: # Create Jinja2 template jinja_template = Template(template.content) # Render with data rendered = jinja_template.render(**data) return rendered except Exception as e: raise ValueError(f"Failed to render template: {e}") def validate_template_data(self, name: str, template_type: TemplateType, data: Dict[str, Any]) -> List[str]: """Validate that required template variables are provided""" template = self.get_template(name, template_type) if not template: return ["Template not found"] missing_vars = [] for var in template.variables: if var not in data and not self._is_nested_available(var, data): missing_vars.append(var) return missing_vars def _is_nested_available(self, var_path: str, data: Dict[str, Any]) -> bool: """Check if a nested variable path is available in data""" parts = var_path.split('.') current = data for part in parts: if isinstance(current, dict) and part in current: current = current[part] else: return False return True def get_template_preview(self, name: str, template_type: TemplateType) -> str: """Get a preview of template with sample data""" sample_data = { "video_metadata": { "title": "Sample Video Title", "channel_name": "Sample Channel", "duration": "10:30", "published_at": "2025-01-25" }, "video_url": "https://youtube.com/watch?v=sample", "summary": "This is a sample summary of the video content.", "key_points": [ "First key point", "Second key point", "Third key point" ], "main_themes": ["Technology", "Innovation", "Future"], "actionable_insights": [ "Apply this concept to your workflow", "Consider this approach for better results" ], "export_metadata": { "exported_at": "2025-01-25 12:00:00" } } try: return self.render_template(name, template_type, sample_data) except Exception as e: return f"Preview generation failed: {e}"