mirror of
https://github.com/greflm13/StaticGalleryBuilder.git
synced 2026-02-05 11:09:26 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0973868782 | |||
| cd06c526af | |||
| d155b05798 | |||
| 3352894e4d | |||
| d743ede95d | |||
| 9403e84d78 | |||
| 961d79754e | |||
| 383dd59851 | |||
| 549c15ca6c | |||
| 57d949677f | |||
| cc6ad14506 |
20
README.md
20
README.md
@@ -44,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
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -218,6 +223,21 @@
|
|||||||
"clear": true
|
"clear": true
|
||||||
},
|
},
|
||||||
"group": "build"
|
"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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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] = {}
|
||||||
@@ -197,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")):
|
||||||
@@ -213,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)
|
||||||
|
|
||||||
|
|||||||
@@ -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%;
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
73
hl_config.yaml
Normal file
73
hl_config.yaml
Normal 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"
|
||||||
@@ -2,8 +2,15 @@ from dataclasses import dataclass
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
try:
|
||||||
from rich_argparse import RichHelpFormatter, HelpPreviewAction
|
from rich_argparse import RichHelpFormatter, HelpPreviewAction
|
||||||
|
|
||||||
|
RICH = True
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
RICH = False
|
||||||
|
|
||||||
|
|
||||||
from modules.logger import logger
|
from modules.logger import logger
|
||||||
|
|
||||||
if __package__ is None:
|
if __package__ is None:
|
||||||
@@ -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,19 +107,24 @@ 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)
|
||||||
|
else:
|
||||||
|
parser = argparse.ArgumentParser(description="Generate HTML files for a static image hosting website.")
|
||||||
parser.add_argument("-a", "--author-name", help="Name of the author of the images.", default=DEFAULT_AUTHOR, type=str, dest="author_name", metavar="AUTHOR")
|
parser.add_argument("-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")
|
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("--generate-help-preview", action=HelpPreviewAction, path="help.svg", )
|
||||||
parser.add_argument("--ignore-other-files", help="Ignore files that do not match the specified extensions.", action="store_true", default=False, dest="ignore_other_files")
|
parser.add_argument("--ignore-other-files", help="Ignore files that do not match the specified extensions.", action="store_true", default=False, dest="ignore_other_files")
|
||||||
|
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("--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("--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("--version", action="version", version=f"%(prog)s {version}")
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
@@ -299,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://"):
|
||||||
@@ -331,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,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -12,9 +12,11 @@ Functions:
|
|||||||
- setup_consolelogger(): Configures the logging system to output logs in console format.
|
- 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
|
||||||
|
|
||||||
@@ -44,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)
|
||||||
@@ -63,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")
|
||||||
_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"]
|
||||||
|
|
||||||
@@ -89,22 +104,37 @@ def setup_logger():
|
|||||||
log_handler.setFormatter(formatter)
|
log_handler.setFormatter(formatter)
|
||||||
|
|
||||||
_logger.addHandler(log_handler)
|
_logger.addHandler(log_handler)
|
||||||
_logger.setLevel(logging.INFO)
|
_logger.setLevel(level=level)
|
||||||
|
|
||||||
return _logger
|
return _logger
|
||||||
|
|
||||||
|
|
||||||
def setup_consolelogger():
|
def setup_consolelogger(level=logging.INFO):
|
||||||
"""
|
"""
|
||||||
Configures the logging system to output logs in console format.
|
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:
|
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.
|
||||||
"""
|
"""
|
||||||
_logger = setup_logger()
|
_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.addHandler(logging.StreamHandler())
|
||||||
|
_logger.setLevel(level=level)
|
||||||
return _logger
|
return _logger
|
||||||
|
|
||||||
|
|
||||||
|
rotate_log_file(compress=True)
|
||||||
logger = setup_logger()
|
logger = setup_logger()
|
||||||
consolelogger = setup_consolelogger()
|
consolelogger = setup_consolelogger()
|
||||||
|
|||||||
@@ -178,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"},
|
||||||
@@ -188,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,
|
||||||
@@ -240,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
|
||||||
@@ -255,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)
|
||||||
|
|||||||
@@ -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
BIN
test/example/DSC03508.ARW
Executable file
Binary file not shown.
4
view-latest-log.sh
Executable file
4
view-latest-log.sh
Executable 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
3
view-logs.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
LESS=-SR hl $(ls -tr logs/*.{jsonl,jsonl.gz}) --config hl_config.yaml
|
||||||
Reference in New Issue
Block a user