From 9b7c3dc6975bd54bd1d765f67b9d7d04dd104f77 Mon Sep 17 00:00:00 2001 From: Flo Greistorfer Date: Wed, 2 Jul 2025 13:35:42 +0200 Subject: [PATCH] added --reread-sidecar option --- README.md | 1 + StaticGalleryBuilder.code-workspace | 17 ++- help.svg | 168 +++++++++++++++------------- modules/argumentparser.py | 40 ++++--- modules/generate_html.py | 17 ++- templates/index.html.j2 | 102 ++++++++++++----- 6 files changed, 213 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index b7e80c5..0d82fad 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ The script supports several command-line options to customize its behavior. Belo - `--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. +- `--reread-sidecar`: Reread sidecar file data. - `--reverse-sort`: Sort images by reverse name order. - `--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. diff --git a/StaticGalleryBuilder.code-workspace b/StaticGalleryBuilder.code-workspace index 611b306..0f2a966 100644 --- a/StaticGalleryBuilder.code-workspace +++ b/StaticGalleryBuilder.code-workspace @@ -90,7 +90,7 @@ "Scans", "--exclude-folder", "*/Galleries/*", - "--folderthumbnails", + "--folderthumbnails" ], "console": "integratedTerminal", "name": "production", @@ -256,6 +256,21 @@ "showReuseMessage": false, "clear": true } + }, + { + "command": "COLUMNS=120 ./builder.py --generate-help-preview help.svg", + "isBackground": false, + "label": "Create help svg", + "problemMatcher": [], + "type": "shell", + "presentation": { + "echo": false, + "reveal": "always", + "focus": true, + "panel": "dedicated", + "showReuseMessage": false, + "clear": true + } } ] } diff --git a/help.svg b/help.svg index d73a5c8..0c6e7bf 100644 --- a/help.svg +++ b/help.svg @@ -1,4 +1,4 @@ - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + - + - + - - Usage:builder.py [-h] [-aAUTHOR] [-eEXTENSION] [-lLICENSE] [-m] [-n-pROOT-tTITLE-wURL -                  [--exclude-folderFOLDER] [--ignore-other-files] [--regenerate-thumbnails] [--reread-metadata] -                  [--reverse-sort] [--theme-pathPATH] [--use-fancy-folders] [--version] - -Generate HTML files for a static image hosting website. - -Options: --h--helpshow this help message and exit --a--author-nameAUTHOR -Name of the author of the images. --e--file-extensionsEXTENSION -File extensions to include (can be specified multiple times). --l--license-typeLICENSE -Specify the license type for the images. --m--web-manifestGenerate a web manifest file. --n--non-interactive-mode -Run in non-interactive mode, disabling progress bars. --p--root-directoryROOT -Root directory containing the images. --t--site-titleTITLE -Title of the image hosting site. --w--web-root-urlURL -Base URL of the web root for the image hosting site. ---exclude-folderFOLDER -Folders to exclude from processing, globs supported (can be specified multiple times). ---ignore-other-filesIgnore files that do not match the specified extensions. ---regenerate-thumbnails -Regenerate thumbnails even if they already exist. ---reread-metadataReread image metadata ---reverse-sortSort images in reverse order. ---theme-pathPATHPath to the CSS theme file. ---use-fancy-foldersEnable fancy folder view instead of the default Apache directory listing. ---versionshow program's version number and exit + + Usage:builder.py [-h] [-aAUTHOR] [-eEXTENSION] [-lLICENSE] [-m] [-n-pROOT-tTITLE-wURL +                  [--exclude-folderFOLDER] [--folderthumbnails] [--ignore-other-files] [--regenerate-thumbnails] +                  [--reread-metadata] [--reread-sidecar] [--reverse-sort] [--theme-pathPATH] [--use-fancy-folders] +                  [--version] + +generate HTML files for a static image hosting website + +Options: +-h--helpshow this help message and exit +-a--author-nameAUTHOR +name of the author of the images +-e--file-extensionsEXTENSION +file extensions to include (can be specified multiple times) +-l--license-typeLICENSE +specify the license type for the images +-m--web-manifestgenerate a web manifest file +-n--non-interactive-mode +run in non-interactive mode, disabling progress bars +-p--root-directoryROOT +root directory containing the images +-t--site-titleTITLE +title of the image hosting site +-w--web-root-urlURL +base URL of the web root for the image hosting site +--exclude-folderFOLDER +folders to exclude from processing, globs supported (can be specified multiple times) +--folderthumbnailsgenerate subfolder thumbnails (first image in folder will be shown) +--ignore-other-filesignore files that do not match the specified extensions +--regenerate-thumbnails +regenerate thumbnails even if they already exist +--reread-metadatareread image metadata +--reread-sidecarreread sidecar files +--reverse-sortsort images in reverse order +--theme-pathPATHpath to the CSS theme file +--use-fancy-foldersenable fancy folder view instead of the default Apache directory listing +--versionshow program's version number and exit diff --git a/modules/argumentparser.py b/modules/argumentparser.py index 45a957f..a00c4f0 100644 --- a/modules/argumentparser.py +++ b/modules/argumentparser.py @@ -67,6 +67,7 @@ class Args: non_interactive_mode: bool regenerate_thumbnails: bool reread_metadata: bool + reread_sidecar: bool reverse_sort: bool root_directory: str site_title: str @@ -87,6 +88,7 @@ class Args: result["non_interactive_mode"] = self.non_interactive_mode result["regenerate_thumbnails"] = self.regenerate_thumbnails result["reread_metadata"] = self.reread_metadata + result["reread_sidecar"] = self.reread_sidecar result["reverse_sort"] = self.reverse_sort result["root_directory"] = self.root_directory result["site_title"] = self.site_title @@ -112,27 +114,28 @@ def parse_arguments(version: str) -> Args: """ # fmt: off if RICH: - parser = argparse.ArgumentParser(description="Generate HTML files for a static image hosting website.", formatter_class=RichHelpFormatter) + 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("-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("-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("-p", "--root-directory", help="Root directory containing the images.", required=True, type=str, dest="root_directory", metavar="ROOT") - 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("--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("--folderthumbnails", help="Generate subfolder thumbnails (first image in folder will be shown)", action="store_true", default=False, dest="folder_thumbs") + 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("-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("-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("-p", "--root-directory", help="root directory containing the images", required=True, type=str, dest="root_directory", metavar="ROOT") + 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("--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("--folderthumbnails", help="generate subfolder thumbnails (first image in folder will be shown)", action="store_true", default=False, dest="folder_thumbs") 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("--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("--reverse-sort", help="Sort images in reverse order.", action="store_true", default=False, dest="reverse_sort") - 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("--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("--reread-sidecar", help="reread sidecar files", action="store_true", default=False, dest="reread_sidecar") + parser.add_argument("--reverse-sort", help="sort images in reverse order", action="store_true", default=False, dest="reverse_sort") + 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("--version", action="version", version=f"%(prog)s {version}") parsed_args = parser.parse_args() # fmt: on @@ -147,6 +150,7 @@ def parse_arguments(version: str) -> Args: non_interactive_mode=parsed_args.non_interactive_mode, regenerate_thumbnails=parsed_args.regenerate_thumbnails, reread_metadata=parsed_args.reread_metadata, + reread_sidecar=parsed_args.reread_sidecar, reverse_sort=parsed_args.reverse_sort, root_directory=parsed_args.root_directory, site_title=parsed_args.site_title, diff --git a/modules/generate_html.py b/modules/generate_html.py index 06685ac..201583d 100644 --- a/modules/generate_html.py +++ b/modules/generate_html.py @@ -242,6 +242,13 @@ def get_image_info(item: str, folder: str) -> dict[str, Any]: pass except KeyError: pass + sidecarfile = os.path.join(folder, item + ".xmp") + if os.path.exists(sidecarfile): + logger.info("xmp sidecar file found", extra={"file": sidecarfile}) + try: + tags = get_tags(sidecarfile) + except Exception as e: + logger.error(e) if None in tags: tags.remove(None) return {"w": width, "h": height, "tags": tags, "exifdata": exifdata, "xmp": xmp} @@ -261,7 +268,6 @@ def insert_path(d, path): def finalize(d): if isinstance(d, defaultdict): - # Sort keys before recursion return {k: finalize(d[k]) for k in sorted(d)} return d or [] @@ -342,10 +348,10 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, metadata: d dict[str, Any]: dictionary containing image details for HTML rendering. """ extsplit = os.path.splitext(item) + sidecarfile = os.path.join(folder, item + ".xmp") if item not in metadata["images"] or _args.reread_metadata: metadata["images"][item] = get_image_info(item, folder) - sidecarfile = os.path.join(folder, item + ".xmp") - if os.path.exists(sidecarfile): + if _args.reread_sidecar and os.path.exists(sidecarfile): logger.info("xmp sidecar file found", extra={"file": sidecarfile}) try: metadata["images"][item]["tags"] = get_tags(sidecarfile) @@ -382,7 +388,7 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, metadata: d return image, metadata -def generate_html(folder: str, title: str, _args: Args, raw: list[str], version: str, logo) -> list[str]: +def generate_html(folder: str, title: str, _args: Args, raw: list[str], version: str, logo: str) -> list[str]: """ Generates HTML content for a folder of images. @@ -500,10 +506,11 @@ def process_subfolder(item: str, folder: str, baseurl: str, subfolders: list[dic else: thumb = f"{_args.web_root_url}.thumbnails/{baseurl}{urllib.parse.quote(item)}/{urllib.parse.quote(thumbitems[0])}.jpg" - subfolders.append({"url": subfolder_url, "name": item, "thumb": thumb, "metadata": f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}/.metadata.json"}) if item not in _args.exclude_folders: if not any(fnmatch.fnmatchcase(os.path.join(folder, item), exclude) for exclude in _args.exclude_folders): + subfolders.append({"url": subfolder_url, "name": item, "thumb": thumb, "metadata": f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}/.metadata.json"}) return generate_html(os.path.join(folder, item), os.path.join(folder, item).removeprefix(_args.root_directory), _args, raw, version, logo) + subfolders.append({"url": subfolder_url, "name": item, "thumb": thumb, "metadata": None}) return [] diff --git a/templates/index.html.j2 b/templates/index.html.j2 index a69545d..eef003b 100644 --- a/templates/index.html.j2 +++ b/templates/index.html.j2 @@ -93,9 +93,6 @@ {% if subdirectories %} - {%- for subdirectory in subdirectories %} - - {%- endfor %}
{%- for subdirectory in subdirectories %} @@ -221,6 +218,10 @@ function setupTagHandlers() { const tagContainer = document.getElementById("tagdropdown"); + if (tagContainer == null) { + return; + } + tagContainer.addEventListener("change", debounce(filter, 150)); tagContainer.addEventListener("click", function (event) { @@ -264,48 +265,81 @@ const curr = window.location.href.split("#"); const content = document.getRootNode().innerHTML; const title = document.title; - const ischecked = document.getElementById("recursive").checked; - const folders = document.getElementsByClassName("folders")[0]; + const isChecked = document.getElementById("recursive").checked; + const folders = document.querySelector(".folders"); - if (sub == undefined) { - sub = subfolders; + if (!isChecked) { + if (folders) folders.style.display = ""; + window.history.replaceState({ html: content, pageTitle: title }, "", curr[0].split("?")[0] + "#" + curr[1]); + requestMetadata(); + return; } - if (ischecked) { - window.history.replaceState({ "html": content, "pageTitle": title }, "", curr[0].split("?")[0] + "?recursive#" + curr[1]); - if (folders != undefined) { - folders.style.display = "none"; - } + if (folders) folders.style.display = "none"; + window.history.replaceState({ html: content, pageTitle: title }, "", curr[0].split("?")[0] + "?recursive#" + curr[1]); + + const visited = new Set(); + const existingItems = new Set(); + const newItems = []; + + try { + const response = await fetch(".metadata.json"); + if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); + const data = await response.json(); + + items = []; + subfolders = data.subfolders || []; + sub = subfolders; + + for (const image of Object.values(data.images || {})) { + newItems.push(image); + existingItems.add(image.src); + } + } catch (error) { + console.error("Failed to fetch base .metadata.json:", error); + return; + } + + async function fetchFoldersRecursively(folderList) { + if (!Array.isArray(folderList)) return; + + const nextLevel = []; + + await Promise.all(folderList.map(async (folder) => { + if (!folder || !folder.metadata || visited.has(folder.url)) return; + visited.add(folder.url); + + if (!folder.metadata) return; - for (const folder of sub) { try { const response = await fetch(folder.metadata); + if (!response.ok) throw new Error(`Failed to fetch ${folder.metadata}`); const data = await response.json(); - const existingItems = new Set(items.map(item => item.src)); - - for (const image of Object.values(data.images)) { + for (const image of Object.values(data.images || {})) { if (!existingItems.has(image.src)) { - items.push(image); + newItems.push(image); + existingItems.add(image.src); } } - if (data.subfolders.length > 0) { - await recursive(data.subfolders); + if (Array.isArray(data.subfolders)) { + nextLevel.push(...data.subfolders); } - - filter(); } catch (error) { - console.error('Failed to fetch folder metadata:', error); + console.error("Failed to fetch folder metadata:", error); } + })); + + if (nextLevel.length > 0) { + await fetchFoldersRecursively(nextLevel); } - } else { - window.history.replaceState({ "html": content, "pageTitle": title }, "", curr[0].split("?")[0] + "#" + curr[1]); - if (folders != undefined) { - folders.style.display = ""; - } - requestMetadata(); } + + await fetchFoldersRecursively(sub); + + items = [...newItems]; + filter(); } const totopbutton = document.getElementById("totop"); @@ -360,6 +394,7 @@ function filter() { shown = []; + let isRecursiveChecked = false; const curr = window.location.href.split("#")[0] + "#"; const path = decodeURIComponent(window.location.href.split("#")[0].replace("index.html", "")) @@ -377,7 +412,9 @@ const urltags = selected_tags.join(","); - const isRecursiveChecked = document.getElementById("recursive").checked; + try { + isRecursiveChecked = document.getElementById("recursive").checked; + } catch { } for (const item of items) { const tags = item.tags || []; @@ -420,6 +457,10 @@ const toggleLink = document.getElementById("tagtogglelink"); const dropdown = document.getElementById("tagdropdown"); + if (toggleLink == null) { + return; + } + toggleLink.addEventListener("click", function (event) { event.stopPropagation(); const svg = this.querySelector("svg"); @@ -449,7 +490,8 @@ requestMetadata(); setupDropdownToggle(); setupTagHandlers(); - document.getElementById("recursive").addEventListener("change", debounce(recursive, 150)); + const recurseEl = document.getElementById("recursive") + if (recurseEl != null) { recurseEl.addEventListener("change", debounce(recursive, 150)); } } window.addEventListener ?