"""Gallery building and rendering logic.""" import shutil from pathlib import Path from typing import Dict, Any, Optional, Union from jinja2 import Environment, FileSystemLoader, Template from gallery.config import GalleryConfig from gallery.utils.datetime_utils import ( datetime_from_timestamp, strftime_filter, ) from gallery.utils.metadata import ( load_folder_metadata, merge_metadata, save_metadata_cache, ) from gallery.utils.processing import ( process_plot_files, needs_update, render_gallery_page, ) def get_template(template_dir: Optional[Union[Path, str]] = None): """ Get the Jinja2 template for gallery rendering. Args: template_dir: Path to template directory. If None, uses package default. Returns: Jinja2 Template object """ if isinstance(template_dir, str): template_dir = Path(template_dir) if template_dir is None: # Use package-included template import gallery gallery_module_path = Path(gallery.__file__).parent template_dir = gallery_module_path / "templates" env = Environment(loader=FileSystemLoader(str(template_dir))) env.filters['datetime_from_timestamp'] = datetime_from_timestamp env.filters['strftime'] = strftime_filter return env.get_template("gallery.html") def build_gallery( config: GalleryConfig, source_dir: Path, web_dir: Path, template: Template = None, relative_path: Path = None, inherited_metadata: Optional[Dict[str, Any]] = None, ) -> None: """ Recursively build gallery structure from source directory. Processes all PDF files in the source directory, converts them to PNG, copies both to the web directory, and generates index.html files with navigation and thumbnails. Includes metadata support. Args: config: Gallery configuration object template: Jinja2 template for rendering source_dir: Source directory containing PDF files web_dir: Target web directory for gallery output relative_path: Relative path from gallery root (for navigation) inherited_metadata: Metadata inherited from parent directories """ if not template: template = Template("./templates/gallery.html") if relative_path is None: relative_path = Path(".") if inherited_metadata is None: inherited_metadata = {} folder_metadata = load_folder_metadata(source_dir) current_metadata = merge_metadata(inherited_metadata, folder_metadata) # Find all plot files (both PDF and HTML) pdf_files = list(source_dir.glob("*.pdf")) html_files = list(source_dir.glob("*.html")) plot_files = pdf_files + html_files items = [] plot_metadata_cache = {} # Process all plot files (PDFs and HTMLs) for plot_file in plot_files: item = process_plot_files( config=config, plot_file=plot_file, web_dir=web_dir, current_metadata=current_metadata, ) items.append(item) plot_metadata_cache[plot_file.stem] = item["metadata"] if config.cache_enabled: save_metadata_cache(web_dir, plot_metadata_cache) # Process subdirectories subdirs = [d for d in source_dir.iterdir() if d.is_dir()] subdir_names = [] for subdir in subdirs: subdir_web = web_dir / subdir.name subdir_web.mkdir(exist_ok=True) subdir_relative = relative_path / subdir.name build_gallery( config, subdir, subdir_web, template, subdir_relative, current_metadata if config.inherit_from_parent else {} ) subdir_names.append(subdir.name) render_gallery_page( config=config, template=template, web_dir=web_dir, items=items, subdirs=subdir_names, relative_path=relative_path, metadata=current_metadata ) def copy_assets( config: GalleryConfig, assets_src: Optional[Path] = None, verbose: bool = False ) -> bool: """ Copy assets to the web directory. Args: config: Gallery configuration object assets_src: Path to assets source. If None, uses package default. verbose: Whether to print status messages Returns: True if successful, False otherwise """ try: if assets_src is None: # Use package-included assets import gallery gallery_module_path = Path(gallery.__file__).parent assets_src = gallery_module_path / "assets" if not assets_src.exists(): if verbose: print( f"Warning: Assets directory {assets_src} not found" ) return False gallery_root = Path(config.web_folder) / config.plot_root assets_dst = gallery_root.parent / "assets" # Use the newest mtime across all source asset files as the staleness check, # so any change to any JS/CSS file triggers a redeploy. sentinel_dst = assets_dst / "css" / "main.css" newest_src_mtime = max( (f.stat().st_mtime for f in assets_src.rglob('*') if f.is_file()), default=0, ) dst_mtime = sentinel_dst.stat().st_mtime if sentinel_dst.exists() else 0 if not assets_dst.exists() or newest_src_mtime > (dst_mtime + 30): if assets_dst.exists(): shutil.rmtree(assets_dst) shutil.copytree(assets_src, assets_dst) if verbose: print(f"Updated assets from {assets_src} to {assets_dst}") return True except Exception as e: if verbose: print(f"Warning: Could not copy assets: {e}") return False