4 Commits

Author SHA1 Message Date
7e23b3625a fixed logo timeout 2025-03-31 20:24:27 +02:00
bce51dc3d6 added folderthumbnails 2025-02-20 14:59:00 +01:00
57250b3adc updated figcaptions 2025-02-18 12:53:37 +01:00
08895902ec .html at end of link 2025-01-30 09:02:29 +01:00
8 changed files with 132 additions and 93 deletions

View File

@@ -1 +1 @@
2.5.0 2.6.2

View File

@@ -40,6 +40,7 @@
"--reverse-sort", "--reverse-sort",
"--regenerate-thumbnails", "--regenerate-thumbnails",
"--reread-metadata", "--reread-metadata",
"--folderthumbnails",
], ],
"console": "integratedTerminal", "console": "integratedTerminal",
"name": "Testfolder", "name": "Testfolder",
@@ -64,6 +65,7 @@
"-m", "-m",
// "--regenerate-thumbnails", // "--regenerate-thumbnails",
// "--reread-metadata", // "--reread-metadata",
"--folderthumbnails",
], ],
"console": "integratedTerminal", "console": "integratedTerminal",
"name": "woek", "name": "woek",
@@ -99,7 +101,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
}, },
"[python]": { "[python]": {
"editor.defaultFormatter": "ms-python.black-formatter", "editor.defaultFormatter": "charliermarsh.ruff",
}, },
"black-formatter.args": [ "black-formatter.args": [
"-l 260", "-l 260",
@@ -151,6 +153,7 @@
"yaml.schemas": { "yaml.schemas": {
"https://raw.githubusercontent.com/pamburus/hl/master/schema/json/config.schema.json": "file:///home/user/git/github.com/greflm13/StaticGalleryBuilder/hl_config.yaml" "https://raw.githubusercontent.com/pamburus/hl/master/schema/json/config.schema.json": "file:///home/user/git/github.com/greflm13/StaticGalleryBuilder/hl_config.yaml"
}, },
"ruff.lineLength": 180,
}, },
"tasks": { "tasks": {
"version": "2.0.0", "version": "2.0.0",

View File

@@ -3,7 +3,7 @@ import os
import re import re
import sys import sys
import shutil import shutil
import fnmatch import urllib.error
import urllib.parse import urllib.parse
import urllib.request import urllib.request
from multiprocessing import Pool, freeze_support from multiprocessing import Pool, freeze_support
@@ -161,37 +161,6 @@ def generate_thumbnail(arguments: tuple[str, str, str]) -> None:
logger.debug("thumbnail already exists for %s", item, extra={"path": image}) 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: def main(args) -> None:
""" """
Main function to process images and generate a static image hosting website. Main function to process images and generate a static image hosting website.
@@ -206,7 +175,8 @@ def main(args) -> None:
logger.info("getting logo from sorogon.eu") logger.info("getting logo from sorogon.eu")
req = urllib.request.Request("https://files.sorogon.eu/logo.svg") req = urllib.request.Request("https://files.sorogon.eu/logo.svg")
with urllib.request.urlopen(req) as res: try:
with urllib.request.urlopen(req, timeout=10) as res:
logo = res.read().decode() logo = res.read().decode()
if logo.startswith("<?xml"): if logo.startswith("<?xml"):
@@ -215,6 +185,8 @@ def main(args) -> None:
logo = re.sub(r"<!--.+-->", "", logo).strip() logo = re.sub(r"<!--.+-->", "", logo).strip()
logo = logo.replace("\n", " ") logo = logo.replace("\n", " ")
logo = " ".join(logo.split()) logo = " ".join(logo.split())
except urllib.error.URLError:
logo = "&lt;/srgn&gt;"
if args.reread_metadata: if args.reread_metadata:
logger.warning("reread metadata flag is set to true, all image metadata will be reread") logger.warning("reread metadata flag is set to true, all image metadata will be reread")
@@ -235,20 +207,13 @@ def main(args) -> None:
if args.non_interactive_mode: if args.non_interactive_mode:
logger.info("generating HTML files") 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, logo) thumbnails = list_folder(args.root_directory, args.site_title, args, raw, VERSION, logo)
with Pool(os.cpu_count()) as pool: with Pool(os.cpu_count()) as pool:
logger.info("generating thumbnails") 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) thumbnails = list_folder(args.root_directory, args.site_title, args, raw, VERSION, logo)
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)
with Pool(os.cpu_count()) as pool: with Pool(os.cpu_count()) as pool:
logger.info("generating thumbnails") logger.info("generating thumbnails")

View File

@@ -28,6 +28,7 @@ body {
.folders figure { .folders figure {
margin-bottom: 32px; margin-bottom: 32px;
margin-top: 50px; margin-top: 50px;
width: 180px;
} }
.header h1 { .header h1 {
@@ -42,11 +43,21 @@ body {
} }
.folders figcaption { .folders figcaption {
width: 120px; width: 100%;
overflow: hidden;
font-size: smaller; font-size: smaller;
text-align: center; text-align: center;
} }
.folderthumb {
height: 40px;
width: 70px !important;
overflow: hidden;
aspect-ratio: 1 / 1;
object-fit: contain;
margin: 20px 20px 0px -90px;
}
.row { .row {
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
@@ -206,15 +217,27 @@ figure {
max-width: 25%; max-width: 25%;
} }
.folders figure {
width: 160px;
}
.folders img { .folders img {
width: 80px; width: 80px;
} }
.folders figcaption { .folders figcaption {
width: 100px;
font-size: small; font-size: small;
} }
.folderthumb {
height: 30px;
width: 50px !important;
overflow: hidden;
aspect-ratio: 1 / 1;
object-fit: contain;
margin: 15px 20px 0px -70px;
}
.navbar { .navbar {
font-size: medium; font-size: medium;
} }
@@ -227,15 +250,27 @@ figure {
max-width: 50%; max-width: 50%;
} }
.folders figure {
width: 140px;
}
.folders img { .folders img {
width: 60px; width: 60px;
} }
.folders figcaption { .folders figcaption {
width: 80px;
font-size: x-small; 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 { .navbar {
font-size: small; font-size: small;
} }
@@ -256,15 +291,27 @@ figure {
max-width: 100%; max-width: 100%;
} }
.folders figure {
width: 120px;
}
.folders img { .folders img {
width: 40px; width: 40px;
} }
.folders figcaption { .folders figcaption {
width: 60px;
font-size: xx-small; 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 { .navbar {
font-size: xx-small; font-size: xx-small;
} }

View File

@@ -33,6 +33,8 @@ class Args:
A list of folders to exclude from processing. A list of folders to exclude from processing.
file_extensions : list[str] file_extensions : list[str]
A list of file extensions to include. A list of file extensions to include.
folder_thumbs : bool
Wether to generate subfolder thumbnails.
generate_webmanifest : bool generate_webmanifest : bool
Whether to generate a web manifest file. Whether to generate a web manifest file.
ignore_other_files : bool ignore_other_files : bool
@@ -58,6 +60,7 @@ class Args:
author_name: str author_name: str
exclude_folders: list[str] exclude_folders: list[str]
file_extensions: list[str] file_extensions: list[str]
folder_thumbs: bool
generate_webmanifest: bool generate_webmanifest: bool
ignore_other_files: bool ignore_other_files: bool
license_type: Optional[str] license_type: Optional[str]
@@ -76,14 +79,15 @@ class Args:
result["author_name"] = self.author_name result["author_name"] = self.author_name
result["exclude_folders"] = self.exclude_folders result["exclude_folders"] = self.exclude_folders
result["file_extensions"] = self.file_extensions result["file_extensions"] = self.file_extensions
result["folder_thumbs"] = self.folder_thumbs
result["generate_webmanifest"] = self.generate_webmanifest result["generate_webmanifest"] = self.generate_webmanifest
result["ignore_other_files"] = self.ignore_other_files result["ignore_other_files"] = self.ignore_other_files
if self.license_type is not None: if self.license_type is not None:
result["license_type"] = self.license_type result["license_type"] = self.license_type
result["non_interactive_mode"] = self.non_interactive_mode result["non_interactive_mode"] = self.non_interactive_mode
result["regenerate_thumbnails"] = self.regenerate_thumbnails result["regenerate_thumbnails"] = self.regenerate_thumbnails
result["reverse_sort"] = self.reverse_sort
result["reread_metadata"] = self.reread_metadata result["reread_metadata"] = self.reread_metadata
result["reverse_sort"] = self.reverse_sort
result["root_directory"] = self.root_directory result["root_directory"] = self.root_directory
result["site_title"] = self.site_title result["site_title"] = self.site_title
result["theme_path"] = self.theme_path result["theme_path"] = self.theme_path
@@ -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("-t", "--site-title", help="Title of the image hosting site.", required=True, type=str, dest="site_title", metavar="TITLE")
parser.add_argument("-w", "--web-root-url", help="Base URL of the web root for the image hosting site.", required=True, type=str, dest="web_root_url", metavar="URL") parser.add_argument("-w", "--web-root-url", help="Base URL of the web root for the image hosting site.", required=True, type=str, dest="web_root_url", metavar="URL")
parser.add_argument("--exclude-folder", help="Folders to exclude from processing, globs supported (can be specified multiple times).", action="append", dest="exclude_folders", metavar="FOLDER") parser.add_argument("--exclude-folder", help="Folders to exclude from processing, globs supported (can be specified multiple times).", action="append", dest="exclude_folders", metavar="FOLDER")
parser.add_argument("--folderthumbnails", help="Generate subfolder thumbnails (first image in folder will be shown)", action="store_true", default=False, dest="folder_thumbs")
if RICH: if RICH:
parser.add_argument("--generate-help-preview", action=HelpPreviewAction, path="help.svg", ) parser.add_argument("--generate-help-preview", action=HelpPreviewAction, path="help.svg", )
parser.add_argument("--ignore-other-files", help="Ignore files that do not match the specified extensions.", action="store_true", default=False, dest="ignore_other_files") parser.add_argument("--ignore-other-files", help="Ignore files that do not match the specified extensions.", action="store_true", default=False, dest="ignore_other_files")
@@ -135,6 +140,7 @@ def parse_arguments(version: str) -> Args:
author_name=parsed_args.author_name, author_name=parsed_args.author_name,
exclude_folders=parsed_args.exclude_folders, exclude_folders=parsed_args.exclude_folders,
file_extensions=parsed_args.file_extensions, file_extensions=parsed_args.file_extensions,
folder_thumbs=parsed_args.folder_thumbs,
generate_webmanifest=parsed_args.generate_webmanifest, generate_webmanifest=parsed_args.generate_webmanifest,
ignore_other_files=parsed_args.ignore_other_files, ignore_other_files=parsed_args.ignore_other_files,
license_type=parsed_args.license_type, license_type=parsed_args.license_type,

View File

@@ -24,7 +24,7 @@ FAVICON_PATH = ".static/favicon.ico"
GLOBAL_CSS_PATH = ".static/global.css" GLOBAL_CSS_PATH = ".static/global.css"
EXCLUDES = ["index.html", "manifest.json", "robots.txt"] 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 Image.MAX_IMAGE_PIXELS = 933120000
# Initialize Jinja2 environment for template rendering # Initialize Jinja2 environment for template rendering
@@ -209,10 +209,21 @@ def generate_html(folder: str, title: str, _args: Args, raw: list[str], version:
create_thumbnail_folder(foldername, _args.root_directory) 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}) logger.info("processing contents", extra={"folder": folder})
if not _args.non_interactive_mode:
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: for item in items:
if item not in EXCLUDES and not item.startswith("."): if item not in EXCLUDES and not item.startswith("."):
if os.path.isdir(os.path.join(folder, item)): if os.path.isdir(os.path.join(folder, item)):
@@ -226,12 +237,6 @@ def generate_html(folder: str, title: str, _args: Args, raw: list[str], version:
if item == "LICENSE": if item == "LICENSE":
process_license(folder, item) process_license(folder, item)
if not _args.non_interactive_mode:
pbardict[folder].update(1)
if not _args.non_interactive_mode:
pbardict[folder].close()
update_sizelist(sizelist, folder) update_sizelist(sizelist, folder)
if should_generate_html(images, contains_files, _args): if should_generate_html(images, contains_files, _args):
@@ -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}) 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:
pbardict["htmlbar"].update(1)
def create_thumbnail_folder(foldername: str, root_directory: str) -> None: 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. _args (Args): Parsed command line arguments.
raw (list[str]): Raw image file extensions. 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)}" subfolder_url = (
subfolders.append({"url": subfolder_url, "name": item}) 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 item not in _args.exclude_folders:
if not any(fnmatch.fnmatchcase(os.path.join(folder, item), exclude) for exclude 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) 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: with open(os.path.join(folder, item), encoding="utf-8") as f:
logger.info("processing LICENSE", extra={"path": os.path.join(folder, item)}) logger.info("processing LICENSE", extra={"path": os.path.join(folder, item)})
licens[urllib.parse.quote(folder)] = f.read().replace("\n", "</br>\n").replace(" ", "&emsp;").replace(" ", "&ensp;").replace("sp; ", "sp;&ensp;").replace("&ensp;&ensp;", "&emsp;") licens[urllib.parse.quote(folder)] = (
f.read().replace("\n", "</br>\n").replace(" ", "&emsp;").replace(" ", "&ensp;").replace("sp; ", "sp;&ensp;").replace("&ensp;&ensp;", "&emsp;")
)
def process_info_file(folder: str, item: str) -> None: def process_info_file(folder: str, item: str) -> None:
@@ -360,7 +377,7 @@ def create_html_file(folder: str, title: str, foldername: str, images: list[dict
if folder_license: if folder_license:
license_html = os.path.join(folder, "license.html") license_html = os.path.join(folder, "license.html")
license_url = _args.web_root_url + urllib.parse.quote(foldername) + "license" license_url = _args.web_root_url + urllib.parse.quote(foldername) + "license.html"
with open(license_html, "w+", encoding="utf-8") as f: with open(license_html, "w+", encoding="utf-8") as f:
logger.info("writing license html file", extra={"path": license_html}) logger.info("writing license html file", extra={"path": license_html})
gtml = env.get_template("license.html.j2") gtml = env.get_template("license.html.j2")
@@ -404,7 +421,7 @@ def create_html_file(folder: str, title: str, foldername: str, images: list[dict
f.write(content) 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. 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: Returns:
list[tuple[str, str]]: list of thumbnails generated. 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) generate_html(folder, title, _args, raw, version, logo)
return thumbnails return thumbnails

View File

@@ -1,8 +1,8 @@
CairoSVG==2.7.1 CairoSVG==2.7.1
Jinja2==3.1.4 Jinja2==3.1.5
pillow==10.4.0 Pillow==11.1.0
pyinstaller==6.11.1 pyinstaller==6.11.1
python-json-logger==2.0.7 python_json_logger==2.0.7
rich-argparse==1.5.2 rich_argparse==1.7.0
setuptools==70.3.0 selenium==4.28.1
tqdm==4.66.4 tqdm==4.66.4

View File

@@ -57,6 +57,9 @@
<a href="{{ subdirectory.url }}"> <a href="{{ subdirectory.url }}">
<figure> <figure>
<img class="foldericon" /> <img class="foldericon" />
{%- if subdirectory.thumb %}
<img class="folderthumb" src="{{ subdirectory.thumb }}" />
{%- endif %}
<figcaption>{{ subdirectory.name }}</figcaption> <figcaption>{{ subdirectory.name }}</figcaption>
</figure> </figure>
</a> </a>