sigal 2.3__py3-none-any.whl → 2.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sigal/__init__.py +2 -285
- sigal/__main__.py +312 -0
- sigal/gallery.py +188 -158
- sigal/image.py +113 -115
- sigal/log.py +11 -11
- sigal/plugins/adjust.py +4 -4
- sigal/plugins/compress_assets.py +26 -25
- sigal/plugins/copyright.py +8 -8
- sigal/plugins/encrypt/encrypt.py +7 -7
- sigal/plugins/encrypt/endec.py +2 -2
- sigal/plugins/extended_caching.py +26 -22
- sigal/plugins/feeds.py +19 -21
- sigal/plugins/media_page.py +1 -1
- sigal/plugins/nomedia.py +1 -1
- sigal/plugins/nonmedia_files.py +59 -93
- sigal/plugins/titleregexp.py +98 -0
- sigal/plugins/watermark.py +13 -13
- sigal/plugins/zip_gallery.py +17 -8
- sigal/settings.py +92 -78
- sigal/signals.py +10 -10
- sigal/templates/sigal.conf.py +18 -14
- sigal/themes/default/templates/decrypt.html +1 -0
- sigal/themes/default/templates/description.html +29 -0
- sigal/themes/default/templates/footer.html +3 -0
- sigal/themes/galleria/templates/album_items.html +4 -23
- sigal/themes/photoswipe/static/photoswipe-dynamic-caption-plugin.esm.js +414 -0
- sigal/themes/photoswipe/static/photoswipe-dynamic-caption-plugin.esm.min.js +5 -0
- sigal/themes/photoswipe/static/photoswipe-fullscreen.esm.js +129 -0
- sigal/themes/photoswipe/static/photoswipe-fullscreen.esm.min.js +8 -0
- sigal/themes/photoswipe/static/photoswipe-lightbox.esm.js +1960 -0
- sigal/themes/photoswipe/static/photoswipe-lightbox.esm.js.map +1 -0
- sigal/themes/photoswipe/static/photoswipe-lightbox.esm.min.js +5 -0
- sigal/themes/photoswipe/static/photoswipe-video-plugin.esm.js +257 -0
- sigal/themes/photoswipe/static/photoswipe-video-plugin.esm.min.js +1 -0
- sigal/themes/photoswipe/static/photoswipe.css +385 -140
- sigal/themes/photoswipe/static/photoswipe.esm.js +7081 -0
- sigal/themes/photoswipe/static/photoswipe.esm.js.map +1 -0
- sigal/themes/photoswipe/static/photoswipe.esm.min.js +5 -0
- sigal/themes/photoswipe/static/styles.css +53 -0
- sigal/themes/photoswipe/templates/album.html +69 -74
- sigal/utils.py +80 -12
- sigal/version.py +20 -4
- sigal/video.py +43 -24
- sigal/writer.py +26 -8
- {sigal-2.3.dist-info → sigal-2.5.dist-info}/LICENSE +1 -1
- {sigal-2.3.dist-info → sigal-2.5.dist-info}/METADATA +23 -30
- {sigal-2.3.dist-info → sigal-2.5.dist-info}/RECORD +50 -50
- {sigal-2.3.dist-info → sigal-2.5.dist-info}/WHEEL +1 -1
- sigal-2.5.dist-info/entry_points.txt +2 -0
- sigal/plugins/upload_s3.py +0 -106
- sigal/themes/photoswipe/static/app.js +0 -214
- sigal/themes/photoswipe/static/default-skin/default-skin.css +0 -485
- sigal/themes/photoswipe/static/default-skin/default-skin.css.map +0 -10
- sigal/themes/photoswipe/static/default-skin/default-skin.png +0 -0
- sigal/themes/photoswipe/static/default-skin/default-skin.svg +0 -36
- sigal/themes/photoswipe/static/default-skin/preloader.gif +0 -0
- sigal/themes/photoswipe/static/echo/blank.gif +0 -0
- sigal/themes/photoswipe/static/echo/echo.js +0 -135
- sigal/themes/photoswipe/static/echo/echo.min.js +0 -2
- sigal/themes/photoswipe/static/photoswipe-ui-default.js +0 -871
- sigal/themes/photoswipe/static/photoswipe-ui-default.min.js +0 -1
- sigal/themes/photoswipe/static/photoswipe.css.map +0 -10
- sigal/themes/photoswipe/static/photoswipe.js +0 -3592
- sigal/themes/photoswipe/static/photoswipe.min.js +0 -1
- sigal-2.3.dist-info/entry_points.txt +0 -2
- {sigal-2.3.dist-info → sigal-2.5.dist-info}/top_level.txt +0 -0
sigal/gallery.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2009-
|
|
1
|
+
# Copyright (c) 2009-2023 - Simon Conseil
|
|
2
2
|
# Copyright (c) 2013 - Christophe-Marie Duquesne
|
|
3
3
|
# Copyright (c) 2014 - Jonas Kaufmann
|
|
4
4
|
# Copyright (c) 2015 - François D.
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
# IN THE SOFTWARE.
|
|
26
26
|
|
|
27
27
|
import fnmatch
|
|
28
|
+
import io
|
|
28
29
|
import logging
|
|
29
30
|
import multiprocessing
|
|
30
31
|
import os
|
|
@@ -44,7 +45,13 @@ from natsort import natsort_keygen, ns
|
|
|
44
45
|
from PIL import Image as PILImage
|
|
45
46
|
|
|
46
47
|
from . import image, signals, video
|
|
47
|
-
from .image import
|
|
48
|
+
from .image import (
|
|
49
|
+
EXIF_EXTENSIONS,
|
|
50
|
+
get_exif_tags,
|
|
51
|
+
get_image_metadata,
|
|
52
|
+
get_size,
|
|
53
|
+
process_image,
|
|
54
|
+
)
|
|
48
55
|
from .settings import Status, get_thumb
|
|
49
56
|
from .utils import (
|
|
50
57
|
Devnull,
|
|
@@ -54,6 +61,7 @@ from .utils import (
|
|
|
54
61
|
get_mod_date,
|
|
55
62
|
is_valid_html5_video,
|
|
56
63
|
read_markdown,
|
|
64
|
+
should_reprocess_album,
|
|
57
65
|
url_from_path,
|
|
58
66
|
)
|
|
59
67
|
from .video import process_video
|
|
@@ -73,7 +81,7 @@ class Media:
|
|
|
73
81
|
|
|
74
82
|
"""
|
|
75
83
|
|
|
76
|
-
type =
|
|
84
|
+
type = ""
|
|
77
85
|
"""Type of media, e.g. ``"image"`` or ``"video"``."""
|
|
78
86
|
|
|
79
87
|
def __init__(self, filename, path, settings):
|
|
@@ -91,7 +99,7 @@ class Media:
|
|
|
91
99
|
self.src_ext = os.path.splitext(filename)[1].lower()
|
|
92
100
|
"""Input extension."""
|
|
93
101
|
|
|
94
|
-
self.src_path = join(settings[
|
|
102
|
+
self.src_path = join(settings["source"], path, self.src_filename)
|
|
95
103
|
|
|
96
104
|
self.thumb_name = get_thumb(self.settings, self.dst_filename)
|
|
97
105
|
|
|
@@ -108,7 +116,7 @@ class Media:
|
|
|
108
116
|
def __getstate__(self):
|
|
109
117
|
state = self.__dict__.copy()
|
|
110
118
|
# remove un-pickable objects
|
|
111
|
-
state[
|
|
119
|
+
state["logger"] = None
|
|
112
120
|
return state
|
|
113
121
|
|
|
114
122
|
def __setstate__(self, state):
|
|
@@ -118,11 +126,11 @@ class Media:
|
|
|
118
126
|
|
|
119
127
|
@property
|
|
120
128
|
def dst_path(self):
|
|
121
|
-
return join(self.settings[
|
|
129
|
+
return join(self.settings["destination"], self.path, self.dst_filename)
|
|
122
130
|
|
|
123
131
|
@property
|
|
124
132
|
def thumb_path(self):
|
|
125
|
-
return join(self.settings[
|
|
133
|
+
return join(self.settings["destination"], self.path, self.thumb_name)
|
|
126
134
|
|
|
127
135
|
@property
|
|
128
136
|
def url(self):
|
|
@@ -134,22 +142,22 @@ class Media:
|
|
|
134
142
|
"""Path to the original image, if ``keep_orig`` is set (relative to the
|
|
135
143
|
album directory). Copy the file if needed.
|
|
136
144
|
"""
|
|
137
|
-
if self.settings[
|
|
145
|
+
if self.settings["keep_orig"]:
|
|
138
146
|
s = self.settings
|
|
139
|
-
if s[
|
|
147
|
+
if s["use_orig"]:
|
|
140
148
|
# The image *is* the original, just use it
|
|
141
149
|
return self.src_filename
|
|
142
|
-
orig_path = join(s[
|
|
150
|
+
orig_path = join(s["destination"], self.path, s["orig_dir"])
|
|
143
151
|
check_or_create_dir(orig_path)
|
|
144
152
|
big_path = join(orig_path, self.src_filename)
|
|
145
153
|
if not isfile(big_path):
|
|
146
154
|
copy(
|
|
147
155
|
self.src_path,
|
|
148
156
|
big_path,
|
|
149
|
-
symlink=s[
|
|
150
|
-
rellink=self.settings[
|
|
157
|
+
symlink=s["orig_link"],
|
|
158
|
+
rellink=self.settings["rel_link"],
|
|
151
159
|
)
|
|
152
|
-
return join(s[
|
|
160
|
+
return join(s["orig_dir"], self.src_filename)
|
|
153
161
|
|
|
154
162
|
@property
|
|
155
163
|
def big_url(self):
|
|
@@ -162,44 +170,47 @@ class Media:
|
|
|
162
170
|
"""Path to the thumbnail image (relative to the album directory)."""
|
|
163
171
|
|
|
164
172
|
if not isfile(self.thumb_path):
|
|
165
|
-
self.logger.debug(
|
|
173
|
+
self.logger.debug("Generating thumbnail for %r", self)
|
|
166
174
|
path = self.dst_path if os.path.exists(self.dst_path) else self.src_path
|
|
167
175
|
try:
|
|
168
176
|
# if thumbnail is missing (if settings['make_thumbs'] is False)
|
|
169
177
|
s = self.settings
|
|
170
|
-
if self.type ==
|
|
178
|
+
if self.type == "image":
|
|
171
179
|
image.generate_thumbnail(
|
|
172
|
-
path, self.thumb_path, s[
|
|
180
|
+
path, self.thumb_path, s["thumb_size"], fit=s["thumb_fit"]
|
|
173
181
|
)
|
|
174
|
-
elif self.type ==
|
|
182
|
+
elif self.type == "video":
|
|
175
183
|
video.generate_thumbnail(
|
|
176
184
|
path,
|
|
177
185
|
self.thumb_path,
|
|
178
|
-
s[
|
|
179
|
-
s[
|
|
180
|
-
fit=s[
|
|
181
|
-
converter=s[
|
|
186
|
+
s["thumb_size"],
|
|
187
|
+
s["thumb_video_delay"],
|
|
188
|
+
fit=s["thumb_fit"],
|
|
189
|
+
converter=s["video_converter"],
|
|
190
|
+
black_retries=s["thumb_video_black_retries"],
|
|
191
|
+
black_offset=s["thumb_video_black_retry_offset"],
|
|
192
|
+
black_max_colors=s["thumb_video_black_max_colors"],
|
|
182
193
|
)
|
|
183
194
|
except Exception as e:
|
|
184
|
-
self.logger.error(
|
|
195
|
+
self.logger.error("Failed to generate thumbnail: %s", e)
|
|
185
196
|
return
|
|
186
197
|
return url_from_path(self.thumb_name)
|
|
187
198
|
|
|
188
199
|
@cached_property
|
|
189
200
|
def description(self):
|
|
190
201
|
"""Description extracted from the Markdown <imagename>.md file."""
|
|
191
|
-
return self.markdown_metadata.get(
|
|
202
|
+
return self.markdown_metadata.get("description", "")
|
|
192
203
|
|
|
193
204
|
@cached_property
|
|
194
205
|
def title(self):
|
|
195
206
|
"""Title extracted from the metadata, or defaults to the filename."""
|
|
196
|
-
title = self.markdown_metadata.get(
|
|
207
|
+
title = self.markdown_metadata.get("title", "")
|
|
197
208
|
return title if title else self.basename
|
|
198
209
|
|
|
199
210
|
@cached_property
|
|
200
211
|
def meta(self):
|
|
201
212
|
"""Other metadata extracted from the Markdown <imagename>.md file."""
|
|
202
|
-
return self.markdown_metadata.get(
|
|
213
|
+
return self.markdown_metadata.get("meta", {})
|
|
203
214
|
|
|
204
215
|
@cached_property
|
|
205
216
|
def markdown_metadata(self):
|
|
@@ -208,11 +219,11 @@ class Media:
|
|
|
208
219
|
|
|
209
220
|
@property
|
|
210
221
|
def markdown_metadata_filepath(self):
|
|
211
|
-
return splitext(self.src_path)[0] +
|
|
222
|
+
return splitext(self.src_path)[0] + ".md"
|
|
212
223
|
|
|
213
224
|
def _get_markdown_metadata(self):
|
|
214
225
|
"""Get metadata from filename.md."""
|
|
215
|
-
meta = {
|
|
226
|
+
meta = {"title": "", "description": "", "meta": {}}
|
|
216
227
|
if isfile(self.markdown_metadata_filepath):
|
|
217
228
|
meta.update(read_markdown(self.markdown_metadata_filepath))
|
|
218
229
|
return meta
|
|
@@ -229,11 +240,14 @@ class Media:
|
|
|
229
240
|
class Image(Media):
|
|
230
241
|
"""Gather all informations on an image file."""
|
|
231
242
|
|
|
232
|
-
type =
|
|
243
|
+
type = "image"
|
|
233
244
|
|
|
234
245
|
def __init__(self, filename, path, settings):
|
|
235
246
|
super().__init__(filename, path, settings)
|
|
236
|
-
imgformat = settings.get(
|
|
247
|
+
imgformat = settings.get("img_format")
|
|
248
|
+
|
|
249
|
+
# Register all formats
|
|
250
|
+
PILImage.init()
|
|
237
251
|
|
|
238
252
|
if imgformat and PILImage.EXTENSION[self.src_ext] != imgformat.upper():
|
|
239
253
|
# Find the extension that should match img_format
|
|
@@ -246,17 +260,17 @@ class Image(Media):
|
|
|
246
260
|
def date(self):
|
|
247
261
|
"""The date from the EXIF DateTimeOriginal metadata if available, or
|
|
248
262
|
from the file date."""
|
|
249
|
-
return self.exif and self.exif.get(
|
|
263
|
+
return self.exif and self.exif.get("dateobj", None) or self._get_file_date()
|
|
250
264
|
|
|
251
265
|
@cached_property
|
|
252
266
|
def exif(self):
|
|
253
267
|
"""If not `None` contains a dict with the most common tags. For more
|
|
254
268
|
information, see :ref:`simple-exif-data`.
|
|
255
269
|
"""
|
|
256
|
-
datetime_format = self.settings[
|
|
270
|
+
datetime_format = self.settings["datetime_format"]
|
|
257
271
|
return (
|
|
258
272
|
get_exif_tags(self.raw_exif, datetime_format=datetime_format)
|
|
259
|
-
if self.raw_exif and self.src_ext in
|
|
273
|
+
if self.raw_exif and self.src_ext in EXIF_EXTENSIONS
|
|
260
274
|
else None
|
|
261
275
|
)
|
|
262
276
|
|
|
@@ -271,18 +285,18 @@ class Image(Media):
|
|
|
271
285
|
|
|
272
286
|
# If a title or description hasn't been obtained by other means, look
|
|
273
287
|
# for the information in IPTC fields
|
|
274
|
-
if not meta[
|
|
275
|
-
meta[
|
|
276
|
-
if not meta[
|
|
277
|
-
meta[
|
|
288
|
+
if not meta["title"]:
|
|
289
|
+
meta["title"] = self.file_metadata["iptc"].get("title", "")
|
|
290
|
+
if not meta["description"]:
|
|
291
|
+
meta["description"] = self.file_metadata["iptc"].get("description", "")
|
|
278
292
|
|
|
279
293
|
return meta
|
|
280
294
|
|
|
281
295
|
@cached_property
|
|
282
296
|
def raw_exif(self):
|
|
283
297
|
"""If not `None`, contains the raw EXIF tags."""
|
|
284
|
-
if self.src_ext in
|
|
285
|
-
return self.file_metadata[
|
|
298
|
+
if self.src_ext in EXIF_EXTENSIONS:
|
|
299
|
+
return self.file_metadata["exif"]
|
|
286
300
|
|
|
287
301
|
@cached_property
|
|
288
302
|
def size(self):
|
|
@@ -301,20 +315,20 @@ class Image(Media):
|
|
|
301
315
|
|
|
302
316
|
def has_location(self):
|
|
303
317
|
"""True if location information is available for EXIF GPSInfo."""
|
|
304
|
-
return self.exif is not None and
|
|
318
|
+
return self.exif is not None and "gps" in self.exif
|
|
305
319
|
|
|
306
320
|
|
|
307
321
|
class Video(Media):
|
|
308
322
|
"""Gather all informations on a video file."""
|
|
309
323
|
|
|
310
|
-
type =
|
|
324
|
+
type = "video"
|
|
311
325
|
|
|
312
326
|
def __init__(self, filename, path, settings):
|
|
313
327
|
super().__init__(filename, path, settings)
|
|
314
328
|
|
|
315
|
-
if not settings[
|
|
316
|
-
video_format = settings[
|
|
317
|
-
ext =
|
|
329
|
+
if not settings["use_orig"] or not is_valid_html5_video(self.src_ext):
|
|
330
|
+
video_format = settings["video_format"]
|
|
331
|
+
ext = "." + video_format
|
|
318
332
|
self.dst_filename = self.basename + ext
|
|
319
333
|
self.mime = get_mime(ext)
|
|
320
334
|
else:
|
|
@@ -323,12 +337,12 @@ class Video(Media):
|
|
|
323
337
|
@cached_property
|
|
324
338
|
def date(self):
|
|
325
339
|
"""The date from the Date metadata if available, or from the file date."""
|
|
326
|
-
if
|
|
340
|
+
if "date" in self.meta:
|
|
327
341
|
try:
|
|
328
342
|
self.logger.debug(
|
|
329
343
|
"Reading date from image metadata : %s", self.src_filename
|
|
330
344
|
)
|
|
331
|
-
return datetime.fromisoformat(self.meta[
|
|
345
|
+
return datetime.fromisoformat(self.meta["date"][0])
|
|
332
346
|
except Exception:
|
|
333
347
|
self.logger.debug(
|
|
334
348
|
"Reading date from image metadata failed : %s", self.src_filename
|
|
@@ -362,24 +376,24 @@ class Album:
|
|
|
362
376
|
self.gallery = gallery
|
|
363
377
|
self.settings = settings
|
|
364
378
|
self.subdirs = dirnames
|
|
365
|
-
self.output_file = settings[
|
|
379
|
+
self.output_file = settings["output_filename"]
|
|
366
380
|
self._thumbnail = None
|
|
367
381
|
|
|
368
|
-
if path ==
|
|
369
|
-
self.src_path = settings[
|
|
370
|
-
self.dst_path = settings[
|
|
382
|
+
if path == ".":
|
|
383
|
+
self.src_path = settings["source"]
|
|
384
|
+
self.dst_path = settings["destination"]
|
|
371
385
|
else:
|
|
372
|
-
self.src_path = join(settings[
|
|
373
|
-
self.dst_path = join(settings[
|
|
386
|
+
self.src_path = join(settings["source"], path)
|
|
387
|
+
self.dst_path = join(settings["destination"], path)
|
|
374
388
|
|
|
375
389
|
self.logger = logging.getLogger(__name__)
|
|
376
390
|
|
|
377
391
|
# optionally add index.html to the URLs
|
|
378
|
-
self.url_ext = self.output_file if settings[
|
|
392
|
+
self.url_ext = self.output_file if settings["index_in_url"] else ""
|
|
379
393
|
|
|
380
394
|
self.index_url = (
|
|
381
|
-
url_from_path(os.path.relpath(settings[
|
|
382
|
-
+
|
|
395
|
+
url_from_path(os.path.relpath(settings["destination"], self.dst_path))
|
|
396
|
+
+ "/"
|
|
383
397
|
+ self.url_ext
|
|
384
398
|
)
|
|
385
399
|
|
|
@@ -391,9 +405,9 @@ class Album:
|
|
|
391
405
|
for f in filenames:
|
|
392
406
|
ext = splitext(f)[1]
|
|
393
407
|
media = None
|
|
394
|
-
if ext.lower() in settings[
|
|
408
|
+
if ext.lower() in settings["img_extensions"]:
|
|
395
409
|
media = Image(f, self.path, settings)
|
|
396
|
-
elif ext.lower() in settings[
|
|
410
|
+
elif ext.lower() in settings["video_extensions"]:
|
|
397
411
|
media = Video(f, self.path, settings)
|
|
398
412
|
|
|
399
413
|
# Allow modification of the media, including overriding the class
|
|
@@ -410,13 +424,11 @@ class Album:
|
|
|
410
424
|
signals.album_initialized.send(self)
|
|
411
425
|
|
|
412
426
|
def __repr__(self):
|
|
413
|
-
return "<{}>(path={!r}, title={!r})"
|
|
414
|
-
self.__class__.__name__, self.path, self.title
|
|
415
|
-
)
|
|
427
|
+
return f"<{self.__class__.__name__}>(path={self.path!r}, title={self.title!r})"
|
|
416
428
|
|
|
417
429
|
def __str__(self):
|
|
418
|
-
return f
|
|
419
|
-
f
|
|
430
|
+
return f"{self.path} : " + ", ".join(
|
|
431
|
+
f"{count} {_type}s" for _type, count in self.medias_count.items()
|
|
420
432
|
)
|
|
421
433
|
|
|
422
434
|
def __len__(self):
|
|
@@ -428,27 +440,27 @@ class Album:
|
|
|
428
440
|
@cached_property
|
|
429
441
|
def description(self):
|
|
430
442
|
"""Description extracted from the Markdown index.md file."""
|
|
431
|
-
return self.markdown_metadata.get(
|
|
443
|
+
return self.markdown_metadata.get("description", "")
|
|
432
444
|
|
|
433
445
|
@cached_property
|
|
434
446
|
def title(self):
|
|
435
447
|
"""Title extracted from the Markdown index.md file."""
|
|
436
|
-
title = self.markdown_metadata.get(
|
|
437
|
-
path = self.path if self.path !=
|
|
448
|
+
title = self.markdown_metadata.get("title", "")
|
|
449
|
+
path = self.path if self.path != "." else self.src_path
|
|
438
450
|
return title if title else os.path.basename(path)
|
|
439
451
|
|
|
440
452
|
@cached_property
|
|
441
453
|
def meta(self):
|
|
442
454
|
"""Other metadata extracted from the Markdown index.md file."""
|
|
443
|
-
return self.markdown_metadata.get(
|
|
455
|
+
return self.markdown_metadata.get("meta", {})
|
|
444
456
|
|
|
445
457
|
@cached_property
|
|
446
458
|
def author(self):
|
|
447
459
|
"""Author extracted from the Markdown index.md file or settings."""
|
|
448
460
|
try:
|
|
449
|
-
return self.meta[
|
|
461
|
+
return self.meta["author"][0]
|
|
450
462
|
except KeyError:
|
|
451
|
-
return self.settings.get(
|
|
463
|
+
return self.settings.get("author")
|
|
452
464
|
|
|
453
465
|
@property
|
|
454
466
|
def markdown_metadata_filepath(self):
|
|
@@ -457,7 +469,7 @@ class Album:
|
|
|
457
469
|
@cached_property
|
|
458
470
|
def markdown_metadata(self):
|
|
459
471
|
"""Get metadata from filename.md: title, description, meta."""
|
|
460
|
-
meta = {
|
|
472
|
+
meta = {"title": "", "description": "", "meta": {}}
|
|
461
473
|
if isfile(self.markdown_metadata_filepath):
|
|
462
474
|
meta.update(read_markdown(self.markdown_metadata_filepath))
|
|
463
475
|
return meta
|
|
@@ -467,63 +479,74 @@ class Album:
|
|
|
467
479
|
check_or_create_dir(self.dst_path)
|
|
468
480
|
|
|
469
481
|
if self.medias:
|
|
470
|
-
check_or_create_dir(join(self.dst_path, self.settings[
|
|
482
|
+
check_or_create_dir(join(self.dst_path, self.settings["thumb_dir"]))
|
|
471
483
|
|
|
472
|
-
if self.medias and self.settings[
|
|
473
|
-
self.orig_path = join(self.dst_path, self.settings[
|
|
484
|
+
if self.medias and self.settings["keep_orig"]:
|
|
485
|
+
self.orig_path = join(self.dst_path, self.settings["orig_dir"])
|
|
474
486
|
check_or_create_dir(self.orig_path)
|
|
475
487
|
|
|
476
488
|
def sort_subdirs(self, albums_sort_attr):
|
|
477
489
|
if self.subdirs:
|
|
478
490
|
if not albums_sort_attr:
|
|
479
|
-
albums_sort_attr = self.settings[
|
|
480
|
-
reverse = self.settings[
|
|
491
|
+
albums_sort_attr = self.settings["albums_sort_attr"]
|
|
492
|
+
reverse = self.settings["albums_sort_reverse"]
|
|
481
493
|
|
|
482
|
-
if
|
|
483
|
-
|
|
484
|
-
|
|
494
|
+
if "sort" in self.meta:
|
|
495
|
+
# override default sort order from settings
|
|
496
|
+
albums_sort_attr = self.meta["sort"][0]
|
|
497
|
+
if albums_sort_attr[0] == "-":
|
|
485
498
|
albums_sort_attr = albums_sort_attr[1:]
|
|
486
499
|
reverse = True
|
|
487
500
|
else:
|
|
488
501
|
reverse = False
|
|
489
502
|
|
|
490
|
-
root_path = self.path if self.path !=
|
|
491
|
-
if albums_sort_attr.startswith("meta."):
|
|
492
|
-
meta_key = albums_sort_attr.split(".", 1)[1]
|
|
503
|
+
root_path = self.path if self.path != "." else ""
|
|
493
504
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
505
|
+
def sort_key(s):
|
|
506
|
+
sort_attr = albums_sort_attr
|
|
507
|
+
if not isinstance(sort_attr, list):
|
|
508
|
+
sort_attr = [sort_attr]
|
|
497
509
|
|
|
498
|
-
|
|
510
|
+
album = self.gallery.albums[join(root_path, s)]
|
|
499
511
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
512
|
+
for k in sort_attr:
|
|
513
|
+
try:
|
|
514
|
+
if k.startswith("meta."):
|
|
515
|
+
meta_key = k.split(".", 1)[1]
|
|
516
|
+
return album.meta.get(meta_key)[0]
|
|
517
|
+
else:
|
|
518
|
+
return getattr(album, k)
|
|
519
|
+
except AttributeError:
|
|
520
|
+
continue
|
|
521
|
+
except TypeError:
|
|
522
|
+
continue
|
|
523
|
+
return ""
|
|
503
524
|
|
|
504
|
-
key = natsort_keygen(key=sort_key, alg=ns.LOCALE)
|
|
525
|
+
key = natsort_keygen(key=sort_key, alg=ns.SIGNED | ns.LOCALE)
|
|
505
526
|
self.subdirs.sort(key=key, reverse=reverse)
|
|
506
527
|
|
|
507
528
|
signals.albums_sorted.send(self)
|
|
508
529
|
|
|
509
530
|
def sort_medias(self, medias_sort_attr):
|
|
510
531
|
if self.medias:
|
|
511
|
-
if medias_sort_attr ==
|
|
512
|
-
medias_sort_attr =
|
|
532
|
+
if medias_sort_attr == "filename":
|
|
533
|
+
medias_sort_attr = "dst_filename"
|
|
513
534
|
|
|
514
|
-
if medias_sort_attr ==
|
|
535
|
+
if medias_sort_attr == "date":
|
|
515
536
|
key = lambda s: s.date or datetime.now()
|
|
516
|
-
elif medias_sort_attr.startswith(
|
|
537
|
+
elif medias_sort_attr.startswith("meta."):
|
|
517
538
|
meta_key = medias_sort_attr.split(".", 1)[1]
|
|
518
539
|
key = natsort_keygen(
|
|
519
|
-
key=lambda s: s.meta.get(meta_key, [
|
|
540
|
+
key=lambda s: s.meta.get(meta_key, [""])[0],
|
|
541
|
+
alg=ns.SIGNED | ns.LOCALE,
|
|
520
542
|
)
|
|
521
543
|
else:
|
|
522
544
|
key = natsort_keygen(
|
|
523
|
-
key=lambda s: getattr(s, medias_sort_attr),
|
|
545
|
+
key=lambda s: getattr(s, medias_sort_attr),
|
|
546
|
+
alg=ns.SIGNED | ns.LOCALE,
|
|
524
547
|
)
|
|
525
548
|
|
|
526
|
-
self.medias.sort(key=key, reverse=self.settings[
|
|
549
|
+
self.medias.sort(key=key, reverse=self.settings["medias_sort_reverse"])
|
|
527
550
|
|
|
528
551
|
signals.medias_sorted.send(self)
|
|
529
552
|
|
|
@@ -531,14 +554,14 @@ class Album:
|
|
|
531
554
|
def images(self):
|
|
532
555
|
"""List of images (:class:`~sigal.gallery.Image`)."""
|
|
533
556
|
for media in self.medias:
|
|
534
|
-
if media.type ==
|
|
557
|
+
if media.type == "image":
|
|
535
558
|
yield media
|
|
536
559
|
|
|
537
560
|
@property
|
|
538
561
|
def videos(self):
|
|
539
562
|
"""List of videos (:class:`~sigal.gallery.Video`)."""
|
|
540
563
|
for media in self.medias:
|
|
541
|
-
if media.type ==
|
|
564
|
+
if media.type == "video":
|
|
542
565
|
yield media
|
|
543
566
|
|
|
544
567
|
@property
|
|
@@ -546,7 +569,7 @@ class Album:
|
|
|
546
569
|
"""List of :class:`~sigal.gallery.Album` objects for each
|
|
547
570
|
sub-directory.
|
|
548
571
|
"""
|
|
549
|
-
root_path = self.path if self.path !=
|
|
572
|
+
root_path = self.path if self.path != "." else ""
|
|
550
573
|
return [self.gallery.albums[join(root_path, path)] for path in self.subdirs]
|
|
551
574
|
|
|
552
575
|
@property
|
|
@@ -556,8 +579,8 @@ class Album:
|
|
|
556
579
|
@property
|
|
557
580
|
def url(self):
|
|
558
581
|
"""URL of the album, relative to its parent."""
|
|
559
|
-
url = self.name.encode(
|
|
560
|
-
return url_quote(url) +
|
|
582
|
+
url = self.name.encode("utf-8")
|
|
583
|
+
return url_quote(url) + "/" + self.url_ext
|
|
561
584
|
|
|
562
585
|
@property
|
|
563
586
|
def thumbnail(self):
|
|
@@ -568,7 +591,7 @@ class Album:
|
|
|
568
591
|
return self._thumbnail
|
|
569
592
|
|
|
570
593
|
# Test the thumbnail from the Markdown file.
|
|
571
|
-
thumbnail = self.meta.get(
|
|
594
|
+
thumbnail = self.meta.get("thumbnail", [""])[0]
|
|
572
595
|
|
|
573
596
|
if thumbnail and isfile(join(self.src_path, thumbnail)):
|
|
574
597
|
self._thumbnail = url_from_path(
|
|
@@ -580,18 +603,18 @@ class Album:
|
|
|
580
603
|
# find and return the first landscape image
|
|
581
604
|
for f in self.medias:
|
|
582
605
|
ext = splitext(f.dst_filename)[1]
|
|
583
|
-
if ext.lower() not in self.settings[
|
|
606
|
+
if ext.lower() not in self.settings["img_extensions"]:
|
|
584
607
|
continue
|
|
585
608
|
|
|
586
609
|
# Use f.size if available as it is quicker (in cache), but
|
|
587
610
|
# fallback to the size of src_path if dst_path is missing
|
|
588
611
|
size = f.input_size
|
|
589
612
|
if size is None:
|
|
590
|
-
size = f.file_metadata[
|
|
613
|
+
size = f.file_metadata["size"]
|
|
591
614
|
|
|
592
|
-
if size[
|
|
615
|
+
if size["width"] > size["height"]:
|
|
593
616
|
try:
|
|
594
|
-
self._thumbnail = url_quote(self.name) +
|
|
617
|
+
self._thumbnail = url_quote(self.name) + "/" + f.thumbnail
|
|
595
618
|
except Exception as e:
|
|
596
619
|
self.logger.info(
|
|
597
620
|
"Failed to get thumbnail for %s: %s", f.dst_filename, e
|
|
@@ -610,7 +633,7 @@ class Album:
|
|
|
610
633
|
if media.thumbnail is not None:
|
|
611
634
|
try:
|
|
612
635
|
self._thumbnail = (
|
|
613
|
-
url_quote(self.name) +
|
|
636
|
+
url_quote(self.name) + "/" + media.thumbnail
|
|
614
637
|
)
|
|
615
638
|
except Exception as e:
|
|
616
639
|
self.logger.info(
|
|
@@ -633,7 +656,7 @@ class Album:
|
|
|
633
656
|
if not self._thumbnail:
|
|
634
657
|
for path, album in self.gallery.get_albums(self.path):
|
|
635
658
|
if album.thumbnail:
|
|
636
|
-
self._thumbnail = url_quote(self.name) +
|
|
659
|
+
self._thumbnail = url_quote(self.name) + "/" + album.thumbnail
|
|
637
660
|
self.logger.debug(
|
|
638
661
|
"Using thumbnail from sub-directory for %r : %s",
|
|
639
662
|
self,
|
|
@@ -641,7 +664,7 @@ class Album:
|
|
|
641
664
|
)
|
|
642
665
|
return self._thumbnail
|
|
643
666
|
|
|
644
|
-
self.logger.error(
|
|
667
|
+
self.logger.error("Thumbnail not found for %r", self)
|
|
645
668
|
|
|
646
669
|
@property
|
|
647
670
|
def random_thumbnail(self):
|
|
@@ -655,18 +678,18 @@ class Album:
|
|
|
655
678
|
"""List of ``(url, title)`` tuples defining the current breadcrumb
|
|
656
679
|
path.
|
|
657
680
|
"""
|
|
658
|
-
if self.path ==
|
|
681
|
+
if self.path == ".":
|
|
659
682
|
return []
|
|
660
683
|
|
|
661
684
|
path = self.path
|
|
662
|
-
breadcrumb = [((self.url_ext or
|
|
685
|
+
breadcrumb = [((self.url_ext or "."), self.title)]
|
|
663
686
|
|
|
664
687
|
while True:
|
|
665
|
-
path = os.path.normpath(os.path.join(path,
|
|
666
|
-
if path ==
|
|
688
|
+
path = os.path.normpath(os.path.join(path, ".."))
|
|
689
|
+
if path == ".":
|
|
667
690
|
break
|
|
668
691
|
|
|
669
|
-
url = url_from_path(os.path.relpath(path, self.path)) +
|
|
692
|
+
url = url_from_path(os.path.relpath(path, self.path)) + "/" + self.url_ext
|
|
670
693
|
breadcrumb.append((url, self.gallery.albums[path].title))
|
|
671
694
|
|
|
672
695
|
breadcrumb.reverse()
|
|
@@ -685,30 +708,30 @@ class Album:
|
|
|
685
708
|
|
|
686
709
|
|
|
687
710
|
class Gallery:
|
|
688
|
-
def __init__(self, settings, ncpu=None,
|
|
711
|
+
def __init__(self, settings, ncpu=None, show_progress=False):
|
|
689
712
|
self.settings = settings
|
|
690
713
|
self.logger = logging.getLogger(__name__)
|
|
691
714
|
self.stats = defaultdict(int)
|
|
692
715
|
self.init_pool(ncpu)
|
|
693
|
-
check_or_create_dir(settings[
|
|
716
|
+
check_or_create_dir(settings["destination"])
|
|
694
717
|
|
|
695
|
-
if settings[
|
|
696
|
-
PILImage.MAX_IMAGE_PIXELS = settings[
|
|
718
|
+
if settings["max_img_pixels"]:
|
|
719
|
+
PILImage.MAX_IMAGE_PIXELS = settings["max_img_pixels"]
|
|
697
720
|
|
|
698
721
|
# Build the list of directories with images
|
|
699
722
|
albums = self.albums = {}
|
|
700
|
-
src_path = self.settings[
|
|
723
|
+
src_path = self.settings["source"]
|
|
701
724
|
|
|
702
|
-
ignore_dirs = settings[
|
|
703
|
-
ignore_files = settings[
|
|
725
|
+
ignore_dirs = settings["ignore_directories"]
|
|
726
|
+
ignore_files = settings["ignore_files"]
|
|
704
727
|
|
|
705
728
|
progressChars = cycle(["/", "-", "\\", "|"])
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
self.progressbar_target = None if show_progress else Devnull()
|
|
729
|
+
try:
|
|
730
|
+
isatty = os.isatty(sys.stdout.fileno())
|
|
731
|
+
except io.UnsupportedOperation:
|
|
732
|
+
isatty = False
|
|
733
|
+
|
|
734
|
+
self.progressbar_target = None if show_progress and isatty else Devnull()
|
|
712
735
|
|
|
713
736
|
for path, dirs, files in os.walk(src_path, followlinks=True, topdown=False):
|
|
714
737
|
if show_progress:
|
|
@@ -719,7 +742,12 @@ class Gallery:
|
|
|
719
742
|
if ignore_dirs and any(
|
|
720
743
|
fnmatch.fnmatch(relpath, ignore) for ignore in ignore_dirs
|
|
721
744
|
):
|
|
722
|
-
self.logger.info(
|
|
745
|
+
self.logger.info("Ignoring %s", relpath)
|
|
746
|
+
# Remove sub-directories
|
|
747
|
+
for d in dirs[:]:
|
|
748
|
+
path = join(relpath, d) if relpath != "." else d
|
|
749
|
+
if path in albums.keys():
|
|
750
|
+
del albums[path]
|
|
723
751
|
continue
|
|
724
752
|
|
|
725
753
|
# Remove files that match the ignore_files settings
|
|
@@ -728,22 +756,22 @@ class Gallery:
|
|
|
728
756
|
for ignore in ignore_files:
|
|
729
757
|
files_path -= set(fnmatch.filter(files_path, ignore))
|
|
730
758
|
|
|
731
|
-
self.logger.debug(
|
|
759
|
+
self.logger.debug("Files before filtering: %r", files)
|
|
732
760
|
files = [os.path.split(f)[1] for f in files_path]
|
|
733
|
-
self.logger.debug(
|
|
761
|
+
self.logger.debug("Files after filtering: %r", files)
|
|
734
762
|
|
|
735
763
|
# Remove sub-directories that have been ignored in a previous
|
|
736
764
|
# iteration (as topdown=False, sub-directories are processed before
|
|
737
765
|
# their parent
|
|
738
766
|
for d in dirs[:]:
|
|
739
|
-
path = join(relpath, d) if relpath !=
|
|
767
|
+
path = join(relpath, d) if relpath != "." else d
|
|
740
768
|
if path not in albums.keys():
|
|
741
769
|
dirs.remove(d)
|
|
742
770
|
|
|
743
771
|
album = Album(relpath, settings, dirs, files, self)
|
|
744
772
|
|
|
745
773
|
if not album.medias and not album.albums:
|
|
746
|
-
self.logger.info(
|
|
774
|
+
self.logger.info("Skip empty album: %r", album)
|
|
747
775
|
else:
|
|
748
776
|
album.create_output_directories()
|
|
749
777
|
albums[relpath] = album
|
|
@@ -753,27 +781,27 @@ class Gallery:
|
|
|
753
781
|
|
|
754
782
|
with progressbar(
|
|
755
783
|
albums.values(),
|
|
756
|
-
label="
|
|
784
|
+
label="{:>16s}".format("Sorting albums"),
|
|
757
785
|
file=self.progressbar_target,
|
|
758
786
|
) as progress_albums:
|
|
759
787
|
for album in progress_albums:
|
|
760
|
-
album.sort_subdirs(settings[
|
|
788
|
+
album.sort_subdirs(settings["albums_sort_attr"])
|
|
761
789
|
|
|
762
790
|
with progressbar(
|
|
763
791
|
albums.values(),
|
|
764
|
-
label="
|
|
792
|
+
label="{:>16s}".format("Sorting medias"),
|
|
765
793
|
file=self.progressbar_target,
|
|
766
794
|
) as progress_albums:
|
|
767
795
|
for album in progress_albums:
|
|
768
|
-
album.sort_medias(settings[
|
|
796
|
+
album.sort_medias(settings["medias_sort_attr"])
|
|
769
797
|
|
|
770
|
-
self.logger.debug(
|
|
798
|
+
self.logger.debug("Albums:\n%r", albums.values())
|
|
771
799
|
signals.gallery_initialized.send(self)
|
|
772
800
|
|
|
773
801
|
@property
|
|
774
802
|
def title(self):
|
|
775
803
|
"""Title of the gallery."""
|
|
776
|
-
return self.settings[
|
|
804
|
+
return self.settings["title"] or self.albums["."].title
|
|
777
805
|
|
|
778
806
|
def init_pool(self, ncpu):
|
|
779
807
|
try:
|
|
@@ -787,16 +815,15 @@ class Gallery:
|
|
|
787
815
|
try:
|
|
788
816
|
ncpu = int(ncpu)
|
|
789
817
|
except ValueError:
|
|
790
|
-
self.logger.error(
|
|
818
|
+
self.logger.error("ncpu should be an integer value")
|
|
791
819
|
ncpu = cpu_count
|
|
792
820
|
|
|
793
821
|
self.logger.info("Using %s cores", ncpu)
|
|
794
822
|
if ncpu > 1:
|
|
795
|
-
|
|
796
823
|
self.pool = multiprocessing.Pool(
|
|
797
824
|
processes=ncpu,
|
|
798
825
|
initializer=pool_init,
|
|
799
|
-
initargs=(self.settings[
|
|
826
|
+
initargs=(self.settings["max_img_pixels"],),
|
|
800
827
|
)
|
|
801
828
|
else:
|
|
802
829
|
self.pool = None
|
|
@@ -837,12 +864,12 @@ class Gallery:
|
|
|
837
864
|
f for album in albums for f in self.process_dir(album, force=force)
|
|
838
865
|
]
|
|
839
866
|
except KeyboardInterrupt:
|
|
840
|
-
sys.exit(
|
|
867
|
+
sys.exit("Interrupted")
|
|
841
868
|
|
|
842
869
|
bar_opt = {
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
870
|
+
"label": "Processing files",
|
|
871
|
+
"show_pos": True,
|
|
872
|
+
"file": self.progressbar_target,
|
|
846
873
|
}
|
|
847
874
|
|
|
848
875
|
if self.pool:
|
|
@@ -854,7 +881,7 @@ class Gallery:
|
|
|
854
881
|
bar.update(1)
|
|
855
882
|
except KeyboardInterrupt:
|
|
856
883
|
self.pool.terminate()
|
|
857
|
-
sys.exit(
|
|
884
|
+
sys.exit("Interrupted")
|
|
858
885
|
except pickle.PicklingError:
|
|
859
886
|
self.logger.critical(
|
|
860
887
|
"Failed to process files with the multiprocessing feature."
|
|
@@ -862,7 +889,7 @@ class Gallery:
|
|
|
862
889
|
"defined in the settings file, which can't be serialized.",
|
|
863
890
|
exc_info=True,
|
|
864
891
|
)
|
|
865
|
-
sys.exit(
|
|
892
|
+
sys.exit("Abort")
|
|
866
893
|
finally:
|
|
867
894
|
self.pool.close()
|
|
868
895
|
self.pool.join()
|
|
@@ -876,14 +903,15 @@ class Gallery:
|
|
|
876
903
|
]
|
|
877
904
|
self.remove_files(failed_files)
|
|
878
905
|
|
|
879
|
-
if self.settings[
|
|
906
|
+
if self.settings["write_html"]:
|
|
880
907
|
album_writer = AlbumPageWriter(self.settings, index_title=self.title)
|
|
908
|
+
album_writer.copy_theme_files()
|
|
881
909
|
album_list_writer = AlbumListPageWriter(
|
|
882
910
|
self.settings, index_title=self.title
|
|
883
911
|
)
|
|
884
912
|
with progressbar(
|
|
885
913
|
self.albums.values(),
|
|
886
|
-
label="
|
|
914
|
+
label="{:>16s}".format("Writing files"),
|
|
887
915
|
item_show_func=log_func,
|
|
888
916
|
show_eta=False,
|
|
889
917
|
file=self.progressbar_target,
|
|
@@ -892,7 +920,7 @@ class Gallery:
|
|
|
892
920
|
if album.albums:
|
|
893
921
|
if album.medias:
|
|
894
922
|
self.logger.warning(
|
|
895
|
-
"Album %s contains sub-albums and images. "
|
|
923
|
+
"Album '%s' contains sub-albums and images. "
|
|
896
924
|
"Please move images to their own sub-album. "
|
|
897
925
|
"Images in album %s will not be visible.",
|
|
898
926
|
album.title,
|
|
@@ -901,31 +929,33 @@ class Gallery:
|
|
|
901
929
|
album_list_writer.write(album)
|
|
902
930
|
else:
|
|
903
931
|
album_writer.write(album)
|
|
904
|
-
print(
|
|
932
|
+
print("")
|
|
905
933
|
|
|
906
934
|
signals.gallery_build.send(self)
|
|
907
935
|
|
|
908
936
|
def remove_files(self, medias):
|
|
909
|
-
self.logger.error(
|
|
937
|
+
self.logger.error("Some files have failed to be processed:")
|
|
910
938
|
for media in medias:
|
|
911
|
-
self.logger.error(
|
|
939
|
+
self.logger.error(" - %s", media.dst_filename)
|
|
912
940
|
album = self.albums[media.path]
|
|
913
941
|
for f in album.medias:
|
|
914
942
|
if f.dst_filename == media.dst_filename:
|
|
915
|
-
self.stats[f.type +
|
|
943
|
+
self.stats[f.type + "_failed"] += 1
|
|
916
944
|
album.medias.remove(f)
|
|
917
945
|
break
|
|
918
946
|
self.logger.error(
|
|
919
947
|
'You can run "sigal build" in verbose (--verbose) or'
|
|
920
|
-
|
|
948
|
+
" debug (--debug) mode to get more details."
|
|
921
949
|
)
|
|
922
950
|
|
|
923
951
|
def process_dir(self, album, force=False):
|
|
924
952
|
"""Process a list of images in a directory."""
|
|
925
953
|
for f in album:
|
|
926
|
-
if isfile(f.dst_path) and not
|
|
954
|
+
if isfile(f.dst_path) and not should_reprocess_album(
|
|
955
|
+
album.path, album.name, force
|
|
956
|
+
):
|
|
927
957
|
self.logger.info("%s exists - skipping", f.dst_filename)
|
|
928
|
-
self.stats[f.type +
|
|
958
|
+
self.stats[f.type + "_skipped"] += 1
|
|
929
959
|
else:
|
|
930
960
|
self.stats[f.type] += 1
|
|
931
961
|
yield f
|
|
@@ -938,9 +968,9 @@ def pool_init(max_img_pixels):
|
|
|
938
968
|
|
|
939
969
|
def process_file(media):
|
|
940
970
|
processor = None
|
|
941
|
-
if media.type ==
|
|
971
|
+
if media.type == "image":
|
|
942
972
|
processor = process_image
|
|
943
|
-
elif media.type ==
|
|
973
|
+
elif media.type == "video":
|
|
944
974
|
processor = process_video
|
|
945
975
|
|
|
946
976
|
# Allow overriding of the processor
|
|
@@ -952,7 +982,7 @@ def process_file(media):
|
|
|
952
982
|
if processor:
|
|
953
983
|
return processor(media)
|
|
954
984
|
else:
|
|
955
|
-
logging.warning(
|
|
985
|
+
logging.warning("Processor not found for media %s", media.path)
|
|
956
986
|
return Status.FAILURE
|
|
957
987
|
|
|
958
988
|
|