| 
#!/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
 | 
 | 
 | 
 | 
logging.basicConfig()
 | 
 | 
logger = logging.getLogger()
 | 
 | 
logger.setLevel(logging.DEBUG)
 | 
 | 
 | 
 | 
 | 
 | 
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
 | 
 | 
 | 
 | 
        Returns:
 | 
 | 
            str: this coordinate in decimal degrees
 | 
 | 
        """
 | 
 | 
        if self.hemisphere == 'W' or self.hemisphere == 'S':
 | 
 | 
            sign = -1
 | 
 | 
        else:
 | 
 | 
            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
 | 
 | 
 | 
 | 
        Returns:
 | 
 | 
            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
 | 
 | 
 | 
 | 
 | 
 | 
 | 
 | 
    Args:
 | 
 | 
        gallery (str): path to gallery of images
 | 
 | 
        gps_caption (bool, optional): whether to add the GPS info from Exif into captions. Defaults to False.
 | 
 | 
 | 
 | 
    Returns:
 | 
 | 
        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)
 | 
 | 
        try:
 | 
 | 
            gps = GPS(
 | 
 | 
                latitude=Coord._make(img['gps_latitude'] +
 | 
 | 
                                     (img['gps_latitude_ref'],)),
 | 
 | 
                longitude=Coord._make(img['gps_longitude'] +
 | 
 | 
                                      (img['gps_longitude_ref'],)),
 | 
 | 
            )
 | 
 | 
        except KeyError:
 | 
 | 
            gps = None
 | 
 | 
        metadata.append(
 | 
 | 
            Metadata(
 | 
 | 
                datetime=datetime.strptime(
 | 
 | 
                    img['datetime'], '%Y:%m:%d %H:%M:%S'),
 | 
 | 
                filename=basename(image),
 | 
 | 
                location=gps
 | 
 | 
            )
 | 
 | 
        )
 | 
 | 
        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:
 | 
 | 
        logger.info(f'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})')
 | 
 | 
            yml.write('\n')
 | 
 | 
        yml.write('---')
 | 
 | 
 | 
 | 
    path = join(gallery, 'index.txt')
 | 
 | 
    title = basename(gallery).replace("_", " ")
 | 
 | 
    with open(path, 'w', encoding='utf-8') as idx:
 | 
 | 
        logger.info(
 | 
 | 
            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')
 | 
 | 
        idx.write('\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
 | 
 | 
 | 
 | 
    Args:
 | 
 | 
        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:
 | 
 | 
        logger.info(
 | 
 | 
            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'\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')
 | 
 | 
    parser.add_argument(
 | 
 | 
        '--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(args.gallery):
 | 
 | 
        logger.critical(f'{args.gallery} is not a directory?')
 | 
 | 
        sys.exit(-100)
 | 
 | 
    args.gallery = abspath(args.gallery)
 | 
 | 
 | 
 | 
    logger.debug(f'gallery: {args.gallery}')
 | 
 | 
 | 
 | 
    gallery = make_gallery(args.gallery, args.gps)
 | 
 | 
 | 
 | 
    if args.blog:
 | 
 | 
        if not isdir(args.blog):
 | 
 | 
            logger.critical(f'{args.blog} is not a directory?')
 | 
 | 
            sys.exit(-200)
 | 
 | 
 | 
 | 
        args.blog = abspath(args.blog)
 | 
 | 
        logger.debug(f'blog: {args.blog}')
 | 
 | 
        make_blog(args.blog, gallery)
 |