From b225bf676d626e3a45505e0c53e361a0d55490d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Wed, 21 Feb 2018 18:13:08 -0500 Subject: [PATCH 1/4] first prototype of a stream plugin --- sigal/plugins/stream.py | 94 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 sigal/plugins/stream.py diff --git a/sigal/plugins/stream.py b/sigal/plugins/stream.py new file mode 100644 index 00000000..fca950e1 --- /dev/null +++ b/sigal/plugins/stream.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- + +"""Plugin which adds a "stream page" listing all images. + +This plugin is similar to the feeds plugin in that it allows listing +images in (reverse) chronological order, to show the most recent +ones. This allows users to browse the gallery as a "flat" list of +images, a bit like you would on sites like Flickr or Instagram. + +Settigns: + +- ``stream_page``, see below + +Example:: + + stream_page = { 'filename': 'stream.html', 'nb_items': 10, 'title': 'Stream' } + +In the above example, a 10-item page will be created at +/stream.html. If any entry is missing, a default will be used as +defined above (except `nb_items` which defaults to no limit. + +.. todo:: What happens if an album with the same name exists? + +""" + +import copy +import logging +import os.path + +from sigal import signals +from sigal.gallery import Album +from sigal.writer import Writer + +logger = logging.getLogger(__name__) + + +def generate_stream(gallery): + settings = gallery.settings.get('stream_page', {}) + albums = gallery.albums.values() + if len(albums) < 1: + logging.debug('no albums, empty stream') + return + # fake meta-album to regroup them all + feed_album = Album('.', gallery.settings, [], [], gallery) + feed_album.output_file = settings.get('filename', 'stream.html') + for album in albums: + merge(feed_album, album) + feed_album = flatten(feed_album) + nb_medias = len(feed_album.medias) + nb_items = settings.get('nb_items', 0) + nb_items = min(nb_items, nb_medias) if nb_items > 0 else nb_medias + del feed_album.medias[nb_items:] + # copy-pasted from feeds.py + feed_album.medias.sort(key=lambda m: m.date, reverse=True) + writer = Writer(gallery.settings, + index_title=settings.get('title', 'Stream')) + # copy-pasted from Writer.write() + path = os.path.join(feed_album.dst_path, feed_album.output_file) + logger.info('Generating streams page: %s', path) + writer.write(feed_album) + + +def flatten(self): + '''flatten album hierarchy in a single album + + this modifies the album: make sure to call `.copy()` if you need + the original album untouched. + ''' + ret = copy.copy(self) + for album in ret.albums: + ret.merge(album) + album.flatten() + return ret + + +def merge(self, album): + '''merge in another album into this album + + ..todo:: could be reimplemented as __add__()? but then you'd have + consumers expecting `all operators`_ to be supported? + + .. _all operators: https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types + ''' + for media in album.medias: + media = copy.copy(media) + media.thumb_name = os.path.join(album.path, media.thumb_name) + media.filename = os.path.join(album.path, media.filename) + self.medias.append(media) + # XXX; should be factored out? copied from Album.__init__() + self.medias_count[media.type] += 1 + + +def register(settings): + signals.gallery_build.connect(generate_stream) From 1cdeb323a67253fad59219de3df65453d0e6b020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Tue, 27 Feb 2018 11:45:20 -0500 Subject: [PATCH 2/4] move functions into Album class to allow reuse --- sigal/gallery.py | 29 +++++++++++++++++++++++++++++ sigal/plugins/stream.py | 35 ++--------------------------------- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/sigal/gallery.py b/sigal/gallery.py index bb233bf7..83370e43 100644 --- a/sigal/gallery.py +++ b/sigal/gallery.py @@ -27,6 +27,7 @@ from __future__ import absolute_import, print_function +from copy import copy as shallowcopy import fnmatch import logging import multiprocessing @@ -527,6 +528,34 @@ def zip(self): self.logger.debug('Created ZIP archive %s', archive_path) return zip_gallery + def flatten(self): + '''flatten album hierarchy in a single album + + this modifies the album: make sure to call `.copy()` if you need + the original album untouched. + ''' + ret = shallowcopy(self) + for album in ret.albums: + ret.merge(album) + album.flatten() + return ret + + def merge(self, album): + '''merge in another album into this album + + ..todo:: could be reimplemented as __add__()? but then you'd have + consumers expecting `all operators`_ to be supported? + + .. _all operators: https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types + ''' + for media in album.medias: + media = shallowcopy(media) + media.thumb_name = os.path.join(album.path, media.thumb_name) + media.filename = os.path.join(album.path, media.filename) + self.medias.append(media) + # XXX; should be factored out? copied from Album.__init__() + self.medias_count[media.type] += 1 + class Gallery(object): diff --git a/sigal/plugins/stream.py b/sigal/plugins/stream.py index fca950e1..97e79171 100644 --- a/sigal/plugins/stream.py +++ b/sigal/plugins/stream.py @@ -23,7 +23,6 @@ """ -import copy import logging import os.path @@ -44,8 +43,8 @@ def generate_stream(gallery): feed_album = Album('.', gallery.settings, [], [], gallery) feed_album.output_file = settings.get('filename', 'stream.html') for album in albums: - merge(feed_album, album) - feed_album = flatten(feed_album) + feed_album.merge(album) + feed_album = feed_album.flatten() nb_medias = len(feed_album.medias) nb_items = settings.get('nb_items', 0) nb_items = min(nb_items, nb_medias) if nb_items > 0 else nb_medias @@ -60,35 +59,5 @@ def generate_stream(gallery): writer.write(feed_album) -def flatten(self): - '''flatten album hierarchy in a single album - - this modifies the album: make sure to call `.copy()` if you need - the original album untouched. - ''' - ret = copy.copy(self) - for album in ret.albums: - ret.merge(album) - album.flatten() - return ret - - -def merge(self, album): - '''merge in another album into this album - - ..todo:: could be reimplemented as __add__()? but then you'd have - consumers expecting `all operators`_ to be supported? - - .. _all operators: https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types - ''' - for media in album.medias: - media = copy.copy(media) - media.thumb_name = os.path.join(album.path, media.thumb_name) - media.filename = os.path.join(album.path, media.filename) - self.medias.append(media) - # XXX; should be factored out? copied from Album.__init__() - self.medias_count[media.type] += 1 - - def register(settings): signals.gallery_build.connect(generate_stream) From 648b431f0d1ae3c2070d48ce561d7a417f0802ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Tue, 27 Feb 2018 11:45:50 -0500 Subject: [PATCH 3/4] also add a flatten function to galleries this allows us to move more code into the main classes and test those in one shot --- sigal/gallery.py | 7 +++++++ sigal/plugins/stream.py | 8 ++------ tests/test_gallery.py | 11 +++++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/sigal/gallery.py b/sigal/gallery.py index 83370e43..c7dd37de 100644 --- a/sigal/gallery.py +++ b/sigal/gallery.py @@ -761,6 +761,13 @@ def process_dir(self, album, force=False): yield (f.type, f.path, f.filename, f.src_path, album.dst_path, self.settings) + def flatten(self, path='.'): + '''flatten all albums into a single one''' + flat_album = Album(path, self.settings, [], [], self) + for album in self.albums.values(): + flat_album.merge(album) + return flat_album.flatten() + def process_file(args): # args => ftype, path, filename, src_path, dst_path, settings diff --git a/sigal/plugins/stream.py b/sigal/plugins/stream.py index 97e79171..e86519f7 100644 --- a/sigal/plugins/stream.py +++ b/sigal/plugins/stream.py @@ -27,7 +27,6 @@ import os.path from sigal import signals -from sigal.gallery import Album from sigal.writer import Writer logger = logging.getLogger(__name__) @@ -39,12 +38,9 @@ def generate_stream(gallery): if len(albums) < 1: logging.debug('no albums, empty stream') return - # fake meta-album to regroup them all - feed_album = Album('.', gallery.settings, [], [], gallery) + # fake meta-album to regroup the top-level ones + feed_album = gallery.flatten() feed_album.output_file = settings.get('filename', 'stream.html') - for album in albums: - feed_album.merge(album) - feed_album = feed_album.flatten() nb_medias = len(feed_album.medias) nb_items = settings.get('nb_items', 0) nb_items = min(nb_items, nb_medias) if nb_items > 0 else nb_medias diff --git a/tests/test_gallery.py b/tests/test_gallery.py index 22bbe199..0b92f976 100644 --- a/tests/test_gallery.py +++ b/tests/test_gallery.py @@ -245,6 +245,17 @@ def test_medias_sort(settings): 'archlinux-kiss-1024x640.png', '21.jpg', '22.jpg'] +def test_flatten_merge(settings): + gal = Gallery(settings, ncpu=1) + flat_album = gal.flatten() + # expected = sum([len(album['medias']) for album in REF.values()]) + # XXX: not sure why, but the above Gallery does not skip .nomedia + # even if the plugin is present + expected = 25 + assert expected == len(flat_album.medias), 'right number of items' + assert '/' not in ''.join(gal.albums['dir1'].medias), 'original paths preserved' + + def test_gallery(settings, tmpdir): "Test the Gallery class." From d868fda43c28cc5b0d11935cf52615f48b40bac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= Date: Tue, 27 Feb 2018 11:56:22 -0500 Subject: [PATCH 4/4] basic tests for the streams plugin --- tests/test_plugins.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_plugins.py b/tests/test_plugins.py index c3d8ad8c..045e53c3 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -29,3 +29,27 @@ def test_plugins(settings, tmpdir, disconnect_signals): for file in files: assert "ignore" not in file + + +def test_stream(settings, tmpdir): + settings['destination'] = str(tmpdir) + if "sigal.plugins.stream" not in settings["plugins"]: + settings['plugins'] += ["sigal.plugins.stream"] + title = 'Stream unit test' + nb_items = 3 + filename = 'stream-test.html' + settings['stream_page'] = {'filename': filename, + 'nb_items': nb_items, + 'title': title} + + init_plugins(settings) + gal = Gallery(settings) + gal.build() + + out_html = os.path.join(settings['destination'], filename) + assert os.path.isfile(out_html) + + with open(out_html) as stream_page: + content = stream_page.read() + assert title in content + assert content.count('data-big') == nb_items