Skip to main content (Source)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: FAFOL
"""A utility to help build image galleries for Nikola."""
import argparse
import logging
import sys
from datetime import datetime
from glob import glob
from os.path import abspath, basename, isdir, join
from typing import NamedTuple, Union
from urllib.parse import quote
from exif import Image
logger = logging.getLogger()
class Coord(NamedTuple):
    """A DMS coordinate"""
    degrees: float
    minutes: float
    seconds: float
    hemisphere: str = ''
    def __str__(self) -> str:
        Make a string version using decimal degrees instead of DMS
            str: this coordinate in decimal degrees
        if self.hemisphere == 'W' or self.hemisphere == 'S':
            sign = -1
            sign = 1
        return f'{(self.degrees + self.minutes / 60.0 + self.seconds/3600.0) * sign}'
    def __repr__(self) -> str:
        return f'Coord(degrees={self.degrees}, minutes={self.minutes}, seconds={self.seconds}, hemisphere={self.hemisphere})'
class GPS(NamedTuple):
    """A GPS location in DMS"""
    latitude: Coord
    longitude: Coord
    def __str__(self) -> str:
        Make a string version using decimal degrees instead of DMS
            str: this position in decimal degrees
        return f'{self.latitude}, {self.longitude}'
    def __repr__(self) -> str:
        return f'GPS(latitude={self.latitude!r}, longitude={self.longitude!r})'
class Metadata(NamedTuple):
    """Metadata for an image or gallery"""
    datetime: Union[str, datetime]
    filename: str
    location: GPS = None
    def __str__(self) -> str:
        if self.location is None:
            return f'{self.filename}, taken on {self.datetime}'
        return f'{self.filename}, taken on {self.datetime}, at {self.location}'
def make_gallery(gallery: str, gps_caption: bool = False) -> Metadata:
    Make the gallery-specific metadata files
        gallery (str): path to gallery of images
        gps_caption (bool, optional): whether to add the GPS info from Exif into captions. Defaults to False.
        Metadata: gallery metadata
    images = set(glob(join(gallery, '*.jpg')) + glob(join(gallery, '*.JPG')) +
                 glob(join(gallery, '*.jpeg')) + glob(join(gallery, '*.JPEG')))
    logger.debug(f'Found {len(images)} in gallery')
    metadata = []
    date = datetime.min
    for image in images:
        img = Image(image)
            gps = GPS(
                latitude=Coord._make(img['gps_latitude'] +
                longitude=Coord._make(img['gps_longitude'] +
        except KeyError:
            gps = None
                    img['datetime'], '%Y:%m:%d %H:%M:%S'),
        if metadata[-1].datetime > date:
            date = metadata[-1].datetime
        logger.debug(f'Image: {metadata[-1]}')
    path = join(gallery, 'metadata.yml')
    with open(path, 'w', encoding='utf-8') as yml:'Writing metadata for {len(images)} to {path}')
        for order, data in enumerate(sorted(metadata, key=lambda d: d.datetime)):
            yml.write(f'---\norder: {order}\nname: {data.filename}\ncaption: ')
            if data.location is not None and gps_caption:
                yml.write(f' (location {data.location})')
    path = join(gallery, 'index.txt')
    title = basename(gallery).replace("_", " ")
    with open(path, 'w', encoding='utf-8') as idx:
            f'Writing index file to {path}: title {title}, date {date}')
        idx.write(f'.. title: {title}\n')
        idx.write(f'.. date: {date}\n')
        idx.write('.. location: \n')
        idx.write('.. tags: \n')
    return Metadata(datetime=date, filename=basename(gallery), location=None)
def make_blog(blog: str, gallery_data: Metadata) -> None:
    Make the blog entry stub to announce the new gallery
        blog (str): path to blog posts
        gallery_data (Metadata): metadata for gallery
    slug = gallery.filename.lower().replace(" ", "-")
    path = join(blog, f'{slug}.md')
    gallery_url = f'/galleries/{quote(gallery.filename)}/'
    with open(path, 'w', encoding='utf-8') as blog:
            f'Writing blog entry stub to {path}: gallery {gallery_url}, date {gallery.datetime}')
        blog.write(f'slug: {slug}\n'
                   f'title: {gallery.filename}\n'
                   f'date: {gallery.datetime}\n'
                   f'location: \n'
                   f'tags: \n'
                   f'mood: \n'
                   f'category: blog\n'
                   f'status: draft\n'
                   f'A new gallery has been created: [{gallery.filename}]({gallery_url})\n'
if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('gallery', help='path to the gallery of images')
        '--blog', help='if given, the directory to create a placeholder blog post')
    parser.add_argument('--gps', action='store_true',
                        help='whether to include GPS tags in captions')
    args = parser.parse_args()
    if not isdir(
        logger.critical(f'{} is not a directory?')
        sys.exit(-100) = abspath(
    logger.debug(f'gallery: {}')
    gallery = make_gallery(, args.gps)
        if not isdir(
            logger.critical(f'{} is not a directory?')
            sys.exit(-200) = abspath(
        logger.debug(f'blog: {}')
        make_blog(, gallery)