diff --git a/sigal/gallery.py b/sigal/gallery.py index ce749f6..55aefa1 100644 --- a/sigal/gallery.py +++ b/sigal/gallery.py @@ -34,7 +34,8 @@ from clint.textui import progress, colored from os.path import join from PIL import Image as PILImage -from .image import generate_image, generate_thumbnail +import sigal.image +import sigal.video from .settings import get_thumb from .writer import Writer @@ -49,8 +50,9 @@ class PathsDb(object): """ - def __init__(self, path, ext_list): - self.ext_list = ext_list + 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.logger = logging.getLogger(__name__) # basepath must to be a unicode string so that os.walk will return @@ -93,22 +95,26 @@ class PathsDb(object): self.db['paths_list'].append(relpath) self.db[relpath] = { 'img': [f for f in filenames - if os.path.splitext(f)[1] in self.ext_list], + if os.path.splitext(f)[1] in self.img_ext_list], + 'vid': [f for f in filenames + if os.path.splitext(f)[1] in self.vid_ext_list], 'subdir': dirnames } self.db[relpath].update(get_metadata(path)) - path_im = [path for path in self.db['paths_list'] - if self.db[path]['img'] and path != '.'] - path_noim = [path for path in self.db['paths_list'] - if not self.db[path]['img'] and path != '.'] + path_media = [path for path in self.db['paths_list'] + if (self.db[path]['img'] or self.db[path]['vid']) and + path != '.'] + path_nomedia = [path for path in self.db['paths_list'] if not + (self.db[path]['img'] or self.db[path]['vid']) and path != + '.'] # dir with images: check the thumbnail, and find it if necessary - for path in path_im: + for path in path_media: self.check_thumbnail(path) # dir without images, start with the deepest ones - for path in reversed(sorted(path_noim, key=lambda x: x.count('/'))): + for path in reversed(sorted(path_nomedia, key=lambda x: x.count('/'))): for subdir in self.get_subdirs(path): # use the thumbnail of their sub-directories if self.db[subdir].get('thumbnail', ''): @@ -162,7 +168,8 @@ class Gallery(object): theme=theme) self.paths = PathsDb(self.settings['source'], - self.settings['ext_list']) + self.settings['img_ext_list'], + self.settings['vid_ext_list']) self.paths.build() self.db = self.paths.db @@ -181,20 +188,22 @@ class Gallery(object): for path in reversed(self.db['paths_list']): imglist = [os.path.normpath(join(self.settings['source'], path, f)) for f in self.db[path]['img']] + vidlist = [os.path.normpath(join(self.settings['source'], path, f)) + for f in self.db[path]['vid']] # output dir for the current path - img_out = os.path.normpath(join(self.settings['destination'], + outpath = os.path.normpath(join(self.settings['destination'], path)) - check_or_create_dir(img_out) + check_or_create_dir(outpath) - if len(imglist) != 0: - self.process_dir(imglist, img_out, path, + if len(imglist) != 0 or len(vidlist) != 0: + self.process_dir(imglist, vidlist, outpath, path, label_width=label_width) if self.settings['write_html']: self.writer.write(self.db, path) - def process_dir(self, imglist, outpath, dirname, label_width=20): + def process_dir(self, imglist, vidlist, outpath, dirname, label_width=20): """Process a list of images in a directory.""" # Create thumbnails directory and optionally the one for original img @@ -203,28 +212,36 @@ class Gallery(object): if self.settings['keep_orig']: check_or_create_dir(join(outpath, self.settings['orig_dir'])) + media_files = imglist + vidlist + # use progressbar if level is > INFO if self.logger.getEffectiveLevel() > 20: label = colored.green(dirname.ljust(label_width)) - img_iterator = progress.bar(imglist, label=label) + media_iterator = progress.bar(media_files, label=label) else: - img_iterator = iter(imglist) + media_iterator = iter(media_files) self.logger.info("") - self.logger.info(":: Processing '%s' [%i images]", - colored.green(dirname), len(imglist)) + self.logger.info(":: Processing '%s' [%i img/vid]", + colored.green(dirname), len(media_files)) self.logger.info("") try: # loop on images - for f in img_iterator: + for f in media_iterator: filename = os.path.split(f)[1] - outname = join(outpath, filename) + if f in imglist: + outname = join(outpath, filename) + else: + outname = ''.join([os.path.splitext(filename)[0], '.webm']) if os.path.isfile(outname) and not self.force: self.logger.info("%s exists - skipping", filename) else: self.logger.info(filename) - process_image(f, outpath, self.settings) + if f in imglist: + process_image(f, outpath, self.settings) + else: + process_video(f, outpath, self.settings) except KeyboardInterrupt: sys.exit('Interrupted') @@ -247,15 +264,31 @@ def process_image(filepath, outpath, settings): if settings['keep_orig']: shutil.copy(filepath, join(outpath, settings['orig_dir'], filename)) - generate_image(filepath, outname, settings['img_size'], None, + sigal.image.generate_image(filepath, outname, settings['img_size'], None, options=options, copyright_text=settings['copyright'], method=settings['img_processor']) if settings['make_thumbs']: thumb_name = join(outpath, get_thumb(settings, filename)) - generate_thumbnail(outname, thumb_name, settings['thumb_size'], None, + sigal.image.generate_thumbnail(outname, thumb_name, settings['thumb_size'], None, fit=settings['thumb_fit'], options=options) +def process_video(filepath, outpath, settings): + """Process one image: resize, create thumbnail.""" + + filename = os.path.split(filepath)[1] + base, ext = os.path.splitext(filename) + outname = join(outpath, base + '.webm') + + if settings['keep_orig']: + shutil.copy(filepath, join(outpath, settings['orig_dir'], filename)) + + sigal.video.generate_video(filepath, outname) + + if settings['make_thumbs']: + thumb_name = join(outpath, get_thumb(settings, base + '.jpg')) + sigal.video.generate_thumbnail(outname, thumb_name) + def get_metadata(path): """ Get album metadata from DESCRIPTION_FILE: diff --git a/sigal/settings.py b/sigal/settings.py index 583f15c..a71bf9d 100644 --- a/sigal/settings.py +++ b/sigal/settings.py @@ -38,7 +38,8 @@ _DEFAULT_CONFIG = { 'orig_dir': 'original', 'jpg_options': {'quality': 85, 'optimize': True, 'progressive': True}, 'copyright': '', - 'ext_list': ['.jpg', '.jpeg', '.JPG', '.JPEG', '.png'], + 'img_ext_list': ['.jpg', '.jpeg', '.JPG', '.JPEG', '.png'], + 'vid_ext_list': ['.MOV', '.mov', '.avi', '.mp4', '.webm', '.ogv'], 'theme': 'colorbox', 'write_html': True, 'index_in_url': False, diff --git a/sigal/themes/galleria/templates/index.html b/sigal/themes/galleria/templates/index.html index 532a646..700040d 100644 --- a/sigal/themes/galleria/templates/index.html +++ b/sigal/themes/galleria/templates/index.html @@ -13,7 +13,7 @@ @@ -57,7 +57,7 @@ {% endif %} - {% if images %} + {% if image or videos %} {% endif %} @@ -82,7 +86,7 @@ - {% if images %} + {% if images or videos %} diff --git a/sigal/video.py b/sigal/video.py new file mode 100644 index 0000000..1ff35fb --- /dev/null +++ b/sigal/video.py @@ -0,0 +1,44 @@ +# -*- coding:utf-8 -*- + +# Copyright (c) 2013 - Christophe-Marie Duquesne + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from __future__ import with_statement +import subprocess + +def generate_video(source, outname, options=['-vf', 'scale=800:trunc(ow/a/2)*2']): + # http://stackoverflow.com/questions/8218363/maintaining-ffmpeg-aspect-ratio + """Video processor + + :param source: path to an image + :param outname: name of the generated video + :param options: array of options passed to ffmpeg + + """ + with open("/dev/null") as devnull: + subprocess.call(['ffmpeg', '-i', source, '-y'] + options + + [outname], stderr=devnull) + +def generate_thumbnail(source, outname, options=['-vf', 'scale=80:trunc(ow/a/2)*2']): + # http://stackoverflow.com/questions/8218363/maintaining-ffmpeg-aspect-ratio + "Create a thumbnail image" + with open("/dev/null") as devnull: + subprocess.call(['ffmpeg', '-i', source, '-an', '-r', '1', + '-vframes', '1', '-y'] + options + [outname], stderr=devnull) diff --git a/sigal/writer.py b/sigal/writer.py index 60d09ed..c7c6d5f 100644 --- a/sigal/writer.py +++ b/sigal/writer.py @@ -90,6 +90,7 @@ class Writer(object): 'sigal_link': sigal_link, 'theme': {'name': os.path.basename(self.theme)}, 'images': [], + 'videos': [], 'albums': [], 'breadcumb': '' } @@ -141,6 +142,14 @@ class Writer(object): img_ctx['big'] = get_orig(self.settings, i) ctx['images'].append(img_ctx) + for i in paths[relpath]['vid']: + base, ext = os.path.splitext(i) + vid_ctx = {'file': base + '.webm', + 'thumb': get_thumb(self.settings, base + '.jpg')} + if self.settings['keep_orig']: + vid_ctx['big'] = get_orig(self.settings, i) + ctx['videos'].append(vid_ctx) + for d in paths[relpath]['subdir']: dpath = os.path.normpath(os.path.join(relpath, d)) alb_thumb = paths[dpath]['thumbnail'] diff --git a/tests/sample/pictures/video/stallman-software-freedom-day-low.ogv b/tests/sample/pictures/video/stallman-software-freedom-day-low.ogv new file mode 100644 index 0000000..0a88cb4 Binary files /dev/null and b/tests/sample/pictures/video/stallman-software-freedom-day-low.ogv differ diff --git a/tests/test_gallery.py b/tests/test_gallery.py index 039c5d6..9e91f27 100644 --- a/tests/test_gallery.py +++ b/tests/test_gallery.py @@ -37,6 +37,12 @@ REF = { 'title': u'Accentué', 'thumbnail': u'hélicoïde.jpg', 'img': [u'hélicoïde.jpg', 'superdupont_source_wikipedia_en.jpg'] + }, + 'video': { + 'title': 'Video', + 'thumbnail': '', + 'img': [], + 'video': [] } } @@ -47,7 +53,8 @@ def paths(): default_conf = os.path.join(SAMPLE_DIR, 'sigal.conf.py') settings = read_settings(default_conf) - return PathsDb(os.path.join(SAMPLE_DIR, 'pictures'), settings['ext_list']) + return PathsDb(os.path.join(SAMPLE_DIR, 'pictures'), + settings['img_ext_list'], settings['vid_ext_list']) @pytest.fixture(scope='module') @@ -58,14 +65,15 @@ def db(paths): def test_filelist(db): assert set(db.keys()) == set(['paths_list', 'skipped_dir', '.', - 'dir1', 'dir2', 'dir1/test1', 'dir1/test2', u'accentué']) + 'dir1', 'dir2', 'dir1/test1', 'dir1/test2', u'accentué', 'video']) assert set(db['paths_list']) == set(['.', 'dir1', 'dir1/test1', - 'dir1/test2', 'dir2', u'accentué']) + 'dir1/test2', 'dir2', u'accentué', 'video']) assert set(db['skipped_dir']) == set(['empty', 'dir1/empty']) assert db['.']['img'] == [] - assert set(db['.']['subdir']) == set([u'accentué', 'dir1', 'dir2']) + assert set(db['.']['subdir']) == set([u'accentué', 'dir1', 'dir2', + 'video']) def test_title(db): @@ -86,7 +94,8 @@ def test_imglist(db): def test_get_subdir(paths): 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é']) + 'dir1/test2', u'accentué', + 'video']) def test_get_metadata():