Skip to main content

nikola_gallery_helper.py (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
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)