Merge branch 'refactor'
This commit is contained in:
@@ -7,6 +7,17 @@ Version 0.7.dev
|
||||
|
||||
Released on 2014-xx-xx.
|
||||
|
||||
- Refactor the way to store album and media informations. Albums, images and
|
||||
videos are now represented by objects, and these objects are directly
|
||||
available in the templates. The following template variables have been
|
||||
renamed:
|
||||
|
||||
``albums`` => ``album.albums``, ``breadcrumb`` => ``album.breadcrumb``,
|
||||
``description`` => ``album.description``, ``index_url`` =>
|
||||
``album.index_url``, ``medias`` => ``album.medias``, ``title`` =>
|
||||
``album.title``, ``media.file`` => ``media.filename``, ``media.thumb`` =>
|
||||
``media.thumbnail``, ``zip_gallery`` => ``album.zip``
|
||||
|
||||
Version 0.6.0
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -26,59 +26,42 @@ Variables
|
||||
|
||||
You can use the following variables in your template:
|
||||
|
||||
``albums``
|
||||
List of ``album`` objects. An ``album`` object has the following attributes:
|
||||
|
||||
- ``album.name``
|
||||
- ``album.title``
|
||||
- ``album.url``
|
||||
- ``album.thumb``
|
||||
|
||||
``breadcrumb``
|
||||
List of ``(url, title)`` tuples defining the current breadcrumb path.
|
||||
``album``
|
||||
The current album that is rendered in the HTML file, represented by an
|
||||
:class:`~sigal.gallery.Album` object. ``album.medias`` contains the list
|
||||
of all medias in the album (represented by the
|
||||
:class:`~sigal.gallery.Image` and :class:`~sigal.gallery.Video` objects,
|
||||
inherited from :class:`~sigal.gallery.Media`).
|
||||
|
||||
``index_title``
|
||||
Name of the index. This is either the directory name or the title specified
|
||||
in the ``index.md``.
|
||||
|
||||
``index_url``
|
||||
URL to the index page.
|
||||
|
||||
``medias``
|
||||
List of ``media`` objects. A ``media`` object has the following attributes:
|
||||
|
||||
- ``media.type``: Either ``"img"`` or ``"vid"``.
|
||||
- ``media.file``: Location of the resized image.
|
||||
- ``media.thumb``: Location of the corresponding thumbnail image.
|
||||
- ``media.big``: If not None, location of the unmodified image.
|
||||
- ``media.exif``: If not None contains a dict with the most common tags. For
|
||||
more information, see :ref:`simple-exif-data`.
|
||||
- ``media.raw_exif``: If not ``None``, it contains the raw EXIF tags.
|
||||
|
||||
``meta`` and ``description``
|
||||
Meta data and album description. For details how to annotate your albums
|
||||
with meta data, see :doc:`album_information`.
|
||||
|
||||
``theme.name``
|
||||
Name of the currently used theme.
|
||||
in the ``index.md`` of the ``source`` directory.
|
||||
|
||||
``settings``
|
||||
The entire dictionary from ``sigal.conf.py``. For example, you could use
|
||||
this to output an optional download link for zipped archives:
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
{% if settings.zip_gallery %}
|
||||
<a href="{{ settings.zip_gallery }}">Download archive</a>
|
||||
{% endif %}
|
||||
The entire dictionary from ``sigal.conf.py``.
|
||||
|
||||
``sigal_link``
|
||||
URL to the Sigal homepage.
|
||||
|
||||
``zip_gallery``
|
||||
If not None, it contains the location of a zip archive with all original
|
||||
images of the corresponding directory.
|
||||
``theme.name``, ``theme.url``
|
||||
Name and url of the currently used theme.
|
||||
|
||||
.. autoclass:: sigal.gallery.Album
|
||||
:members:
|
||||
:undoc-members:
|
||||
:inherited-members:
|
||||
|
||||
.. autoclass:: sigal.gallery.Media
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
.. autoclass:: sigal.gallery.Image
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
.. autoclass:: sigal.gallery.Video
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
.. _simple-exif-data:
|
||||
|
||||
|
||||
@@ -105,8 +105,8 @@ def build(source, destination, debug=False, verbose=False, force=False,
|
||||
sys.exit(1)
|
||||
|
||||
locale.setlocale(locale.LC_ALL, settings['locale'])
|
||||
gal = Gallery(settings, force=force, theme=theme, ncpu=ncpu)
|
||||
gal.build()
|
||||
gal = Gallery(settings, theme=theme, ncpu=ncpu)
|
||||
gal.build(force=force)
|
||||
|
||||
# copy extra files
|
||||
for src, dst in settings['files_to_copy']:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import locale
|
||||
import sys
|
||||
|
||||
PY2 = sys.version_info[0] == 2
|
||||
@@ -9,12 +10,23 @@ if not PY2:
|
||||
string_types = (str,)
|
||||
unichr = chr
|
||||
|
||||
from functools import cmp_to_key
|
||||
alpha_sort = {'key': cmp_to_key(locale.strcoll)}
|
||||
|
||||
from http import server
|
||||
import socketserver
|
||||
else:
|
||||
text_type = unicode # NOQA
|
||||
string_types = (str, unicode) # NOQA
|
||||
unichr = unichr
|
||||
alpha_sort = {'cmp': locale.strcoll}
|
||||
|
||||
import SimpleHTTPServer as server
|
||||
import SocketServer as socketserver
|
||||
|
||||
|
||||
class UnicodeMixin(object):
|
||||
if not PY2:
|
||||
__str__ = lambda x: x.__unicode__()
|
||||
else:
|
||||
__str__ = lambda x: unicode(x).encode('utf-8')
|
||||
|
||||
578
sigal/gallery.py
578
sigal/gallery.py
@@ -24,7 +24,6 @@
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
import codecs
|
||||
import locale
|
||||
import logging
|
||||
import markdown
|
||||
import multiprocessing
|
||||
@@ -32,163 +31,364 @@ import os
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
from os.path import join, normpath
|
||||
from collections import defaultdict
|
||||
from os.path import isfile, join
|
||||
from PIL import Image as PILImage
|
||||
from pprint import pformat
|
||||
|
||||
from . import compat
|
||||
from .image import process_image
|
||||
from . import image, video
|
||||
from .compat import UnicodeMixin, alpha_sort
|
||||
from .image import process_image, get_exif_tags
|
||||
from .log import colored, BLUE
|
||||
from .settings import get_thumb, get_orig
|
||||
from .utils import copy, check_or_create_dir
|
||||
from .video import process_video
|
||||
from .writer import Writer
|
||||
|
||||
DESCRIPTION_FILE = "index.md"
|
||||
|
||||
class Media(UnicodeMixin):
|
||||
"""Base Class for media files.
|
||||
|
||||
class FileExtensionError(Exception):
|
||||
"""Raised if we made an error when handling file extensions"""
|
||||
pass
|
||||
Attributes:
|
||||
|
||||
|
||||
class PathsDb(object):
|
||||
"""Container for all the information on the directory structure.
|
||||
|
||||
All the info is stored in a dictionnary, `self.db`. This class also has
|
||||
methods to build this dictionnary.
|
||||
- ``type``: ``"image"`` or ``"video"``.
|
||||
- ``filename``: Filename of the resized image.
|
||||
- ``thumbnail``: Location of the corresponding thumbnail image.
|
||||
- ``big``: If not None, location of the unmodified image.
|
||||
- ``exif``: If not None contains a dict with the most common tags. For more
|
||||
information, see :ref:`simple-exif-data`.
|
||||
- ``raw_exif``: If not ``None``, it contains the raw EXIF tags.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, path, img_ext_list, vid_ext_list):
|
||||
self.img_ext_list = img_ext_list
|
||||
self.vid_ext_list = vid_ext_list
|
||||
self.ext_list = self.img_ext_list + self.vid_ext_list
|
||||
type = ''
|
||||
extensions = ()
|
||||
|
||||
def __init__(self, filename, path, settings):
|
||||
self.filename = filename
|
||||
self.settings = settings
|
||||
self.file_path = join(path, filename)
|
||||
self.src_path = join(settings['source'], path, filename)
|
||||
self.dst_path = join(settings['destination'], path, filename)
|
||||
|
||||
self.thumb_name = get_thumb(self.settings, self.filename)
|
||||
self.thumb_path = join(settings['destination'], path, self.thumb_name)
|
||||
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.raw_exif = None
|
||||
self.exif = None
|
||||
|
||||
# The dict containing all information
|
||||
self.db = {
|
||||
'paths_list': [],
|
||||
'skipped_dir': []
|
||||
}
|
||||
def __repr__(self):
|
||||
return "<%s>(%r)" % (self.__class__.__name__, self.file_path)
|
||||
|
||||
# basepath must to be a unicode string so that os.walk will return
|
||||
# unicode dirnames and filenames. If basepath is a str, we must
|
||||
# convert it to unicode.
|
||||
if compat.PY2 and isinstance(path, str):
|
||||
enc = locale.getpreferredencoding()
|
||||
self.basepath = path.decode(enc)
|
||||
def __unicode__(self):
|
||||
return self.file_path
|
||||
|
||||
@property
|
||||
def big(self):
|
||||
"""Path to the original image, if ``keep_orig`` is set (relative to the
|
||||
album directory).
|
||||
"""
|
||||
if self.settings['keep_orig']:
|
||||
return get_orig(self.settings, self.filename)
|
||||
else:
|
||||
self.basepath = path
|
||||
return None
|
||||
|
||||
self.build()
|
||||
@property
|
||||
def thumbnail(self):
|
||||
"""Path to the thumbnail image (relative to the album directory)."""
|
||||
|
||||
def get_subdirs(self, path):
|
||||
"""Return the list of all sub-directories of path."""
|
||||
if not os.path.isfile(self.thumb_path):
|
||||
# if thumbnail is missing (if settings['make_thumbs'] is False)
|
||||
if self.type == 'image':
|
||||
generator = image.generate_thumbnail
|
||||
elif self.type == 'video':
|
||||
generator = video.generate_thumbnail
|
||||
|
||||
for name in self.db[path].get('subdir', []):
|
||||
subdir = normpath(join(path, name))
|
||||
yield subdir
|
||||
for subname in self.get_subdirs(subdir):
|
||||
yield subname
|
||||
self.logger.debug('Generating thumbnail for %r', self)
|
||||
generator(self.src_path, self.thumb_path,
|
||||
self.settings['thumb_size'],
|
||||
fit=self.settings['thumb_fit'])
|
||||
return self.thumb_name
|
||||
|
||||
def build(self):
|
||||
"Build the list of directories with images"
|
||||
|
||||
if compat.PY2:
|
||||
sort_args = {'cmp': locale.strcoll}
|
||||
class Image(Media):
|
||||
"""Gather all informations on an image file."""
|
||||
|
||||
type = 'image'
|
||||
extensions = ('.jpg', '.jpeg', '.JPG', '.JPEG', '.png')
|
||||
|
||||
def __init__(self, filename, path, settings):
|
||||
super(Image, self).__init__(filename, path, settings)
|
||||
self.raw_exif, self.exif = get_exif_tags(self.src_path)
|
||||
|
||||
|
||||
class Video(Media):
|
||||
"""Gather all informations on a video file."""
|
||||
|
||||
type = 'video'
|
||||
extensions = ('.MOV', '.mov', '.avi', '.mp4', '.webm', '.ogv')
|
||||
|
||||
def __init__(self, filename, path, settings):
|
||||
super(Video, self).__init__(filename, path, settings)
|
||||
base = os.path.splitext(filename)[0]
|
||||
self.file_path = join(path, base + '.webm')
|
||||
self.dst_path = join(settings['destination'], path, base + '.webm')
|
||||
|
||||
|
||||
class Album(UnicodeMixin):
|
||||
"""Gather all informations on an album.
|
||||
|
||||
Attributes:
|
||||
|
||||
:var description_file: Name of the Markdown file which gives information
|
||||
on an album
|
||||
:ivar index_url: URL to the index page.
|
||||
:ivar output_file: Name of the output HTML file
|
||||
:ivar meta: Meta data from the Markdown file.
|
||||
:ivar description: description from the Markdown file.
|
||||
|
||||
For details how to annotate your albums with meta data, see
|
||||
:doc:`album_information`.
|
||||
|
||||
"""
|
||||
|
||||
description_file = "index.md"
|
||||
output_file = 'index.html'
|
||||
|
||||
def __init__(self, path, settings, dirnames, filenames, gallery):
|
||||
self.path = path
|
||||
self.name = path.split(os.path.sep)[-1]
|
||||
self.gallery = gallery
|
||||
self.settings = settings
|
||||
self.orig_path = None
|
||||
self._thumbnail = None
|
||||
|
||||
if path == '.':
|
||||
self.src_path = settings['source']
|
||||
self.dst_path = settings['destination']
|
||||
else:
|
||||
from functools import cmp_to_key
|
||||
sort_args = {'key': cmp_to_key(locale.strcoll)}
|
||||
self.src_path = join(settings['source'], path)
|
||||
self.dst_path = join(settings['destination'], path)
|
||||
|
||||
# get information for each directory
|
||||
for path, dirnames, filenames in os.walk(self.basepath,
|
||||
followlinks=True):
|
||||
relpath = os.path.relpath(path, self.basepath)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self._get_metadata()
|
||||
|
||||
# sort images and sub-albums by name
|
||||
filenames.sort(**sort_args)
|
||||
dirnames.sort(**sort_args)
|
||||
# Create thumbnails directory and optionally the one for original img
|
||||
check_or_create_dir(self.dst_path)
|
||||
check_or_create_dir(join(self.dst_path, settings['thumb_dir']))
|
||||
|
||||
self.db['paths_list'].append(relpath)
|
||||
self.db[relpath] = {
|
||||
'medias': [f for f in filenames
|
||||
if os.path.splitext(f)[1] in self.ext_list],
|
||||
'subdir': dirnames
|
||||
}
|
||||
self.db[relpath].update(get_metadata(path))
|
||||
if settings['keep_orig']:
|
||||
self.orig_path = join(self.dst_path, settings['orig_dir'])
|
||||
check_or_create_dir(self.orig_path)
|
||||
|
||||
path_media = (path for path in self.db['paths_list']
|
||||
if self.db[path]['medias'] and path != '.')
|
||||
path_nomedia = (path for path in self.db['paths_list']
|
||||
if not self.db[path]['medias'] and path != '.')
|
||||
# optionally add index.html to the URLs
|
||||
self.url_ext = self.output_file if settings['index_in_url'] else ''
|
||||
self.url = self.name + '/' + self.url_ext
|
||||
|
||||
# dir with images: check the thumbnail, and find it if necessary
|
||||
for path in path_media:
|
||||
self.check_thumbnail(path)
|
||||
self.index_url = os.path.relpath(settings['destination'],
|
||||
self.dst_path) + '/' + self.url_ext
|
||||
|
||||
# dir without images, start with the deepest ones
|
||||
for path in reversed(sorted(path_nomedia, key=lambda x: x.count('/'))):
|
||||
# stop if it is already set and a valid file
|
||||
alb_thumb = self.db[path].setdefault('thumbnail', '')
|
||||
if alb_thumb and os.path.isfile(join(self.basepath, path,
|
||||
alb_thumb)):
|
||||
self.logger.debug("Thumb for %s : %s", path, alb_thumb)
|
||||
# sort images and sub-albums by name
|
||||
filenames.sort(**alpha_sort)
|
||||
dirnames.sort(**alpha_sort)
|
||||
|
||||
self.subdirs = dirnames
|
||||
|
||||
#: List of all medias in the album (:class:`~sigal.gallery.Image` and
|
||||
#: :class:`~sigal.gallery.Video`).
|
||||
self.medias = []
|
||||
self.medias_count = defaultdict(int)
|
||||
|
||||
for f in filenames:
|
||||
ext = os.path.splitext(f)[1]
|
||||
if ext in Image.extensions:
|
||||
media = Image(f, self.path, settings)
|
||||
elif ext in Video.extensions:
|
||||
media = Video(f, self.path, settings)
|
||||
else:
|
||||
continue
|
||||
|
||||
for subdir in self.get_subdirs(path):
|
||||
# use the thumbnail of their sub-directories
|
||||
if self.db[subdir].get('thumbnail', ''):
|
||||
self.db[path]['thumbnail'] = join(
|
||||
os.path.relpath(subdir, path),
|
||||
self.db[subdir]['thumbnail'])
|
||||
self.logger.debug("Found thumb for %s : %s", path,
|
||||
self.db[path]['thumbnail'])
|
||||
break
|
||||
self.medias_count[media.type] += 1
|
||||
self.medias.append(media)
|
||||
|
||||
if not self.db[path].get('thumbnail', ''):
|
||||
# else remove all info about this directory
|
||||
self.logger.info("Directory '%s' is empty", path)
|
||||
self.db['skipped_dir'].append(path)
|
||||
self.db['paths_list'].remove(path)
|
||||
del self.db[path]
|
||||
parent = normpath(join(path, '..'))
|
||||
child = os.path.relpath(path, parent)
|
||||
self.db[parent]['subdir'].remove(child)
|
||||
def __repr__(self):
|
||||
return "<%s>(%r)" % (self.__class__.__name__, self.path)
|
||||
|
||||
self.logger.debug('Database:\n%s', pformat(self.db, width=120))
|
||||
def __unicode__(self):
|
||||
return (u"{} : ".format(self.path) +
|
||||
', '.join("{} {}s".format(count, _type)
|
||||
for _type, count in self.medias_count.items()))
|
||||
|
||||
def check_thumbnail(self, path):
|
||||
"Find the thumbnail image for a given path."
|
||||
def __len__(self):
|
||||
return sum(self.medias_count.values())
|
||||
|
||||
# stop if it is already set and a valid file
|
||||
alb_thumb = self.db[path].setdefault('thumbnail', '')
|
||||
if alb_thumb and os.path.isfile(join(self.basepath, path, alb_thumb)):
|
||||
return
|
||||
def __iter__(self):
|
||||
return iter(self.medias)
|
||||
|
||||
# find and return the first landscape image
|
||||
for f in self.db[path]['medias']:
|
||||
base, ext = os.path.splitext(f)
|
||||
if ext in self.img_ext_list:
|
||||
im = PILImage.open(join(self.basepath, path, f))
|
||||
if im.size[0] > im.size[1]:
|
||||
self.db[path]['thumbnail'] = f
|
||||
return
|
||||
def _get_metadata(self):
|
||||
"""Get album metadata from `description_file` (`index.md`):
|
||||
|
||||
# else simply return the 1st media file
|
||||
if self.db[path]['medias']:
|
||||
self.db[path]['thumbnail'] = self.db[path]['medias'][0]
|
||||
-> title, thumbnail image, description
|
||||
|
||||
"""
|
||||
descfile = join(self.src_path, self.description_file)
|
||||
|
||||
if isfile(descfile):
|
||||
with codecs.open(descfile, "r", "utf-8") as f:
|
||||
text = f.read()
|
||||
|
||||
md = markdown.Markdown(extensions=['meta'])
|
||||
html = md.convert(text)
|
||||
|
||||
self.title = md.Meta.get('title', [''])[0]
|
||||
self.description = html
|
||||
self.meta = md.Meta.copy()
|
||||
else:
|
||||
# default: get title from directory name
|
||||
self.title = os.path.basename(self.path).replace('_', ' ')\
|
||||
.replace('-', ' ').capitalize()
|
||||
self.description = ''
|
||||
self.meta = {}
|
||||
|
||||
@property
|
||||
def images(self):
|
||||
"""List of images (:class:`~sigal.gallery.Image`)."""
|
||||
for media in self.medias:
|
||||
if media.type == 'image':
|
||||
yield media
|
||||
|
||||
@property
|
||||
def videos(self):
|
||||
"""List of videos (:class:`~sigal.gallery.Video`)."""
|
||||
for media in self.medias:
|
||||
if media.type == 'video':
|
||||
yield media
|
||||
|
||||
@property
|
||||
def albums(self):
|
||||
"""List of :class:`~sigal.gallery.Album` objects for each
|
||||
sub-directory.
|
||||
"""
|
||||
root_path = self.path if self.path != '.' else ''
|
||||
return [self.gallery.albums[join(root_path, path)]
|
||||
for path in self.subdirs]
|
||||
|
||||
@property
|
||||
def thumbnail(self):
|
||||
"""Path to the thumbnail of the album."""
|
||||
|
||||
if self._thumbnail:
|
||||
# stop if it is already set
|
||||
return self._thumbnail
|
||||
|
||||
# Test the thumbnail from the Markdown file.
|
||||
thumbnail = self.meta.get('thumbnail', [''])[0]
|
||||
|
||||
if thumbnail and isfile(join(self.src_path, thumbnail)):
|
||||
self._thumbnail = join(self.name, get_thumb(self.settings,
|
||||
thumbnail))
|
||||
self.logger.debug("Thumbnail for %r : %s", self, self._thumbnail)
|
||||
return self._thumbnail
|
||||
else:
|
||||
# find and return the first landscape image
|
||||
for f in self.medias:
|
||||
ext = os.path.splitext(f.filename)[1]
|
||||
if ext in Image.extensions:
|
||||
im = PILImage.open(f.src_path)
|
||||
if im.size[0] > im.size[1]:
|
||||
self._thumbnail = join(self.name, f.thumbnail)
|
||||
self.logger.debug(
|
||||
"Use 1st landscape image as thumbnail for %r : %s",
|
||||
self, self._thumbnail)
|
||||
return self._thumbnail
|
||||
|
||||
# else simply return the 1st media file
|
||||
if not self._thumbnail and self.medias:
|
||||
self._thumbnail = join(self.name, self.medias[0].thumbnail)
|
||||
self.logger.debug("Use the 1st image as thumbnail for %r : %s",
|
||||
self, self._thumbnail)
|
||||
return self._thumbnail
|
||||
|
||||
# use the thumbnail of their sub-directories
|
||||
if not self._thumbnail:
|
||||
for path, album in self.gallery.get_albums(self.path):
|
||||
if album.thumbnail:
|
||||
self._thumbnail = join(self.name, album.thumbnail)
|
||||
self.logger.debug(
|
||||
"Using thumbnail from sub-directory for %r : %s",
|
||||
self, self._thumbnail)
|
||||
return self._thumbnail
|
||||
|
||||
self.logger.error('Thumbnail not found for %r', self)
|
||||
return None
|
||||
|
||||
@property
|
||||
def breadcrumb(self):
|
||||
"""List of ``(url, title)`` tuples defining the current breadcrumb
|
||||
path.
|
||||
"""
|
||||
if self.path == '.':
|
||||
return []
|
||||
|
||||
path = self.path
|
||||
breadcrumb = [((self.url_ext or '.'), self.title)]
|
||||
|
||||
while True:
|
||||
path = os.path.normpath(os.path.join(path, '..'))
|
||||
if path == '.':
|
||||
break
|
||||
|
||||
url = os.path.relpath(path, self.path) + '/' + self.url_ext
|
||||
breadcrumb.append((url, self.gallery.albums[path].title))
|
||||
|
||||
breadcrumb.reverse()
|
||||
return breadcrumb
|
||||
|
||||
@property
|
||||
def zip(self):
|
||||
"""Make a ZIP archive with all media files and return its path.
|
||||
|
||||
If the ``zip_gallery`` setting is set,it contains the location of a zip
|
||||
archive with all original images of the corresponding directory.
|
||||
|
||||
"""
|
||||
zip_gallery = self.settings['zip_gallery']
|
||||
if zip_gallery:
|
||||
archive_path = join(self.dst_path, zip_gallery)
|
||||
archive = zipfile.ZipFile(archive_path, 'w')
|
||||
|
||||
for p in self:
|
||||
archive.write(p.dst_path, os.path.split(p.dst_path)[1])
|
||||
|
||||
archive.close()
|
||||
self.logger.debug('Created ZIP archive %s', archive_path)
|
||||
return zip_gallery
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class Gallery(object):
|
||||
|
||||
def __init__(self, settings, force=False, theme=None, ncpu=None):
|
||||
def __init__(self, settings, theme=None, ncpu=None):
|
||||
self.settings = settings
|
||||
self.force = force
|
||||
self.theme = theme
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.stats = {'image': 0, 'image_skipped': 0,
|
||||
'video': 0, 'video_skipped': 0}
|
||||
self.init_pool(ncpu)
|
||||
check_or_create_dir(settings['destination'])
|
||||
|
||||
# Build the list of directories with images
|
||||
albums = self.albums = {}
|
||||
src_path = self.settings['source']
|
||||
|
||||
for path, dirs, files in os.walk(src_path, followlinks=True):
|
||||
relpath = os.path.relpath(path, src_path)
|
||||
albums[relpath] = Album(relpath, self.settings, dirs, files, self)
|
||||
|
||||
self.logger.debug('Albums:\n%r', albums.values())
|
||||
|
||||
def init_pool(self, ncpu):
|
||||
try:
|
||||
cpu_count = multiprocessing.cpu_count()
|
||||
except NotImplementedError:
|
||||
@@ -203,33 +403,41 @@ class Gallery(object):
|
||||
self.logger.error('ncpu should be an integer value')
|
||||
ncpu = cpu_count
|
||||
|
||||
self.logger.info("Using %s cores", ncpu)
|
||||
if ncpu > 1:
|
||||
self.pool = multiprocessing.Pool(processes=ncpu)
|
||||
else:
|
||||
self.pool = None
|
||||
|
||||
self.logger.info("Using %s cores", ncpu)
|
||||
def get_albums(self, path):
|
||||
"""Return the list of all sub-directories of path."""
|
||||
|
||||
paths = PathsDb(self.settings['source'], self.settings['img_ext_list'],
|
||||
self.settings['vid_ext_list'])
|
||||
self.db = paths.db
|
||||
for name in self.albums[path].subdirs:
|
||||
subdir = os.path.normpath(join(path, name))
|
||||
yield subdir, self.albums[subdir]
|
||||
for subname, album in self.get_albums(subdir):
|
||||
yield subname, self.albums[subdir]
|
||||
|
||||
def build(self):
|
||||
def build(self, force=False):
|
||||
"Create the image gallery"
|
||||
|
||||
check_or_create_dir(self.settings['destination'])
|
||||
|
||||
# loop on directories in reversed order, to process subdirectories
|
||||
# before their parent
|
||||
|
||||
if self.pool:
|
||||
media_list = []
|
||||
processor = media_list.append
|
||||
else:
|
||||
processor = process_file
|
||||
|
||||
for path in reversed(self.db['paths_list']):
|
||||
if len(self.db[path]['medias']) != 0:
|
||||
for files in self.process_dir(path):
|
||||
media_list.append(files)
|
||||
try:
|
||||
for album in self.albums.values():
|
||||
if len(album) > 0:
|
||||
for files in self.process_dir(album, force=force):
|
||||
processor(files)
|
||||
except KeyboardInterrupt:
|
||||
sys.exit('Interrupted')
|
||||
|
||||
if self.pool:
|
||||
try:
|
||||
# map_async is needed to handle KeyboardInterrupt correctly
|
||||
self.pool.map_async(worker, media_list).get(9999)
|
||||
@@ -240,86 +448,48 @@ class Gallery(object):
|
||||
sys.exit('Interrupted')
|
||||
|
||||
print('')
|
||||
else:
|
||||
try:
|
||||
for path in reversed(self.db['paths_list']):
|
||||
if len(self.db[path]['medias']) != 0:
|
||||
for files in self.process_dir(path):
|
||||
process_file(files)
|
||||
print('')
|
||||
except KeyboardInterrupt:
|
||||
sys.exit('Interrupted')
|
||||
|
||||
if self.settings['write_html']:
|
||||
self.writer = Writer(self.settings, self.settings['destination'],
|
||||
theme=self.theme)
|
||||
self.writer = Writer(self.settings, theme=self.theme,
|
||||
index_title=self.albums['.'].title)
|
||||
|
||||
for path in reversed(self.db['paths_list']):
|
||||
self.writer.write(self.db, path)
|
||||
for album in self.albums.values():
|
||||
self.writer.write(album)
|
||||
|
||||
def process_dir(self, path):
|
||||
def process_dir(self, album, force=False):
|
||||
"""Process a list of images in a directory."""
|
||||
|
||||
media_files = [normpath(join(self.settings['source'], path, f))
|
||||
for f in self.db[path]['medias']]
|
||||
|
||||
# output dir for the current path
|
||||
outpath = normpath(join(self.settings['destination'], path))
|
||||
check_or_create_dir(outpath)
|
||||
|
||||
# Create thumbnails directory and optionally the one for original img
|
||||
check_or_create_dir(join(outpath, self.settings['thumb_dir']))
|
||||
|
||||
if self.settings['keep_orig']:
|
||||
check_or_create_dir(join(outpath, self.settings['orig_dir']))
|
||||
|
||||
if sys.stdout.isatty():
|
||||
print(colored('->', BLUE),
|
||||
u"{} : {} files".format(path, len(media_files)))
|
||||
print(colored('->', BLUE), str(album))
|
||||
else:
|
||||
self.logger.warn("%s : %d files", path, len(media_files))
|
||||
self.logger.warn(album)
|
||||
|
||||
# loop on images
|
||||
if self.settings['zip_gallery']:
|
||||
zip_files(join(outpath, self.settings['zip_gallery']), media_files)
|
||||
|
||||
for f in media_files:
|
||||
filename = os.path.split(f)[1]
|
||||
base, ext = os.path.splitext(filename)
|
||||
|
||||
if ext in self.settings['img_ext_list']:
|
||||
outname = join(outpath, filename)
|
||||
filetype = 'image'
|
||||
elif ext in self.settings['vid_ext_list']:
|
||||
outname = join(outpath, base + '.webm')
|
||||
filetype = 'video'
|
||||
else:
|
||||
raise FileExtensionError
|
||||
|
||||
if os.path.isfile(outname) and not self.force:
|
||||
self.logger.info("%s exists - skipping", filename)
|
||||
self.stats[filetype + '_skipped'] += 1
|
||||
for f in album:
|
||||
if isfile(f.dst_path) and not force:
|
||||
self.logger.info("%s exists - skipping", f.filename)
|
||||
self.stats[f.type + '_skipped'] += 1
|
||||
else:
|
||||
if self.settings['keep_orig']:
|
||||
copy(f, join(outpath, self.settings['orig_dir'], filename),
|
||||
copy(f.src_path, join(album.orig_path, f.filename),
|
||||
symlink=self.settings['orig_link'])
|
||||
|
||||
self.stats[filetype] += 1
|
||||
yield filetype, f, outpath, self.settings
|
||||
self.stats[f.type] += 1
|
||||
yield f.type, f.src_path, album.dst_path, self.settings
|
||||
|
||||
|
||||
def process_file(args):
|
||||
ftype, src_path, dst_path, settings = args
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info('Processing %s', args[1])
|
||||
logger.info('Processing %s', src_path)
|
||||
|
||||
if logger.getEffectiveLevel() > 20:
|
||||
print('.', end='')
|
||||
sys.stdout.flush()
|
||||
|
||||
if args[0] == 'image':
|
||||
return process_image(*args[1:])
|
||||
elif args[0] == 'video':
|
||||
return process_video(*args[1:])
|
||||
if ftype == 'image':
|
||||
return process_image(src_path, dst_path, settings)
|
||||
elif ftype == 'video':
|
||||
return process_video(src_path, dst_path, settings)
|
||||
|
||||
|
||||
def worker(args):
|
||||
@@ -327,49 +497,3 @@ def worker(args):
|
||||
process_file(args)
|
||||
except KeyboardInterrupt:
|
||||
return 'KeyboardException'
|
||||
|
||||
|
||||
def get_metadata(path):
|
||||
""" Get album metadata from DESCRIPTION_FILE:
|
||||
|
||||
- title
|
||||
- thumbnail image
|
||||
- description
|
||||
|
||||
"""
|
||||
descfile = join(path, DESCRIPTION_FILE)
|
||||
|
||||
if not os.path.isfile(descfile):
|
||||
# default: get title from directory name
|
||||
meta = {
|
||||
'title': os.path.basename(path).replace('_', ' ')
|
||||
.replace('-', ' ').capitalize(),
|
||||
'description': '',
|
||||
'thumbnail': '',
|
||||
'meta': {}
|
||||
}
|
||||
else:
|
||||
with codecs.open(descfile, "r", "utf-8") as f:
|
||||
text = f.read()
|
||||
|
||||
md = markdown.Markdown(extensions=['meta'])
|
||||
html = md.convert(text)
|
||||
|
||||
meta = {
|
||||
'title': md.Meta.get('title', [''])[0],
|
||||
'description': html,
|
||||
'thumbnail': md.Meta.get('thumbnail', [''])[0],
|
||||
'meta': md.Meta.copy()
|
||||
}
|
||||
|
||||
return meta
|
||||
|
||||
|
||||
def zip_files(archive_path, filepaths):
|
||||
archive = zipfile.ZipFile(archive_path, 'w')
|
||||
|
||||
for p in filepaths:
|
||||
filename = os.path.split(p)[1]
|
||||
archive.write(p, filename)
|
||||
|
||||
archive.close()
|
||||
|
||||
@@ -21,10 +21,13 @@
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
from pprint import pformat
|
||||
|
||||
from .compat import PY2
|
||||
|
||||
_DEFAULT_CONFIG = {
|
||||
'adjust_options': {'color': 1.0, 'brightness': 1.0,
|
||||
'contrast': 1.0, 'sharpness': 1.0},
|
||||
@@ -122,6 +125,12 @@ def read_settings(filename=None):
|
||||
settings_path, path)))
|
||||
logger.debug("Rewrite %s : %s -> %s", p, path, settings[p])
|
||||
|
||||
# paths must to be unicode strings so that os.walk will return
|
||||
# unicode dirnames and filenames
|
||||
if PY2 and isinstance(settings[p], str):
|
||||
enc = locale.getpreferredencoding()
|
||||
settings[p] = settings[p].decode(enc)
|
||||
|
||||
for key in ('img_size', 'thumb_size', 'video_size'):
|
||||
w, h = settings[key]
|
||||
if h > w:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
|
||||
<title>{{ title }}</title>
|
||||
<title>{{ album.title }}</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="{{ author }}">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<div class="four columns">
|
||||
<div class="sidebar">
|
||||
<h1><a href="{{ index_url }}">{{ index_title }}</a></h1>
|
||||
<h1><a href="{{ album.index_url }}">{{ index_title }}</a></h1>
|
||||
|
||||
{% if settings.links %}
|
||||
<nav id="menu">
|
||||
@@ -41,9 +41,9 @@
|
||||
|
||||
<div id="main" role="main" class="twelve columns offset-by-four">
|
||||
<header>
|
||||
{% if breadcrumb %}
|
||||
{% if album.breadcrumb %}
|
||||
<h2>
|
||||
{%- for url, title in breadcrumb -%}
|
||||
{%- for url, title in album.breadcrumb -%}
|
||||
<a href="{{ url }}">{{ title }}</a>
|
||||
{%- if not loop.last %} » {% endif -%}
|
||||
{% endfor -%}
|
||||
@@ -52,8 +52,8 @@
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
{% if albums %}
|
||||
{% for album in albums %}
|
||||
{% if album.albums %}
|
||||
{% for alb in album.albums %}
|
||||
{% if loop.index % 3 == 1 %}
|
||||
<div id="albums" class="row">
|
||||
{% endif%}
|
||||
@@ -61,10 +61,10 @@
|
||||
<div class="four columns thumbnail
|
||||
{% if loop.index % 3 == 1 %}alpha{% endif%}
|
||||
{% if loop.index % 3 == 0 %}omega{% endif%}">
|
||||
<a href="{{ album.url }}">
|
||||
<img src="{{ album.thumb }}" class="album_thumb"
|
||||
alt="{{ album.name }}" title="{{ album.name }}" /></a>
|
||||
<span class="album_title">{{ album.title }}</span>
|
||||
<a href="{{ alb.url }}">
|
||||
<img src="{{ alb.thumbnail }}" class="album_thumb"
|
||||
alt="{{ alb.name }}" title="{{ alb.name }}" /></a>
|
||||
<span class="album_title">{{ alb.title }}</span>
|
||||
</div>
|
||||
|
||||
{% if loop.last or loop.index % 3 == 0 %}
|
||||
@@ -73,7 +73,7 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if medias %}
|
||||
{% if album.medias %}
|
||||
{% macro img_description(media) -%}
|
||||
{% if media.big %} data-big="{{ media.big }}"{% endif %}
|
||||
{% if media.exif %}
|
||||
@@ -83,31 +83,31 @@
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
<div id="gallery" class="row">
|
||||
{% for media in medias %}
|
||||
{% if media.type == "img" %}
|
||||
{% for media in album.medias %}
|
||||
{% if media.type == "image" %}
|
||||
<div class="four columns thumbnail
|
||||
{% if loop.index % 3 == 1 %}alpha{% endif%}
|
||||
{% if loop.index % 3 == 0 %}omega{% endif%}">
|
||||
<a href="{{ media.file }}" class="gallery" title="{{ media.file }}" {{ img_description(media) }}>
|
||||
<img src="{{ media.thumb }}" alt="{{ media.file }}"
|
||||
title="{{ media.file }}" /></a>
|
||||
<a href="{{ media.filename }}" class="gallery" title="{{ media.filename }}" {{ img_description(media) }}>
|
||||
<img src="{{ media.thumbnail }}" alt="{{ media.filename }}"
|
||||
title="{{ media.filename }}" /></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if media.type == "vid" %}
|
||||
{% if media.type == "video" %}
|
||||
<div class="four columns thumbnail
|
||||
{% if loop.index % 3 == 1 %}alpha{% endif%}
|
||||
{% if loop.index % 3 == 0 %}omega{% endif%}">
|
||||
<a href="#{{ media.file|replace('.', '')|replace(' ', '') }}"
|
||||
class="gallery" inline='yes' title="{{ media.file }}"
|
||||
<a href="#{{ media.filename|replace('.', '')|replace(' ', '') }}"
|
||||
class="gallery" inline='yes' title="{{ media.filename }}"
|
||||
{% if media.big %} data-big="{{ media.big }}"{% endif %}>
|
||||
<img src="{{ media.thumb }}" alt="{{ media.file }}"
|
||||
title="{{ media.file }}" /></a>
|
||||
<img src="{{ media.thumbnail }}" alt="{{ media.filename }}"
|
||||
title="{{ media.filename }}" /></a>
|
||||
</div>
|
||||
<!-- This contains the hidden content for the video -->
|
||||
<div style='display:none'>
|
||||
<div id="{{ media.file|replace('.', '')|replace(' ', '') }}">
|
||||
<div id="{{ media.filename|replace('.', '')|replace(' ', '') }}">
|
||||
<video controls>
|
||||
<source src='{{ media.file }}' type='video/webm' />
|
||||
<source src='{{ media.filename }}' type='video/webm' />
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,24 +116,24 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if zip_gallery %}
|
||||
{% if album.zip %}
|
||||
<div id="additionnal-infos" class="row">
|
||||
<p>
|
||||
<a href="{{ zip_gallery }}"
|
||||
<a href="{{ album.zip }}"
|
||||
title="Download a zip archive with all images">Download ZIP</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if description %}
|
||||
{% if album.description %}
|
||||
<div id="description" class="row">
|
||||
{{ description }}
|
||||
{{ album.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if medias %}
|
||||
{% if album.medias %}
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
|
||||
<script>!window.jQuery && document.write(unescape('%3Cscript src="{{ theme.url }}/js/jquery-1.10.2.min.js"%3E%3C/script%3E'))</script>
|
||||
<script src="{{ theme.url }}/js/jquery.colorbox.min.js"></script>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
|
||||
<title>{{ title|striptags }}</title>
|
||||
<title>{{ album.title|striptags }}</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="{{ author }}">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
@@ -19,7 +19,7 @@
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1><a href="{{ index_url }}">{{ index_title }}</a></h1>
|
||||
<h1><a href="{{ album.index_url }}">{{ index_title }}</a></h1>
|
||||
|
||||
{% if settings.links %}
|
||||
<nav id="menu">
|
||||
@@ -31,9 +31,9 @@
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% if breadcrumb %}
|
||||
{% if album.breadcrumb %}
|
||||
<h2>
|
||||
{%- for url, title in breadcrumb -%}
|
||||
{%- for url, title in album.breadcrumb -%}
|
||||
<a href="{{ url }}">{{ title }}</a>
|
||||
{%- if not loop.last %} » {% endif -%}
|
||||
{% endfor -%}
|
||||
@@ -43,21 +43,21 @@
|
||||
</header>
|
||||
|
||||
<div id="main" role="main">
|
||||
{% if albums %}
|
||||
{% if album.albums %}
|
||||
<div id="albums">
|
||||
<!-- <h1>Albums</h1> -->
|
||||
<ul>
|
||||
{% for album in albums %}
|
||||
<li><a href="{{ album.url }}">
|
||||
<img src="{{ album.thumb }}" class="album_thumb" alt="{{ album.name }}" title="{{ album.name }}" /></a>
|
||||
<span class="album_title">{{ album.title }}</span>
|
||||
{% for alb in album.albums %}
|
||||
<li><a href="{{ alb.url }}">
|
||||
<img src="{{ alb.thumbnail }}" class="album_thumb" alt="{{ alb.name }}" title="{{ alb.name }}" /></a>
|
||||
<span class="album_title">{{ alb.title }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if medias %}
|
||||
{% if album.medias %}
|
||||
{% macro img_description(media) -%}
|
||||
{%- if media.big %}<a href='{{ media.big }}'>Full size</a>{% endif %}
|
||||
{%- if media.exif %}
|
||||
@@ -72,19 +72,19 @@
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
<div id="gallery">
|
||||
{% for media in medias %}
|
||||
{% if media.type == "img" %}
|
||||
<a href="{{ media.file }}">
|
||||
<img src="{{ media.thumb }}" alt="{{ media.file }}"
|
||||
data-title="{{ media.file }}"
|
||||
{% for media in album.medias %}
|
||||
{% if media.type == "image" %}
|
||||
<a href="{{ media.filename }}">
|
||||
<img src="{{ media.thumbnail }}" alt="{{ media.filename }}"
|
||||
data-title="{{ media.filename }}"
|
||||
data-description="{{ img_description(media) }}"/>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if media.type == "vid" %}
|
||||
{% if media.type == "video" %}
|
||||
<a href="{{ theme.url }}/img/empty.png">
|
||||
<img src="{{ media.thumb }}" alt="{{ media.file }}"
|
||||
<img src="{{ media.thumbnail }}" alt="{{ media.filename }}"
|
||||
data-layer="<video controls>
|
||||
<source src='{{ media.file }}' type='video/webm' />
|
||||
<source src='{{ media.filename }}' type='video/webm' />
|
||||
</video>" />
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -92,18 +92,18 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if zip_gallery %}
|
||||
{% if album.zip %}
|
||||
<div id="additionnal-infos" class="row">
|
||||
<p>
|
||||
<a href="{{ zip_gallery }}"
|
||||
<a href="{{ album.zip }}"
|
||||
title="Download a zip archive with all images">Download ZIP</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if description %}
|
||||
{% if album.description %}
|
||||
<div id="description">
|
||||
{{ description }}
|
||||
{{ album.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -114,7 +114,7 @@
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{% if medias %}
|
||||
{% if album.medias %}
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
|
||||
<script>!window.jQuery && document.write(unescape('%3Cscript src="{{ theme.url }}/js/jquery-1.8.2.min.js"%3E%3C/script%3E'))</script>
|
||||
<script src="{{ theme.url }}/js/galleria-1.2.9.min.js"></script>
|
||||
|
||||
144
sigal/writer.py
144
sigal/writer.py
@@ -24,7 +24,6 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import codecs
|
||||
import copy
|
||||
import jinja2
|
||||
import logging
|
||||
import os
|
||||
@@ -34,9 +33,6 @@ from distutils.dir_util import copy_tree
|
||||
from jinja2 import Environment, FileSystemLoader, ChoiceLoader, PrefixLoader
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
|
||||
import sigal.image
|
||||
import sigal.video
|
||||
from .settings import get_thumb, get_orig
|
||||
from .pkgmeta import __url__ as sigal_link
|
||||
|
||||
THEMES_PATH = os.path.normpath(os.path.join(
|
||||
@@ -46,17 +42,14 @@ THEMES_PATH = os.path.normpath(os.path.join(
|
||||
class Writer(object):
|
||||
"""Generate html pages for each directory of images."""
|
||||
|
||||
def __init__(self, settings, output_dir, theme=None,
|
||||
template_file="index.html", output_file="index.html"):
|
||||
self.settings = settings
|
||||
self.output_dir = os.path.abspath(output_dir)
|
||||
self.theme = theme or settings['theme']
|
||||
self.template_file = template_file
|
||||
self.output_file = output_file
|
||||
self.logger = logging.getLogger(__name__)
|
||||
template_file = 'index.html'
|
||||
|
||||
# optionally add index.html to the URLs
|
||||
self.url_ext = self.output_file if settings['index_in_url'] else ''
|
||||
def __init__(self, settings, theme=None, index_title=''):
|
||||
self.settings = settings
|
||||
self.output_dir = settings['destination']
|
||||
self.theme = theme or settings['theme']
|
||||
self.index_title = index_title
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# search the theme in sigal/theme if the given one does not exists
|
||||
if not os.path.exists(self.theme):
|
||||
@@ -66,8 +59,8 @@ class Writer(object):
|
||||
|
||||
self.logger.info("Theme : %s", self.theme)
|
||||
theme_relpath = os.path.join(self.theme, 'templates')
|
||||
default_loader = FileSystemLoader(
|
||||
os.path.join(THEMES_PATH, 'default', 'templates'))
|
||||
default_loader = FileSystemLoader(os.path.join(THEMES_PATH, 'default',
|
||||
'templates'))
|
||||
|
||||
# setup jinja env
|
||||
env_options = {'trim_blocks': True}
|
||||
@@ -92,121 +85,28 @@ class Writer(object):
|
||||
self.logger.error('The index.html template was not found.')
|
||||
sys.exit(1)
|
||||
|
||||
self.copy_assets()
|
||||
|
||||
self.ctx = {
|
||||
'sigal_link': sigal_link,
|
||||
'theme': {'name': os.path.basename(self.theme)},
|
||||
'medias': [],
|
||||
'albums': [],
|
||||
'breadcrumb': ''
|
||||
}
|
||||
|
||||
def copy_assets(self):
|
||||
"""Copy the theme files in the output dir."""
|
||||
|
||||
# Copy the theme files in the output dir
|
||||
self.theme_path = os.path.join(self.output_dir, 'static')
|
||||
copy_tree(os.path.join(self.theme, 'static'), self.theme_path)
|
||||
|
||||
def get_breadcrumb(self, paths, relpath):
|
||||
"""Paths to upper directories (with titles and links)."""
|
||||
|
||||
tmp_path = relpath
|
||||
breadcrumb = [((self.url_ext or '.'), paths[tmp_path]['title'])]
|
||||
|
||||
while True:
|
||||
tmp_path = os.path.normpath(os.path.join(tmp_path, '..'))
|
||||
if tmp_path == '.':
|
||||
break
|
||||
|
||||
url = os.path.relpath(tmp_path, relpath) + '/' + self.url_ext
|
||||
breadcrumb.append((url, paths[tmp_path]['title']))
|
||||
|
||||
return reversed(breadcrumb)
|
||||
|
||||
def generate_context(self, paths, relpath):
|
||||
def generate_context(self, album):
|
||||
"""Generate the context dict for the given path."""
|
||||
|
||||
path = os.path.normpath(os.path.join(self.output_dir, relpath))
|
||||
index_url = os.path.relpath(self.output_dir, path) + '/' + self.url_ext
|
||||
self.logger.info("Output path : %s", path)
|
||||
|
||||
ctx = copy.deepcopy(self.ctx)
|
||||
ctx.update({
|
||||
self.logger.info("Output album : %s", album.dst_path)
|
||||
return {
|
||||
'album': album,
|
||||
'index_title': self.index_title,
|
||||
'settings': self.settings,
|
||||
'index_url': index_url,
|
||||
'index_title': paths['.']['title']
|
||||
})
|
||||
ctx['theme']['url'] = os.path.relpath(self.theme_path, path)
|
||||
'sigal_link': sigal_link,
|
||||
'theme': {'name': os.path.basename(self.theme),
|
||||
'url': os.path.relpath(self.theme_path, album.dst_path)},
|
||||
}
|
||||
|
||||
if relpath != '.':
|
||||
ctx['breadcrumb'] = self.get_breadcrumb(paths, relpath)
|
||||
|
||||
if len(paths[relpath]['medias']) > 0 and self.settings['zip_gallery']:
|
||||
ctx['zip_gallery'] = self.settings['zip_gallery']
|
||||
|
||||
for i in paths[relpath]['medias']:
|
||||
media_ctx = {}
|
||||
base, ext = os.path.splitext(i)
|
||||
if ext in self.settings['img_ext_list']:
|
||||
media_ctx['type'] = 'img'
|
||||
media_ctx['file'] = i
|
||||
|
||||
file_path = os.path.join(path, i)
|
||||
raw, simple = sigal.image.get_exif_tags(file_path)
|
||||
|
||||
if raw is not None:
|
||||
media_ctx['raw_exif'] = raw
|
||||
if simple is not None:
|
||||
media_ctx['exif'] = simple
|
||||
else:
|
||||
media_ctx['type'] = 'vid'
|
||||
media_ctx['file'] = base + '.webm'
|
||||
media_ctx['thumb'] = get_thumb(self.settings, i)
|
||||
if self.settings['keep_orig']:
|
||||
media_ctx['big'] = get_orig(self.settings, i)
|
||||
ctx['medias'].append(media_ctx)
|
||||
|
||||
for d in paths[relpath]['subdir']:
|
||||
dpath = os.path.normpath(os.path.join(relpath, d))
|
||||
alb_thumb = paths[dpath]['thumbnail']
|
||||
thumb_name = get_thumb(self.settings, alb_thumb)
|
||||
thumb_path = os.path.join(self.output_dir, dpath, thumb_name)
|
||||
self.logger.debug("Thumbnail path : %s", thumb_path)
|
||||
|
||||
# generate the thumbnail if it is missing (if
|
||||
# settings['make_thumbs'] is False)
|
||||
if not os.path.isfile(thumb_path):
|
||||
source = os.path.join(self.output_dir, dpath, alb_thumb)
|
||||
ext = os.path.splitext(source)[1]
|
||||
self.logger.debug("Generating thumbnail for %s", source)
|
||||
|
||||
if ext in self.settings['img_ext_list']:
|
||||
generator = sigal.image.generate_thumbnail
|
||||
elif ext in self.settings['vid_ext_list']:
|
||||
generator = sigal.video.generate_thumbnail
|
||||
else:
|
||||
generator = None
|
||||
self.logger.error('Unsupported file type for %s', source)
|
||||
|
||||
if generator:
|
||||
generator(source, thumb_path, self.settings['thumb_size'],
|
||||
fit=self.settings['thumb_fit'])
|
||||
|
||||
ctx['albums'].append({
|
||||
'url': d + '/' + self.url_ext,
|
||||
'title': paths[dpath]['title'],
|
||||
'thumb': os.path.join(d, thumb_name)
|
||||
})
|
||||
|
||||
return ctx
|
||||
|
||||
def write(self, paths, relpath):
|
||||
def write(self, album):
|
||||
"""Generate the HTML page and save it."""
|
||||
|
||||
ctx = self.generate_context(paths, relpath)
|
||||
page = self.template.render(paths[relpath], **ctx)
|
||||
page = self.template.render(**self.generate_context(album))
|
||||
output_file = os.path.join(album.dst_path, album.output_file)
|
||||
|
||||
output_file = os.path.join(self.output_dir, relpath, self.output_file)
|
||||
with codecs.open(output_file, 'w', 'utf-8') as f:
|
||||
f.write(page)
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from sigal.gallery import Gallery, PathsDb, get_metadata
|
||||
from os.path import join
|
||||
from sigal.gallery import Album, Media, Image, Video, Gallery
|
||||
from sigal.settings import read_settings
|
||||
|
||||
CURRENT_DIR = os.path.dirname(__file__)
|
||||
@@ -12,23 +13,31 @@ SAMPLE_DIR = os.path.join(CURRENT_DIR, 'sample')
|
||||
REF = {
|
||||
'dir1': {
|
||||
'title': 'An example gallery',
|
||||
'thumbnail': 'test1/11.jpg',
|
||||
'name': 'dir1',
|
||||
'thumbnail': 'dir1/test1/thumbnails/11.tn.jpg',
|
||||
'subdirs': ['test1', 'test2'],
|
||||
'medias': [],
|
||||
},
|
||||
'dir1/test1': {
|
||||
'title': 'An example sub-category',
|
||||
'thumbnail': '11.jpg',
|
||||
'name': 'test1',
|
||||
'thumbnail': 'test1/thumbnails/11.tn.jpg',
|
||||
'subdirs': [],
|
||||
'medias': ['11.jpg', 'archlinux-kiss-1024x640.png',
|
||||
'flickr_jerquiaga_2394751088_cc-by-nc.jpg'],
|
||||
},
|
||||
'dir1/test2': {
|
||||
'title': 'Test2',
|
||||
'thumbnail': '21.jpg',
|
||||
'name': 'test2',
|
||||
'thumbnail': 'test2/thumbnails/21.tn.jpg',
|
||||
'subdirs': [],
|
||||
'medias': ['21.jpg', '22.jpg'],
|
||||
},
|
||||
'dir2': {
|
||||
'title': 'Another example gallery with a very long name',
|
||||
'thumbnail': 'm57_the_ring_nebula-587px.jpg',
|
||||
'name': 'dir2',
|
||||
'thumbnail': 'dir2/thumbnails/m57_the_ring_nebula-587px.tn.jpg',
|
||||
'subdirs': [],
|
||||
'medias': ['exo20101028-b-full.jpg',
|
||||
'm57_the_ring_nebula-587px.jpg',
|
||||
'Hubble ultra deep field.jpg',
|
||||
@@ -36,86 +45,107 @@ REF = {
|
||||
},
|
||||
u'accentué': {
|
||||
'title': u'Accentué',
|
||||
'thumbnail': u'hélicoïde.jpg',
|
||||
'name': u'accentué',
|
||||
'thumbnail': u'accentué/thumbnails/hélicoïde.tn.jpg',
|
||||
'subdirs': [],
|
||||
'medias': [u'hélicoïde.jpg', 'superdupont_source_wikipedia_en.jpg'],
|
||||
},
|
||||
'video': {
|
||||
'title': 'Video',
|
||||
'thumbnail': 'stallman software-freedom-day-low.ogv',
|
||||
'name': 'video',
|
||||
'thumbnail': 'video/thumbnails/stallman software-freedom-day-low.tn.jpg',
|
||||
'subdirs': [],
|
||||
'medias': ['stallman software-freedom-day-low.ogv']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def paths():
|
||||
"""Read the sample config file and build the PathsDb object."""
|
||||
|
||||
default_conf = os.path.join(SAMPLE_DIR, 'sigal.conf.py')
|
||||
settings = read_settings(default_conf)
|
||||
return PathsDb(os.path.join(SAMPLE_DIR, 'pictures'),
|
||||
settings['img_ext_list'], settings['vid_ext_list'])
|
||||
def settings():
|
||||
"""Read the sample config file."""
|
||||
return read_settings(os.path.join(SAMPLE_DIR, 'sigal.conf.py'))
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def db(paths):
|
||||
paths.build()
|
||||
return paths.db
|
||||
def test_media(settings):
|
||||
m = Media('11.jpg', 'dir1/test1', settings)
|
||||
path = join('dir1', 'test1')
|
||||
file_path = join(path, '11.jpg')
|
||||
thumb = join('thumbnails', '11.tn.jpg')
|
||||
|
||||
assert m.filename == '11.jpg'
|
||||
assert m.file_path == file_path
|
||||
assert m.src_path == join(settings['source'], file_path)
|
||||
assert m.dst_path == join(settings['destination'], file_path)
|
||||
assert m.thumb_name == thumb
|
||||
assert m.thumb_path == join(settings['destination'], path, thumb)
|
||||
|
||||
assert repr(m) == "<Media>('{}')".format(file_path)
|
||||
assert str(m) == file_path
|
||||
|
||||
|
||||
def test_filelist(db):
|
||||
assert set(db.keys()) == set([
|
||||
'paths_list', 'skipped_dir', '.', 'dir1', 'dir2', 'dir1/test1',
|
||||
'dir1/test2', u'accentué', 'video'])
|
||||
def test_media_orig(settings):
|
||||
settings['keep_orig'] = False
|
||||
m = Media('11.jpg', 'dir1/test1', settings)
|
||||
assert m.big is None
|
||||
|
||||
assert set(db['paths_list']) == set([
|
||||
'.', 'dir1', 'dir1/test1', 'dir1/test2', 'dir2', u'accentué', 'video'])
|
||||
|
||||
assert set(db['skipped_dir']) == set(['empty', 'dir1/empty'])
|
||||
assert db['.']['medias'] == []
|
||||
assert set(db['.']['subdir']) == set([u'accentué', 'dir1', 'dir2',
|
||||
'video'])
|
||||
settings['keep_orig'] = True
|
||||
m = Media('11.jpg', 'dir1/test1', settings)
|
||||
assert m.big == 'original/11.jpg'
|
||||
|
||||
|
||||
def test_title(db):
|
||||
for p in REF.keys():
|
||||
assert db[p]['title'] == REF[p]['title']
|
||||
def test_image(settings, tmpdir):
|
||||
settings['destination'] = str(tmpdir)
|
||||
m = Image('11.jpg', 'dir1/test1', settings)
|
||||
assert m.exif['datetime'] == u'Sunday, 22. January 2006'
|
||||
|
||||
os.makedirs(join(settings['destination'], 'dir1', 'test1', 'thumbnails'))
|
||||
assert m.thumbnail == join('thumbnails', '11.tn.jpg')
|
||||
assert os.path.isfile(m.thumb_path)
|
||||
|
||||
|
||||
def test_thumbnail(db):
|
||||
for p in REF.keys():
|
||||
assert db[p]['thumbnail'] == REF[p]['thumbnail']
|
||||
def test_video(settings, tmpdir):
|
||||
settings['destination'] = str(tmpdir)
|
||||
m = Video('stallman software-freedom-day-low.ogv', 'video', settings)
|
||||
file_path = join('video', 'stallman software-freedom-day-low.webm')
|
||||
assert m.file_path == file_path
|
||||
assert m.dst_path == join(settings['destination'], file_path)
|
||||
|
||||
os.makedirs(join(settings['destination'], 'video', 'thumbnails'))
|
||||
assert m.thumbnail == join('thumbnails',
|
||||
'stallman software-freedom-day-low.tn.jpg')
|
||||
assert os.path.isfile(m.thumb_path)
|
||||
|
||||
|
||||
def test_medialist(db):
|
||||
for p in REF.keys():
|
||||
assert set(db[p]['medias']) == set(REF[p]['medias'])
|
||||
@pytest.mark.parametrize("path,album", REF.items())
|
||||
def test_album(path, album, settings, tmpdir):
|
||||
gal = Gallery(settings, ncpu=1)
|
||||
a = Album(path, settings, album['subdirs'], album['medias'], gal)
|
||||
|
||||
assert a.title == album['title']
|
||||
assert a.name == album['name']
|
||||
assert a.subdirs == album['subdirs']
|
||||
assert a.thumbnail == album['thumbnail']
|
||||
assert [m.filename for m in a.medias] == album['medias']
|
||||
assert len(a) == len(album['medias'])
|
||||
|
||||
|
||||
def test_get_subdir(paths):
|
||||
assert set(paths.get_subdirs('dir1/test1')) == set()
|
||||
assert set(paths.get_subdirs('dir1')) == set(['dir1/test1', 'dir1/test2'])
|
||||
assert set(paths.get_subdirs('.')) == set([
|
||||
'dir1', 'dir2', 'dir1/test1', 'dir1/test2', u'accentué', 'video'])
|
||||
def test_album_medias(settings, tmpdir):
|
||||
gal = Gallery(settings, ncpu=1)
|
||||
|
||||
album = REF['dir1/test1']
|
||||
a = Album('dir1/test1', settings, album['subdirs'], album['medias'], gal)
|
||||
assert list(im.filename for im in a.images) == album['medias']
|
||||
assert list(a.videos) == []
|
||||
|
||||
album = REF['video']
|
||||
a = Album('video', settings, album['subdirs'], album['medias'], gal)
|
||||
assert list(im.filename for im in a.videos) == album['medias']
|
||||
assert list(a.images) == []
|
||||
|
||||
|
||||
def test_get_metadata():
|
||||
"Test the get_metadata function."
|
||||
|
||||
m = get_metadata(os.path.join(SAMPLE_DIR, 'pictures', 'dir1'))
|
||||
assert m['title'] == REF['dir1']['title']
|
||||
assert m['thumbnail'] == ''
|
||||
|
||||
m = get_metadata(os.path.join(SAMPLE_DIR, 'pictures', 'dir2'))
|
||||
assert m['title'] == REF['dir2']['title']
|
||||
assert m['thumbnail'] == REF['dir2']['thumbnail']
|
||||
|
||||
|
||||
def test_gallery(tmpdir):
|
||||
def test_gallery(settings, tmpdir):
|
||||
"Test the Gallery class."
|
||||
|
||||
default_conf = os.path.join(SAMPLE_DIR, 'sigal.conf.py')
|
||||
settings = read_settings(default_conf)
|
||||
settings['destination'] = str(tmpdir)
|
||||
gal = Gallery(settings, ncpu=1)
|
||||
gal.build()
|
||||
@@ -127,4 +157,3 @@ def test_gallery(tmpdir):
|
||||
html = f.read()
|
||||
|
||||
assert '<title>Sigal test gallery</title>' in html
|
||||
|
||||
|
||||
Reference in New Issue
Block a user