diff --git a/.gitignore b/.gitignore index ec9d20b..14e8f5c 100644 --- a/.gitignore +++ b/.gitignore @@ -166,4 +166,5 @@ test/.thumbnails test/**/index.html test/**/.sizelist.json test/manifest.json -themes/previews \ No newline at end of file +themes/previews +logs \ No newline at end of file diff --git a/.version b/.version index b539ade..cc6612c 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.2.7 \ No newline at end of file +2.3.0 \ No newline at end of file diff --git a/builder.py b/builder.py index 5f85ce8..41c2352 100755 --- a/builder.py +++ b/builder.py @@ -11,10 +11,12 @@ from typing import Dict, List, Tuple from tqdm.auto import tqdm from PIL import Image, ImageOps +from modules.logger import logger from modules.argumentparser import parse_arguments, Args from modules.svg_handling import icons, webmanifest, extract_colorscheme from modules.generate_html import list_folder, EXCLUDES + # fmt: off # Constants if __package__ is None: @@ -32,6 +34,7 @@ RAW_EXTENSIONS = [ ] IMG_EXTENSIONS = [".jpg", ".jpeg", ".png"] NOT_LIST = ["*/Galleries/*", "Archives"] +LOG_FILE = os.path.join(SCRIPTDIR, "log.json") # fmt: on pbardict: Dict[str, tqdm] = {} @@ -77,10 +80,13 @@ def copy_static_files(_args: Args) -> None: static_dir = os.path.join(_args.root_directory, ".static") if os.path.exists(static_dir): print("Removing existing .static folder...") + logger.info("removing existing .static folder") shutil.rmtree(static_dir) print("Copying static files...") + logger.info("copying static files") 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: theme = f.read() split = theme.split(".foldericon {") @@ -92,20 +98,26 @@ def copy_static_files(_args: Args) -> None: for match in re.finditer(r"content: (.*);", foldericon): foldericon = match[1] foldericon = foldericon.replace('"', "") + logger.info("found foldericon", extra={"foldericon": foldericon}) break if "url" in foldericon: shutil.copyfile(_args.theme_path, os.path.join(static_dir, "theme.css")) + logger.info("foldericon in theme file, using it") return with open(os.path.join(SCRIPTDIR, foldericon), "r", encoding="utf-8") as f: + logger.info("Reading foldericon svg") svg = f.read() if "svg.j2" in foldericon: + logger.info("foldericon in theme file is a jinja2 template") colorscheme = extract_colorscheme(_args.theme_path) svg = svg.replace("{{ color1 }}", colorscheme["color1"]) svg = svg.replace("{{ color2 }}", colorscheme["color2"]) svg = svg.replace("{{ color3 }}", colorscheme["color3"]) svg = svg.replace("{{ color4 }}", colorscheme["color4"]) + logger.info("replaced colors in svg") svg = urllib.parse.quote(svg) 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) @@ -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. """ folder, item, root_directory = arguments + image = os.path.join(folder, item) 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" if os.path.exists(oldpath): @@ -127,14 +140,19 @@ def generate_thumbnail(arguments: Tuple[str, str, str]) -> None: except FileNotFoundError: pass if not os.path.exists(path): + logger.info("generating thumbnail for %s", item, extra={"path": image}) try: - with Image.open(os.path.join(folder, item)) as imgfile: + with Image.open(image) as imgfile: imgrgb = imgfile.convert("RGB") img = ImageOps.exif_transpose(imgrgb) img.thumbnail((512, 512)) img.save(path, "JPEG", quality=75, optimize=True, mode="RGB") 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: @@ -163,6 +181,7 @@ def get_total_folders(folder: str, _args: Args, _total: int = 0) -> int: 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 _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) return _total @@ -179,12 +198,15 @@ def main() -> None: lock_file = os.path.join(args.root_directory, ".lock") if os.path.exists(lock_file): print("Another instance of this program is running.") + logger.info("nother instance of this program is running") exit() try: Path(lock_file).touch() 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")): + logger.info("removing old thumbnails folder") shutil.rmtree(os.path.join(args.root_directory, ".thumbnails")) os.makedirs(os.path.join(args.root_directory, ".thumbnails"), exist_ok=True) @@ -192,17 +214,21 @@ def main() -> None: icons(args) if args.generate_webmanifest: + logger.info("generating webmanifest") print("Generating webmanifest...") webmanifest(args) if args.non_interactive_mode: + logger.info("generating HTML files") print("Generating HTML files...") thumbnails = list_folder(0, args.root_directory, args.site_title, args, raw, VERSION) with Pool(os.cpu_count()) as pool: + logger.info("generating thumbnails") print("Generating thumbnails...") pool.map(generate_thumbnail, thumbnails) else: 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) pbardict["traversingbar"].desc = "Traversing filesystem" pbardict["traversingbar"].update(0) @@ -211,6 +237,7 @@ def main() -> None: thumbnails = list_folder(total, args.root_directory, args.site_title, args, raw, VERSION) with Pool(os.cpu_count()) as pool: + logger.info("generating thumbnails") for _ in tqdm( pool.imap_unordered(generate_thumbnail, thumbnails), total=len(thumbnails), diff --git a/modules/argumentparser.py b/modules/argumentparser.py index 31f460e..e300bb0 100644 --- a/modules/argumentparser.py +++ b/modules/argumentparser.py @@ -4,6 +4,8 @@ import os import argparse from rich_argparse import RichHelpFormatter, HelpPreviewAction +from modules.logger import logger + if __package__ is None: PACKAGE = "" else: @@ -129,4 +131,5 @@ def parse_arguments(version: str) -> Args: use_fancy_folders=parsed_args.use_fancy_folders, web_root_url=parsed_args.web_root_url, ) + logger.debug("parsed arguments", extra={"args": _args.to_dict()}) return _args diff --git a/modules/css_color.py b/modules/css_color.py index bd7d355..b05f03e 100644 --- a/modules/css_color.py +++ b/modules/css_color.py @@ -2,6 +2,8 @@ import re import colorsys from typing import Dict +from modules.logger import logger + 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] 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]+);" colorscheme = {} @@ -30,6 +33,8 @@ def extract_colorscheme(theme_path: str) -> Dict[str, str]: color_value = match[1] hex_color_value = css_color_to_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 @@ -86,10 +91,12 @@ def css_color_to_hex(css_color: str) -> str: # Helper function to convert RGB tuple to hexadecimal string 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) # Helper function to convert HSL tuple to RGB tuple 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)) # 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()) if not match: + logger.error("invalid CSS color format", extra={"css_color": css_color}) raise ValueError("Invalid CSS color format") 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"]) a = float(groups["a"]) if groups["a"] else 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)) 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)) 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 rgb_color = hsl_to_rgb((h, s, l)) 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)) else: + logger.debug("converting hsl color to hex", extra={"color": css_color, "hsl": (h, s, l)}) return rgb_to_hex(rgb_color) # fmt: off @@ -182,7 +194,9 @@ def css_color_to_hex(css_color: str) -> str: 'turquoise': '#40e0d0', 'violet': '#ee82ee', 'wheat': '#f5deb3', 'white': '#ffffff', 'whitesmoke': '#f5f5f5', 'yellow': '#ffff00', 'yellowgreen': '#9acd32' } + logger.debug("parsing css color string", extra={"css_color": css_color}) return named_colors[groups['name'].lower()] # fmt: on + logger.error("invalid CSS color format", extra={"css_color": css_color}) raise ValueError("Invalid CSS color format") diff --git a/modules/generate_html.py b/modules/generate_html.py index 0652732..dcba176 100644 --- a/modules/generate_html.py +++ b/modules/generate_html.py @@ -9,6 +9,7 @@ from tqdm.auto import tqdm from PIL import Image, ExifTags, TiffImagePlugin from jinja2 import Environment, FileSystemLoader +from modules.logger import logger import modules.cclicense as cclicense from modules.argumentparser import Args @@ -45,12 +46,15 @@ def initialize_sizelist(folder: str) -> Dict[str, Dict[str, int]]: sizelist = {} sizelist_path = os.path.join(folder, ".sizelist.json") 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: sizelistfile.write("{}") with open(sizelist_path, "r+", encoding="utf-8") as sizelistfile: + logger.info("reading size list file", extra={"file": sizelist_path}) try: sizelist = json.loads(sizelistfile.read()) except json.decoder.JSONDecodeError: + logger.warning("invalid JSON in size list file", extra={"file": sizelist_path}) 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. """ sizelist_path = os.path.join(folder, ".sizelist.json") - if sizelist != {}: + if sizelist: 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)) else: if os.path.exists(sizelist_path): + logger.info("deleting empty size list file", extra={"file": 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. """ with Image.open(os.path.join(folder, item)) as img: + logger.info("extracting image information", extra={"file": item}) exif = img.getexif() width, height = img.size if exif: ifd = exif.get_ifd(ExifTags.IFD.Exif) - exifdatas = {key: val for key, val in exif.items()} | ifd + exifdatas = dict(exif.items()) | ifd exifdata = {} for tag_id in exifdatas: 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. 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) 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) else: 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")) 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) if not os.path.exists(thumbnails_path): + logger.info("creating thumbnail folder", extra={"path": 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. """ 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() @@ -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. _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 [] 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] + '/'))}" @@ -320,7 +336,8 @@ def create_html_file(folder: str, title: str, foldername: str, images: List[Dict 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) diff --git a/modules/logger.py b/modules/logger.py new file mode 100644 index 0000000..8f4ed0b --- /dev/null +++ b/modules/logger.py @@ -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() diff --git a/modules/svg_handling.py b/modules/svg_handling.py index 4f8ac32..2165040 100644 --- a/modules/svg_handling.py +++ b/modules/svg_handling.py @@ -13,6 +13,7 @@ try: except ImportError: SVGSUPPORT = False +from modules.logger import logger from modules.argumentparser import Args 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") content = svg.render(colorscheme=colorscheme) 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) return content @@ -73,6 +75,7 @@ def save_png_icon(content: str, iconspath: str) -> None: tmpimg = BytesIO() cairosvg.svg2png(bytestring=content, write_to=tmpimg) with Image.open(tmpimg) as iconfile: + logger.info("saving png icon", extra={"iconspath": iconspath}) 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 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"): - 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) @@ -102,12 +107,14 @@ def icons(_args: Args) -> None: _args : Args Parsed command-line arguments. """ - print("Generating 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) content = render_svg_icon(colorscheme, iconspath) if not SVGSUPPORT: print("Please install cairosvg to generate favicon from svg icon.") + logger.error("svg support not available") return save_png_icon(content, iconspath) 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"], ) 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) @@ -156,6 +164,7 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List List[Icon] 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] icon_list = [ {"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() sizes = size.split("x") 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( url=os.path.join(iconspath, svg), write_to=tmpimg, @@ -173,6 +183,7 @@ def create_icons_from_svg(files: List[str], iconspath: str, _args: Args) -> List scale=1, ) with Image.open(tmpimg) as iconfile: + logger.info("saving png file", extra={"iconpath": iconpath}) iconfile.save(iconpath, format="PNG") icon_list.append( { @@ -215,6 +226,7 @@ def create_icons_from_png(iconspath: str, web_root_url: str) -> List[Icon]: continue with Image.open(os.path.join(iconspath, icon)) as iconfile: 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": "any"}) return icon_list @@ -231,14 +243,11 @@ def webmanifest(_args: Args) -> None: """ iconspath = os.path.join(_args.root_directory, ".static", "icons") 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) if not icon_list: print("No icons found in the static/icons folder!") + logger.error("no icons found in the static/icons folder", extra={"iconspath": iconspath}) return colorscheme = extract_colorscheme(os.path.join(_args.root_directory, ".static", "theme.css")) diff --git a/requirements.txt b/requirements.txt index b037b56..c928027 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ Jinja2==3.1.4 numpy==2.0.0 pillow==10.4.0 pyinstaller==6.9.0 +python-json-logger==2.0.7 rich-argparse==1.5.2 setuptools==70.3.0 tqdm==4.66.4 \ No newline at end of file diff --git a/test/example/DSC03470.JPG b/test/example/DSC03470.JPG new file mode 100755 index 0000000..9426889 Binary files /dev/null and b/test/example/DSC03470.JPG differ diff --git a/test/example/DSC03508.JPG b/test/example/DSC03508.JPG new file mode 100755 index 0000000..9e256fc Binary files /dev/null and b/test/example/DSC03508.JPG differ diff --git a/themes/screenshots/alpenglow-dark.png b/themes/screenshots/alpenglow-dark.png index 1f7c2ea..ce6a3a3 100644 Binary files a/themes/screenshots/alpenglow-dark.png and b/themes/screenshots/alpenglow-dark.png differ diff --git a/themes/screenshots/alpenglow.png b/themes/screenshots/alpenglow.png index a7c9052..2872c61 100644 Binary files a/themes/screenshots/alpenglow.png and b/themes/screenshots/alpenglow.png differ diff --git a/themes/screenshots/aritim-dark.png b/themes/screenshots/aritim-dark.png index 9645d20..c7969fa 100644 Binary files a/themes/screenshots/aritim-dark.png and b/themes/screenshots/aritim-dark.png differ diff --git a/themes/screenshots/aritim.png b/themes/screenshots/aritim.png index 6af7ff2..cb9aa9e 100644 Binary files a/themes/screenshots/aritim.png and b/themes/screenshots/aritim.png differ diff --git a/themes/screenshots/autumn.png b/themes/screenshots/autumn.png index 7acc3cb..8b65920 100644 Binary files a/themes/screenshots/autumn.png and b/themes/screenshots/autumn.png differ diff --git a/themes/screenshots/carnation.png b/themes/screenshots/carnation.png index b733413..18b9ad7 100644 Binary files a/themes/screenshots/carnation.png and b/themes/screenshots/carnation.png differ diff --git a/themes/screenshots/catpuccin.png b/themes/screenshots/catpuccin.png index c051a13..b38fe37 100644 Binary files a/themes/screenshots/catpuccin.png and b/themes/screenshots/catpuccin.png differ diff --git a/themes/screenshots/cornflower.png b/themes/screenshots/cornflower.png index 594dc3b..d6527ab 100644 Binary files a/themes/screenshots/cornflower.png and b/themes/screenshots/cornflower.png differ diff --git a/themes/screenshots/default-dark.png b/themes/screenshots/default-dark.png index 56a12c3..7713541 100644 Binary files a/themes/screenshots/default-dark.png and b/themes/screenshots/default-dark.png differ diff --git a/themes/screenshots/default.png b/themes/screenshots/default.png index 5074bde..8228f51 100644 Binary files a/themes/screenshots/default.png and b/themes/screenshots/default.png differ diff --git a/themes/screenshots/ivy.png b/themes/screenshots/ivy.png index 6289a9c..64ba62c 100644 Binary files a/themes/screenshots/ivy.png and b/themes/screenshots/ivy.png differ diff --git a/themes/screenshots/kjoe.png b/themes/screenshots/kjoe.png index df2f1ae..42a247a 100644 Binary files a/themes/screenshots/kjoe.png and b/themes/screenshots/kjoe.png differ diff --git a/themes/screenshots/monokai-vibrant.png b/themes/screenshots/monokai-vibrant.png index 797b0b7..bd4dcd3 100644 Binary files a/themes/screenshots/monokai-vibrant.png and b/themes/screenshots/monokai-vibrant.png differ diff --git a/themes/screenshots/rainbow.png b/themes/screenshots/rainbow.png index f0d332b..a5fb726 100644 Binary files a/themes/screenshots/rainbow.png and b/themes/screenshots/rainbow.png differ diff --git a/themes/screenshots/spring.png b/themes/screenshots/spring.png index e763e5f..b9f103b 100644 Binary files a/themes/screenshots/spring.png and b/themes/screenshots/spring.png differ diff --git a/themes/screenshots/steam.png b/themes/screenshots/steam.png index 05c31b2..7df684a 100644 Binary files a/themes/screenshots/steam.png and b/themes/screenshots/steam.png differ diff --git a/themes/screenshots/summer.png b/themes/screenshots/summer.png index 1b7203b..f9abd36 100644 Binary files a/themes/screenshots/summer.png and b/themes/screenshots/summer.png differ diff --git a/themes/screenshots/sunflower.png b/themes/screenshots/sunflower.png index 7d94b84..96008e7 100644 Binary files a/themes/screenshots/sunflower.png and b/themes/screenshots/sunflower.png differ diff --git a/themes/screenshots/winter.png b/themes/screenshots/winter.png index 2c5f7ac..082bc92 100644 Binary files a/themes/screenshots/winter.png and b/themes/screenshots/winter.png differ