diff --git a/Makefile b/Makefile index a644d6e..a90f07e 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ fmt: poetry run ruff format lint: - poetry run ruff check + poetry run ruff check --fix test: poetry run mypy . diff --git a/photoalbum/generate.py b/photoalbum/generate.py index 314fccd..0a002a1 100644 --- a/photoalbum/generate.py +++ b/photoalbum/generate.py @@ -1,38 +1,85 @@ import logging +from dataclasses import dataclass from pathlib import Path +from pprint import pformat +from typing import Iterator +from jinja2 import Environment, FileSystemLoader, select_autoescape from PIL import Image, UnidentifiedImageError -from rich.progress import track, Progress +from rich.console import Console +from rich.progress import track from photoalbum.config import Config logger = logging.getLogger(__name__) +@dataclass +class ImageDirectory: + path: Path + children: list["ImageDirectory"] + images: list[Path] + is_root: bool = False + + def walk(self) -> Iterator["ImageDirectory"]: + yield self + for child in self.children: + yield from child.walk() + + def image_paths(self) -> list[Path]: + """ + Iterate through all images in this dir and children + """ + images = [] + for image_dir in self.walk(): + images += image_dir.images + return images + + def generate(config: Config, album_path: Path) -> None: """ Main generation function """ - images = find_images(album_path) - generate_thumbnails(config, images) - generate_html(config, album_path, images) + root_dir = find_images(album_path) + logger.debug(pformat(root_dir)) + generate_thumbnails(config, root_dir) + generate_html(config, root_dir) -def find_images(path: Path) -> list[Path]: +def find_images(root_path: Path) -> ImageDirectory: """ - Returns paths of all images in the given path + Build up an ImageDirectory to track all of the directories and their images. + + A directory with no images nor childern with images will not be tracked, since we + don't want to render an album page for those. """ - images = [] - for parent_path, dirnames, filenames in path.walk(): - if parent_path.name == "slides": + # image_dirs keeps track of all directories we find with images in them, so we can + # attach them as children to parent directories + image_dirs: dict[Path, ImageDirectory] = { + root_path: ImageDirectory(path=root_path, children=[], images=[], is_root=True) + } + + for dirpath, dirnames, filenames in root_path.walk(top_down=False): + if dirpath.name in {"slides", "_templates", "static"}: continue - for filename in filenames: - file_path = parent_path / filename - if is_image(file_path): - images.append(file_path) + image_dir = image_dirs.get( + dirpath, ImageDirectory(path=dirpath, children=[], images=[]) + ) - return images + for dirname in sorted(dirnames): + child_path = dirpath / dirname + if child_path in image_dirs: + image_dir.children.append(image_dirs[child_path]) + + for filename in sorted(filenames): + file_path = dirpath / filename + if is_image(file_path): + image_dir.images.append(file_path) + + image_dirs[image_dir.path] = image_dir + + return image_dirs[root_path] def is_image(path: Path) -> bool: @@ -46,11 +93,12 @@ def is_image(path: Path) -> bool: return False -def generate_thumbnails(config: Config, images: list[Path]) -> None: +def generate_thumbnails(config: Config, root_dir: ImageDirectory) -> None: """ Find all of the images and generate thumbnails and on-screen versions """ - for image_path in track(images, description="Making thumbnails..."): + for image_path in track(root_dir.image_paths(), description="Making thumbnails..."): + logger.debug(image_path) orig_img = Image.open(image_path) slides_path = image_path.parent / "slides" @@ -60,34 +108,41 @@ def generate_thumbnails(config: Config, images: list[Path]) -> None: thumb_img.thumbnail(config.thumbnail_size) thumb_filename = image_path.stem + ".thumb" + image_path.suffix thumb_img.save(slides_path / thumb_filename) - logger.info(f"Generated thumbnail size \"{image_path}\" -> \"{thumb_filename}\"") + logger.info(f'Generated thumbnail size "{image_path}" -> "{thumb_filename}"') screen_img = orig_img.copy() screen_img.thumbnail(config.view_size) screen_filename = image_path.stem + ".screen" + image_path.suffix screen_img.save(slides_path / screen_filename) - logger.info(f"Generated screen size \"{image_path}\" -> \"{screen_filename}\"") + logger.info(f'Generated screen size "{image_path}" -> "{screen_filename}"') -def generate_html(config: Config, root_path: Path, images: list[Path]) -> None: +def generate_html(config: Config, root_dir: ImageDirectory) -> None: """ Recursively generate HTML files for this directory and all children """ + jinja_env = Environment( + loader=FileSystemLoader(root_dir.path / "_templates"), + autoescape=select_autoescape(), + ) - # Find all the album dirs that either have photos, or have child album dirs - # The paths in this set are all relative to the root_path - album_paths = {Path('.')} - for image_path in images: - rel_path = image_path.relative_to(root_path) - for parent in rel_path.parents: - album_paths.add(parent) + album_tmpl = jinja_env.get_template("album.html") + photo_tmpl = jinja_env.get_template("photo.html") - # Generate album pages for all album dirs - for album_path in track(album_paths, description="Generating album HTML..."): - html_path = album_path / "index.html" - logger.debug(html_path) + with Console().status("Rendering HTML..."): + for album_dir in root_dir.walk(): + html_path = album_dir.path / "index.html" + logger.debug(f"Rendering {html_path}") + with html_path.open("w") as f: + f.write(album_tmpl.render()) - # Generate photo pages for all photos - for image_path in track(images, description="Generating photo HTML..."): - html_path = image_path.parent / "slides" / image_path.with_suffix(".html").name - logger.debug(html_path) + for image_path in album_dir.images: + # TODO: If a file with a matching name but .txt or .md, add that as the + # description for the image + html_path = ( + image_path.parent / "slides" / image_path.with_suffix(".html").name + ) + html_path.parent.mkdir(exist_ok=True) + logger.debug(f"Rendering {html_path}") + with html_path.open("w") as f: + f.write(photo_tmpl.render()) diff --git a/photoalbum/skel/_templates/album.jinja2 b/photoalbum/skel/_templates/album.html similarity index 100% rename from photoalbum/skel/_templates/album.jinja2 rename to photoalbum/skel/_templates/album.html diff --git a/photoalbum/skel/_templates/photo.jinja2 b/photoalbum/skel/_templates/photo.html similarity index 100% rename from photoalbum/skel/_templates/photo.jinja2 rename to photoalbum/skel/_templates/photo.html