Compare commits
10 commits
04586c8c45
...
d297110d88
Author | SHA1 | Date | |
---|---|---|---|
d297110d88 | |||
e2338df37c | |||
4d45aa5296 | |||
f7998cd708 | |||
cd47a82ecf | |||
6cd2e52408 | |||
f60a14ae21 | |||
c93f1997fc | |||
c08c5609ca | |||
acd736c970 |
11 changed files with 130 additions and 49 deletions
19
Makefile
19
Makefile
|
@ -7,6 +7,9 @@ ci: init lint test
|
|||
# Final pre-flight checks then deploy everywhere!
|
||||
shipit: all build staging prod
|
||||
|
||||
version := $(shell yq -p toml .tool.poetry.version < pyproject.toml)
|
||||
scie_platforms := linux-aarch64 linux-x86_64 macos-aarch64 macos-x86_64
|
||||
|
||||
|
||||
init:
|
||||
poetry install
|
||||
|
@ -33,11 +36,17 @@ test-fast:
|
|||
test-watch:
|
||||
find . -name '*py' -or -name '*html' -or -name poetry.lock | entr -r -c make test-fast
|
||||
|
||||
run:
|
||||
podman-compose up
|
||||
clean:
|
||||
rm -rv dist || true
|
||||
|
||||
build:
|
||||
docker:
|
||||
podman build -t nickpegg/photojawn . --build-arg GIT_COMMIT=$(shell git rev-parse --short HEAD)
|
||||
|
||||
clean:
|
||||
podman-compose down --rmi all
|
||||
dist:
|
||||
poetry build
|
||||
poetry run pex --project . -o dist/photojawn -c photojawn --scie eager $(foreach plat,$(scie_platforms), --scie-platform $(plat))
|
||||
|
||||
release: dist
|
||||
git push --tags
|
||||
gh release create --verify-tag v$(version)
|
||||
gh release upload v$(version) dist/photojawn-$(version)-*whl $(foreach plat,$(scie_platforms),dist/photojawn-$(plat))
|
||||
|
|
11
README.md
11
README.md
|
@ -10,7 +10,16 @@ It's everything I need and nothing I don't.
|
|||
|
||||
## Getting Started
|
||||
|
||||
TODO: Installation instructions lol
|
||||
### Installation
|
||||
|
||||
Photojawn requires at least Python 3.12. If you don't know what version you
|
||||
have, you can check by running `python -V`.
|
||||
|
||||
1. Head on over to the [releases](https://github.com/nickpegg/photojawn/releases) page
|
||||
2. If you have Python >=3.12, you can install using the wheel (`.whl` file)
|
||||
a. To install, run `pip install photojawn-<version>-py3-none-any.whl`
|
||||
3. If you don't have Python >=3.12, you can download one of the standalone binaries depending on your OS and architecture, e.g. `photojawn-linux-x86_64`.
|
||||
|
||||
|
||||
### Initialization
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
import sys
|
||||
from argparse import ArgumentParser, Namespace
|
||||
from pathlib import Path
|
||||
|
||||
|
@ -15,16 +16,17 @@ def main() -> None:
|
|||
setup_logging(args.logging)
|
||||
|
||||
# Load config from file if it exists
|
||||
conf_path = Path(args.album_path) / Path(args.config)
|
||||
if conf_path.exists():
|
||||
logger.debug(f"Reading config from {conf_path}")
|
||||
config = Config.from_yaml(conf_path.read_bytes())
|
||||
elif args.action != "init":
|
||||
logger.error(
|
||||
f"No config file found at {conf_path}. If this is a new photo directory, "
|
||||
"please run `photojawn init` in there first."
|
||||
)
|
||||
return
|
||||
if hasattr(args, "album_path"):
|
||||
conf_path = Path(args.album_path) / Path(args.config)
|
||||
if conf_path.exists():
|
||||
logger.debug(f"Reading config from {conf_path}")
|
||||
config = Config.from_yaml(conf_path.read_bytes())
|
||||
elif args.action != "init":
|
||||
logger.error(
|
||||
f"No config file found at {conf_path}. If this is a new photo directory, "
|
||||
"please run `photojawn init` in there first."
|
||||
)
|
||||
return
|
||||
|
||||
# Call the subcommand function
|
||||
match args.action:
|
||||
|
@ -98,7 +100,12 @@ def parse_args() -> Namespace:
|
|||
help="Path to the main photos directory",
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
args = parser.parse_args()
|
||||
if not hasattr(args, "action"):
|
||||
parser.print_help()
|
||||
sys.exit(0)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
########################################
|
||||
|
|
|
@ -16,6 +16,9 @@ class Config:
|
|||
# Size of the image when looking at the standalone image page
|
||||
view_size: tuple[int, int] = (1920, 1080)
|
||||
|
||||
# Directory inside the photo directory to output the site to
|
||||
output_dir: str = "site"
|
||||
|
||||
# Quick mode:
|
||||
# - Don't regenerate thumbnails if they already exist
|
||||
quick: bool = False
|
||||
|
@ -29,6 +32,8 @@ class Config:
|
|||
|
||||
for key, val in data.items():
|
||||
match key:
|
||||
case "output_dir":
|
||||
conf.output_dir = val
|
||||
case "thumnail_size":
|
||||
conf.thumbnail_size = tuple(val)
|
||||
case "view_size":
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import logging
|
||||
import shutil
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Iterator, Optional
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
@ -17,7 +18,6 @@ logger = logging.getLogger(__name__)
|
|||
@dataclass
|
||||
class ImageDirectory:
|
||||
path: Path
|
||||
rel_path: Path # Path relative to the root dir
|
||||
children: list["ImageDirectory"]
|
||||
images: list["ImagePath"]
|
||||
is_root: bool = False
|
||||
|
@ -75,13 +75,22 @@ def generate(config: Config, album_path: Path) -> None:
|
|||
"""
|
||||
Main generation function
|
||||
"""
|
||||
root_dir = find_images(album_path)
|
||||
logger.debug(pformat(root_dir))
|
||||
generate_thumbnails(config, root_dir)
|
||||
# Change the working directory to the album_path so that all paths are relative to
|
||||
# it when we find images. We need to do this because all the paths in HTML need to
|
||||
# be relative to it and we don't want to have to do a bunch of path gymnastics to
|
||||
# re-relative all those paths.
|
||||
orig_wd = Path.cwd()
|
||||
os.chdir(album_path)
|
||||
|
||||
root_dir = find_images(config, Path("."))
|
||||
generate_images(config, root_dir)
|
||||
generate_html(config, root_dir)
|
||||
shutil.copytree("static", Path(config.output_dir) / "static")
|
||||
|
||||
os.chdir(orig_wd)
|
||||
|
||||
|
||||
def find_images(root_path: Path) -> ImageDirectory:
|
||||
def find_images(config: Config, root_path: Path) -> ImageDirectory:
|
||||
"""
|
||||
Build up an ImageDirectory to track all of the directories and their images.
|
||||
|
||||
|
@ -91,20 +100,17 @@ def find_images(root_path: Path) -> ImageDirectory:
|
|||
# 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, rel_path=Path("."), children=[], images=[], is_root=True
|
||||
)
|
||||
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"}:
|
||||
if dirpath.name in {"slides", "_templates", "static", config.output_dir}:
|
||||
continue
|
||||
|
||||
image_dir = image_dirs.get(
|
||||
dirpath,
|
||||
ImageDirectory(
|
||||
path=dirpath,
|
||||
rel_path=dirpath.relative_to(root_path),
|
||||
children=[],
|
||||
images=[],
|
||||
),
|
||||
|
@ -141,8 +147,14 @@ def find_images(root_path: Path) -> ImageDirectory:
|
|||
|
||||
image_dir.images.append(ip)
|
||||
|
||||
if image_dir.cover_path is None and len(image_dir.images) > 0:
|
||||
image_dir.cover_path = image_dir.images[0]
|
||||
if image_dir.cover_path is None:
|
||||
if len(image_dir.images) > 0:
|
||||
image_dir.cover_path = image_dir.images[0]
|
||||
elif len(image_dir.children) > 0:
|
||||
cover = image_dir.children[0].cover_path
|
||||
logger.debug(f"nested cover path for {image_dir.path.name}: {cover}")
|
||||
image_dir.cover_path = cover
|
||||
|
||||
image_dirs[image_dir.path] = image_dir
|
||||
|
||||
return image_dirs[root_path]
|
||||
|
@ -159,20 +171,26 @@ def is_image(path: Path) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def generate_thumbnails(config: Config, root_dir: ImageDirectory) -> None:
|
||||
def generate_images(config: Config, root_dir: ImageDirectory) -> None:
|
||||
"""
|
||||
Find all of the images and generate thumbnails and on-screen versions
|
||||
Find all of the images and generate various image sizes
|
||||
"""
|
||||
# Include cover images here because we want thumbnails for all of them
|
||||
|
||||
all_images = root_dir.image_paths() + root_dir.cover_image_paths()
|
||||
for image_path in track(all_images, description="Making thumbnails..."):
|
||||
|
||||
for image_path in track(all_images, description="Making smaller images..."):
|
||||
orig_img = Image.open(image_path.path)
|
||||
|
||||
slides_path = image_path.path.parent / "slides"
|
||||
slides_path.mkdir(exist_ok=True)
|
||||
slides_path = config.output_dir / image_path.path.parent / "slides"
|
||||
slides_path.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
thumb_path = image_path.thumbnail_path()
|
||||
# Copy the original image in
|
||||
orig_new_path = config.output_dir / image_path.path
|
||||
if not orig_new_path.exists() or not config.quick:
|
||||
logger.info(f"Copying original image to {orig_new_path}")
|
||||
orig_img.save(orig_new_path)
|
||||
|
||||
thumb_path = config.output_dir / image_path.thumbnail_path()
|
||||
if not thumb_path.exists() or not config.quick:
|
||||
thumb_img = orig_img.copy()
|
||||
thumb_img.thumbnail(config.thumbnail_size)
|
||||
|
@ -181,7 +199,7 @@ def generate_thumbnails(config: Config, root_dir: ImageDirectory) -> None:
|
|||
f'Generated thumbnail size "{image_path.path}" -> "{thumb_path}"'
|
||||
)
|
||||
|
||||
screen_path = image_path.display_path()
|
||||
screen_path = config.output_dir / image_path.display_path()
|
||||
if not screen_path.exists() or not config.quick:
|
||||
screen_img = orig_img.copy()
|
||||
screen_img.thumbnail(config.view_size)
|
||||
|
@ -207,8 +225,8 @@ def generate_html(config: Config, root_dir: ImageDirectory) -> None:
|
|||
for album_dir in root_dir.walk():
|
||||
html_path = album_dir.path / "index.html"
|
||||
root_path = root_dir.path.relative_to(html_path.parent, walk_up=True)
|
||||
html_path = config.output_dir / html_path
|
||||
|
||||
# TODO build breadcrumbs here, (href, name)
|
||||
breadcrumbs = []
|
||||
if not album_dir.is_root:
|
||||
crumb_pos = album_dir.path.parent
|
||||
|
@ -240,6 +258,7 @@ def generate_html(config: Config, root_dir: ImageDirectory) -> None:
|
|||
|
||||
html_path = image_path.html_path()
|
||||
root_path = root_dir.path.relative_to(html_path.parent, walk_up=True)
|
||||
html_path = config.output_dir / html_path
|
||||
html_path.parent.mkdir(exist_ok=True)
|
||||
|
||||
prev_image = None
|
||||
|
|
|
@ -9,20 +9,25 @@
|
|||
{% endfor %}
|
||||
/ {{album_dir.path.name}}
|
||||
</h1>
|
||||
<hr>
|
||||
{% endif %}
|
||||
|
||||
{% if album_dir.description %}
|
||||
<div class="caption">
|
||||
{{ album_dir.description | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if album_dir.children %}
|
||||
<h2>Albums</h2>
|
||||
<div id="album-children">
|
||||
{% for child in album_dir.children %}
|
||||
<div class="album">
|
||||
<a href="{{child.path.name}}/">
|
||||
<div>
|
||||
{% if child.cover_path %}
|
||||
<img src="{{child.path.name}}/slides/{{child.cover_path.thumbnail_filename()}}" />
|
||||
<img
|
||||
src="{{child.cover_path.path.parent.relative_to(album_dir.path)}}/slides/{{child.cover_path.thumbnail_filename()}}" />
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="photo-description">
|
||||
<div id="photo-description" class="caption">
|
||||
{% if image_path.description %}
|
||||
{{ image_path.description | safe }}
|
||||
{% endif %}
|
||||
|
|
|
@ -3,3 +3,7 @@ thumbnail_size: [256, 256]
|
|||
|
||||
# Max size of images when viewing a single one on screen
|
||||
view_size: [1024, 768]
|
||||
|
||||
# Directory where the generated site will be created in. All original images will be
|
||||
# copied in to here with the same directory structure.
|
||||
output_dir: "site"
|
||||
|
|
|
@ -22,15 +22,14 @@ a {
|
|||
|
||||
#content {
|
||||
text-align: center;
|
||||
max-width: 1200px;
|
||||
margin: 0.5em;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
#content > * {
|
||||
width: fit-content;
|
||||
max-width: 1500px;
|
||||
margin-top: 1em;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
ul {
|
||||
|
@ -47,6 +46,9 @@ ul {
|
|||
margin: 1em;
|
||||
padding: 0.75em;
|
||||
background-color: lightgrey;
|
||||
height: min-content;
|
||||
border: thin solid black;
|
||||
box-shadow: 0.25em 0.25em #ccc;
|
||||
}
|
||||
|
||||
#album-photos {
|
||||
|
@ -81,6 +83,12 @@ ul {
|
|||
height: auto;
|
||||
}
|
||||
|
||||
.caption {
|
||||
max-width: 700px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
border: solid black;
|
||||
border-width: 0 3px 3px 0;
|
||||
|
|
18
poetry.lock
generated
18
poetry.lock
generated
|
@ -290,6 +290,20 @@ files = [
|
|||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pex"
|
||||
version = "2.14.1"
|
||||
description = "The PEX packaging toolchain."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,<3.14,>=2.7"
|
||||
files = [
|
||||
{file = "pex-2.14.1-py2.py3-none-any.whl", hash = "sha256:43210f64e5461d91ad6c99d80724e83f5010bfa82a1664bf44ffdb14e2defb58"},
|
||||
{file = "pex-2.14.1.tar.gz", hash = "sha256:e71296873101732deffa5341d7dc9a9d8ed9e58413acc07579f7b39998c273a2"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
subprocess = ["subprocess32 (>=3.2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "10.4.0"
|
||||
|
@ -590,5 +604,5 @@ files = [
|
|||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "3f22f947559bd9d54a838d51953828baa88e2485fb90c33931359511805cacc9"
|
||||
python-versions = "^3.12,<3.14"
|
||||
content-hash = "9ec27256e1d8988f06634a217388d357a211f6120d6ade104fdd87182b18279c"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "photojawn"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = "A simple photo album static site generator"
|
||||
authors = ["Nick Pegg <nick@nickpegg.com>"]
|
||||
repository = "https://github.com/nickpegg/photojawn"
|
||||
|
@ -12,7 +12,7 @@ photojawn = 'photojawn.cli:main'
|
|||
|
||||
[tool.poetry.dependencies]
|
||||
# TODO: make sure we support >=3.10 via tests
|
||||
python = "<4.0,>=3.11"
|
||||
python = "^3.12,<3.14"
|
||||
jinja2 = "^3.1.4"
|
||||
pillow = "^10.4.0"
|
||||
rich = "^13.7.1"
|
||||
|
@ -26,6 +26,7 @@ mypy = "*"
|
|||
pytest-testmon = "*"
|
||||
types-pyyaml = "^6.0.12.20240724"
|
||||
types-markdown = "^3.6.0.20240316"
|
||||
pex = "^2.14.1"
|
||||
|
||||
|
||||
[tool.mypy]
|
||||
|
|
Loading…
Add table
Reference in a new issue