8 Commits

15 changed files with 195 additions and 57 deletions

View File

@@ -1 +1 @@
2.3.4 2.4.0

View File

@@ -44,20 +44,20 @@ The script supports several command-line options to customize its behavior. Belo
### Options ### Options
- `-h, --help`: Show the help message and exit.
- `-p ROOT, --root-directory ROOT`: Specify the root folder where the images are stored. This option is required.
- `-w URL, --web-root-url URL`: Specify the base URL for the web root of the image hosting site. This option is required.
- `-t TITLE, --site-title TITLE`: Specify the title of the image hosting site. This option is required.
- `-r, --regenerate-thumbnails`: Regenerate thumbnails even if they already exist.
- `-n, --non-interactive-mode`: Run in non-interactive mode, disabling progress bars.
- `-l LICENSE, --license-type LICENSE`: Specify the license type for the images. Choices are `cc-zero`, `cc-by`, `cc-by-sa`, `cc-by-nd`, `cc-by-nc`, `cc-by-nc-sa`, and `cc-by-nc-nd`.
- `-a AUTHOR, --author-name AUTHOR`: Specify the name of the author of the images. Default is "Author". - `-a AUTHOR, --author-name AUTHOR`: Specify the name of the author of the images. Default is "Author".
- `-e EXTENSION, --file-extensions EXTENSION`: Specify the file extensions to include. This option can be specified multiple times. - `-e EXTENSION, --file-extensions EXTENSION`: Specify the file extensions to include. This option can be specified multiple times.
- `-l LICENSE, --license-type LICENSE`: Specify the license type for the images. Choices are `cc-zero`, `cc-by`, `cc-by-sa`, `cc-by-nd`, `cc-by-nc`, `cc-by-nc-sa`, and `cc-by-nc-nd`.
- `-m, --web-manifest`: Generate a web manifest file.
- `-n, --non-interactive-mode`: Run in non-interactive mode, disabling progress bars.
- `-p ROOT, --root-directory ROOT`: Specify the root folder where the images are stored. **(This option is required)**.
- `-t TITLE, --site-title TITLE`: Specify the title of the image hosting site. **(This option is required)**.
- `-w URL, --web-root-url URL`: Specify the base URL for the web root of the image hosting site. **(This option is required)**.
- `--exclude-folder FOLDER`: Specify folders to exclude from processing. This option can be specified multiple times.
- `--ignore-other-files`: Ignore files that do not match the specified extensions.
- `--regenerate-thumbnails`: Regenerate thumbnails even if they already exist.
- `--reread-metadata`: Reread image metadata if it already exists.
- `--theme-path PATH`: Specify the path to the CSS theme file. Default is the provided default theme. - `--theme-path PATH`: Specify the path to the CSS theme file. Default is the provided default theme.
- `--use-fancy-folders`: Enable fancy folder view instead of the default Apache directory listing. - `--use-fancy-folders`: Enable fancy folder view instead of the default Apache directory listing.
- `--ignore-other-files`: Ignore files that do not match the specified extensions.
- `--exclude-folder FOLDER`: Specify folders to exclude from processing. This option can be specified multiple times.
- `-m, --web-manifest`: Generate a web manifest file.
### Examples ### Examples

View File

@@ -39,7 +39,8 @@
"cc-by-nc-sa", "cc-by-nc-sa",
"-n", "-n",
"-m", "-m",
"-r" "--regenerate-thumbnails",
"--reread-metadata",
], ],
"console": "integratedTerminal", "console": "integratedTerminal",
"name": "Testfolder", "name": "Testfolder",
@@ -57,12 +58,13 @@
"-t", "-t",
"Pictures", "Pictures",
"--theme", "--theme",
"themes/catpuccin.css", "themes/default.css",
"--use-fancy-folders", "--use-fancy-folders",
"--web-manifest", "--web-manifest",
"-n", "-n",
"-m", "-m",
"-r", // "--regenerate-thumbnails",
// "--reread-metadata",
], ],
"console": "integratedTerminal", "console": "integratedTerminal",
"name": "woek", "name": "woek",
@@ -147,6 +149,9 @@
"python.analysis.inlayHints.callArgumentNames": "off", "python.analysis.inlayHints.callArgumentNames": "off",
"python.analysis.inlayHints.functionReturnTypes": false, "python.analysis.inlayHints.functionReturnTypes": false,
"python.analysis.inlayHints.variableTypes": false, "python.analysis.inlayHints.variableTypes": false,
"yaml.schemas": {
"https://raw.githubusercontent.com/pamburus/hl/master/schema/json/config.schema.json": "file:///home/user/git/github.com/greflm13/StaticGalleryBuilder/hl_config.yaml"
},
}, },
"tasks": { "tasks": {
"version": "2.0.0", "version": "2.0.0",
@@ -218,6 +223,21 @@
"clear": true "clear": true
}, },
"group": "build" "group": "build"
},
{
"command": "LESS=-SR hl logs/latest.jsonl --config hl_config.yaml",
"isBackground": false,
"label": "View Latest Log",
"problemMatcher": [],
"type": "shell",
"presentation": {
"echo": false,
"reveal": "always",
"focus": true,
"panel": "dedicated",
"showReuseMessage": false,
"clear": true
}
} }
], ],
}, },

View File

@@ -196,11 +196,13 @@ def main() -> None:
lock_file = os.path.join(args.root_directory, ".lock") lock_file = os.path.join(args.root_directory, ".lock")
if os.path.exists(lock_file): if os.path.exists(lock_file):
print("Another instance of this program is running.") print("Another instance of this program is running.")
logger.info("nother instance of this program is running") logger.error("another instance of this program is running")
exit() exit()
try: try:
Path(lock_file).touch() Path(lock_file).touch()
if args.reread_metadata:
logger.warning("reread metadata flag is set to true, all image metadata will be reread")
if args.regenerate_thumbnails: if args.regenerate_thumbnails:
logger.warning("regenerate thumbnails flag is set to true, all thumbnails will be regenerated") logger.warning("regenerate thumbnails flag is set to true, all thumbnails will be regenerated")
if os.path.exists(os.path.join(args.root_directory, ".thumbnails")): if os.path.exists(os.path.join(args.root_directory, ".thumbnails")):
@@ -212,7 +214,6 @@ def main() -> None:
icons(args) icons(args)
if args.generate_webmanifest: if args.generate_webmanifest:
logger.info("generating webmanifest")
print("Generating webmanifest...") print("Generating webmanifest...")
webmanifest(args) webmanifest(args)

View File

@@ -90,7 +90,7 @@ figure {
.footer a img { .footer a img {
height: 22px !important; height: 22px !important;
margin-left: 3px; margin-left: 3px;
vertical-align: text-bottom vertical-align: text-bottom;
} }
.navbar { .navbar {
@@ -157,12 +157,12 @@ figure {
opacity: 1; opacity: 1;
} }
/* Create eight equal columns that sits next to each other */
.column { .column {
-ms-flex: 12.5%; -ms-flex: 12.5%;
flex: 12.5%; flex: 12.5%;
max-width: 12.5%; max-width: 12.5%;
padding: 0 4px; padding: 0 4px;
display: inline-block;
} }
.column img { .column img {
@@ -187,7 +187,6 @@ figure {
border-style: none; border-style: none;
} }
/* Responsive layout - makes a four column-layout instead of eight columns */
@media screen and (max-width: 1000px) { @media screen and (max-width: 1000px) {
.column { .column {
-ms-flex: 25%; -ms-flex: 25%;
@@ -209,7 +208,6 @@ figure {
} }
} }
/* Responsive layout - makes a two column-layout instead of four columns */
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {
.column { .column {
-ms-flex: 50%; -ms-flex: 50%;
@@ -239,7 +237,6 @@ figure {
} }
} }
/* Responsive layout - makes the two columns stack on top of each other instead of next to each other */
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
.column { .column {
-ms-flex: 100%; -ms-flex: 100%;

View File

@@ -272,13 +272,14 @@ a.pswp__share--download:hover {
color: #BBB; } color: #BBB; }
.pswp__caption__center { .pswp__caption__center {
text-align: left; text-align: center;
max-width: 420px; max-width: 420px;
margin: 0 auto; margin: 0 auto;
font-size: 13px; font-size: 13px;
padding: 10px; padding: 10px;
line-height: 20px; line-height: 20px;
color: #CCC; } color: #CCC;
font-weight: bold; }
.pswp__caption--empty { .pswp__caption--empty {
display: none; } display: none; }

73
hl_config.yaml Normal file
View File

@@ -0,0 +1,73 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/pamburus/hl/master/schema/json/config.schema.json
$schema: https://raw.githubusercontent.com/pamburus/hl/master/schema/json/config.schema.json
# Time format, see https://man7.org/linux/man-pages/man1/date.1.html for details.
time-format: "%b %d %T.%3N"
# Time zone name, see column "TZ identifier" at
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones page.
time-zone: "Europe/Vienna"
# Settings for fields processing.
fields:
# Configuration of the predefined set of fields.
predefined:
time:
show: auto
names: ["asctime"]
logger:
names: ["defaultlogger", "consolelogger"]
level:
show: auto
variants:
- names: ["levelname"]
values:
debug: ["DEBUG"]
info: ["INFO"]
warning: ["WARNING", "WARN"]
error: ["ERROR", "FATAL", "CRITICAL"]
- names: ["levelno"]
values:
debug: [10]
info: [20]
warning: [30]
error: [40, 50]
message:
names: ["message"]
caller:
names: ["funcName"]
caller-file:
names: ["filename"]
caller-line:
names: ["lineno"]
# List of wildcard field names to ignore.
ignore: ["_*"]
# List of exact field names to hide.
hide: ["pathname", "created", "levelno", "taskname", "relativeCreated", "thread", "process", "msecs"]
# Formatting settings.
formatting:
flatten: always
punctuation:
logger-name-separator: ":"
field-key-value-separator: "="
string-opening-quote: "'"
string-closing-quote: "'"
source-location-separator: "→ "
hidden-fields-indicator: " ..."
level-left-separator: "│"
level-right-separator: "│"
input-number-prefix: "#"
input-number-left-separator: ""
input-number-right-separator: " │ "
input-name-left-separator: ""
input-name-right-separator: " │ "
input-name-clipping: "··"
input-name-common-part: "··"
array-separator: " "
# Number of processing threads, configured automatically based on CPU count if not specified.
concurrency: ~
# Currently selected theme.
theme: "neutral"

View File

@@ -2,7 +2,14 @@ from dataclasses import dataclass
from typing import List, Optional from typing import List, Optional
import os import os
import argparse import argparse
from rich_argparse import RichHelpFormatter, HelpPreviewAction
try:
from rich_argparse import RichHelpFormatter, HelpPreviewAction
RICH = True
except ModuleNotFoundError:
RICH = False
from modules.logger import logger from modules.logger import logger
@@ -58,6 +65,7 @@ class Args:
license_type: Optional[str] license_type: Optional[str]
non_interactive_mode: bool non_interactive_mode: bool
regenerate_thumbnails: bool regenerate_thumbnails: bool
reread_metadata: bool
root_directory: str root_directory: str
site_title: str site_title: str
theme_path: str theme_path: str
@@ -75,6 +83,7 @@ class Args:
result["license_type"] = self.license_type result["license_type"] = self.license_type
result["non_interactive_mode"] = self.non_interactive_mode result["non_interactive_mode"] = self.non_interactive_mode
result["regenerate_thumbnails"] = self.regenerate_thumbnails result["regenerate_thumbnails"] = self.regenerate_thumbnails
result["reread_metadata"] = self.reread_metadata
result["root_directory"] = self.root_directory result["root_directory"] = self.root_directory
result["site_title"] = self.site_title result["site_title"] = self.site_title
result["theme_path"] = self.theme_path result["theme_path"] = self.theme_path
@@ -98,19 +107,24 @@ def parse_arguments(version: str) -> Args:
An instance of the Args class containing the parsed arguments. An instance of the Args class containing the parsed arguments.
""" """
# fmt: off # fmt: off
parser = argparse.ArgumentParser(description="Generate HTML files for a static image hosting website.", formatter_class=RichHelpFormatter) if RICH:
parser = argparse.ArgumentParser(description="Generate HTML files for a static image hosting website.", formatter_class=RichHelpFormatter)
else:
parser = argparse.ArgumentParser(description="Generate HTML files for a static image hosting website.")
parser.add_argument("-a", "--author-name", help="Name of the author of the images.", default=DEFAULT_AUTHOR, type=str, dest="author_name", metavar="AUTHOR") parser.add_argument("-a", "--author-name", help="Name of the author of the images.", default=DEFAULT_AUTHOR, type=str, dest="author_name", metavar="AUTHOR")
parser.add_argument("-e", "--file-extensions", help="File extensions to include (can be specified multiple times).", action="append", dest="file_extensions", metavar="EXTENSION") parser.add_argument("-e", "--file-extensions", help="File extensions to include (can be specified multiple times).", action="append", dest="file_extensions", metavar="EXTENSION")
parser.add_argument("-l", "--license-type", help="Specify the license type for the images.", choices=["cc-zero", "cc-by", "cc-by-sa", "cc-by-nd", "cc-by-nc", "cc-by-nc-sa", "cc-by-nc-nd"], default=None, dest="license_type", metavar="LICENSE") parser.add_argument("-l", "--license-type", help="Specify the license type for the images.", choices=["cc-zero", "cc-by", "cc-by-sa", "cc-by-nd", "cc-by-nc", "cc-by-nc-sa", "cc-by-nc-nd"], default=None, dest="license_type", metavar="LICENSE")
parser.add_argument("-m", "--web-manifest", help="Generate a web manifest file.", action="store_true", default=False, dest="generate_webmanifest") parser.add_argument("-m", "--web-manifest", help="Generate a web manifest file.", action="store_true", default=False, dest="generate_webmanifest")
parser.add_argument("-n", "--non-interactive-mode", help="Run in non-interactive mode, disabling progress bars.", action="store_true", default=False, dest="non_interactive_mode") parser.add_argument("-n", "--non-interactive-mode", help="Run in non-interactive mode, disabling progress bars.", action="store_true", default=False, dest="non_interactive_mode")
parser.add_argument("-p", "--root-directory", help="Root directory containing the images.", required=True, type=str, dest="root_directory", metavar="ROOT") parser.add_argument("-p", "--root-directory", help="Root directory containing the images.", required=True, type=str, dest="root_directory", metavar="ROOT")
parser.add_argument("-r", "--regenerate-thumbnails", help="Regenerate thumbnails even if they already exist.", action="store_true", default=False, dest="regenerate_thumbnails")
parser.add_argument("-t", "--site-title", help="Title of the image hosting site.", required=True, type=str, dest="site_title", metavar="TITLE") parser.add_argument("-t", "--site-title", help="Title of the image hosting site.", required=True, type=str, dest="site_title", metavar="TITLE")
parser.add_argument("-w", "--web-root-url", help="Base URL of the web root for the image hosting site.", required=True, type=str, dest="web_root_url", metavar="URL") parser.add_argument("-w", "--web-root-url", help="Base URL of the web root for the image hosting site.", required=True, type=str, dest="web_root_url", metavar="URL")
parser.add_argument("--exclude-folder", help="Folders to exclude from processing, globs supported (can be specified multiple times).", action="append", dest="exclude_folders", metavar="FOLDER") parser.add_argument("--exclude-folder", help="Folders to exclude from processing, globs supported (can be specified multiple times).", action="append", dest="exclude_folders", metavar="FOLDER")
parser.add_argument("--generate-help-preview", action=HelpPreviewAction, path="help.svg", ) if RICH:
parser.add_argument("--generate-help-preview", action=HelpPreviewAction, path="help.svg", )
parser.add_argument("--ignore-other-files", help="Ignore files that do not match the specified extensions.", action="store_true", default=False, dest="ignore_other_files") parser.add_argument("--ignore-other-files", help="Ignore files that do not match the specified extensions.", action="store_true", default=False, dest="ignore_other_files")
parser.add_argument("--regenerate-thumbnails", help="Regenerate thumbnails even if they already exist.", action="store_true", default=False, dest="regenerate_thumbnails")
parser.add_argument("--reread-metadata", help="Reread image metadata", action="store_true", default=False, dest="reread_metadata")
parser.add_argument("--theme-path", help="Path to the CSS theme file.", default=DEFAULT_THEME_PATH, type=str, dest="theme_path", metavar="PATH") parser.add_argument("--theme-path", help="Path to the CSS theme file.", default=DEFAULT_THEME_PATH, type=str, dest="theme_path", metavar="PATH")
parser.add_argument("--use-fancy-folders", help="Enable fancy folder view instead of the default Apache directory listing.", action="store_true", default=False, dest="use_fancy_folders") parser.add_argument("--use-fancy-folders", help="Enable fancy folder view instead of the default Apache directory listing.", action="store_true", default=False, dest="use_fancy_folders")
parser.add_argument("--version", action="version", version=f"%(prog)s {version}") parser.add_argument("--version", action="version", version=f"%(prog)s {version}")
@@ -125,6 +139,7 @@ def parse_arguments(version: str) -> Args:
license_type=parsed_args.license_type, license_type=parsed_args.license_type,
non_interactive_mode=parsed_args.non_interactive_mode, non_interactive_mode=parsed_args.non_interactive_mode,
regenerate_thumbnails=parsed_args.regenerate_thumbnails, regenerate_thumbnails=parsed_args.regenerate_thumbnails,
reread_metadata=parsed_args.reread_metadata,
root_directory=parsed_args.root_directory, root_directory=parsed_args.root_directory,
site_title=parsed_args.site_title, site_title=parsed_args.site_title,
theme_path=parsed_args.theme_path, theme_path=parsed_args.theme_path,

View File

@@ -1,8 +1,10 @@
import os import os
import re
import urllib.parse import urllib.parse
import fnmatch import fnmatch
import json import json
from typing import Any, Dict, List, Tuple from typing import Any, Dict, List, Tuple
from datetime import datetime
import numpy as np import numpy as np
from tqdm.auto import tqdm from tqdm.auto import tqdm
@@ -89,11 +91,13 @@ def get_image_info(item: str, folder: str) -> Dict[str, Any]:
Returns: Returns:
Dict[str, Any]: A dictionary containing image width, height, and EXIF data. Dict[str, Any]: A dictionary containing image width, height, and EXIF data.
""" """
with Image.open(os.path.join(folder, item)) as img: file = os.path.join(folder, item)
logger.info("extracting image information", extra={"file": item}) with Image.open(file) as img:
logger.info("extracting image information", extra={"file": file})
exif = img.getexif() exif = img.getexif()
width, height = img.size width, height = img.size
if exif: if exif:
logger.info("extracting EXIF data", extra={"file": file})
ifd = exif.get_ifd(ExifTags.IFD.Exif) ifd = exif.get_ifd(ExifTags.IFD.Exif)
exifdatas = dict(exif.items()) | ifd exifdatas = dict(exif.items()) | ifd
exifdata = {} exifdata = {}
@@ -101,7 +105,7 @@ def get_image_info(item: str, folder: str) -> Dict[str, Any]:
tag = ExifTags.TAGS.get(tag_id, tag_id) tag = ExifTags.TAGS.get(tag_id, tag_id)
content = exifdatas.get(tag_id) content = exifdatas.get(tag_id)
if isinstance(content, bytes): if isinstance(content, bytes):
content = content.hex(" ") content = "0x" + content.hex()
if isinstance(content, TiffImagePlugin.IFDRational): if isinstance(content, TiffImagePlugin.IFDRational):
content = content.limit_rational(1000000) content = content.limit_rational(1000000)
if isinstance(content, tuple): if isinstance(content, tuple):
@@ -111,15 +115,22 @@ def get_image_info(item: str, folder: str) -> Dict[str, Any]:
newtuple = newtuple + (i.limit_rational(1000000),) newtuple = newtuple + (i.limit_rational(1000000),)
if newtuple: if newtuple:
content = newtuple content = newtuple
if tag in ["DateTime", "DateTimeOriginal", "DateTimeDigitized"]:
epr = r'\d{4}:\d{2}:\d{2} \d{2}:\d{2}:\d{2}'
if re.match(epr, content):
content = datetime.strptime(content, "%Y:%m:%d %H:%M:%S").strftime("%Y-%m-%d %H:%M:%S")
else:
content = None
exifdata[tag] = content exifdata[tag] = content
if "Orientation" in exifdata and exifdata["Orientation"] in [6, 8]: if "Orientation" in exifdata and exifdata["Orientation"] in [6, 8]:
logger.info("image is rotated", extra={"file": file})
width, height = height, width width, height = height, width
for key in ["PrintImageMatching", "UserComment", "MakerNote"]: for key in ["PrintImageMatching", "UserComment", "MakerNote"]:
if key in exifdata: if key in exifdata:
del exifdata[key] del exifdata[key]
return {"width": width, "height": height, "exifdata": exifdata} return {"width": width, "height": height, "exifdata": exifdata}
else: else:
return {"width": width, "height": height} return {"width": width, "height": height, "exifdata": None}
def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: Dict[str, Dict[str, int]], raw: List[str]) -> Dict[str, Any]: def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: Dict[str, Dict[str, int]], raw: List[str]) -> Dict[str, Any]:
@@ -138,7 +149,7 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: D
Dict[str, Any]: Dictionary containing image details for HTML rendering. Dict[str, Any]: Dictionary containing image details for HTML rendering.
""" """
extsplit = os.path.splitext(item) extsplit = os.path.splitext(item)
if item not in sizelist or _args.regenerate_thumbnails: if item not in sizelist or _args.reread_metadata:
sizelist[item] = get_image_info(item, folder) sizelist[item] = get_image_info(item, folder)
image = { image = {
@@ -147,6 +158,7 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: D
"name": item, "name": item,
"width": sizelist[item]["width"], "width": sizelist[item]["width"],
"height": sizelist[item]["height"], "height": sizelist[item]["height"],
"exifdata": sizelist[item]["exifdata"],
} }
path = os.path.join(_args.root_directory, ".thumbnails", baseurl, item + ".jpg") path = os.path.join(_args.root_directory, ".thumbnails", baseurl, item + ".jpg")
if not os.path.exists(path) or _args.regenerate_thumbnails: if not os.path.exists(path) or _args.regenerate_thumbnails:
@@ -155,12 +167,15 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: D
thumbnails.append((folder, item, _args.root_directory)) thumbnails.append((folder, item, _args.root_directory))
for _raw in raw: for _raw in raw:
if os.path.exists(os.path.join(folder, extsplit[0] + _raw)): file = os.path.join(folder, extsplit[0] + _raw)
url = urllib.parse.quote(extsplit[0]) + _raw if os.path.exists(file):
url = f"{_args.web_root_url}{baseurl}{urllib.parse.quote(extsplit[0])}{_raw}"
if _raw in (".tif", ".tiff"): if _raw in (".tif", ".tiff"):
image["tiff"] = f"{_args.web_root_url}{baseurl}{url}" logger.info("tiff file found", extra={"file": file})
image["tiff"] = url
else: else:
image["raw"] = f"{_args.web_root_url}{baseurl}{url}" logger.info("raw file found", extra={"file": file, "extension": _raw})
image["raw"] = url
return image return image
@@ -299,7 +314,6 @@ def create_html_file(folder: str, title: str, foldername: str, images: List[Dict
""" """
html_file = os.path.join(folder, "index.html") html_file = os.path.join(folder, "index.html")
logger.info("generating html file with jinja2", extra={"path": html_file}) logger.info("generating html file with jinja2", extra={"path": html_file})
image_chunks = np.array_split(images, 8) if images else []
header = os.path.basename(folder) or title header = os.path.basename(folder) or title
parent = None if not foldername else f"{_args.web_root_url}{urllib.parse.quote(foldername.removesuffix(folder.split('/')[-1] + '/'))}" parent = None if not foldername else f"{_args.web_root_url}{urllib.parse.quote(foldername.removesuffix(folder.split('/')[-1] + '/'))}"
if parent and _args.web_root_url.startswith("file://"): if parent and _args.web_root_url.startswith("file://"):
@@ -331,7 +345,7 @@ def create_html_file(folder: str, title: str, foldername: str, images: List[Dict
header=header, header=header,
license=license_info, license=license_info,
subdirectories=subfolders, subdirectories=subfolders,
images=image_chunks, images=images,
info=_info, info=_info,
allimages=images, allimages=images,
webmanifest=_args.generate_webmanifest, webmanifest=_args.generate_webmanifest,

View File

@@ -48,14 +48,14 @@ def log_format(keys):
def rotate_log_file(compress=False): def rotate_log_file(compress=False):
""" """
Renames the existing 'latest.jsonl' file to a timestamped file based on the first log entry's asctime. Truncates the 'latest.jsonl' file after optionally compressing its contents to a timestamped file.
Optionally compresses the old log file using gzip. The 'latest.jsonl' file is not deleted or moved, just emptied.
Args: Args:
compress (bool): If True, compress the old log file using gzip. compress (bool): If True, compress the old log file using gzip.
""" """
if os.path.exists(LATEST_LOG_FILE): if os.path.exists(LATEST_LOG_FILE):
with open(LATEST_LOG_FILE, "r", encoding="utf-8") as f: with open(LATEST_LOG_FILE, "r+", encoding="utf-8") as f:
first_line = f.readline() first_line = f.readline()
try: try:
first_log = json.loads(first_line) first_log = json.loads(first_line)
@@ -64,16 +64,23 @@ def rotate_log_file(compress=False):
except (json.JSONDecodeError, KeyError): except (json.JSONDecodeError, KeyError):
first_timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") first_timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
safe_timestamp = first_timestamp.replace(":", "-").replace(" ", "_") safe_timestamp = first_timestamp.replace(":", "-").replace(" ", "_")
old_log_filename = os.path.join(LOG_DIR, f"{safe_timestamp}.jsonl") old_log_filename = os.path.join(LOG_DIR, f"{safe_timestamp}.jsonl")
os.rename(LATEST_LOG_FILE, old_log_filename) # Write contents to the new file
with open(old_log_filename, "w", encoding="utf-8") as old_log_file:
f.seek(0) # Go back to the beginning of the file
shutil.copyfileobj(f, old_log_file)
if compress: if compress:
with open(old_log_filename, "rb") as f_in: with open(old_log_filename, "rb") as f_in:
with gzip.open(f"{old_log_filename}.gz", "wb") as f_out: with gzip.open(f"{old_log_filename}.gz", "wb") as f_out:
shutil.copyfileobj(f_in, f_out) shutil.copyfileobj(f_in, f_out)
os.remove(old_log_filename) os.remove(old_log_filename)
# Truncate the original file
f.seek(0)
f.truncate()
def setup_logger(level=logging.INFO): def setup_logger(level=logging.INFO):

View File

@@ -178,8 +178,8 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List
List[Icon] List[Icon]
List of icons created from the SVG file. List of icons created from the SVG file.
""" """
logger.info("creating icons for web application", extra={"iconspath": iconspath})
svg = [file for file in files if file.endswith(".svg")][0] svg = [file for file in files if file.endswith(".svg")][0]
logger.info("creating icons for web application", extra={"iconspath": iconspath, "svg": svg})
icon_list = [ icon_list = [
{"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "maskable"}, {"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "maskable"},
{"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "any"}, {"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "any"},
@@ -188,7 +188,7 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List
tmpimg = BytesIO() tmpimg = BytesIO()
sizes = size.split("x") sizes = size.split("x")
iconpath = os.path.join(iconspath, os.path.splitext(svg)[0] + "-" + size + ".png") iconpath = os.path.join(iconspath, os.path.splitext(svg)[0] + "-" + size + ".png")
logger.info("converting svg to png", extra={"iconpath": iconpath, "size": size}) logger.info("converting svg to png", extra={"svg": svg, "size": size})
cairosvg.svg2png( cairosvg.svg2png(
url=os.path.join(iconspath, svg), url=os.path.join(iconspath, svg),
write_to=tmpimg, write_to=tmpimg,
@@ -240,7 +240,7 @@ def create_icons_from_png(iconspath: str, web_root_url: str) -> List[Icon]:
continue continue
with Image.open(os.path.join(iconspath, icon)) as iconfile: with Image.open(os.path.join(iconspath, icon)) as iconfile:
iconsize = f"{iconfile.size[0]}x{iconfile.size[1]}" iconsize = f"{iconfile.size[0]}x{iconfile.size[1]}"
logger.info("using icon", extra={"icon": icon, "size": iconsize}) logger.info("using icon", extra={"iconspath": iconspath, "icon": icon, "size": iconsize})
icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "maskable"}) icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "maskable"})
icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "any"}) icon_list.append({"src": f"{web_root_url}.static/icons/{icon}", "sizes": iconsize, "type": "image/png", "purpose": "any"})
return icon_list return icon_list
@@ -255,6 +255,8 @@ def webmanifest(_args: Args) -> None:
_args : Args _args : Args
Parsed command-line arguments. Parsed command-line arguments.
""" """
logger.info("generating webmanifest")
iconspath = os.path.join(_args.root_directory, ".static", "icons") iconspath = os.path.join(_args.root_directory, ".static", "icons")
files = os.listdir(iconspath) files = os.listdir(iconspath)
icon_list = create_icons_from_svg(files, iconspath, _args) if SVGSUPPORT and any(file.endswith(".svg") for file in files) else create_icons_from_png(iconspath, _args.web_root_url) icon_list = create_icons_from_svg(files, iconspath, _args) if SVGSUPPORT and any(file.endswith(".svg") for file in files) else create_icons_from_png(iconspath, _args.web_root_url)

View File

@@ -56,9 +56,8 @@
{% if images %} {% if images %}
{%- set ns = namespace(count = 0) -%} {%- set ns = namespace(count = 0) -%}
<div class="row"> <div class="row">
{%- for imageblock in images %} {%- for image in images %}
<div class="column"> <div class="column">
{%- for image in imageblock %}
<figure> <figure>
<img src="{{ image.thumbnail }}" alt="{{ image.name }}" onclick="openSwipe({{ ns.count }})" /> <img src="{{ image.thumbnail }}" alt="{{ image.name }}" onclick="openSwipe({{ ns.count }})" />
{%- set ns.count = ns.count + 1 %} {%- set ns.count = ns.count + 1 %}
@@ -71,7 +70,6 @@
{%- endif %} {%- endif %}
</figcaption> </figcaption>
</figure> </figure>
{%- endfor %}
</div> </div>
{%- endfor %} {%- endfor %}
</div> </div>
@@ -147,7 +145,11 @@
var pswpElement = document.querySelectorAll('.pswp')[0]; var pswpElement = document.querySelectorAll('.pswp')[0];
var items = [ var items = [
{%- for image in allimages %} {%- for image in allimages %}
{%- if image.exifdata.DateTime %}
{ src: "{{ image.url }}", w: {{ image.width }}, h: {{ image.height }}, msrc: "{{ image.thumbnail }}", title: "Captured: {{ image.exifdata.DateTime }}" },
{%- else %}
{ src: "{{ image.url }}", w: {{ image.width }}, h: {{ image.height }}, msrc: "{{ image.thumbnail }}" }, { src: "{{ image.url }}", w: {{ image.width }}, h: {{ image.height }}, msrc: "{{ image.thumbnail }}" },
{%- endif %}
{%- endfor %} {%- endfor %}
]; ];
var re = /pid=(\d+)/; var re = /pid=(\d+)/;
@@ -178,8 +180,7 @@
} }
function topFunction() { function topFunction() {
document.body.scrollTop = 0; window.scrollTo({ top: 0, behavior: 'smooth' })
document.documentElement.scrollTop = 0;
} }
</script> </script>
{%- endif %} {%- endif %}

BIN
test/example/DSC03508.ARW Executable file

Binary file not shown.

4
view-latest-log.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
LESS=-SR hl logs/latest.jsonl --config hl_config.yaml

3
view-logs.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
LESS=-SR hl $(ls -tr logs/*.{jsonl,jsonl.gz}) --config hl_config.yaml