19 Commits

Author SHA1 Message Date
0973868782 changed from fixed columns to inline-block 2024-10-17 09:07:07 +02:00
cd06c526af smooth scroll to top 2024-10-10 08:52:50 +02:00
d155b05798 False 2024-10-08 07:25:57 +02:00
3352894e4d rich_argparse optional dependency 2024-10-08 07:23:39 +02:00
d743ede95d added regular expression to datetime conversion 2024-09-23 12:37:52 +02:00
9403e84d78 added metadata regeneration option and display of date captured if avialable 2024-09-20 10:17:13 +02:00
961d79754e log file truncation and modification of log messages 2024-09-18 11:49:34 +02:00
383dd59851 added log viewer "hl" 2024-09-18 11:04:53 +02:00
549c15ca6c now gzips rotated logs 2024-09-18 09:26:53 +02:00
57d949677f fixed spelling mistake 2024-09-17 13:35:22 +02:00
cc6ad14506 removed deprecated constant 2024-09-17 13:33:57 +02:00
cace0f8593 added glob 2024-09-17 13:26:58 +02:00
0484200fad update README 2024-09-17 13:19:24 +02:00
74d97dac2a better handling of imagemagick subprocess 2024-09-17 13:12:54 +02:00
6c636905e2 added missing documentation 2024-09-17 11:37:26 +02:00
c79473d646 modified themes and added logging to preview generator 2024-09-17 11:33:36 +02:00
3cb5269985 log start and end 2024-09-16 22:42:58 +02:00
3bde09aebc info → debug 2024-09-16 22:40:44 +02:00
effc05826a additional logging 2024-09-16 22:39:31 +02:00
43 changed files with 468 additions and 202 deletions

View File

@@ -12,7 +12,7 @@ jobs:
- name: Install Dependencies - name: Install Dependencies
run: pip install -r requirements.txt run: pip install -r requirements.txt
- name: Build Package - name: Build Package
run: pyinstaller builder.py modules/*.py -n StaticGalleryBuilder -F --add-data files:files --add-data templates:templates --add-data .version:. run: pyinstaller builder.py modules/*.py -n StaticGalleryBuilder-$(cat .version)-linux -F --add-data files:files --add-data templates:templates --add-data .version:.
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
@@ -20,4 +20,4 @@ jobs:
make_latest: true make_latest: true
generate_release_notes: true generate_release_notes: true
files: | files: |
dist/StaticGalleryBuilder dist/StaticGalleryBuilder*

View File

@@ -1 +1 @@
2.3.0 2.4.0

View File

@@ -26,13 +26,14 @@ Python script to generate static HTML files for website galleries.
- `Pillow` library - `Pillow` library
- `rich_argparse` library - `rich_argparse` library
- `cairosvg` library (for SVG to PNG icon conversion) - `cairosvg` library (for SVG to PNG icon conversion)
- `python-json-logger` library (for logging)
## Installation ## Installation
Install the required libraries using pip: Install the required libraries using pip:
```sh ```sh
pip install numpy tqdm Jinja2 Pillow rich-argparse cairosvg pip install numpy tqdm Jinja2 Pillow rich-argparse cairosvg python-json-logger
``` ```
## Usage ## Usage
@@ -43,20 +44,20 @@ The script supports several command-line options to customize its behavior. Belo
### Options ### Options
- `-h, --help`: Show the help message and exit.
- `-p ROOT, --root-directory ROOT`: Specify the root folder where the images are stored. This option is required.
- `-w URL, --web-root-url URL`: Specify the base URL for the web root of the image hosting site. This option is required.
- `-t TITLE, --site-title TITLE`: Specify the title of the image hosting site. This option is required.
- `-r, --regenerate-thumbnails`: Regenerate thumbnails even if they already exist.
- `-n, --non-interactive-mode`: Run in non-interactive mode, disabling progress bars.
- `-l LICENSE, --license-type LICENSE`: Specify the license type for the images. Choices are `cc-zero`, `cc-by`, `cc-by-sa`, `cc-by-nd`, `cc-by-nc`, `cc-by-nc-sa`, and `cc-by-nc-nd`.
- `-a AUTHOR, --author-name AUTHOR`: Specify the name of the author of the images. Default is "Author". - `-a AUTHOR, --author-name AUTHOR`: Specify the name of the author of the images. Default is "Author".
- `-e EXTENSION, --file-extensions EXTENSION`: Specify the file extensions to include. This option can be specified multiple times. - `-e EXTENSION, --file-extensions EXTENSION`: Specify the file extensions to include. This option can be specified multiple times.
- `-l LICENSE, --license-type LICENSE`: Specify the license type for the images. Choices are `cc-zero`, `cc-by`, `cc-by-sa`, `cc-by-nd`, `cc-by-nc`, `cc-by-nc-sa`, and `cc-by-nc-nd`.
- `-m, --web-manifest`: Generate a web manifest file.
- `-n, --non-interactive-mode`: Run in non-interactive mode, disabling progress bars.
- `-p ROOT, --root-directory ROOT`: Specify the root folder where the images are stored. **(This option is required)**.
- `-t TITLE, --site-title TITLE`: Specify the title of the image hosting site. **(This option is required)**.
- `-w URL, --web-root-url URL`: Specify the base URL for the web root of the image hosting site. **(This option is required)**.
- `--exclude-folder FOLDER`: Specify folders to exclude from processing. This option can be specified multiple times.
- `--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.
- `--theme-path PATH`: Specify the path to the CSS theme file. Default is the provided default theme. - `--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. - `--use-fancy-folders`: Enable fancy folder view instead of the default Apache directory listing.
- `--ignore-other-files`: Ignore files that do not match the specified extensions.
- `--exclude-folder FOLDER`: Specify folders to exclude from processing. This option can be specified multiple times.
- `-m, --web-manifest`: Generate a web manifest file.
### Examples ### Examples

View File

@@ -39,7 +39,8 @@
"cc-by-nc-sa", "cc-by-nc-sa",
"-n", "-n",
"-m", "-m",
"-r" "--regenerate-thumbnails",
"--reread-metadata",
], ],
"console": "integratedTerminal", "console": "integratedTerminal",
"name": "Testfolder", "name": "Testfolder",
@@ -57,12 +58,13 @@
"-t", "-t",
"Pictures", "Pictures",
"--theme", "--theme",
"themes/catpuccin.css", "themes/default.css",
"--use-fancy-folders", "--use-fancy-folders",
"--web-manifest", "--web-manifest",
"-n", "-n",
"-m", "-m",
"-r", // "--regenerate-thumbnails",
// "--reread-metadata",
], ],
"console": "integratedTerminal", "console": "integratedTerminal",
"name": "woek", "name": "woek",
@@ -147,6 +149,9 @@
"python.analysis.inlayHints.callArgumentNames": "off", "python.analysis.inlayHints.callArgumentNames": "off",
"python.analysis.inlayHints.functionReturnTypes": false, "python.analysis.inlayHints.functionReturnTypes": false,
"python.analysis.inlayHints.variableTypes": false, "python.analysis.inlayHints.variableTypes": false,
"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"
},
}, },
"tasks": { "tasks": {
"version": "2.0.0", "version": "2.0.0",
@@ -157,6 +162,14 @@
"label": "Delete Lockfile", "label": "Delete Lockfile",
"problemMatcher": [], "problemMatcher": [],
"type": "shell", "type": "shell",
"presentation": {
"echo": false,
"reveal": "never",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": true
}
}, },
{ {
"command": "rm -f /home/user/woek/Pictures/.lock", "command": "rm -f /home/user/woek/Pictures/.lock",
@@ -164,7 +177,68 @@
"label": "Delete Lockfile 2", "label": "Delete Lockfile 2",
"problemMatcher": [], "problemMatcher": [],
"type": "shell", "type": "shell",
"presentation": {
"echo": false,
"reveal": "never",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": true
}
}, },
{
"command": "pyinstaller builder.py modules/*.py -n StaticGalleryBuilder-$(cat .version)-linux -F --add-data files:files --add-data templates:templates --add-data .version:.",
"isBackground": false,
"label": "Build",
"problemMatcher": [],
"type": "shell",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": false
},
"group": {
"kind": "build",
"isDefault": true
},
"dependsOn": [
"Clean"
]
},
{
"command": "rm -rf build dist",
"isBackground": true,
"label": "Clean",
"problemMatcher": [],
"type": "shell",
"presentation": {
"echo": true,
"reveal": "never",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": true
},
"group": "build"
},
{
"command": "LESS=-SR hl logs/latest.jsonl --config hl_config.yaml",
"isBackground": false,
"label": "View Latest Log",
"problemMatcher": [],
"type": "shell",
"presentation": {
"echo": false,
"reveal": "always",
"focus": true,
"panel": "dedicated",
"showReuseMessage": false,
"clear": true
}
}
], ],
}, },
} }

View File

@@ -34,7 +34,6 @@ 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] = {}
@@ -101,8 +100,8 @@ def copy_static_files(_args: Args) -> None:
logger.info("found foldericon", extra={"foldericon": foldericon}) 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"))
logger.info("foldericon in theme file, using it") logger.info("foldericon in theme file, using it")
shutil.copyfile(_args.theme_path, os.path.join(static_dir, "theme.css"))
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") logger.info("Reading foldericon svg")
@@ -110,10 +109,8 @@ def copy_static_files(_args: Args) -> None:
if "svg.j2" in foldericon: if "svg.j2" in foldericon:
logger.info("foldericon in theme file is a jinja2 template") 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"]) for color_key, color_value in colorscheme.items():
svg = svg.replace("{{ color2 }}", colorscheme["color2"]) svg = svg.replace(f"{{{{ {color_key} }}}}", color_value)
svg = svg.replace("{{ color3 }}", colorscheme["color3"])
svg = svg.replace("{{ color4 }}", colorscheme["color4"])
logger.info("replaced colors in svg") 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:
@@ -152,7 +149,7 @@ def generate_thumbnail(arguments: Tuple[str, str, str]) -> None:
print(f"Failed to generate thumbnail for {image}") print(f"Failed to generate thumbnail for {image}")
return return
else: else:
logger.info("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: def get_total_folders(folder: str, _args: Args, _total: int = 0) -> int:
@@ -190,6 +187,7 @@ def main() -> 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.
""" """
logger.info("starting builder", extra={"version": VERSION})
thumbnails: List[Tuple[str, str, str, bool]] = [] thumbnails: List[Tuple[str, str, str, bool]] = []
args = parse_arguments(VERSION) args = parse_arguments(VERSION)
@@ -198,11 +196,13 @@ 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") logger.error("another instance of this program is running")
exit() exit()
try: try:
Path(lock_file).touch() Path(lock_file).touch()
if args.reread_metadata:
logger.warning("reread metadata flag is set to true, all image metadata will be reread")
if args.regenerate_thumbnails: if args.regenerate_thumbnails:
logger.warning("regenerate thumbnails flag is set to true, all thumbnails will be regenerated") 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")):
@@ -214,7 +214,6 @@ 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)
@@ -249,6 +248,7 @@ def main() -> None:
pass pass
finally: finally:
os.remove(lock_file) os.remove(lock_file)
logger.info("finished builder", extra={"version": VERSION})
return return

View File

@@ -90,7 +90,7 @@ figure {
.footer a img { .footer a img {
height: 22px !important; height: 22px !important;
margin-left: 3px; margin-left: 3px;
vertical-align: text-bottom vertical-align: text-bottom;
} }
.navbar { .navbar {
@@ -157,12 +157,12 @@ figure {
opacity: 1; opacity: 1;
} }
/* Create eight equal columns that sits next to each other */
.column { .column {
-ms-flex: 12.5%; -ms-flex: 12.5%;
flex: 12.5%; flex: 12.5%;
max-width: 12.5%; max-width: 12.5%;
padding: 0 4px; padding: 0 4px;
display: inline-block;
} }
.column img { .column img {
@@ -187,7 +187,6 @@ figure {
border-style: none; border-style: none;
} }
/* Responsive layout - makes a four column-layout instead of eight columns */
@media screen and (max-width: 1000px) { @media screen and (max-width: 1000px) {
.column { .column {
-ms-flex: 25%; -ms-flex: 25%;
@@ -209,7 +208,6 @@ figure {
} }
} }
/* Responsive layout - makes a two column-layout instead of four columns */
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {
.column { .column {
-ms-flex: 50%; -ms-flex: 50%;
@@ -239,7 +237,6 @@ figure {
} }
} }
/* Responsive layout - makes the two columns stack on top of each other instead of next to each other */
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
.column { .column {
-ms-flex: 100%; -ms-flex: 100%;

View File

@@ -272,13 +272,14 @@ a.pswp__share--download:hover {
color: #BBB; } color: #BBB; }
.pswp__caption__center { .pswp__caption__center {
text-align: left; text-align: center;
max-width: 420px; max-width: 420px;
margin: 0 auto; margin: 0 auto;
font-size: 13px; font-size: 13px;
padding: 10px; padding: 10px;
line-height: 20px; line-height: 20px;
color: #CCC; } color: #CCC;
font-weight: bold; }
.pswp__caption--empty { .pswp__caption--empty {
display: none; } display: none; }

View File

@@ -4,21 +4,18 @@ import sys
import time import time
import shutil import shutil
import base64 import base64
import logging
import fileinput import fileinput
import urllib.parse import urllib.parse
import urllib.request import urllib.request
from typing import List from typing import List
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from modules.logger import consolelogger as logger
from modules.css_color import extract_colorscheme from modules.css_color import extract_colorscheme
# Set up logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
def replace_all(file, search_exp, replace_exp): def replace_all(file, search_exp, replace_exp):
for line in fileinput.input(file, inplace=1): for line in fileinput.input(file, inplace=1):
@@ -36,11 +33,14 @@ def take_screenshot(html_file_path: str, css_file: str, output_file: str, driver
output_file (str): Path where the screenshot will be saved. output_file (str): Path where the screenshot will be saved.
driver (webdriver.Chrome): The Chrome WebDriver instance. driver (webdriver.Chrome): The Chrome WebDriver instance.
""" """
logger.info("taking screenshot for %s", css_file)
try: try:
# Open the HTML file or URL # Open the HTML file or URL
if html_file_path.startswith(("http://", "https://")): if html_file_path.startswith(("http://", "https://")):
logger.info("opening URL: %s", html_file_path)
driver.get(html_file_path) driver.get(html_file_path)
else: else:
logger.info("opening file: %s", html_file_path)
driver.get(f"file://{os.path.abspath(html_file_path)}") driver.get(f"file://{os.path.abspath(html_file_path)}")
# Remove current theme.css # Remove current theme.css
@@ -52,9 +52,11 @@ def take_screenshot(html_file_path: str, css_file: str, output_file: str, driver
} }
}); });
""" """
logger.info("removing current theme.css")
driver.execute_script(remove_css_script) driver.execute_script(remove_css_script)
with open(css_file, "r", encoding="utf-8") as f: with open(css_file, "r", encoding="utf-8") as f:
logger.info("reading CSS file: %s", css_file)
css_content = f.read() css_content = f.read()
# Extract folder icon content # Extract folder icon content
@@ -65,20 +67,26 @@ def take_screenshot(html_file_path: str, css_file: str, output_file: str, driver
folder_icon_content = re.sub(r"/\*.*\*/", "", folder_icon_content) folder_icon_content = re.sub(r"/\*.*\*/", "", folder_icon_content)
for match in re.finditer(r"content: (.*);", folder_icon_content): for match in re.finditer(r"content: (.*);", folder_icon_content):
logger.info("found foldericon", extra={"foldericon": folder_icon_content})
folder_icon_content = match.group(1).replace('"', "") folder_icon_content = match.group(1).replace('"', "")
break break
if "url" not in folder_icon_content: if "url" not in folder_icon_content:
logger.info("Reading foldericon svg")
with open(folder_icon_content, "r", encoding="utf-8") as f: with open(folder_icon_content, "r", encoding="utf-8") as f:
svg = f.read() svg = f.read()
if "svg.j2" in folder_icon_content: if "svg.j2" in folder_icon_content:
logger.info("foldericon in theme file is a jinja2 template")
colorscheme = extract_colorscheme(css_file) colorscheme = extract_colorscheme(css_file)
for color_key, color_value in colorscheme.items(): for color_key, color_value in colorscheme.items():
svg = svg.replace(f"{{{{ {color_key} }}}}", color_value) svg = svg.replace(f"{{{{ {color_key} }}}}", color_value)
logger.info("replaced colors in svg")
svg = urllib.parse.quote(svg) svg = urllib.parse.quote(svg)
css_content = f'{css_head}\n.foldericon {{\n content: url("data:image/svg+xml,{svg}");\n}}\n{css_tail}' css_content = f'{css_head}\n.foldericon {{\n content: url("data:image/svg+xml,{svg}");\n}}\n{css_tail}'
# Encode CSS content as Base64 # Encode CSS content as Base64
logger.info("encoding css content as base64")
encoded_css = base64.b64encode(css_content.encode("utf-8")).decode("utf-8") encoded_css = base64.b64encode(css_content.encode("utf-8")).decode("utf-8")
# Inject CSS into HTML using JavaScript # Inject CSS into HTML using JavaScript
@@ -87,24 +95,28 @@ def take_screenshot(html_file_path: str, css_file: str, output_file: str, driver
style.innerHTML = atob('{encoded_css}'); style.innerHTML = atob('{encoded_css}');
document.head.appendChild(style); document.head.appendChild(style);
""" """
logger.info("injecting CSS into HTML")
driver.execute_script(apply_css_script) driver.execute_script(apply_css_script)
# Wait for a while to ensure CSS is applied # Wait for a while to ensure CSS is applied
time.sleep(2) # time.sleep(1)
# Move mouse to info # Move mouse to info
logger.info("moving mouse to info")
hoverable = driver.find_element(By.CLASS_NAME, "tooltip") hoverable = driver.find_element(By.CLASS_NAME, "tooltip")
webdriver.ActionChains(driver).move_to_element(hoverable).perform() webdriver.ActionChains(driver).move_to_element(hoverable).perform()
# Capture screenshot # Capture screenshot
logger.info("taking screenshot")
driver.save_screenshot(output_file) driver.save_screenshot(output_file)
logging.info("Screenshot saved to %s", output_file) logger.info("screenshot saved to %s", output_file)
except Exception as e: except Exception as e:
logging.error("Failed to take screenshot for %s: %s", css_file, e) logger.error("failed to take screenshot for %s: %s", css_file, e)
def create_preview(html_file_path: str, css_file: str, previews_folder: str): def create_preview(html_file_path: str, css_file: str, previews_folder: str):
logger.info("creating preview for %s", css_file)
out_file = os.path.basename(css_file).removesuffix(".css") + ".html" out_file = os.path.basename(css_file).removesuffix(".css") + ".html"
urllib.request.urlretrieve(html_file_path, os.path.join(previews_folder, out_file)) urllib.request.urlretrieve(html_file_path, os.path.join(previews_folder, out_file))
basename = os.path.basename(css_file) basename = os.path.basename(css_file)
@@ -127,21 +139,25 @@ def create_preview(html_file_path: str, css_file: str, previews_folder: str):
foldericon = foldericon.replace('"', "") foldericon = foldericon.replace('"', "")
break break
if "url" in foldericon: if "url" in foldericon:
logger.info("foldericon in theme file, using it")
shutil.copyfile(css_file, os.path.join(path, "previews", basename)) shutil.copyfile(css_file, os.path.join(path, "previews", basename))
else: return
with open(os.path.join(path, foldericon.removeprefix("themes/")), "r", encoding="utf-8") as f: with open(os.path.join(path, foldericon.removeprefix("themes/")), "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(css_file) colorscheme = extract_colorscheme(css_file)
svg = svg.replace("{{ color1 }}", colorscheme["color1"]) for color_key, color_value in colorscheme.items():
svg = svg.replace("{{ color2 }}", colorscheme["color2"]) svg = svg.replace(f"{{{{ {color_key} }}}}", color_value)
svg = svg.replace("{{ color3 }}", colorscheme["color3"]) logger.info("replaced colors in svg")
svg = svg.replace("{{ color4 }}", colorscheme["color4"])
svg = urllib.parse.quote(svg) svg = urllib.parse.quote(svg)
if os.path.exists(os.path.join(path, "previews", basename)): if os.path.exists(os.path.join(path, "previews", basename)):
os.remove(os.path.join(path, "previews", basename)) os.remove(os.path.join(path, "previews", basename))
with open(os.path.join(path, "previews", basename), "x", encoding="utf-8") as f: with open(os.path.join(path, "previews", basename), "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)
logger.info("preview created for %s", css_file)
def write_readme(directory_path: str, themes: List[str]) -> None: def write_readme(directory_path: str, themes: List[str]) -> None:
@@ -155,6 +171,7 @@ def write_readme(directory_path: str, themes: List[str]) -> None:
readme_path = os.path.join(directory_path, "README.md") readme_path = os.path.join(directory_path, "README.md")
try: try:
with open(readme_path, "r", encoding="utf-8") as f: with open(readme_path, "r", encoding="utf-8") as f:
logger.info("reading README.md", extra={"file": readme_path})
readme = f.read() readme = f.read()
readme_head = readme.split("## Previews of included themes")[0] readme_head = readme.split("## Previews of included themes")[0]
@@ -162,14 +179,15 @@ def write_readme(directory_path: str, themes: List[str]) -> None:
readme_head += "".join([f"\n### {theme}\n\n![{theme}](screenshots/{theme}.png)\n" for theme in themes]) readme_head += "".join([f"\n### {theme}\n\n![{theme}](screenshots/{theme}.png)\n" for theme in themes])
with open(readme_path, "w", encoding="utf-8") as f: with open(readme_path, "w", encoding="utf-8") as f:
logger.info("writing README.md", extra={"file": readme_path})
f.write(readme_head) f.write(readme_head)
logging.info("README.md updated with previews of included themes.") logger.info("README.md updated with previews of included themes.")
except FileNotFoundError: except FileNotFoundError:
logging.error("README.md not found in %s", directory_path) logger.error("README.md not found in %s", directory_path)
except Exception as e: except Exception as e:
logging.error("Failed to write README.md: %s", e) logger.error("failed to write README.md: %s", e)
def write_index(directory_path: str, themes: List[str]) -> None: def write_index(directory_path: str, themes: List[str]) -> None:
@@ -198,7 +216,7 @@ def main(directory_path: str, html_file_path: str) -> None:
html_file_path (str): Path to the HTML file or URL for rendering. html_file_path (str): Path to the HTML file or URL for rendering.
""" """
if not os.path.exists(directory_path): if not os.path.exists(directory_path):
logging.error('Error: Folder path "%s" does not exist.', directory_path) logger.error('Error: Folder path "%s" does not exist.', directory_path)
return return
# Setup Chrome options # Setup Chrome options
@@ -207,8 +225,9 @@ def main(directory_path: str, html_file_path: str) -> None:
chrome_options.add_argument("--window-size=1920,1080") # Set window size to at least 1920x1080 chrome_options.add_argument("--window-size=1920,1080") # Set window size to at least 1920x1080
# Initialize Chrome WebDriver # Initialize Chrome WebDriver
chromedriver_path = "/usr/bin/chromedriver" # Replace with your actual path chromedriver_path = "/usr/bin/chromedriver"
service = Service(chromedriver_path) service = Service(chromedriver_path)
logger.info("Using chromedriver at %s", chromedriver_path, extra={"chrome_options": chrome_options})
driver = webdriver.Chrome(service=service, options=chrome_options) driver = webdriver.Chrome(service=service, options=chrome_options)
try: try:
@@ -235,13 +254,16 @@ def main(directory_path: str, html_file_path: str) -> None:
write_index(directory_path, themes) write_index(directory_path, themes)
finally: finally:
logger.info("closing chrome webdriver")
driver.quit() driver.quit()
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) != 3: if len(sys.argv) != 3:
logging.error("Usage: python script_name.py directory_path html_file_path") logger.error("Usage: python script_name.py directory_path html_file_path")
else: else:
dir_path = sys.argv[1] dir_path = sys.argv[1]
html_path = sys.argv[2] html_path = sys.argv[2]
logger.info("Starting script", extra={"directory_path": dir_path, "html_file_path": html_path})
main(dir_path, html_path) main(dir_path, html_path)
logger.info("Done!", extra={"directory_path": dir_path})

142
help.svg
View File

@@ -19,118 +19,118 @@
font-weight: 700; font-weight: 700;
} }
.terminal-3756623299-matrix { .terminal-2271905219-matrix {
font-family: Fira Code, monospace; font-family: Fira Code, monospace;
font-size: 20px; font-size: 20px;
line-height: 24.4px; line-height: 24.4px;
font-variant-east-asian: full-width; font-variant-east-asian: full-width;
} }
.terminal-3756623299-title { .terminal-2271905219-title {
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
font-family: arial; font-family: arial;
} }
.terminal-3756623299-r1 { fill: #ff8700 } .terminal-2271905219-r1 { fill: #ff8700 }
.terminal-3756623299-r2 { fill: #c5c8c6 } .terminal-2271905219-r2 { fill: #c5c8c6 }
.terminal-3756623299-r3 { fill: #808080 } .terminal-2271905219-r3 { fill: #808080 }
.terminal-3756623299-r4 { fill: #68a0b3 } .terminal-2271905219-r4 { fill: #68a0b3 }
.terminal-3756623299-r5 { fill: #00af87 } .terminal-2271905219-r5 { fill: #00af87 }
</style> </style>
<defs> <defs>
<clipPath id="terminal-3756623299-clip-terminal"> <clipPath id="terminal-2271905219-clip-terminal">
<rect x="0" y="0" width="1463.0" height="755.4" /> <rect x="0" y="0" width="1463.0" height="755.4" />
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-0"> <clipPath id="terminal-2271905219-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/> <rect x="0" y="1.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-1"> <clipPath id="terminal-2271905219-line-1">
<rect x="0" y="25.9" width="1464" height="24.65"/> <rect x="0" y="25.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-2"> <clipPath id="terminal-2271905219-line-2">
<rect x="0" y="50.3" width="1464" height="24.65"/> <rect x="0" y="50.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-3"> <clipPath id="terminal-2271905219-line-3">
<rect x="0" y="74.7" width="1464" height="24.65"/> <rect x="0" y="74.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-4"> <clipPath id="terminal-2271905219-line-4">
<rect x="0" y="99.1" width="1464" height="24.65"/> <rect x="0" y="99.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-5"> <clipPath id="terminal-2271905219-line-5">
<rect x="0" y="123.5" width="1464" height="24.65"/> <rect x="0" y="123.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-6"> <clipPath id="terminal-2271905219-line-6">
<rect x="0" y="147.9" width="1464" height="24.65"/> <rect x="0" y="147.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-7"> <clipPath id="terminal-2271905219-line-7">
<rect x="0" y="172.3" width="1464" height="24.65"/> <rect x="0" y="172.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-8"> <clipPath id="terminal-2271905219-line-8">
<rect x="0" y="196.7" width="1464" height="24.65"/> <rect x="0" y="196.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-9"> <clipPath id="terminal-2271905219-line-9">
<rect x="0" y="221.1" width="1464" height="24.65"/> <rect x="0" y="221.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-10"> <clipPath id="terminal-2271905219-line-10">
<rect x="0" y="245.5" width="1464" height="24.65"/> <rect x="0" y="245.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-11"> <clipPath id="terminal-2271905219-line-11">
<rect x="0" y="269.9" width="1464" height="24.65"/> <rect x="0" y="269.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-12"> <clipPath id="terminal-2271905219-line-12">
<rect x="0" y="294.3" width="1464" height="24.65"/> <rect x="0" y="294.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-13"> <clipPath id="terminal-2271905219-line-13">
<rect x="0" y="318.7" width="1464" height="24.65"/> <rect x="0" y="318.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-14"> <clipPath id="terminal-2271905219-line-14">
<rect x="0" y="343.1" width="1464" height="24.65"/> <rect x="0" y="343.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-15"> <clipPath id="terminal-2271905219-line-15">
<rect x="0" y="367.5" width="1464" height="24.65"/> <rect x="0" y="367.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-16"> <clipPath id="terminal-2271905219-line-16">
<rect x="0" y="391.9" width="1464" height="24.65"/> <rect x="0" y="391.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-17"> <clipPath id="terminal-2271905219-line-17">
<rect x="0" y="416.3" width="1464" height="24.65"/> <rect x="0" y="416.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-18"> <clipPath id="terminal-2271905219-line-18">
<rect x="0" y="440.7" width="1464" height="24.65"/> <rect x="0" y="440.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-19"> <clipPath id="terminal-2271905219-line-19">
<rect x="0" y="465.1" width="1464" height="24.65"/> <rect x="0" y="465.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-20"> <clipPath id="terminal-2271905219-line-20">
<rect x="0" y="489.5" width="1464" height="24.65"/> <rect x="0" y="489.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-21"> <clipPath id="terminal-2271905219-line-21">
<rect x="0" y="513.9" width="1464" height="24.65"/> <rect x="0" y="513.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-22"> <clipPath id="terminal-2271905219-line-22">
<rect x="0" y="538.3" width="1464" height="24.65"/> <rect x="0" y="538.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-23"> <clipPath id="terminal-2271905219-line-23">
<rect x="0" y="562.7" width="1464" height="24.65"/> <rect x="0" y="562.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-24"> <clipPath id="terminal-2271905219-line-24">
<rect x="0" y="587.1" width="1464" height="24.65"/> <rect x="0" y="587.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-25"> <clipPath id="terminal-2271905219-line-25">
<rect x="0" y="611.5" width="1464" height="24.65"/> <rect x="0" y="611.5" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-26"> <clipPath id="terminal-2271905219-line-26">
<rect x="0" y="635.9" width="1464" height="24.65"/> <rect x="0" y="635.9" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-27"> <clipPath id="terminal-2271905219-line-27">
<rect x="0" y="660.3" width="1464" height="24.65"/> <rect x="0" y="660.3" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-28"> <clipPath id="terminal-2271905219-line-28">
<rect x="0" y="684.7" width="1464" height="24.65"/> <rect x="0" y="684.7" width="1464" height="24.65"/>
</clipPath> </clipPath>
<clipPath id="terminal-3756623299-line-29"> <clipPath id="terminal-2271905219-line-29">
<rect x="0" y="709.1" width="1464" height="24.65"/> <rect x="0" y="709.1" width="1464" height="24.65"/>
</clipPath> </clipPath>
</defs> </defs>
@@ -142,40 +142,40 @@
<circle cx="44" cy="0" r="7" fill="#28c840"/> <circle cx="44" cy="0" r="7" fill="#28c840"/>
</g> </g>
<g transform="translate(9, 41)" clip-path="url(#terminal-3756623299-clip-terminal)"> <g transform="translate(9, 41)" clip-path="url(#terminal-2271905219-clip-terminal)">
<g class="terminal-3756623299-matrix"> <g class="terminal-2271905219-matrix">
<text class="terminal-3756623299-r1" x="0" y="20" textLength="73.2" clip-path="url(#terminal-3756623299-line-0)">Usage:</text><text class="terminal-3756623299-r3" x="85.4" y="20" textLength="122" clip-path="url(#terminal-3756623299-line-0)">builder.py</text><text class="terminal-3756623299-r2" x="207.4" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">&#160;[</text><text class="terminal-3756623299-r4" x="231.8" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-h</text><text class="terminal-3756623299-r2" x="256.2" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">]&#160;</text><text class="terminal-3756623299-r4" x="280.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-p</text><text class="terminal-3756623299-r5" x="317.2" y="20" textLength="48.8" clip-path="url(#terminal-3756623299-line-0)">ROOT</text><text class="terminal-3756623299-r4" x="378.2" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-w</text><text class="terminal-3756623299-r5" x="414.8" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">URL</text><text class="terminal-3756623299-r4" x="463.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-t</text><text class="terminal-3756623299-r5" x="500.2" y="20" textLength="61" clip-path="url(#terminal-3756623299-line-0)">TITLE</text><text class="terminal-3756623299-r2" x="561.2" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">&#160;[</text><text class="terminal-3756623299-r4" x="585.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-r</text><text class="terminal-3756623299-r2" x="610" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">]&#160;[</text><text class="terminal-3756623299-r4" x="646.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-n</text><text class="terminal-3756623299-r2" x="671" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">]&#160;[</text><text class="terminal-3756623299-r4" x="707.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-l</text><text class="terminal-3756623299-r5" x="744.2" y="20" textLength="85.4" clip-path="url(#terminal-3756623299-line-0)">LICENSE</text><text class="terminal-3756623299-r2" x="829.6" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">]&#160;[</text><text class="terminal-3756623299-r4" x="866.2" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-a</text><text class="terminal-3756623299-r5" x="902.8" y="20" textLength="73.2" clip-path="url(#terminal-3756623299-line-0)">AUTHOR</text><text class="terminal-3756623299-r2" x="976" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">]&#160;[</text><text class="terminal-3756623299-r4" x="1012.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-e</text><text class="terminal-3756623299-r5" x="1049.2" y="20" textLength="109.8" clip-path="url(#terminal-3756623299-line-0)">EXTENSION</text><text class="terminal-3756623299-r2" x="1159" y="20" textLength="36.6" clip-path="url(#terminal-3756623299-line-0)">]&#160;[</text><text class="terminal-3756623299-r4" x="1195.6" y="20" textLength="24.4" clip-path="url(#terminal-3756623299-line-0)">-m</text><text class="terminal-3756623299-r2" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-3756623299-line-0)">]</text><text class="terminal-3756623299-r2" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-3756623299-line-0)"> <text class="terminal-2271905219-r1" x="0" y="20" textLength="73.2" clip-path="url(#terminal-2271905219-line-0)">Usage:</text><text class="terminal-2271905219-r3" x="85.4" y="20" textLength="122" clip-path="url(#terminal-2271905219-line-0)">builder.py</text><text class="terminal-2271905219-r2" x="207.4" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">&#160;[</text><text class="terminal-2271905219-r4" x="231.8" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-h</text><text class="terminal-2271905219-r2" x="256.2" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">]&#160;[</text><text class="terminal-2271905219-r4" x="292.8" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-a</text><text class="terminal-2271905219-r5" x="329.4" y="20" textLength="73.2" clip-path="url(#terminal-2271905219-line-0)">AUTHOR</text><text class="terminal-2271905219-r2" x="402.6" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">]&#160;[</text><text class="terminal-2271905219-r4" x="439.2" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-e</text><text class="terminal-2271905219-r5" x="475.8" y="20" textLength="109.8" clip-path="url(#terminal-2271905219-line-0)">EXTENSION</text><text class="terminal-2271905219-r2" x="585.6" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">]&#160;[</text><text class="terminal-2271905219-r4" x="622.2" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-l</text><text class="terminal-2271905219-r5" x="658.8" y="20" textLength="85.4" clip-path="url(#terminal-2271905219-line-0)">LICENSE</text><text class="terminal-2271905219-r2" x="744.2" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">]&#160;[</text><text class="terminal-2271905219-r4" x="780.8" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-m</text><text class="terminal-2271905219-r2" x="805.2" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">]&#160;[</text><text class="terminal-2271905219-r4" x="841.8" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-n</text><text class="terminal-2271905219-r2" x="866.2" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">]&#160;</text><text class="terminal-2271905219-r4" x="890.6" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-p</text><text class="terminal-2271905219-r5" x="927.2" y="20" textLength="48.8" clip-path="url(#terminal-2271905219-line-0)">ROOT</text><text class="terminal-2271905219-r2" x="976" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">&#160;[</text><text class="terminal-2271905219-r4" x="1000.4" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-r</text><text class="terminal-2271905219-r2" x="1024.8" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">]&#160;</text><text class="terminal-2271905219-r4" x="1049.2" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-t</text><text class="terminal-2271905219-r5" x="1085.8" y="20" textLength="61" clip-path="url(#terminal-2271905219-line-0)">TITLE</text><text class="terminal-2271905219-r4" x="1159" y="20" textLength="24.4" clip-path="url(#terminal-2271905219-line-0)">-w</text><text class="terminal-2271905219-r5" x="1195.6" y="20" textLength="36.6" clip-path="url(#terminal-2271905219-line-0)">URL</text><text class="terminal-2271905219-r2" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-2271905219-line-0)">
</text><text class="terminal-3756623299-r2" x="0" y="44.4" textLength="231.8" clip-path="url(#terminal-3756623299-line-1)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-3756623299-r4" x="231.8" y="44.4" textLength="146.4" clip-path="url(#terminal-3756623299-line-1)">--theme-path</text><text class="terminal-3756623299-r5" x="390.4" y="44.4" textLength="48.8" clip-path="url(#terminal-3756623299-line-1)">PATH</text><text class="terminal-3756623299-r2" x="439.2" y="44.4" textLength="36.6" clip-path="url(#terminal-3756623299-line-1)">]&#160;[</text><text class="terminal-3756623299-r4" x="475.8" y="44.4" textLength="231.8" clip-path="url(#terminal-3756623299-line-1)">--use-fancy-folders</text><text class="terminal-3756623299-r2" x="707.6" y="44.4" textLength="36.6" clip-path="url(#terminal-3756623299-line-1)">]&#160;[</text><text class="terminal-3756623299-r4" x="744.2" y="44.4" textLength="244" clip-path="url(#terminal-3756623299-line-1)">--ignore-other-files</text><text class="terminal-3756623299-r2" x="988.2" y="44.4" textLength="36.6" clip-path="url(#terminal-3756623299-line-1)">]&#160;[</text><text class="terminal-3756623299-r4" x="1024.8" y="44.4" textLength="195.2" clip-path="url(#terminal-3756623299-line-1)">--exclude-folder</text><text class="terminal-3756623299-r5" x="1232.2" y="44.4" textLength="73.2" clip-path="url(#terminal-3756623299-line-1)">FOLDER</text><text class="terminal-3756623299-r2" x="1305.4" y="44.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-1)">]</text><text class="terminal-3756623299-r2" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-1)"> </text><text class="terminal-2271905219-r2" x="0" y="44.4" textLength="231.8" clip-path="url(#terminal-2271905219-line-1)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-2271905219-r4" x="231.8" y="44.4" textLength="195.2" clip-path="url(#terminal-2271905219-line-1)">--exclude-folder</text><text class="terminal-2271905219-r5" x="439.2" y="44.4" textLength="73.2" clip-path="url(#terminal-2271905219-line-1)">FOLDER</text><text class="terminal-2271905219-r2" x="512.4" y="44.4" textLength="36.6" clip-path="url(#terminal-2271905219-line-1)">]&#160;[</text><text class="terminal-2271905219-r4" x="549" y="44.4" textLength="244" clip-path="url(#terminal-2271905219-line-1)">--ignore-other-files</text><text class="terminal-2271905219-r2" x="793" y="44.4" textLength="36.6" clip-path="url(#terminal-2271905219-line-1)">]&#160;[</text><text class="terminal-2271905219-r4" x="829.6" y="44.4" textLength="146.4" clip-path="url(#terminal-2271905219-line-1)">--theme-path</text><text class="terminal-2271905219-r5" x="988.2" y="44.4" textLength="48.8" clip-path="url(#terminal-2271905219-line-1)">PATH</text><text class="terminal-2271905219-r2" x="1037" y="44.4" textLength="36.6" clip-path="url(#terminal-2271905219-line-1)">]&#160;[</text><text class="terminal-2271905219-r4" x="1073.6" y="44.4" textLength="231.8" clip-path="url(#terminal-2271905219-line-1)">--use-fancy-folders</text><text class="terminal-2271905219-r2" x="1305.4" y="44.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-1)">]</text><text class="terminal-2271905219-r2" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-1)">
</text><text class="terminal-3756623299-r2" x="0" y="68.8" textLength="231.8" clip-path="url(#terminal-3756623299-line-2)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-3756623299-r4" x="231.8" y="68.8" textLength="109.8" clip-path="url(#terminal-3756623299-line-2)">--version</text><text class="terminal-3756623299-r2" x="341.6" y="68.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-2)">]</text><text class="terminal-3756623299-r2" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-2)"> </text><text class="terminal-2271905219-r2" x="0" y="68.8" textLength="231.8" clip-path="url(#terminal-2271905219-line-2)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;[</text><text class="terminal-2271905219-r4" x="231.8" y="68.8" textLength="109.8" clip-path="url(#terminal-2271905219-line-2)">--version</text><text class="terminal-2271905219-r2" x="341.6" y="68.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-2)">]</text><text class="terminal-2271905219-r2" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-2)">
</text><text class="terminal-3756623299-r2" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-3)"> </text><text class="terminal-2271905219-r2" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-3)">
</text><text class="terminal-3756623299-r2" x="0" y="117.6" textLength="671" clip-path="url(#terminal-3756623299-line-4)">Generate&#160;HTML&#160;files&#160;for&#160;a&#160;static&#160;image&#160;hosting&#160;website.</text><text class="terminal-3756623299-r2" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-4)"> </text><text class="terminal-2271905219-r2" x="0" y="117.6" textLength="671" clip-path="url(#terminal-2271905219-line-4)">Generate&#160;HTML&#160;files&#160;for&#160;a&#160;static&#160;image&#160;hosting&#160;website.</text><text class="terminal-2271905219-r2" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-4)">
</text><text class="terminal-3756623299-r2" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-3756623299-line-5)"> </text><text class="terminal-2271905219-r2" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-2271905219-line-5)">
</text><text class="terminal-3756623299-r1" x="0" y="166.4" textLength="97.6" clip-path="url(#terminal-3756623299-line-6)">Options:</text><text class="terminal-3756623299-r2" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-6)"> </text><text class="terminal-2271905219-r1" x="0" y="166.4" textLength="97.6" clip-path="url(#terminal-2271905219-line-6)">Options:</text><text class="terminal-2271905219-r2" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-6)">
</text><text class="terminal-3756623299-r4" x="24.4" y="190.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-7)">-h</text><text class="terminal-3756623299-r2" x="48.8" y="190.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-7)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="190.8" textLength="73.2" clip-path="url(#terminal-3756623299-line-7)">--help</text><text class="terminal-3756623299-r2" x="292.8" y="190.8" textLength="378.2" clip-path="url(#terminal-3756623299-line-7)">show&#160;this&#160;help&#160;message&#160;and&#160;exit</text><text class="terminal-3756623299-r2" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-7)"> </text><text class="terminal-2271905219-r4" x="24.4" y="190.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-7)">-h</text><text class="terminal-2271905219-r2" x="48.8" y="190.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-7)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="190.8" textLength="73.2" clip-path="url(#terminal-2271905219-line-7)">--help</text><text class="terminal-2271905219-r2" x="292.8" y="190.8" textLength="378.2" clip-path="url(#terminal-2271905219-line-7)">show&#160;this&#160;help&#160;message&#160;and&#160;exit</text><text class="terminal-2271905219-r2" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-7)">
</text><text class="terminal-3756623299-r4" x="24.4" y="215.2" textLength="24.4" clip-path="url(#terminal-3756623299-line-8)">-p</text><text class="terminal-3756623299-r2" x="48.8" y="215.2" textLength="24.4" clip-path="url(#terminal-3756623299-line-8)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="215.2" textLength="195.2" clip-path="url(#terminal-3756623299-line-8)">--root-directory</text><text class="terminal-3756623299-r5" x="280.6" y="215.2" textLength="48.8" clip-path="url(#terminal-3756623299-line-8)">ROOT</text><text class="terminal-3756623299-r2" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-8)"> </text><text class="terminal-2271905219-r4" x="24.4" y="215.2" textLength="24.4" clip-path="url(#terminal-2271905219-line-8)">-a</text><text class="terminal-2271905219-r2" x="48.8" y="215.2" textLength="24.4" clip-path="url(#terminal-2271905219-line-8)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="215.2" textLength="158.6" clip-path="url(#terminal-2271905219-line-8)">--author-name</text><text class="terminal-2271905219-r5" x="244" y="215.2" textLength="73.2" clip-path="url(#terminal-2271905219-line-8)">AUTHOR</text><text class="terminal-2271905219-r2" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-8)">
</text><text class="terminal-3756623299-r2" x="292.8" y="239.6" textLength="451.4" clip-path="url(#terminal-3756623299-line-9)">Root&#160;directory&#160;containing&#160;the&#160;images.</text><text class="terminal-3756623299-r2" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-9)"> </text><text class="terminal-2271905219-r2" x="292.8" y="239.6" textLength="402.6" clip-path="url(#terminal-2271905219-line-9)">Name&#160;of&#160;the&#160;author&#160;of&#160;the&#160;images.</text><text class="terminal-2271905219-r2" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-9)">
</text><text class="terminal-3756623299-r4" x="24.4" y="264" textLength="24.4" clip-path="url(#terminal-3756623299-line-10)">-w</text><text class="terminal-3756623299-r2" x="48.8" y="264" textLength="24.4" clip-path="url(#terminal-3756623299-line-10)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="264" textLength="170.8" clip-path="url(#terminal-3756623299-line-10)">--web-root-url</text><text class="terminal-3756623299-r5" x="256.2" y="264" textLength="36.6" clip-path="url(#terminal-3756623299-line-10)">URL</text><text class="terminal-3756623299-r2" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-3756623299-line-10)"> </text><text class="terminal-2271905219-r4" x="24.4" y="264" textLength="24.4" clip-path="url(#terminal-2271905219-line-10)">-e</text><text class="terminal-2271905219-r2" x="48.8" y="264" textLength="24.4" clip-path="url(#terminal-2271905219-line-10)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="264" textLength="207.4" clip-path="url(#terminal-2271905219-line-10)">--file-extensions</text><text class="terminal-2271905219-r5" x="292.8" y="264" textLength="109.8" clip-path="url(#terminal-2271905219-line-10)">EXTENSION</text><text class="terminal-2271905219-r2" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-2271905219-line-10)">
</text><text class="terminal-3756623299-r2" x="292.8" y="288.4" textLength="634.4" clip-path="url(#terminal-3756623299-line-11)">Base&#160;URL&#160;of&#160;the&#160;web&#160;root&#160;for&#160;the&#160;image&#160;hosting&#160;site.</text><text class="terminal-3756623299-r2" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-11)"> </text><text class="terminal-2271905219-r2" x="292.8" y="288.4" textLength="744.2" clip-path="url(#terminal-2271905219-line-11)">File&#160;extensions&#160;to&#160;include&#160;(can&#160;be&#160;specified&#160;multiple&#160;times).</text><text class="terminal-2271905219-r2" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-11)">
</text><text class="terminal-3756623299-r4" x="24.4" y="312.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-12)">-t</text><text class="terminal-3756623299-r2" x="48.8" y="312.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-12)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="312.8" textLength="146.4" clip-path="url(#terminal-3756623299-line-12)">--site-title</text><text class="terminal-3756623299-r5" x="231.8" y="312.8" textLength="61" clip-path="url(#terminal-3756623299-line-12)">TITLE</text><text class="terminal-3756623299-r2" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-12)"> </text><text class="terminal-2271905219-r4" x="24.4" y="312.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-12)">-l</text><text class="terminal-2271905219-r2" x="48.8" y="312.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-12)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="312.8" textLength="170.8" clip-path="url(#terminal-2271905219-line-12)">--license-type</text><text class="terminal-2271905219-r5" x="256.2" y="312.8" textLength="85.4" clip-path="url(#terminal-2271905219-line-12)">LICENSE</text><text class="terminal-2271905219-r2" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-12)">
</text><text class="terminal-3756623299-r2" x="292.8" y="337.2" textLength="390.4" clip-path="url(#terminal-3756623299-line-13)">Title&#160;of&#160;the&#160;image&#160;hosting&#160;site.</text><text class="terminal-3756623299-r2" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-13)"> </text><text class="terminal-2271905219-r2" x="292.8" y="337.2" textLength="488" clip-path="url(#terminal-2271905219-line-13)">Specify&#160;the&#160;license&#160;type&#160;for&#160;the&#160;images.</text><text class="terminal-2271905219-r2" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-13)">
</text><text class="terminal-3756623299-r4" x="24.4" y="361.6" textLength="24.4" clip-path="url(#terminal-3756623299-line-14)">-r</text><text class="terminal-3756623299-r2" x="48.8" y="361.6" textLength="24.4" clip-path="url(#terminal-3756623299-line-14)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="361.6" textLength="280.6" clip-path="url(#terminal-3756623299-line-14)">--regenerate-thumbnails</text><text class="terminal-3756623299-r2" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-14)"> </text><text class="terminal-2271905219-r4" x="24.4" y="361.6" textLength="24.4" clip-path="url(#terminal-2271905219-line-14)">-m</text><text class="terminal-2271905219-r2" x="48.8" y="361.6" textLength="24.4" clip-path="url(#terminal-2271905219-line-14)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="361.6" textLength="170.8" clip-path="url(#terminal-2271905219-line-14)">--web-manifest</text><text class="terminal-2271905219-r2" x="292.8" y="361.6" textLength="353.8" clip-path="url(#terminal-2271905219-line-14)">Generate&#160;a&#160;web&#160;manifest&#160;file.</text><text class="terminal-2271905219-r2" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-14)">
</text><text class="terminal-3756623299-r2" x="292.8" y="386" textLength="597.8" clip-path="url(#terminal-3756623299-line-15)">Regenerate&#160;thumbnails&#160;even&#160;if&#160;they&#160;already&#160;exist.</text><text class="terminal-3756623299-r2" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-3756623299-line-15)"> </text><text class="terminal-2271905219-r4" x="24.4" y="386" textLength="24.4" clip-path="url(#terminal-2271905219-line-15)">-n</text><text class="terminal-2271905219-r2" x="48.8" y="386" textLength="24.4" clip-path="url(#terminal-2271905219-line-15)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="386" textLength="268.4" clip-path="url(#terminal-2271905219-line-15)">--non-interactive-mode</text><text class="terminal-2271905219-r2" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-2271905219-line-15)">
</text><text class="terminal-3756623299-r4" x="24.4" y="410.4" textLength="24.4" clip-path="url(#terminal-3756623299-line-16)">-n</text><text class="terminal-3756623299-r2" x="48.8" y="410.4" textLength="24.4" clip-path="url(#terminal-3756623299-line-16)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="410.4" textLength="268.4" clip-path="url(#terminal-3756623299-line-16)">--non-interactive-mode</text><text class="terminal-3756623299-r2" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-16)"> </text><text class="terminal-2271905219-r2" x="292.8" y="410.4" textLength="646.6" clip-path="url(#terminal-2271905219-line-16)">Run&#160;in&#160;non-interactive&#160;mode,&#160;disabling&#160;progress&#160;bars.</text><text class="terminal-2271905219-r2" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-16)">
</text><text class="terminal-3756623299-r2" x="292.8" y="434.8" textLength="646.6" clip-path="url(#terminal-3756623299-line-17)">Run&#160;in&#160;non-interactive&#160;mode,&#160;disabling&#160;progress&#160;bars.</text><text class="terminal-3756623299-r2" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-17)"> </text><text class="terminal-2271905219-r4" x="24.4" y="434.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-17)">-p</text><text class="terminal-2271905219-r2" x="48.8" y="434.8" textLength="24.4" clip-path="url(#terminal-2271905219-line-17)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="434.8" textLength="195.2" clip-path="url(#terminal-2271905219-line-17)">--root-directory</text><text class="terminal-2271905219-r5" x="280.6" y="434.8" textLength="48.8" clip-path="url(#terminal-2271905219-line-17)">ROOT</text><text class="terminal-2271905219-r2" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-17)">
</text><text class="terminal-3756623299-r4" x="24.4" y="459.2" textLength="24.4" clip-path="url(#terminal-3756623299-line-18)">-l</text><text class="terminal-3756623299-r2" x="48.8" y="459.2" textLength="24.4" clip-path="url(#terminal-3756623299-line-18)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="459.2" textLength="170.8" clip-path="url(#terminal-3756623299-line-18)">--license-type</text><text class="terminal-3756623299-r5" x="256.2" y="459.2" textLength="85.4" clip-path="url(#terminal-3756623299-line-18)">LICENSE</text><text class="terminal-3756623299-r2" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-18)"> </text><text class="terminal-2271905219-r2" x="292.8" y="459.2" textLength="451.4" clip-path="url(#terminal-2271905219-line-18)">Root&#160;directory&#160;containing&#160;the&#160;images.</text><text class="terminal-2271905219-r2" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-18)">
</text><text class="terminal-3756623299-r2" x="292.8" y="483.6" textLength="488" clip-path="url(#terminal-3756623299-line-19)">Specify&#160;the&#160;license&#160;type&#160;for&#160;the&#160;images.</text><text class="terminal-3756623299-r2" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-19)"> </text><text class="terminal-2271905219-r4" x="24.4" y="483.6" textLength="24.4" clip-path="url(#terminal-2271905219-line-19)">-r</text><text class="terminal-2271905219-r2" x="48.8" y="483.6" textLength="24.4" clip-path="url(#terminal-2271905219-line-19)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="483.6" textLength="280.6" clip-path="url(#terminal-2271905219-line-19)">--regenerate-thumbnails</text><text class="terminal-2271905219-r2" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-19)">
</text><text class="terminal-3756623299-r4" x="24.4" y="508" textLength="24.4" clip-path="url(#terminal-3756623299-line-20)">-a</text><text class="terminal-3756623299-r2" x="48.8" y="508" textLength="24.4" clip-path="url(#terminal-3756623299-line-20)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="508" textLength="158.6" clip-path="url(#terminal-3756623299-line-20)">--author-name</text><text class="terminal-3756623299-r5" x="244" y="508" textLength="73.2" clip-path="url(#terminal-3756623299-line-20)">AUTHOR</text><text class="terminal-3756623299-r2" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-3756623299-line-20)"> </text><text class="terminal-2271905219-r2" x="292.8" y="508" textLength="597.8" clip-path="url(#terminal-2271905219-line-20)">Regenerate&#160;thumbnails&#160;even&#160;if&#160;they&#160;already&#160;exist.</text><text class="terminal-2271905219-r2" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-2271905219-line-20)">
</text><text class="terminal-3756623299-r2" x="292.8" y="532.4" textLength="402.6" clip-path="url(#terminal-3756623299-line-21)">Name&#160;of&#160;the&#160;author&#160;of&#160;the&#160;images.</text><text class="terminal-3756623299-r2" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-21)"> </text><text class="terminal-2271905219-r4" x="24.4" y="532.4" textLength="24.4" clip-path="url(#terminal-2271905219-line-21)">-t</text><text class="terminal-2271905219-r2" x="48.8" y="532.4" textLength="24.4" clip-path="url(#terminal-2271905219-line-21)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="532.4" textLength="146.4" clip-path="url(#terminal-2271905219-line-21)">--site-title</text><text class="terminal-2271905219-r5" x="231.8" y="532.4" textLength="61" clip-path="url(#terminal-2271905219-line-21)">TITLE</text><text class="terminal-2271905219-r2" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-21)">
</text><text class="terminal-3756623299-r4" x="24.4" y="556.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-22)">-e</text><text class="terminal-3756623299-r2" x="48.8" y="556.8" textLength="24.4" clip-path="url(#terminal-3756623299-line-22)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="556.8" textLength="207.4" clip-path="url(#terminal-3756623299-line-22)">--file-extensions</text><text class="terminal-3756623299-r5" x="292.8" y="556.8" textLength="109.8" clip-path="url(#terminal-3756623299-line-22)">EXTENSION</text><text class="terminal-3756623299-r2" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-22)"> </text><text class="terminal-2271905219-r2" x="292.8" y="556.8" textLength="390.4" clip-path="url(#terminal-2271905219-line-22)">Title&#160;of&#160;the&#160;image&#160;hosting&#160;site.</text><text class="terminal-2271905219-r2" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-22)">
</text><text class="terminal-3756623299-r2" x="292.8" y="581.2" textLength="744.2" clip-path="url(#terminal-3756623299-line-23)">File&#160;extensions&#160;to&#160;include&#160;(can&#160;be&#160;specified&#160;multiple&#160;times).</text><text class="terminal-3756623299-r2" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-23)"> </text><text class="terminal-2271905219-r4" x="24.4" y="581.2" textLength="24.4" clip-path="url(#terminal-2271905219-line-23)">-w</text><text class="terminal-2271905219-r2" x="48.8" y="581.2" textLength="24.4" clip-path="url(#terminal-2271905219-line-23)">,&#160;</text><text class="terminal-2271905219-r4" x="73.2" y="581.2" textLength="170.8" clip-path="url(#terminal-2271905219-line-23)">--web-root-url</text><text class="terminal-2271905219-r5" x="256.2" y="581.2" textLength="36.6" clip-path="url(#terminal-2271905219-line-23)">URL</text><text class="terminal-2271905219-r2" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-23)">
</text><text class="terminal-3756623299-r4" x="24.4" y="605.6" textLength="24.4" clip-path="url(#terminal-3756623299-line-24)">-m</text><text class="terminal-3756623299-r2" x="48.8" y="605.6" textLength="24.4" clip-path="url(#terminal-3756623299-line-24)">,&#160;</text><text class="terminal-3756623299-r4" x="73.2" y="605.6" textLength="170.8" clip-path="url(#terminal-3756623299-line-24)">--web-manifest</text><text class="terminal-3756623299-r2" x="292.8" y="605.6" textLength="353.8" clip-path="url(#terminal-3756623299-line-24)">Generate&#160;a&#160;web&#160;manifest&#160;file.</text><text class="terminal-3756623299-r2" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-24)"> </text><text class="terminal-2271905219-r2" x="292.8" y="605.6" textLength="634.4" clip-path="url(#terminal-2271905219-line-24)">Base&#160;URL&#160;of&#160;the&#160;web&#160;root&#160;for&#160;the&#160;image&#160;hosting&#160;site.</text><text class="terminal-2271905219-r2" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-24)">
</text><text class="terminal-3756623299-r4" x="24.4" y="630" textLength="146.4" clip-path="url(#terminal-3756623299-line-25)">--theme-path</text><text class="terminal-3756623299-r5" x="183" y="630" textLength="48.8" clip-path="url(#terminal-3756623299-line-25)">PATH</text><text class="terminal-3756623299-r2" x="292.8" y="630" textLength="329.4" clip-path="url(#terminal-3756623299-line-25)">Path&#160;to&#160;the&#160;CSS&#160;theme&#160;file.</text><text class="terminal-3756623299-r2" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-3756623299-line-25)"> </text><text class="terminal-2271905219-r4" x="24.4" y="630" textLength="195.2" clip-path="url(#terminal-2271905219-line-25)">--exclude-folder</text><text class="terminal-2271905219-r5" x="231.8" y="630" textLength="73.2" clip-path="url(#terminal-2271905219-line-25)">FOLDER</text><text class="terminal-2271905219-r2" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-2271905219-line-25)">
</text><text class="terminal-3756623299-r4" x="24.4" y="654.4" textLength="231.8" clip-path="url(#terminal-3756623299-line-26)">--use-fancy-folders</text><text class="terminal-3756623299-r2" x="292.8" y="654.4" textLength="890.6" clip-path="url(#terminal-3756623299-line-26)">Enable&#160;fancy&#160;folder&#160;view&#160;instead&#160;of&#160;the&#160;default&#160;Apache&#160;directory&#160;listing.</text><text class="terminal-3756623299-r2" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-3756623299-line-26)"> </text><text class="terminal-2271905219-r2" x="292.8" y="654.4" textLength="1049.2" clip-path="url(#terminal-2271905219-line-26)">Folders&#160;to&#160;exclude&#160;from&#160;processing,&#160;globs&#160;supported&#160;(can&#160;be&#160;specified&#160;multiple&#160;times).</text><text class="terminal-2271905219-r2" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-2271905219-line-26)">
</text><text class="terminal-3756623299-r4" x="24.4" y="678.8" textLength="244" clip-path="url(#terminal-3756623299-line-27)">--ignore-other-files</text><text class="terminal-3756623299-r2" x="292.8" y="678.8" textLength="683.2" clip-path="url(#terminal-3756623299-line-27)">Ignore&#160;files&#160;that&#160;do&#160;not&#160;match&#160;the&#160;specified&#160;extensions.</text><text class="terminal-3756623299-r2" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-3756623299-line-27)"> </text><text class="terminal-2271905219-r4" x="24.4" y="678.8" textLength="244" clip-path="url(#terminal-2271905219-line-27)">--ignore-other-files</text><text class="terminal-2271905219-r2" x="292.8" y="678.8" textLength="683.2" clip-path="url(#terminal-2271905219-line-27)">Ignore&#160;files&#160;that&#160;do&#160;not&#160;match&#160;the&#160;specified&#160;extensions.</text><text class="terminal-2271905219-r2" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-2271905219-line-27)">
</text><text class="terminal-3756623299-r4" x="24.4" y="703.2" textLength="195.2" clip-path="url(#terminal-3756623299-line-28)">--exclude-folder</text><text class="terminal-3756623299-r5" x="231.8" y="703.2" textLength="73.2" clip-path="url(#terminal-3756623299-line-28)">FOLDER</text><text class="terminal-3756623299-r2" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-3756623299-line-28)"> </text><text class="terminal-2271905219-r4" x="24.4" y="703.2" textLength="146.4" clip-path="url(#terminal-2271905219-line-28)">--theme-path</text><text class="terminal-2271905219-r5" x="183" y="703.2" textLength="48.8" clip-path="url(#terminal-2271905219-line-28)">PATH</text><text class="terminal-2271905219-r2" x="292.8" y="703.2" textLength="329.4" clip-path="url(#terminal-2271905219-line-28)">Path&#160;to&#160;the&#160;CSS&#160;theme&#160;file.</text><text class="terminal-2271905219-r2" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-2271905219-line-28)">
</text><text class="terminal-3756623299-r2" x="292.8" y="727.6" textLength="1049.2" clip-path="url(#terminal-3756623299-line-29)">Folders&#160;to&#160;exclude&#160;from&#160;processing,&#160;globs&#160;supported&#160;(can&#160;be&#160;specified&#160;multiple&#160;times).</text><text class="terminal-3756623299-r2" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-3756623299-line-29)"> </text><text class="terminal-2271905219-r4" x="24.4" y="727.6" textLength="231.8" clip-path="url(#terminal-2271905219-line-29)">--use-fancy-folders</text><text class="terminal-2271905219-r2" x="292.8" y="727.6" textLength="890.6" clip-path="url(#terminal-2271905219-line-29)">Enable&#160;fancy&#160;folder&#160;view&#160;instead&#160;of&#160;the&#160;default&#160;Apache&#160;directory&#160;listing.</text><text class="terminal-2271905219-r2" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-2271905219-line-29)">
</text><text class="terminal-3756623299-r4" x="24.4" y="752" textLength="109.8" clip-path="url(#terminal-3756623299-line-30)">--version</text><text class="terminal-3756623299-r2" x="292.8" y="752" textLength="463.6" clip-path="url(#terminal-3756623299-line-30)">show&#160;program&#x27;s&#160;version&#160;number&#160;and&#160;exit</text><text class="terminal-3756623299-r2" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-3756623299-line-30)"> </text><text class="terminal-2271905219-r4" x="24.4" y="752" textLength="109.8" clip-path="url(#terminal-2271905219-line-30)">--version</text><text class="terminal-2271905219-r2" x="292.8" y="752" textLength="463.6" clip-path="url(#terminal-2271905219-line-30)">show&#160;program&#x27;s&#160;version&#160;number&#160;and&#160;exit</text><text class="terminal-2271905219-r2" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-2271905219-line-30)">
</text> </text>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

73
hl_config.yaml Normal file
View File

@@ -0,0 +1,73 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/pamburus/hl/master/schema/json/config.schema.json
$schema: https://raw.githubusercontent.com/pamburus/hl/master/schema/json/config.schema.json
# Time format, see https://man7.org/linux/man-pages/man1/date.1.html for details.
time-format: "%b %d %T.%3N"
# Time zone name, see column "TZ identifier" at
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones page.
time-zone: "Europe/Vienna"
# Settings for fields processing.
fields:
# Configuration of the predefined set of fields.
predefined:
time:
show: auto
names: ["asctime"]
logger:
names: ["defaultlogger", "consolelogger"]
level:
show: auto
variants:
- names: ["levelname"]
values:
debug: ["DEBUG"]
info: ["INFO"]
warning: ["WARNING", "WARN"]
error: ["ERROR", "FATAL", "CRITICAL"]
- names: ["levelno"]
values:
debug: [10]
info: [20]
warning: [30]
error: [40, 50]
message:
names: ["message"]
caller:
names: ["funcName"]
caller-file:
names: ["filename"]
caller-line:
names: ["lineno"]
# List of wildcard field names to ignore.
ignore: ["_*"]
# List of exact field names to hide.
hide: ["pathname", "created", "levelno", "taskname", "relativeCreated", "thread", "process", "msecs"]
# Formatting settings.
formatting:
flatten: always
punctuation:
logger-name-separator: ":"
field-key-value-separator: "="
string-opening-quote: "'"
string-closing-quote: "'"
source-location-separator: "→ "
hidden-fields-indicator: " ..."
level-left-separator: "│"
level-right-separator: "│"
input-number-prefix: "#"
input-number-left-separator: ""
input-number-right-separator: " │ "
input-name-left-separator: ""
input-name-right-separator: " │ "
input-name-clipping: "··"
input-name-common-part: "··"
array-separator: " "
# Number of processing threads, configured automatically based on CPU count if not specified.
concurrency: ~
# Currently selected theme.
theme: "neutral"

View File

@@ -2,7 +2,14 @@ from dataclasses import dataclass
from typing import List, Optional from typing import List, Optional
import os import os
import argparse import argparse
from rich_argparse import RichHelpFormatter, HelpPreviewAction
try:
from rich_argparse import RichHelpFormatter, HelpPreviewAction
RICH = True
except ModuleNotFoundError:
RICH = False
from modules.logger import logger from modules.logger import logger
@@ -58,6 +65,7 @@ class Args:
license_type: Optional[str] license_type: Optional[str]
non_interactive_mode: bool non_interactive_mode: bool
regenerate_thumbnails: bool regenerate_thumbnails: bool
reread_metadata: bool
root_directory: str root_directory: str
site_title: str site_title: str
theme_path: str theme_path: str
@@ -75,6 +83,7 @@ class Args:
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["reread_metadata"] = self.reread_metadata
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
@@ -98,22 +107,27 @@ def parse_arguments(version: str) -> Args:
An instance of the Args class containing the parsed arguments. An instance of the Args class containing the parsed arguments.
""" """
# fmt: off # 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)
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") else:
parser.add_argument("--generate-help-preview", action=HelpPreviewAction, path="help.svg") parser = argparse.ArgumentParser(description="Generate HTML files for a static image hosting website.")
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("--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}")
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("-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("-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("-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("-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("-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("-p", "--root-directory", help="Root directory containing the images.", required=True, type=str, dest="root_directory", metavar="ROOT")
parser.add_argument("-r", "--regenerate-thumbnails", help="Regenerate thumbnails even if they already exist.", action="store_true", default=False, dest="regenerate_thumbnails")
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")
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("--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() parsed_args = parser.parse_args()
# fmt: on # fmt: on
_args = Args( _args = Args(
@@ -125,6 +139,7 @@ def parse_arguments(version: str) -> Args:
license_type=parsed_args.license_type, license_type=parsed_args.license_type,
non_interactive_mode=parsed_args.non_interactive_mode, non_interactive_mode=parsed_args.non_interactive_mode,
regenerate_thumbnails=parsed_args.regenerate_thumbnails, regenerate_thumbnails=parsed_args.regenerate_thumbnails,
reread_metadata=parsed_args.reread_metadata,
root_directory=parsed_args.root_directory, root_directory=parsed_args.root_directory,
site_title=parsed_args.site_title, site_title=parsed_args.site_title,
theme_path=parsed_args.theme_path, theme_path=parsed_args.theme_path,

View File

@@ -34,7 +34,6 @@ def extract_colorscheme(theme_path: str) -> Dict[str, str]:
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.debug("extracted variable", extra={"variable": variable_name, "value": hex_color_value})
logger.info("extracted color scheme", extra={"colorscheme": colorscheme})
return colorscheme return colorscheme

View File

@@ -1,8 +1,10 @@
import os import os
import re
import urllib.parse import urllib.parse
import fnmatch import fnmatch
import json import json
from typing import Any, Dict, List, Tuple from typing import Any, Dict, List, Tuple
from datetime import datetime
import numpy as np import numpy as np
from tqdm.auto import tqdm from tqdm.auto import tqdm
@@ -89,11 +91,13 @@ def get_image_info(item: str, folder: str) -> Dict[str, Any]:
Returns: Returns:
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: file = os.path.join(folder, item)
logger.info("extracting image information", extra={"file": item}) with Image.open(file) as img:
logger.info("extracting image information", extra={"file": file})
exif = img.getexif() exif = img.getexif()
width, height = img.size width, height = img.size
if exif: if exif:
logger.info("extracting EXIF data", extra={"file": file})
ifd = exif.get_ifd(ExifTags.IFD.Exif) ifd = exif.get_ifd(ExifTags.IFD.Exif)
exifdatas = dict(exif.items()) | ifd exifdatas = dict(exif.items()) | ifd
exifdata = {} exifdata = {}
@@ -101,7 +105,7 @@ def get_image_info(item: str, folder: str) -> Dict[str, Any]:
tag = ExifTags.TAGS.get(tag_id, tag_id) tag = ExifTags.TAGS.get(tag_id, tag_id)
content = exifdatas.get(tag_id) content = exifdatas.get(tag_id)
if isinstance(content, bytes): if isinstance(content, bytes):
content = content.hex(" ") content = "0x" + content.hex()
if isinstance(content, TiffImagePlugin.IFDRational): if isinstance(content, TiffImagePlugin.IFDRational):
content = content.limit_rational(1000000) content = content.limit_rational(1000000)
if isinstance(content, tuple): if isinstance(content, tuple):
@@ -111,15 +115,22 @@ def get_image_info(item: str, folder: str) -> Dict[str, Any]:
newtuple = newtuple + (i.limit_rational(1000000),) newtuple = newtuple + (i.limit_rational(1000000),)
if newtuple: if newtuple:
content = newtuple content = newtuple
if tag in ["DateTime", "DateTimeOriginal", "DateTimeDigitized"]:
epr = r'\d{4}:\d{2}:\d{2} \d{2}:\d{2}:\d{2}'
if re.match(epr, content):
content = datetime.strptime(content, "%Y:%m:%d %H:%M:%S").strftime("%Y-%m-%d %H:%M:%S")
else:
content = None
exifdata[tag] = content exifdata[tag] = content
if "Orientation" in exifdata and exifdata["Orientation"] in [6, 8]: if "Orientation" in exifdata and exifdata["Orientation"] in [6, 8]:
logger.info("image is rotated", extra={"file": file})
width, height = height, width width, height = height, width
for key in ["PrintImageMatching", "UserComment", "MakerNote"]: for key in ["PrintImageMatching", "UserComment", "MakerNote"]:
if key in exifdata: if key in exifdata:
del exifdata[key] del exifdata[key]
return {"width": width, "height": height, "exifdata": exifdata} return {"width": width, "height": height, "exifdata": exifdata}
else: else:
return {"width": width, "height": height} return {"width": width, "height": height, "exifdata": None}
def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: Dict[str, Dict[str, int]], raw: List[str]) -> Dict[str, Any]: def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: Dict[str, Dict[str, int]], raw: List[str]) -> Dict[str, Any]:
@@ -138,7 +149,7 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: D
Dict[str, Any]: Dictionary containing image details for HTML rendering. Dict[str, Any]: Dictionary containing image details for HTML rendering.
""" """
extsplit = os.path.splitext(item) extsplit = os.path.splitext(item)
if item not in sizelist or _args.regenerate_thumbnails: if item not in sizelist or _args.reread_metadata:
sizelist[item] = get_image_info(item, folder) sizelist[item] = get_image_info(item, folder)
image = { image = {
@@ -147,6 +158,7 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: D
"name": item, "name": item,
"width": sizelist[item]["width"], "width": sizelist[item]["width"],
"height": sizelist[item]["height"], "height": sizelist[item]["height"],
"exifdata": sizelist[item]["exifdata"],
} }
path = os.path.join(_args.root_directory, ".thumbnails", baseurl, item + ".jpg") path = os.path.join(_args.root_directory, ".thumbnails", baseurl, item + ".jpg")
if not os.path.exists(path) or _args.regenerate_thumbnails: if not os.path.exists(path) or _args.regenerate_thumbnails:
@@ -155,12 +167,15 @@ def process_image(item: str, folder: str, _args: Args, baseurl: str, sizelist: D
thumbnails.append((folder, item, _args.root_directory)) thumbnails.append((folder, item, _args.root_directory))
for _raw in raw: for _raw in raw:
if os.path.exists(os.path.join(folder, extsplit[0] + _raw)): file = os.path.join(folder, extsplit[0] + _raw)
url = urllib.parse.quote(extsplit[0]) + _raw if os.path.exists(file):
url = f"{_args.web_root_url}{baseurl}{urllib.parse.quote(extsplit[0])}{_raw}"
if _raw in (".tif", ".tiff"): if _raw in (".tif", ".tiff"):
image["tiff"] = f"{_args.web_root_url}{baseurl}{url}" logger.info("tiff file found", extra={"file": file})
image["tiff"] = url
else: else:
image["raw"] = f"{_args.web_root_url}{baseurl}{url}" logger.info("raw file found", extra={"file": file, "extension": _raw})
image["raw"] = url
return image return image
@@ -174,6 +189,7 @@ 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.
""" """
logger.info("processing folder", extra={"folder": folder})
if _args.regenerate_thumbnails: if _args.regenerate_thumbnails:
if os.path.exists(os.path.join(folder, ".sizelist.json")): if os.path.exists(os.path.join(folder, ".sizelist.json")):
logger.info("removing .sizelist.json", extra={"folder": folder}) logger.info("removing .sizelist.json", extra={"folder": folder})
@@ -193,6 +209,7 @@ def generate_html(folder: str, title: str, _args: Args, raw: List[str], version:
if not _args.non_interactive_mode: 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) 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: 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)):
@@ -297,7 +314,6 @@ def create_html_file(folder: str, title: str, foldername: str, images: List[Dict
""" """
html_file = os.path.join(folder, "index.html") html_file = os.path.join(folder, "index.html")
logger.info("generating html file with jinja2", extra={"path": html_file}) 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 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] + '/'))}"
if parent and _args.web_root_url.startswith("file://"): if parent and _args.web_root_url.startswith("file://"):
@@ -329,7 +345,7 @@ def create_html_file(folder: str, title: str, foldername: str, images: List[Dict
header=header, header=header,
license=license_info, license=license_info,
subdirectories=subfolders, subdirectories=subfolders,
images=image_chunks, images=images,
info=_info, info=_info,
allimages=images, allimages=images,
webmanifest=_args.generate_webmanifest, webmanifest=_args.generate_webmanifest,

View File

@@ -1,5 +1,5 @@
""" """
loggerdabn.py logger.py
This module provides functionality for setting up a centralized logging system using the 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 `logging` library and the `python-json-logger` to output logs in JSON format. It handles
@@ -9,11 +9,14 @@ Functions:
- log_format(keys): Generates the logging format string based on the list of keys. - 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. - 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. - setup_logger(): Configures the logging system, applies a JSON format, and returns a logger instance.
- setup_consolelogger(): Configures the logging system to output logs in console format.
""" """
import logging
import os import os
import json import json
import gzip
import shutil
import logging
from datetime import datetime from datetime import datetime
from pythonjsonlogger import jsonlogger from pythonjsonlogger import jsonlogger
@@ -43,14 +46,16 @@ def log_format(keys):
return [f"%({i})s" for i in keys] return [f"%({i})s" for i in keys]
def rotate_log_file(): def rotate_log_file(compress=False):
""" """
Renames the existing 'latest.jsonl' file to a timestamped file based on the first log entry's asctime. Truncates the 'latest.jsonl' file after optionally compressing its contents to a timestamped file.
The 'latest.jsonl' file is not deleted or moved, just emptied.
If 'latest.jsonl' exists, it's renamed to the first timestamp found in the log entry. Args:
compress (bool): If True, compress the old log file using gzip.
""" """
if os.path.exists(LATEST_LOG_FILE): if os.path.exists(LATEST_LOG_FILE):
with open(LATEST_LOG_FILE, "r", encoding="utf-8") as f: with open(LATEST_LOG_FILE, "r+", encoding="utf-8") as f:
first_line = f.readline() first_line = f.readline()
try: try:
first_log = json.loads(first_line) first_log = json.loads(first_line)
@@ -62,22 +67,33 @@ def rotate_log_file():
safe_timestamp = first_timestamp.replace(":", "-").replace(" ", "_") safe_timestamp = first_timestamp.replace(":", "-").replace(" ", "_")
old_log_filename = os.path.join(LOG_DIR, f"{safe_timestamp}.jsonl") old_log_filename = os.path.join(LOG_DIR, f"{safe_timestamp}.jsonl")
os.rename(LATEST_LOG_FILE, old_log_filename) # Write contents to the new file
with open(old_log_filename, "w", encoding="utf-8") as old_log_file:
f.seek(0) # Go back to the beginning of the file
shutil.copyfileobj(f, old_log_file)
if compress:
with open(old_log_filename, "rb") as f_in:
with gzip.open(f"{old_log_filename}.gz", "wb") as f_out:
shutil.copyfileobj(f_in, f_out)
os.remove(old_log_filename)
# Truncate the original file
f.seek(0)
f.truncate()
def setup_logger(): def setup_logger(level=logging.INFO):
""" """
Configures the logging system with a custom format and outputs logs in JSON format. 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 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. 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: Returns:
logging.Logger: A configured logger instance that can be used to log messages. logging.Logger: A configured logger instance that can be used to log messages.
""" """
rotate_log_file() _logger = logging.getLogger(name="defaultlogger")
app_logger = logging.getLogger()
supported_keys = ["asctime", "created", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "name", "pathname", "process", "processName", "relativeCreated", "thread", "threadName", "taskName"] supported_keys = ["asctime", "created", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "name", "pathname", "process", "processName", "relativeCreated", "thread", "threadName", "taskName"]
@@ -87,10 +103,38 @@ def setup_logger():
log_handler = logging.FileHandler(LATEST_LOG_FILE) log_handler = logging.FileHandler(LATEST_LOG_FILE)
log_handler.setFormatter(formatter) log_handler.setFormatter(formatter)
app_logger.addHandler(log_handler) _logger.addHandler(log_handler)
app_logger.setLevel(logging.INFO) _logger.setLevel(level=level)
return app_logger return _logger
def setup_consolelogger(level=logging.INFO):
"""
Configures the logging system to output logs in console and 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.
Returns:
logging.Logger: A configured logger instance that can be used to log messages.
"""
_logger = logging.getLogger(name="consolelogger")
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)
_logger.addHandler(log_handler)
_logger.addHandler(logging.StreamHandler())
_logger.setLevel(level=level)
return _logger
rotate_log_file(compress=True)
logger = setup_logger() logger = setup_logger()
consolelogger = setup_consolelogger()

View File

@@ -1,6 +1,7 @@
import os import os
import shutil import shutil
from typing import List, Dict from typing import List, Dict
from subprocess import Popen, PIPE
from PIL import Image from PIL import Image
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
@@ -92,10 +93,23 @@ def generate_favicon(iconspath: str, root_directory: str) -> None:
""" """
favicon = 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}) 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}' _env = dict(os.environ)
lp_key = "LD_LIBRARY_PATH"
lp_orig = _env.get(lp_key + "_ORIG")
if lp_orig is not None:
_env[lp_key] = lp_orig
else:
_env.pop(lp_key, None)
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 {favicon}' magick = shutil.which("convert")
os.system(command) else:
magick = shutil.which("magick")
command = [magick, os.path.join(iconspath, "icon.png"), "-define", "icon:auto-resize=16,32,48,64,72,96,144,192", favicon]
with Popen(command, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=_env, errors="ignore") as p:
out, err = p.communicate()
if p.returncode != 0:
logger.error("error generating favicon: %s", err, extra={"command": command, "out": out, "err": err})
logger.info("favicon generated successfully", extra={"command": command, "out": out, "err": err})
def icons(_args: Args) -> None: def icons(_args: Args) -> None:
@@ -164,8 +178,8 @@ 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]
logger.info("creating icons for web application", extra={"iconspath": iconspath, "svg": svg})
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"},
{"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "any"}, {"src": f"{_args.web_root_url}.static/icons/{svg}", "type": "image/svg+xml", "sizes": "512x512", "purpose": "any"},
@@ -174,7 +188,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}) logger.info("converting svg to png", extra={"svg": svg, "size": size})
cairosvg.svg2png( cairosvg.svg2png(
url=os.path.join(iconspath, svg), url=os.path.join(iconspath, svg),
write_to=tmpimg, write_to=tmpimg,
@@ -226,7 +240,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}) logger.info("using icon", extra={"iconspath": iconspath, "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
@@ -241,6 +255,8 @@ def webmanifest(_args: Args) -> None:
_args : Args _args : Args
Parsed command-line arguments. Parsed command-line arguments.
""" """
logger.info("generating webmanifest")
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 = 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)

View File

@@ -56,9 +56,8 @@
{% if images %} {% if images %}
{%- set ns = namespace(count = 0) -%} {%- set ns = namespace(count = 0) -%}
<div class="row"> <div class="row">
{%- for imageblock in images %} {%- for image in images %}
<div class="column"> <div class="column">
{%- for image in imageblock %}
<figure> <figure>
<img src="{{ image.thumbnail }}" alt="{{ image.name }}" onclick="openSwipe({{ ns.count }})" /> <img src="{{ image.thumbnail }}" alt="{{ image.name }}" onclick="openSwipe({{ ns.count }})" />
{%- set ns.count = ns.count + 1 %} {%- set ns.count = ns.count + 1 %}
@@ -71,7 +70,6 @@
{%- endif %} {%- endif %}
</figcaption> </figcaption>
</figure> </figure>
{%- endfor %}
</div> </div>
{%- endfor %} {%- endfor %}
</div> </div>
@@ -147,7 +145,11 @@
var pswpElement = document.querySelectorAll('.pswp')[0]; var pswpElement = document.querySelectorAll('.pswp')[0];
var items = [ var items = [
{%- for image in allimages %} {%- for image in allimages %}
{%- if image.exifdata.DateTime %}
{ src: "{{ image.url }}", w: {{ image.width }}, h: {{ image.height }}, msrc: "{{ image.thumbnail }}", title: "Captured: {{ image.exifdata.DateTime }}" },
{%- else %}
{ src: "{{ image.url }}", w: {{ image.width }}, h: {{ image.height }}, msrc: "{{ image.thumbnail }}" }, { src: "{{ image.url }}", w: {{ image.width }}, h: {{ image.height }}, msrc: "{{ image.thumbnail }}" },
{%- endif %}
{%- endfor %} {%- endfor %}
]; ];
var re = /pid=(\d+)/; var re = /pid=(\d+)/;
@@ -178,8 +180,7 @@
} }
function topFunction() { function topFunction() {
document.body.scrollTop = 0; window.scrollTo({ top: 0, behavior: 'smooth' })
document.documentElement.scrollTop = 0;
} }
</script> </script>
{%- endif %} {%- endif %}

BIN
test/example/DSC03508.ARW Executable file

Binary file not shown.

View File

@@ -5,22 +5,22 @@
--color2: #b22222; --color2: #b22222;
--color3: #ff4500; --color3: #ff4500;
--color4: #6e0000; --color4: #6e0000;
--bcolor1: #ebebeb; --bcolor1: #171717;
--bcolor2: #191919; --bcolor2: #191919;
--bcolor3: #171717; --bcolor3: #ebebeb;
--bcolor4: #0a0a0a; --bcolor4: #0a0a0a;
} }
.navbar { .navbar {
font-weight: bold; font-weight: bold;
color: var(--bcolor1); color: var(--bcolor3);
background-color: var(--color1); background-color: var(--color1);
font-weight: 900; font-weight: 900;
} }
.navbar li a { .navbar li a {
font-weight: 800; font-weight: 800;
color: var(--bcolor1); color: var(--bcolor3);
} }
/* Change the link color on hover */ /* Change the link color on hover */
@@ -30,7 +30,7 @@
} }
.footer { .footer {
color: var(--bcolor1); color: var(--bcolor3);
background-color: var(--color3); background-color: var(--color3);
font-weight: 700; font-weight: 700;
} }

View File

@@ -5,22 +5,22 @@
--color2: #1346a4; --color2: #1346a4;
--color3: #0e3377; --color3: #0e3377;
--color4: #3674e7; --color4: #3674e7;
--bcolor1: #ebebeb; --bcolor1: #171717;
--bcolor2: #191919; --bcolor2: #191919;
--bcolor3: #171717; --bcolor3: #ebebeb;
--bcolor4: #0a0a0a; --bcolor4: #0a0a0a;
} }
.navbar { .navbar {
font-weight: bold; font-weight: bold;
color: var(--bcolor1); color: var(--bcolor3);
background-color: var(--color1); background-color: var(--color1);
font-weight: 900; font-weight: 900;
} }
.navbar li a { .navbar li a {
font-weight: 800; font-weight: 800;
color: var(--bcolor1); color: var(--bcolor3);
} }
/* Change the link color on hover */ /* Change the link color on hover */
@@ -30,7 +30,7 @@
} }
.footer { .footer {
color: var(--bcolor1); color: var(--bcolor3);
background-color: var(--color3); background-color: var(--color3);
font-weight: 700; font-weight: 700;
} }

View File

@@ -1,6 +1,6 @@
<svg width='64' height='64' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'> <svg width='64' height='64' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M20 6H10L8 4H4C2.89543 4 2 4.89543 2 6V18C2 19.1046 2.89543 20 4 20H20C21.1046 20 22 19.1046 22 18V8C22 6.89543 21.1046 6 20 6Z' fill='{{ color1 }}' /> <path d='M20 6H10L8 4H4C2.89543 4 2 4.89543 2 6V18C2 19.1046 2.89543 20 4 20H20C21.1046 20 22 19.1046 22 18V8C22 6.89543 21.1046 6 20 6Z' fill='{{ color1 }}' />
<path d='M4 4H8L10 6H20C21.1046 6 22 6.89543 22 8H2C2 6.89543 2.89543 6 4 6V4Z' fill='{{ color4 }}' /> <path d='M4 4H8L10 6H20C21.1046 6 22 6.89543 22 8H2C2 6.89543 2.89543 6 4 6V4Z' fill='{{ color4 }}' />
<path d='M10 6H14L12 4H8L10 6Z' fill='{{ color2 }}' /> <path d='M10 6H14L12 4H8L10 6Z' fill='{{ color3 }}' />
<path d='M14 6H18L16 4H12L14 6Z' fill='{{ color3 }}' /> <path d='M14 6H18L16 4H12L14 6Z' fill='{{ color2 }}' />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 493 B

After

Width:  |  Height:  |  Size: 493 B

View File

@@ -5,22 +5,22 @@
--color2: #008000; --color2: #008000;
--color3: #32cd32; --color3: #32cd32;
--color4: #004300; --color4: #004300;
--bcolor1: #ebebeb; --bcolor1: #171717;
--bcolor2: #191919; --bcolor2: #191919;
--bcolor3: #171717; --bcolor3: #ebebeb;
--bcolor4: #0a0a0a; --bcolor4: #0a0a0a;
} }
.navbar { .navbar {
font-weight: bold; font-weight: bold;
color: var(--bcolor1); color: var(--bcolor3);
background-color: var(--color1); background-color: var(--color1);
font-weight: 900; font-weight: 900;
} }
.navbar li a { .navbar li a {
font-weight: 800; font-weight: 800;
color: var(--bcolor1); color: var(--bcolor3);
} }
/* Change the link color on hover */ /* Change the link color on hover */
@@ -30,7 +30,7 @@
} }
.footer { .footer {
color: var(--bcolor1); color: var(--bcolor3);
background-color: var(--color3); background-color: var(--color3);
font-weight: 700; font-weight: 700;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -5,9 +5,9 @@
--color2: #ffd700; --color2: #ffd700;
--color3: #ffe135; --color3: #ffe135;
--color4: #ce8c00; --color4: #ce8c00;
--bcolor1: #ebebeb; --bcolor1: #171717;
--bcolor2: #191919; --bcolor2: #191919;
--bcolor3: #171717; --bcolor3: #ebebeb;
--bcolor4: #0a0a0a; --bcolor4: #0a0a0a;
} }
@@ -60,7 +60,7 @@
.row a { .row a {
font-weight: 800; font-weight: 800;
color: var(--color2); color: var(--color1);
text-decoration: none; text-decoration: none;
} }
@@ -70,7 +70,7 @@
.tooltiptext { .tooltiptext {
font-weight: 600; font-weight: 600;
color: var(--bcolor1); color: var(--bcolor3);
background-color: var(--bcolor2); background-color: var(--bcolor2);
} }

4
view-latest-log.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
LESS=-SR hl logs/latest.jsonl --config hl_config.yaml

3
view-logs.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
LESS=-SR hl $(ls -tr logs/*.{jsonl,jsonl.gz}) --config hl_config.yaml