added json logger

This commit is contained in:
2024-09-16 22:18:36 +02:00
parent a95a6e1722
commit 8ec9701aa9
30 changed files with 183 additions and 15 deletions

3
.gitignore vendored
View File

@@ -166,4 +166,5 @@ test/.thumbnails
test/**/index.html test/**/index.html
test/**/.sizelist.json test/**/.sizelist.json
test/manifest.json test/manifest.json
themes/previews themes/previews
logs

View File

@@ -1 +1 @@
2.2.7 2.3.0

View File

@@ -11,10 +11,12 @@ from typing import Dict, List, Tuple
from tqdm.auto import tqdm from tqdm.auto import tqdm
from PIL import Image, ImageOps from PIL import Image, ImageOps
from modules.logger import logger
from modules.argumentparser import parse_arguments, Args from modules.argumentparser import parse_arguments, Args
from modules.svg_handling import icons, webmanifest, extract_colorscheme from modules.svg_handling import icons, webmanifest, extract_colorscheme
from modules.generate_html import list_folder, EXCLUDES from modules.generate_html import list_folder, EXCLUDES
# fmt: off # fmt: off
# Constants # Constants
if __package__ is None: if __package__ is None:
@@ -32,6 +34,7 @@ RAW_EXTENSIONS = [
] ]
IMG_EXTENSIONS = [".jpg", ".jpeg", ".png"] IMG_EXTENSIONS = [".jpg", ".jpeg", ".png"]
NOT_LIST = ["*/Galleries/*", "Archives"] NOT_LIST = ["*/Galleries/*", "Archives"]
LOG_FILE = os.path.join(SCRIPTDIR, "log.json")
# fmt: on # fmt: on
pbardict: Dict[str, tqdm] = {} pbardict: Dict[str, tqdm] = {}
@@ -77,10 +80,13 @@ def copy_static_files(_args: Args) -> None:
static_dir = os.path.join(_args.root_directory, ".static") static_dir = os.path.join(_args.root_directory, ".static")
if os.path.exists(static_dir): if os.path.exists(static_dir):
print("Removing existing .static folder...") print("Removing existing .static folder...")
logger.info("removing existing .static folder")
shutil.rmtree(static_dir) shutil.rmtree(static_dir)
print("Copying static files...") print("Copying static files...")
logger.info("copying static files")
shutil.copytree(STATIC_FILES_DIR, static_dir, dirs_exist_ok=True) shutil.copytree(STATIC_FILES_DIR, static_dir, dirs_exist_ok=True)
logger.info("reading theme file", extra={"theme": _args.theme_path})
with open(_args.theme_path, "r", encoding="utf-8") as f: with open(_args.theme_path, "r", encoding="utf-8") as f:
theme = f.read() theme = f.read()
split = theme.split(".foldericon {") split = theme.split(".foldericon {")
@@ -92,20 +98,26 @@ def copy_static_files(_args: Args) -> None:
for match in re.finditer(r"content: (.*);", foldericon): for match in re.finditer(r"content: (.*);", foldericon):
foldericon = match[1] foldericon = match[1]
foldericon = foldericon.replace('"', "") foldericon = foldericon.replace('"', "")
logger.info("found foldericon", extra={"foldericon": foldericon})
break break
if "url" in foldericon: if "url" in foldericon:
shutil.copyfile(_args.theme_path, os.path.join(static_dir, "theme.css")) shutil.copyfile(_args.theme_path, os.path.join(static_dir, "theme.css"))
logger.info("foldericon in theme file, using it")
return return
with open(os.path.join(SCRIPTDIR, foldericon), "r", encoding="utf-8") as f: with open(os.path.join(SCRIPTDIR, foldericon), "r", encoding="utf-8") as f:
logger.info("Reading foldericon svg")
svg = f.read() svg = f.read()
if "svg.j2" in foldericon: if "svg.j2" in foldericon:
logger.info("foldericon in theme file is a jinja2 template")
colorscheme = extract_colorscheme(_args.theme_path) colorscheme = extract_colorscheme(_args.theme_path)
svg = svg.replace("{{ color1 }}", colorscheme["color1"]) svg = svg.replace("{{ color1 }}", colorscheme["color1"])
svg = svg.replace("{{ color2 }}", colorscheme["color2"]) svg = svg.replace("{{ color2 }}", colorscheme["color2"])
svg = svg.replace("{{ color3 }}", colorscheme["color3"]) svg = svg.replace("{{ color3 }}", colorscheme["color3"])
svg = svg.replace("{{ color4 }}", colorscheme["color4"]) svg = svg.replace("{{ color4 }}", colorscheme["color4"])
logger.info("replaced colors in svg")
svg = urllib.parse.quote(svg) svg = urllib.parse.quote(svg)
with open(os.path.join(static_dir, "theme.css"), "x", encoding="utf-8") as f: with open(os.path.join(static_dir, "theme.css"), "x", encoding="utf-8") as f:
logger.info("writing theme file")
f.write(themehead + '\n.foldericon {\n content: url("data:image/svg+xml,' + svg + '");\n}\n' + themetail) f.write(themehead + '\n.foldericon {\n content: url("data:image/svg+xml,' + svg + '");\n}\n' + themetail)
@@ -119,6 +131,7 @@ def generate_thumbnail(arguments: Tuple[str, str, str]) -> None:
A tuple containing the folder, item, root directory, and regenerate thumbnails flag. A tuple containing the folder, item, root directory, and regenerate thumbnails flag.
""" """
folder, item, root_directory = arguments folder, item, root_directory = arguments
image = os.path.join(folder, item)
path = os.path.join(root_directory, ".thumbnails", folder.removeprefix(root_directory), item) + ".jpg" path = os.path.join(root_directory, ".thumbnails", folder.removeprefix(root_directory), item) + ".jpg"
oldpath = os.path.join(root_directory, ".thumbnails", folder.removeprefix(root_directory), os.path.splitext(item)[0]) + ".jpg" oldpath = os.path.join(root_directory, ".thumbnails", folder.removeprefix(root_directory), os.path.splitext(item)[0]) + ".jpg"
if os.path.exists(oldpath): if os.path.exists(oldpath):
@@ -127,14 +140,19 @@ def generate_thumbnail(arguments: Tuple[str, str, str]) -> None:
except FileNotFoundError: except FileNotFoundError:
pass pass
if not os.path.exists(path): if not os.path.exists(path):
logger.info("generating thumbnail for %s", item, extra={"path": image})
try: try:
with Image.open(os.path.join(folder, item)) as imgfile: with Image.open(image) as imgfile:
imgrgb = imgfile.convert("RGB") imgrgb = imgfile.convert("RGB")
img = ImageOps.exif_transpose(imgrgb) img = ImageOps.exif_transpose(imgrgb)
img.thumbnail((512, 512)) img.thumbnail((512, 512))
img.save(path, "JPEG", quality=75, optimize=True, mode="RGB") img.save(path, "JPEG", quality=75, optimize=True, mode="RGB")
except OSError: except OSError:
print(f"Failed to generate thumbnail for {os.path.join(folder, item)}") logger.error("Failed to generate thumbnail for %s", item, extra={"path": image})
print(f"Failed to generate thumbnail for {image}")
return
else:
logger.info("thumbnail already exists for %s", item, extra={"path": image})
def get_total_folders(folder: str, _args: Args, _total: int = 0) -> int: def get_total_folders(folder: str, _args: Args, _total: int = 0) -> int:
@@ -163,6 +181,7 @@ def get_total_folders(folder: str, _args: Args, _total: int = 0) -> int:
for item in items: for item in items:
if item not in EXCLUDES and os.path.isdir(os.path.join(folder, item)) and not item.startswith("."): if item not in EXCLUDES and os.path.isdir(os.path.join(folder, item)) and not item.startswith("."):
if item not in _args.exclude_folders and not any(fnmatch.fnmatchcase(os.path.join(folder, item), exclude) for exclude in _args.exclude_folders): if item not in _args.exclude_folders and not any(fnmatch.fnmatchcase(os.path.join(folder, item), exclude) for exclude in _args.exclude_folders):
logger.debug("Found folder %s in %s", item, folder)
_total = get_total_folders(os.path.join(folder, item), _args, _total) _total = get_total_folders(os.path.join(folder, item), _args, _total)
return _total return _total
@@ -179,12 +198,15 @@ 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")
exit() exit()
try: try:
Path(lock_file).touch() Path(lock_file).touch()
if args.regenerate_thumbnails: if args.regenerate_thumbnails:
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")):
logger.info("removing old thumbnails folder")
shutil.rmtree(os.path.join(args.root_directory, ".thumbnails")) shutil.rmtree(os.path.join(args.root_directory, ".thumbnails"))
os.makedirs(os.path.join(args.root_directory, ".thumbnails"), exist_ok=True) os.makedirs(os.path.join(args.root_directory, ".thumbnails"), exist_ok=True)
@@ -192,17 +214,21 @@ 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)
if args.non_interactive_mode: if args.non_interactive_mode:
logger.info("generating HTML files")
print("Generating HTML files...") print("Generating HTML files...")
thumbnails = list_folder(0, args.root_directory, args.site_title, args, raw, VERSION) thumbnails = list_folder(0, args.root_directory, args.site_title, args, raw, VERSION)
with Pool(os.cpu_count()) as pool: with Pool(os.cpu_count()) as pool:
logger.info("generating thumbnails")
print("Generating thumbnails...") print("Generating thumbnails...")
pool.map(generate_thumbnail, thumbnails) pool.map(generate_thumbnail, thumbnails)
else: else:
pbardict["traversingbar"] = tqdm(desc="Traversing filesystem", unit="folders", ascii=True, dynamic_ncols=True) pbardict["traversingbar"] = tqdm(desc="Traversing filesystem", unit="folders", ascii=True, dynamic_ncols=True)
logger.info("getting total number of folders to process")
total = get_total_folders(args.root_directory, args) total = get_total_folders(args.root_directory, args)
pbardict["traversingbar"].desc = "Traversing filesystem" pbardict["traversingbar"].desc = "Traversing filesystem"
pbardict["traversingbar"].update(0) pbardict["traversingbar"].update(0)
@@ -211,6 +237,7 @@ def main() -> None:
thumbnails = list_folder(total, args.root_directory, args.site_title, args, raw, VERSION) thumbnails = list_folder(total, args.root_directory, args.site_title, args, raw, VERSION)
with Pool(os.cpu_count()) as pool: with Pool(os.cpu_count()) as pool:
logger.info("generating thumbnails")
for _ in tqdm( for _ in tqdm(
pool.imap_unordered(generate_thumbnail, thumbnails), pool.imap_unordered(generate_thumbnail, thumbnails),
total=len(thumbnails), total=len(thumbnails),

View File

@@ -4,6 +4,8 @@ import os
import argparse import argparse
from rich_argparse import RichHelpFormatter, HelpPreviewAction from rich_argparse import RichHelpFormatter, HelpPreviewAction
from modules.logger import logger
if __package__ is None: if __package__ is None:
PACKAGE = "" PACKAGE = ""
else: else:
@@ -129,4 +131,5 @@ def parse_arguments(version: str) -> Args:
use_fancy_folders=parsed_args.use_fancy_folders, use_fancy_folders=parsed_args.use_fancy_folders,
web_root_url=parsed_args.web_root_url, web_root_url=parsed_args.web_root_url,
) )
logger.debug("parsed arguments", extra={"args": _args.to_dict()})
return _args return _args

View File

@@ -2,6 +2,8 @@ import re
import colorsys import colorsys
from typing import Dict from typing import Dict
from modules.logger import logger
def extract_colorscheme(theme_path: str) -> Dict[str, str]: def extract_colorscheme(theme_path: str) -> Dict[str, str]:
""" """
@@ -17,6 +19,7 @@ def extract_colorscheme(theme_path: str) -> Dict[str, str]:
Dict[str, str] Dict[str, str]
Dictionary containing color scheme variables and their hexadecimal values. Dictionary containing color scheme variables and their hexadecimal values.
""" """
logger.info("extracting color scheme from theme file", extra={"theme_path": theme_path})
pattern = r"--(color[1-4]|bcolor1):\s*(#[0-9a-fA-F]+|rgba?\([^)]*\)|hsla?\([^)]*\)|[a-zA-Z]+);" pattern = r"--(color[1-4]|bcolor1):\s*(#[0-9a-fA-F]+|rgba?\([^)]*\)|hsla?\([^)]*\)|[a-zA-Z]+);"
colorscheme = {} colorscheme = {}
@@ -30,6 +33,8 @@ def extract_colorscheme(theme_path: str) -> Dict[str, str]:
color_value = match[1] color_value = match[1]
hex_color_value = css_color_to_hex(color_value) hex_color_value = css_color_to_hex(color_value)
colorscheme[variable_name] = hex_color_value colorscheme[variable_name] = hex_color_value
logger.debug("extracted variable", extra={"variable": variable_name, "value": hex_color_value})
logger.info("extracted color scheme", extra={"colorscheme": colorscheme})
return colorscheme return colorscheme
@@ -86,10 +91,12 @@ def css_color_to_hex(css_color: str) -> str:
# Helper function to convert RGB tuple to hexadecimal string # Helper function to convert RGB tuple to hexadecimal string
def rgb_to_hex(rgb: tuple[int, int, int]) -> str: def rgb_to_hex(rgb: tuple[int, int, int]) -> str:
logger.debug("converting rgb tuple to hex string", extra={"rgb": rgb})
return "#{:02x}{:02x}{:02x}".format(*rgb) return "#{:02x}{:02x}{:02x}".format(*rgb)
# Helper function to convert HSL tuple to RGB tuple # Helper function to convert HSL tuple to RGB tuple
def hsl_to_rgb(hsl: tuple[int, float, float]) -> tuple[int, int, int]: def hsl_to_rgb(hsl: tuple[int, float, float]) -> tuple[int, int, int]:
logger.debug("converting hsl tuple to rgb tuple", extra={"hsl": hsl})
return tuple(round(c * 255) for c in colorsys.hls_to_rgb(hsl[0] / 360, hsl[1] / 100, hsl[2] / 100)) return tuple(round(c * 255) for c in colorsys.hls_to_rgb(hsl[0] / 360, hsl[1] / 100, hsl[2] / 100))
# Regular expression pattern to match CSS colors # Regular expression pattern to match CSS colors
@@ -103,6 +110,7 @@ def css_color_to_hex(css_color: str) -> str:
match = color_pattern.match(css_color.strip()) match = color_pattern.match(css_color.strip())
if not match: if not match:
logger.error("invalid CSS color format", extra={"css_color": css_color})
raise ValueError("Invalid CSS color format") raise ValueError("Invalid CSS color format")
groups = match.groupdict() groups = match.groupdict()
@@ -119,8 +127,10 @@ def css_color_to_hex(css_color: str) -> str:
b = int(groups["b"].rstrip("%")) * 255 // 100 if "%" in groups["b"] else int(groups["b"]) b = int(groups["b"].rstrip("%")) * 255 // 100 if "%" in groups["b"] else int(groups["b"])
a = float(groups["a"]) if groups["a"] else 1.0 a = float(groups["a"]) if groups["a"] else 1.0
if a < 1.0: if a < 1.0:
logger.debug("converting rgba color to hex", extra={"color": css_color, "r": r, "g": g, "b": b, "a": a})
return rgb_to_hex((r, g, b)) + "{:02x}".format(round(a * 255)) return rgb_to_hex((r, g, b)) + "{:02x}".format(round(a * 255))
else: else:
logger.debug("converting rgb color to hex", extra={"color": css_color, "r": r, "g": g, "b": b})
return rgb_to_hex((r, g, b)) return rgb_to_hex((r, g, b))
elif groups["hsl"]: elif groups["hsl"]:
@@ -130,8 +140,10 @@ def css_color_to_hex(css_color: str) -> str:
a = float(groups["a"]) if groups["a"] else 1.0 a = float(groups["a"]) if groups["a"] else 1.0
rgb_color = hsl_to_rgb((h, s, l)) rgb_color = hsl_to_rgb((h, s, l))
if a < 1.0: if a < 1.0:
logger.debug("converting hsla color to hex", extra={"color": css_color, "hsl": (h, s, l), "a": a})
return rgb_to_hex(rgb_color) + "{:02x}".format(round(a * 255)) return rgb_to_hex(rgb_color) + "{:02x}".format(round(a * 255))
else: else:
logger.debug("converting hsl color to hex", extra={"color": css_color, "hsl": (h, s, l)})
return rgb_to_hex(rgb_color) return rgb_to_hex(rgb_color)
# fmt: off # fmt: off
@@ -182,7 +194,9 @@ def css_color_to_hex(css_color: str) -> str:
'turquoise': '#40e0d0', 'violet': '#ee82ee', 'wheat': '#f5deb3', 'white': '#ffffff', 'turquoise': '#40e0d0', 'violet': '#ee82ee', 'wheat': '#f5deb3', 'white': '#ffffff',
'whitesmoke': '#f5f5f5', 'yellow': '#ffff00', 'yellowgreen': '#9acd32' 'whitesmoke': '#f5f5f5', 'yellow': '#ffff00', 'yellowgreen': '#9acd32'
} }
logger.debug("parsing css color string", extra={"css_color": css_color})
return named_colors[groups['name'].lower()] return named_colors[groups['name'].lower()]
# fmt: on # fmt: on
logger.error("invalid CSS color format", extra={"css_color": css_color})
raise ValueError("Invalid CSS color format") raise ValueError("Invalid CSS color format")

View File

@@ -9,6 +9,7 @@ from tqdm.auto import tqdm
from PIL import Image, ExifTags, TiffImagePlugin from PIL import Image, ExifTags, TiffImagePlugin
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from modules.logger import logger
import modules.cclicense as cclicense import modules.cclicense as cclicense
from modules.argumentparser import Args from modules.argumentparser import Args
@@ -45,12 +46,15 @@ def initialize_sizelist(folder: str) -> Dict[str, Dict[str, int]]:
sizelist = {} sizelist = {}
sizelist_path = os.path.join(folder, ".sizelist.json") sizelist_path = os.path.join(folder, ".sizelist.json")
if not os.path.exists(sizelist_path): if not os.path.exists(sizelist_path):
logger.info("creating new size list file", extra={"file": sizelist_path})
with open(sizelist_path, "x", encoding="utf-8") as sizelistfile: with open(sizelist_path, "x", encoding="utf-8") as sizelistfile:
sizelistfile.write("{}") sizelistfile.write("{}")
with open(sizelist_path, "r+", encoding="utf-8") as sizelistfile: with open(sizelist_path, "r+", encoding="utf-8") as sizelistfile:
logger.info("reading size list file", extra={"file": sizelist_path})
try: try:
sizelist = json.loads(sizelistfile.read()) sizelist = json.loads(sizelistfile.read())
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
logger.warning("invalid JSON in size list file", extra={"file": sizelist_path})
sizelist = {} sizelist = {}
return sizelist return sizelist
@@ -64,11 +68,13 @@ def update_sizelist(sizelist: Dict[str, Dict[str, Any]], folder: str) -> None:
folder (str): The folder in which the size list file is located. folder (str): The folder in which the size list file is located.
""" """
sizelist_path = os.path.join(folder, ".sizelist.json") sizelist_path = os.path.join(folder, ".sizelist.json")
if sizelist != {}: if sizelist:
with open(sizelist_path, "w", encoding="utf-8") as sizelistfile: with open(sizelist_path, "w", encoding="utf-8") as sizelistfile:
logger.info("writing size list file", extra={"file": sizelist_path})
sizelistfile.write(json.dumps(sizelist, indent=4)) sizelistfile.write(json.dumps(sizelist, indent=4))
else: else:
if os.path.exists(sizelist_path): if os.path.exists(sizelist_path):
logger.info("deleting empty size list file", extra={"file": sizelist_path})
os.remove(sizelist_path) os.remove(sizelist_path)
@@ -84,11 +90,12 @@ def get_image_info(item: str, folder: str) -> Dict[str, Any]:
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: with Image.open(os.path.join(folder, item)) as img:
logger.info("extracting image information", extra={"file": item})
exif = img.getexif() exif = img.getexif()
width, height = img.size width, height = img.size
if exif: if exif:
ifd = exif.get_ifd(ExifTags.IFD.Exif) ifd = exif.get_ifd(ExifTags.IFD.Exif)
exifdatas = {key: val for key, val in exif.items()} | ifd exifdatas = dict(exif.items()) | ifd
exifdata = {} exifdata = {}
for tag_id in exifdatas: for tag_id in exifdatas:
tag = ExifTags.TAGS.get(tag_id, tag_id) tag = ExifTags.TAGS.get(tag_id, tag_id)
@@ -167,6 +174,10 @@ def generate_html(folder: str, title: str, _args: Args, raw: List[str], version:
_args (Args): Parsed command line arguments. _args (Args): Parsed command line arguments.
raw (List[str]): Raw image file names. raw (List[str]): Raw image file names.
""" """
if _args.regenerate_thumbnails:
if os.path.exists(os.path.join(folder, ".sizelist.json")):
logger.info("removing .sizelist.json", extra={"folder": folder})
os.remove(os.path.join(folder, ".sizelist.json"))
sizelist = initialize_sizelist(folder) sizelist = initialize_sizelist(folder)
items = sorted(os.listdir(folder)) items = sorted(os.listdir(folder))
@@ -205,6 +216,7 @@ def generate_html(folder: str, title: str, _args: Args, raw: List[str], version:
create_html_file(folder, title, foldername, images, subfolders, _args, version) create_html_file(folder, title, foldername, images, subfolders, _args, version)
else: else:
if os.path.exists(os.path.join(folder, "index.html")): if os.path.exists(os.path.join(folder, "index.html")):
logger.info("removing existing index.html", extra={"folder": folder})
os.remove(os.path.join(folder, "index.html")) os.remove(os.path.join(folder, "index.html"))
if not _args.non_interactive_mode: if not _args.non_interactive_mode:
@@ -221,6 +233,7 @@ def create_thumbnail_folder(foldername: str, root_directory: str) -> None:
""" """
thumbnails_path = os.path.join(root_directory, ".thumbnails", foldername) thumbnails_path = os.path.join(root_directory, ".thumbnails", foldername)
if not os.path.exists(thumbnails_path): if not os.path.exists(thumbnails_path):
logger.info("creating thumbnail folder", extra={"path": thumbnails_path})
os.mkdir(thumbnails_path) os.mkdir(thumbnails_path)
@@ -252,6 +265,7 @@ def process_info_file(folder: str, item: str) -> None:
item (str): The info file name. item (str): The info file name.
""" """
with open(os.path.join(folder, item), encoding="utf-8") as f: with open(os.path.join(folder, item), encoding="utf-8") as f:
logger.info("processing info file", extra={"path": os.path.join(folder, item)})
info[urllib.parse.quote(folder)] = f.read() info[urllib.parse.quote(folder)] = f.read()
@@ -281,6 +295,8 @@ def create_html_file(folder: str, title: str, foldername: str, images: List[Dict
subfolders (List[Dict[str, str]]): A list of subfolders to include in the HTML. subfolders (List[Dict[str, str]]): A list of subfolders to include in the HTML.
_args (Args): Parsed command line arguments. _args (Args): Parsed command line arguments.
""" """
html_file = os.path.join(folder, "index.html")
logger.info("generating html file with jinja2", extra={"path": html_file})
image_chunks = np.array_split(images, 8) if images else [] 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] + '/'))}"
@@ -320,7 +336,8 @@ def create_html_file(folder: str, title: str, foldername: str, images: List[Dict
version=version, version=version,
) )
with open(os.path.join(folder, "index.html"), "w", encoding="utf-8") as f: with open(html_file, "w", encoding="utf-8") as f:
logger.info("writing html file", extra={"path": html_file})
f.write(content) f.write(content)

96
modules/logger.py Normal file
View File

@@ -0,0 +1,96 @@
"""
loggerdabn.py
This module provides functionality for setting up a centralized logging system using the
`logging` library and the `python-json-logger` to output logs in JSON format. It handles
log rotation by renaming old log files and saving them based on the first timestamp entry.
Functions:
- log_format(keys): Generates the logging format string based on the list of keys.
- rotate_log_file(): Handles renaming the existing log file to a timestamp-based name.
- setup_logger(): Configures the logging system, applies a JSON format, and returns a logger instance.
"""
import logging
import os
import json
from datetime import datetime
from pythonjsonlogger import jsonlogger
# Constants for file paths and exclusions
if __package__ is None:
PACKAGE = ""
else:
PACKAGE = __package__
SCRIPTDIR = os.path.abspath(os.path.dirname(__file__).removesuffix(PACKAGE))
LOG_DIR = os.path.join(SCRIPTDIR, "logs")
LATEST_LOG_FILE = os.path.join(LOG_DIR, "latest.jsonl")
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
def log_format(keys):
"""
Generates a list of format strings based on the given keys.
Args:
keys (list): A list of string keys that represent the log attributes (e.g., 'asctime', 'levelname').
Returns:
list: A list of formatted strings for each key, in the format "%(key)s".
"""
return [f"%({i})s" for i in keys]
def rotate_log_file():
"""
Renames the existing 'latest.jsonl' file to a timestamped file based on the first log entry's asctime.
If 'latest.jsonl' exists, it's renamed to the first timestamp found in the log entry.
"""
if os.path.exists(LATEST_LOG_FILE):
with open(LATEST_LOG_FILE, "r", encoding="utf-8") as f:
first_line = f.readline()
try:
first_log = json.loads(first_line)
first_timestamp = first_log.get("asctime")
first_timestamp = first_timestamp.split(",")[0]
except (json.JSONDecodeError, KeyError):
first_timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
safe_timestamp = first_timestamp.replace(":", "-").replace(" ", "_")
old_log_filename = os.path.join(LOG_DIR, f"{safe_timestamp}.jsonl")
os.rename(LATEST_LOG_FILE, old_log_filename)
def setup_logger():
"""
Configures the logging system with a custom format and outputs logs in JSON format.
The logger will write to the 'logs/latest.jsonl' file, and it will include
multiple attributes such as the time of logging, the filename, function name, log level, etc.
If 'latest.jsonl' already exists, it will be renamed to a timestamped file before creating a new one.
Returns:
logging.Logger: A configured logger instance that can be used to log messages.
"""
rotate_log_file()
app_logger = logging.getLogger()
supported_keys = ["asctime", "created", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "name", "pathname", "process", "processName", "relativeCreated", "thread", "threadName", "taskName"]
custom_format = " ".join(log_format(supported_keys))
formatter = jsonlogger.JsonFormatter(custom_format)
log_handler = logging.FileHandler(LATEST_LOG_FILE)
log_handler.setFormatter(formatter)
app_logger.addHandler(log_handler)
app_logger.setLevel(logging.INFO)
return app_logger
logger = setup_logger()

View File

@@ -13,6 +13,7 @@ try:
except ImportError: except ImportError:
SVGSUPPORT = False SVGSUPPORT = False
from modules.logger import logger
from modules.argumentparser import Args from modules.argumentparser import Args
from modules.css_color import extract_theme_color, extract_colorscheme from modules.css_color import extract_theme_color, extract_colorscheme
@@ -55,6 +56,7 @@ def render_svg_icon(colorscheme: Dict[str, str], iconspath: str) -> str:
svg = env.get_template("icon.svg.j2") svg = env.get_template("icon.svg.j2")
content = svg.render(colorscheme=colorscheme) content = svg.render(colorscheme=colorscheme)
with open(os.path.join(iconspath, "icon.svg"), "w+", encoding="utf-8") as f: with open(os.path.join(iconspath, "icon.svg"), "w+", encoding="utf-8") as f:
logger.info("writing svg icon", extra={"iconspath": iconspath})
f.write(content) f.write(content)
return content return content
@@ -73,6 +75,7 @@ def save_png_icon(content: str, iconspath: str) -> None:
tmpimg = BytesIO() tmpimg = BytesIO()
cairosvg.svg2png(bytestring=content, write_to=tmpimg) cairosvg.svg2png(bytestring=content, write_to=tmpimg)
with Image.open(tmpimg) as iconfile: with Image.open(tmpimg) as iconfile:
logger.info("saving png icon", extra={"iconspath": iconspath})
iconfile.save(os.path.join(iconspath, "icon.png")) iconfile.save(os.path.join(iconspath, "icon.png"))
@@ -87,9 +90,11 @@ def generate_favicon(iconspath: str, root_directory: str) -> None:
root_directory : str root_directory : str
Root directory of the project where the favicon will be saved. Root directory of the project where the favicon will be saved.
""" """
command = f'magick {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {os.path.join(root_directory, ".static", "favicon.ico")}' favicon = os.path.join(root_directory, ".static", "favicon.ico")
logger.info("generating favicon with imagemagick", extra={"iconspath": iconspath, "favicon": favicon})
command = f'magick {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {favicon}'
if not shutil.which("magick"): if not shutil.which("magick"):
command = f'convert {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {os.path.join(root_directory, ".static", "favicon.ico")}' command = f'convert {os.path.join(iconspath, "icon.png")} -define icon:auto-resize=16,32,48,64,72,96,144,192 {favicon}'
os.system(command) os.system(command)
@@ -102,12 +107,14 @@ def icons(_args: Args) -> None:
_args : Args _args : Args
Parsed command-line arguments. Parsed command-line arguments.
""" """
print("Generating icons...")
iconspath = os.path.join(_args.root_directory, ".static", "icons") iconspath = os.path.join(_args.root_directory, ".static", "icons")
logger.info("generating icons", extra={"iconspath": iconspath})
print("Generating icons...")
colorscheme = extract_colorscheme(_args.theme_path) colorscheme = extract_colorscheme(_args.theme_path)
content = render_svg_icon(colorscheme, iconspath) content = render_svg_icon(colorscheme, iconspath)
if not SVGSUPPORT: if not SVGSUPPORT:
print("Please install cairosvg to generate favicon from svg icon.") print("Please install cairosvg to generate favicon from svg icon.")
logger.error("svg support not available")
return return
save_png_icon(content, iconspath) save_png_icon(content, iconspath)
generate_favicon(iconspath, _args.root_directory) generate_favicon(iconspath, _args.root_directory)
@@ -135,6 +142,7 @@ def render_manifest_json(_args: Args, icon_list: List[Icon], colors: Dict[str, s
theme_color=colors["theme_color"], theme_color=colors["theme_color"],
) )
with open(os.path.join(_args.root_directory, ".static", "manifest.json"), "w", encoding="utf-8") as f: with open(os.path.join(_args.root_directory, ".static", "manifest.json"), "w", encoding="utf-8") as f:
logger.info("rendering manifest.json", extra={"path": os.path.join(_args.root_directory, ".static", "manifest.json")})
f.write(content) f.write(content)
@@ -156,6 +164,7 @@ 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]
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"},
@@ -165,6 +174,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})
cairosvg.svg2png( cairosvg.svg2png(
url=os.path.join(iconspath, svg), url=os.path.join(iconspath, svg),
write_to=tmpimg, write_to=tmpimg,
@@ -173,6 +183,7 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List
scale=1, scale=1,
) )
with Image.open(tmpimg) as iconfile: with Image.open(tmpimg) as iconfile:
logger.info("saving png file", extra={"iconpath": iconpath})
iconfile.save(iconpath, format="PNG") iconfile.save(iconpath, format="PNG")
icon_list.append( icon_list.append(
{ {
@@ -215,6 +226,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})
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
@@ -231,14 +243,11 @@ def webmanifest(_args: Args) -> None:
""" """
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 = ( 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)
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)
)
if not icon_list: if not icon_list:
print("No icons found in the static/icons folder!") print("No icons found in the static/icons folder!")
logger.error("no icons found in the static/icons folder", extra={"iconspath": iconspath})
return return
colorscheme = extract_colorscheme(os.path.join(_args.root_directory, ".static", "theme.css")) colorscheme = extract_colorscheme(os.path.join(_args.root_directory, ".static", "theme.css"))

View File

@@ -3,6 +3,7 @@ Jinja2==3.1.4
numpy==2.0.0 numpy==2.0.0
pillow==10.4.0 pillow==10.4.0
pyinstaller==6.9.0 pyinstaller==6.9.0
python-json-logger==2.0.7
rich-argparse==1.5.2 rich-argparse==1.5.2
setuptools==70.3.0 setuptools==70.3.0
tqdm==4.66.4 tqdm==4.66.4

BIN
test/example/DSC03470.JPG Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 MiB

BIN
test/example/DSC03508.JPG Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB