From bce51dc3d6ed24e60292359a1f2c8b64a8fa00cd Mon Sep 17 00:00:00 2001 From: Flo Greistorfer Date: Thu, 20 Feb 2025 14:59:00 +0100 Subject: [PATCH] added folderthumbnails --- .version | 2 +- builder.py | 43 ++-------------------- files/global.css | 36 +++++++++++++++++++ modules/argumentparser.py | 8 ++++- modules/generate_html.py | 75 +++++++++++++++++++++++---------------- requirements.txt | 12 +++---- templates/index.html.j2 | 3 ++ 7 files changed, 100 insertions(+), 79 deletions(-) diff --git a/.version b/.version index 4fd0fe3..b8d12d7 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.5.1 \ No newline at end of file +2.6.1 \ No newline at end of file diff --git a/builder.py b/builder.py index 6e94840..620157f 100755 --- a/builder.py +++ b/builder.py @@ -3,7 +3,6 @@ import os import re import sys import shutil -import fnmatch import urllib.parse import urllib.request from multiprocessing import Pool, freeze_support @@ -161,37 +160,6 @@ def generate_thumbnail(arguments: tuple[str, str, str]) -> None: logger.debug("thumbnail already exists for %s", item, extra={"path": image}) -def get_total_folders(folder: str, _args: Args, _total: int = 0) -> int: - """ - Recursively count the total number of folders to be processed. - - Parameters: - ----------- - folder : str - The current folder being processed. - _args : Args - Parsed command-line arguments. - _total : int, optional - The running total of folders, default is 0. - - Returns: - -------- - int - The total number of folders. - """ - _total += 1 - pbardict["traversingbar"].desc = f"Traversing filesystem - {folder}" - pbardict["traversingbar"].update(1) - - items = sorted(os.listdir(folder)) - 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 - - def main(args) -> None: """ Main function to process images and generate a static image hosting website. @@ -235,20 +203,13 @@ def main(args) -> None: 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, logo) + thumbnails = list_folder(args.root_directory, args.site_title, args, raw, VERSION, logo) 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) - pbardict["traversingbar"].close() - - thumbnails = list_folder(total, args.root_directory, args.site_title, args, raw, VERSION, logo) + thumbnails = list_folder(args.root_directory, args.site_title, args, raw, VERSION, logo) with Pool(os.cpu_count()) as pool: logger.info("generating thumbnails") diff --git a/files/global.css b/files/global.css index cf096a0..55dc205 100644 --- a/files/global.css +++ b/files/global.css @@ -49,6 +49,15 @@ body { text-align: center; } +.folderthumb { + height: 40px; + width: 70px !important; + overflow: hidden; + aspect-ratio: 1 / 1; + object-fit: contain; + margin: 20px 20px 0px -90px; +} + .row { display: -ms-flexbox; display: flex; @@ -220,6 +229,15 @@ figure { font-size: small; } + .folderthumb { + height: 30px; + width: 50px !important; + overflow: hidden; + aspect-ratio: 1 / 1; + object-fit: contain; + margin: 15px 20px 0px -70px; + } + .navbar { font-size: medium; } @@ -244,6 +262,15 @@ figure { font-size: x-small; } + .folderthumb { + height: 25px; + width: 30px !important; + overflow: hidden; + aspect-ratio: 1 / 1; + object-fit: contain; + margin: 10px 20px 0px -50px; + } + .navbar { font-size: small; } @@ -276,6 +303,15 @@ figure { font-size: xx-small; } + .folderthumb { + height: 15px; + width: 20px !important; + overflow: hidden; + aspect-ratio: 1 / 1; + object-fit: contain; + margin: 5px 10px 0px -35px; + } + .navbar { font-size: xx-small; } diff --git a/modules/argumentparser.py b/modules/argumentparser.py index a4dbfee..45a957f 100644 --- a/modules/argumentparser.py +++ b/modules/argumentparser.py @@ -33,6 +33,8 @@ class Args: A list of folders to exclude from processing. file_extensions : list[str] A list of file extensions to include. + folder_thumbs : bool + Wether to generate subfolder thumbnails. generate_webmanifest : bool Whether to generate a web manifest file. ignore_other_files : bool @@ -58,6 +60,7 @@ class Args: author_name: str exclude_folders: list[str] file_extensions: list[str] + folder_thumbs: bool generate_webmanifest: bool ignore_other_files: bool license_type: Optional[str] @@ -76,14 +79,15 @@ class Args: result["author_name"] = self.author_name result["exclude_folders"] = self.exclude_folders result["file_extensions"] = self.file_extensions + result["folder_thumbs"] = self.folder_thumbs result["generate_webmanifest"] = self.generate_webmanifest result["ignore_other_files"] = self.ignore_other_files if self.license_type is not None: result["license_type"] = self.license_type result["non_interactive_mode"] = self.non_interactive_mode result["regenerate_thumbnails"] = self.regenerate_thumbnails - result["reverse_sort"] = self.reverse_sort result["reread_metadata"] = self.reread_metadata + result["reverse_sort"] = self.reverse_sort result["root_directory"] = self.root_directory result["site_title"] = self.site_title result["theme_path"] = self.theme_path @@ -120,6 +124,7 @@ def parse_arguments(version: str) -> Args: 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") @@ -135,6 +140,7 @@ def parse_arguments(version: str) -> Args: author_name=parsed_args.author_name, exclude_folders=parsed_args.exclude_folders, file_extensions=parsed_args.file_extensions, + folder_thumbs=parsed_args.folder_thumbs, generate_webmanifest=parsed_args.generate_webmanifest, ignore_other_files=parsed_args.ignore_other_files, license_type=parsed_args.license_type, diff --git a/modules/generate_html.py b/modules/generate_html.py index a1f26ba..64c959d 100644 --- a/modules/generate_html.py +++ b/modules/generate_html.py @@ -24,7 +24,7 @@ FAVICON_PATH = ".static/favicon.ico" GLOBAL_CSS_PATH = ".static/global.css" EXCLUDES = ["index.html", "manifest.json", "robots.txt"] -# Set the maximum image pixels to prevent decompression bomb DOS attacks +# Set the maximum image pixels Image.MAX_IMAGE_PIXELS = 933120000 # Initialize Jinja2 environment for template rendering @@ -209,28 +209,33 @@ def generate_html(folder: str, title: str, _args: Args, raw: list[str], version: create_thumbnail_folder(foldername, _args.root_directory) - if not _args.non_interactive_mode: - pbardict[folder] = tqdm(total=len(items), desc=f"Getting image infos - {folder}", unit="files", ascii=True, dynamic_ncols=True) - logger.info("processing contents", extra={"folder": folder}) - for item in items: - if item not in EXCLUDES and not item.startswith("."): - if os.path.isdir(os.path.join(folder, item)): - process_subfolder(item, folder, baseurl, subfolders, _args, raw, version, logo) - else: - contains_files = True - if os.path.splitext(item)[1].lower() in _args.file_extensions: - images.append(process_image(item, folder, _args, baseurl, sizelist, raw)) - if item == "info": - process_info_file(folder, item) - if item == "LICENSE": - process_license(folder, item) - - if not _args.non_interactive_mode: - pbardict[folder].update(1) - if not _args.non_interactive_mode: - pbardict[folder].close() + for item in tqdm(items, total=len(items), desc=f"Getting image infos - {folder}", unit="files", ascii=True, dynamic_ncols=True): + if item not in EXCLUDES and not item.startswith("."): + if os.path.isdir(os.path.join(folder, item)): + process_subfolder(item, folder, baseurl, subfolders, _args, raw, version, logo) + else: + contains_files = True + if os.path.splitext(item)[1].lower() in _args.file_extensions: + images.append(process_image(item, folder, _args, baseurl, sizelist, raw)) + if item == "info": + process_info_file(folder, item) + if item == "LICENSE": + process_license(folder, item) + else: + for item in items: + if item not in EXCLUDES and not item.startswith("."): + if os.path.isdir(os.path.join(folder, item)): + process_subfolder(item, folder, baseurl, subfolders, _args, raw, version, logo) + else: + contains_files = True + if os.path.splitext(item)[1].lower() in _args.file_extensions: + images.append(process_image(item, folder, _args, baseurl, sizelist, raw)) + if item == "info": + process_info_file(folder, item) + if item == "LICENSE": + process_license(folder, item) update_sizelist(sizelist, folder) @@ -241,9 +246,6 @@ def generate_html(folder: str, title: str, _args: Args, raw: list[str], version: logger.info("removing existing index.html", extra={"folder": folder}) os.remove(os.path.join(folder, "index.html")) - if not _args.non_interactive_mode: - pbardict["htmlbar"].update(1) - def create_thumbnail_folder(foldername: str, root_directory: str) -> None: """ @@ -271,8 +273,21 @@ def process_subfolder(item: str, folder: str, baseurl: str, subfolders: list[dic _args (Args): Parsed command line arguments. raw (list[str]): Raw image file extensions. """ - subfolder_url = f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}/index.html" if _args.web_root_url.startswith("file://") else f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}" - subfolders.append({"url": subfolder_url, "name": item}) + subfolder_url = ( + f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}/index.html" + if _args.web_root_url.startswith("file://") + else f"{_args.web_root_url}{baseurl}{urllib.parse.quote(item)}" + ) + thumb = None + if _args.folder_thumbs: + thumbitems = [i for i in sorted(os.listdir(os.path.join(folder, item))) if os.path.splitext(i)[1].lower() in _args.file_extensions] + if len(thumbitems) > 0: + if _args.reverse_sort: + thumb = f"{_args.web_root_url}.thumbnails/{baseurl}{urllib.parse.quote(item)}/{urllib.parse.quote(thumbitems[-1])}.jpg" + 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}) if item not in _args.exclude_folders: if not any(fnmatch.fnmatchcase(os.path.join(folder, item), exclude) for exclude in _args.exclude_folders): generate_html(os.path.join(folder, item), os.path.join(folder, item).removeprefix(_args.root_directory), _args, raw, version, logo) @@ -288,7 +303,9 @@ def process_license(folder: str, item: str) -> None: """ with open(os.path.join(folder, item), encoding="utf-8") as f: logger.info("processing LICENSE", extra={"path": os.path.join(folder, item)}) - licens[urllib.parse.quote(folder)] = f.read().replace("\n", "
\n").replace(" ", " ").replace(" ", " ").replace("sp; ", "sp; ").replace("  ", " ") + licens[urllib.parse.quote(folder)] = ( + f.read().replace("\n", "
\n").replace(" ", " ").replace(" ", " ").replace("sp; ", "sp; ").replace("  ", " ") + ) def process_info_file(folder: str, item: str) -> None: @@ -404,7 +421,7 @@ def create_html_file(folder: str, title: str, foldername: str, images: list[dict f.write(content) -def list_folder(total: int, folder: str, title: str, _args: Args, raw: list[str], version: str, logo: str) -> list[tuple[str, str]]: +def list_folder(folder: str, title: str, _args: Args, raw: list[str], version: str, logo: str) -> list[tuple[str, str]]: """ lists and processes a folder, generating HTML files. @@ -418,7 +435,5 @@ def list_folder(total: int, folder: str, title: str, _args: Args, raw: list[str] Returns: list[tuple[str, str]]: list of thumbnails generated. """ - if not _args.non_interactive_mode: - pbardict["htmlbar"] = tqdm(total=total, desc="Generating HTML files", unit="folders", ascii=True, dynamic_ncols=True) generate_html(folder, title, _args, raw, version, logo) return thumbnails diff --git a/requirements.txt b/requirements.txt index 8b0a675..cfcd697 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ CairoSVG==2.7.1 -Jinja2==3.1.4 -pillow==10.4.0 +Jinja2==3.1.5 +Pillow==11.1.0 pyinstaller==6.11.1 -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 +python_json_logger==2.0.7 +rich_argparse==1.7.0 +selenium==4.28.1 +tqdm==4.66.4 diff --git a/templates/index.html.j2 b/templates/index.html.j2 index 5d2844f..8a45517 100644 --- a/templates/index.html.j2 +++ b/templates/index.html.j2 @@ -57,6 +57,9 @@
+ {%- if subdirectory.thumb %} + + {%- endif %}
{{ subdirectory.name }}