"""
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 }}
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 %}
- {{ insight }}
{% 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}"