Version 1.2:

- added support for CreativeCommons Licenses (cc0, cc-by, cc-by-sa, cc-by-nd, cc-by-nc, cc-by-nc-sa, cc-by-nc-nd)
- added command line option for Author
- added command line option for title
- added navbar
This commit is contained in:
2024-06-27 22:52:14 +02:00
parent a8948b3fe8
commit 3072879614
3 changed files with 229 additions and 6 deletions

View File

@@ -9,6 +9,8 @@
- **Folder Navigation:** The HTML files include navigation links to subfolders. - **Folder Navigation:** The HTML files include navigation links to subfolders.
- **Responsive Design:** The generated HTML uses responsive design techniques to ensure the gallery looks good on different screen sizes. - **Responsive Design:** The generated HTML uses responsive design techniques to ensure the gallery looks good on different screen sizes.
- **Non-Interactive Mode:** It can run in a non-interactive mode suitable for automated workflows. - **Non-Interactive Mode:** It can run in a non-interactive mode suitable for automated workflows.
- **License Information:** Optionally include license information in the HTML files.
- **Custom Author and Title:** Allows specifying a custom author and title for the HTML files.
## Requirements ## Requirements
@@ -29,7 +31,7 @@ pip install numpy tqdm
The script supports several command-line options to customize its behavior. Below is the list of available options: The script supports several command-line options to customize its behavior. Below is the list of available options:
```sh ```sh
./generate_html.py [-h] [-f ROOT] [-w WEBROOT] [-i ICON] [-r] [-n] [--fancyfolders] ./generate_html.py [-h] [-f ROOT] [-w WEBROOT] [-i ICON] [-r] [-n] [--fancyfolders] [-l LICENSE] [-a AUTHOR] [-t TITLE]
``` ```
### Options ### Options
@@ -41,6 +43,9 @@ The script supports several command-line options to customize its behavior. Belo
- `-r, --regenerate`: Regenerate thumbnails even if they already exist. - `-r, --regenerate`: Regenerate thumbnails even if they already exist.
- `-n, --non-interactive`: Disable interactive mode, which is useful for automated workflows. - `-n, --non-interactive`: Disable interactive mode, which is useful for automated workflows.
- `--fancyfolders`: Use fancy folders instead of the default Apache directory listing. - `--fancyfolders`: Use fancy folders instead of the default Apache directory listing.
- `-l LICENSE, --license LICENSE`: Specify a license for the content. Options 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 AUTHOR`: Specify the author of the content.
- `-t TITLE, --title TITLE`: Specify the title for the root directory HTML file.
### Example ### Example
@@ -56,6 +61,12 @@ To regenerate thumbnails and run in non-interactive mode:
./generate_html.py -f /data/pictures -w https://pictures.example.com -r -n ./generate_html.py -f /data/pictures -w https://pictures.example.com -r -n
``` ```
To include a license, author, and custom title:
```sh
./generate_html.py -f /data/pictures -w https://pictures.example.com -l cc-by -a "John Doe" -t "My Photo Gallery"
```
## Notes ## Notes
- The root and webroot paths must point to the same folder, one on the filesystem and one on the webserver. Use absolute paths. - The root and webroot paths must point to the same folder, one on the filesystem and one on the webserver. Use absolute paths.

138
cclicense.py Normal file
View File

@@ -0,0 +1,138 @@
def licenseswitch(cclicense: str):
switch = {
"cc-zero": """
<div class="license" xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/">
<a property="dct:title" rel="cc:attributionURL" href="$webroot">$title</a> by <span property="cc:attributionName">$author</span> is marked with
<a href="https://creativecommons.org/publicdomain/zero/1.0/" target="_blank" rel="license noopener noreferrer" style="display: inline-block"
>CC0 1.0<img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/cc.svg"
alt="" /><img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/zero.svg"
alt=""
/></a>
</div>
""",
"cc-by": """
<div class="license" xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/">
<a property="dct:title" rel="cc:attributionURL" href="$webroot">$title</a> by <span property="cc:attributionName">$author</span> is licensed under
<a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="license noopener noreferrer" style="display: inline-block"
>CC BY 4.0<img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/cc.svg"
alt="" /><img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/by.svg"
alt=""
/></a>
</div>
""",
"cc-by-sa": """
<div class="license" xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/">
<a property="dct:title" rel="cc:attributionURL" href="$webroot">$title</a> by <span property="cc:attributionName">$author</span> is licensed under
<a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="license noopener noreferrer" style="display: inline-block"
>CC BY-SA 4.0<img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/cc.svg"
alt="" /><img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/by.svg"
alt="" /><img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/sa.svg"
alt=""
/></a>
</div>
""",
"cc-by-nd": """
<div class="license" xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/">
<a property="dct:title" rel="cc:attributionURL" href="$webroot">$title</a> by <span property="cc:attributionName">$author</span> is licensed under
<a href="https://creativecommons.org/licenses/by-nd/4.0/" target="_blank" rel="license noopener noreferrer" style="display: inline-block"
>CC BY-ND 4.0<img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/cc.svg"
alt="" /><img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/by.svg"
alt="" /><img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/nd.svg"
alt=""
/></a>
</div>
""",
"cc-by-nc": """
<div class="license" xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/">
<a property="dct:title" rel="cc:attributionURL" href="$webroot">$title</a> by <span property="cc:attributionName">$author</span> is licensed under
<a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="license noopener noreferrer" style="display: inline-block"
>CC BY-NC 4.0<img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/cc.svg"
alt="" /><img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/by.svg"
alt="" /><img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/nc.svg"
alt=""
/></a>
</div>
""",
"cc-by-nc-sa": """
<div class="license" xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/">
<a property="dct:title" rel="cc:attributionURL" href="$webroot">$title</a> by <span property="cc:attributionName">$author</span> is licensed under
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" rel="license noopener noreferrer" style="display: inline-block"
>CC BY-NC-SA 4.0<img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/cc.svg"
alt="" /><img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/by.svg"
alt="" /><img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/nc.svg"
alt="" /><img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/sa.svg"
alt=""
/></a>
</div>
""",
"cc-by-nc-nd": """
<div class="license" xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/">
<a property="dct:title" rel="cc:attributionURL" href="$webroot">$title</a> by <span property="cc:attributionName">$author</span> is licensed under
<a href="https://creativecommons.org/licenses/by-nc-nd/4.0/" target="_blank" rel="license noopener noreferrer" style="display: inline-block"
>CC BY-NC-ND 4.0<img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/cc.svg"
alt="" /><img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/by.svg"
alt="" /><img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/nc.svg"
alt="" /><img
style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom"
src="https://mirrors.creativecommons.org/presskit/icons/nd.svg"
alt=""
/></a>
</div>
""",
}
return switch.get(cclicense, "")
def licenseurlswitch(cclicense: str):
switch = {
"cc-zero": "https://creativecommons.org/publicdomain/zero/1.0/",
"cc-by": "https://creativecommons.org/licenses/by/4.0/",
"cc-by-sa": "https://creativecommons.org/licenses/by-sa/4.0/",
"cc-by-nd": "https://creativecommons.org/licenses/by-nd/4.0/",
"cc-by-nc": "https://creativecommons.org/licenses/by-nc/4.0/",
"cc-by-nc-sa": "https://creativecommons.org/licenses/by-nc-sa/4.0/",
"cc-by-nc-nd": "https://creativecommons.org/licenses/by-nc-nd/4.0/",
}
return switch.get(cclicense, "")

View File

@@ -9,11 +9,14 @@ from pathlib import Path
import numpy as np import numpy as np
from tqdm.auto import tqdm from tqdm.auto import tqdm
import cclicense
_ROOT = "/data/pictures/" _ROOT = "/data/pictures/"
_WEBROOT = "https://pictures.example.com/" _WEBROOT = "https://pictures.example.com/"
_FOLDERICON = "https://www.svgrepo.com/show/400249/folder.svg" _FOLDERICON = "https://www.svgrepo.com/show/400249/folder.svg"
_ROOTTITLE = "Pictures" _ROOTTITLE = "Pictures"
_FAVICON = "favicon.ico" _FAVICON = "favicon.ico"
_AUTHOR = "Author"
imgext = [".jpg", ".jpeg"] imgext = [".jpg", ".jpeg"]
rawext = [".3fr", ".ari", ".arw", ".bay", ".braw", ".crw", ".cr2", ".cr3", ".cap", ".data", ".dcs", ".dcr", ".dng", ".drf", ".eip", ".erf", ".fff", ".gpr", ".iiq", ".k25", ".kdc", ".mdc", ".mef", ".mos", ".mrw", ".nef", ".nrw", ".obm", ".orf", ".pef", ".ptx", ".pxn", ".r3d", ".raf", ".raw", ".rwl", ".rw2", ".rwz", ".sr2", ".srf", ".srw", ".tif", ".tiff", ".x3f"] rawext = [".3fr", ".ari", ".arw", ".bay", ".braw", ".crw", ".cr2", ".cr3", ".cap", ".data", ".dcs", ".dcr", ".dng", ".drf", ".eip", ".erf", ".fff", ".gpr", ".iiq", ".k25", ".kdc", ".mdc", ".mef", ".mos", ".mrw", ".nef", ".nrw", ".obm", ".orf", ".pef", ".ptx", ".pxn", ".r3d", ".raf", ".raw", ".rwl", ".rw2", ".rwz", ".sr2", ".srf", ".srw", ".tif", ".tiff", ".x3f"]
excludes = [".lock", _FAVICON, "index.html", "Galleries", ".previews", "Archives"] excludes = [".lock", _FAVICON, "index.html", "Galleries", ".previews", "Archives"]
@@ -35,6 +38,8 @@ HTMLHEADER = """
body { body {
margin: 0; margin: 0;
margin-top: 32px;
margin-bottom: 56px;
font-family: Arial; font-family: Arial;
} }
@@ -50,6 +55,7 @@ HTMLHEADER = """
.folders figure { .folders figure {
margin-bottom: 32px; margin-bottom: 32px;
margin-top: 50px;
} }
.header h1 { .header h1 {
@@ -151,11 +157,63 @@ HTMLHEADER = """
width: 100%; width: 100%;
display: block; display: block;
} }
.license {
position: fixed;
bottom: 0;
width: 100%;
background-color: lightgrey;
padding: 12px;
}
.navbar {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
position: fixed;
top: 0;
width: 100%;
background-color: #333;
}
.navbar li {
float: left;
}
.navbar li a {
display: block;
color: white;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
.navbar li span {
display: block;
color: white;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
/* Change the link color to #111 (black) on hover */
.navbar li a:hover {
background-color: #111;
}
</style> </style>
</head> </head>
<body> <body>
""" """
NAVBAR = """
<ul class="navbar">
<li><a href="$home">Home</a></li>
<li><a href="$parent">Parent Directory</a></li>
<li style="position: absolute; left: 50%; transform: translateX(-50%);"><span>$title</span></li>
$license</ul>
"""
def thumbnail_convert(arguments: tuple[str, str]): def thumbnail_convert(arguments: tuple[str, str]):
folder, item = arguments folder, item = arguments
@@ -178,7 +236,8 @@ def listfolder(folder: str, title: str):
if not os.path.exists(os.path.join(args.root, ".previews", folder.removeprefix(args.root))): if not os.path.exists(os.path.join(args.root, ".previews", folder.removeprefix(args.root))):
os.mkdir(os.path.join(args.root, ".previews", folder.removeprefix(args.root))) os.mkdir(os.path.join(args.root, ".previews", folder.removeprefix(args.root)))
temp_obj = Template(HTMLHEADER) body = Template(HTMLHEADER)
navbar = Template(NAVBAR)
contains_files = False contains_files = False
for item in items: for item in items:
if item not in excludes: if item not in excludes:
@@ -212,9 +271,15 @@ def listfolder(folder: str, title: str):
pbar.update(0) pbar.update(0)
if len(images) > 0 or (args.fancyfolders and not contains_files): if len(images) > 0 or (args.fancyfolders and not contains_files):
with open(os.path.join(folder, "index.html"), "w", encoding="utf-8") as f: with open(os.path.join(folder, "index.html"), "w", encoding="utf-8") as f:
f.write(temp_obj.substitute(title=title, favicon=f"{args.root}/{_FAVICON}")) f.write(body.substitute(title=title, favicon=f"{args.webroot}{_FAVICON}"))
f.write(' <div class="header">\n') f.write(' <div class="header">\n')
f.write(f" <h1>{title}</h1>\n") if folder == args.root:
f.write(f" <h1>{os.path.basename(folder)}</h1>\n")
else:
if args.license:
f.write(navbar.substitute(home=args.webroot, parent=f"{args.webroot}{urllib.parse.quote(folder.removeprefix(args.root).removesuffix(folder.split('/')[-1]))}", title=os.path.basename(folder), license=f' <li style="float:right"><a href="{cclicense.licenseurlswitch(args.license)}" target="_blank">License</a></li>\n'))
else:
f.write(navbar.substitute(home=args.webroot, parent=f"{args.webroot}{urllib.parse.quote(folder.removeprefix(args.root).removesuffix(folder.split('/')[-1]))}", title=os.path.basename(folder), license=""))
f.write(' <div class="folders">\n') f.write(' <div class="folders">\n')
for subfolder in subfolders: for subfolder in subfolders:
f.write(subfolder) f.write(subfolder)
@@ -229,6 +294,8 @@ def listfolder(folder: str, title: str):
f.write(f" {image}\n") f.write(f" {image}\n")
f.write(" </div>\n") f.write(" </div>\n")
f.write(" </div>\n") f.write(" </div>\n")
if args.license:
f.write(_cclicense.substitute(webroot=args.webroot, title=args.title, author=args.author))
f.write(" </body>\n</html>") f.write(" </body>\n</html>")
f.close() f.close()
else: else:
@@ -261,6 +328,7 @@ def main():
global total global total
global args global args
global pbar global pbar
global _cclicense
total = 0 total = 0
# Parse command-line arguments # Parse command-line arguments
@@ -270,6 +338,9 @@ def main():
parser.add_argument("-i", "--foldericon", help="Foldericon url", default=_FOLDERICON, required=False, type=str, dest="foldericon", metavar="ICON") parser.add_argument("-i", "--foldericon", help="Foldericon url", default=_FOLDERICON, required=False, type=str, dest="foldericon", metavar="ICON")
parser.add_argument("-r", "--regenerate", help="Regenerate thumbnails", action="store_true", default=False, required=False, dest="regenerate") parser.add_argument("-r", "--regenerate", help="Regenerate thumbnails", action="store_true", default=False, required=False, dest="regenerate")
parser.add_argument("-n", "--non-interactive", help="Disable interactive mode", action="store_true", default=False, required=False, dest="non_interactive") parser.add_argument("-n", "--non-interactive", help="Disable interactive mode", action="store_true", default=False, required=False, dest="non_interactive")
parser.add_argument("-l", "--license", help="License", default=None, required=False, choices=["cc-zero", "cc-by", "cc-by-sa", "cc-by-nd", "cc-by-nc", "cc-by-nc-sa", "cc-by-nc-nd"], dest="license")
parser.add_argument("-a", "--author", help="Author", default=_AUTHOR, required=False, type=str, dest="author")
parser.add_argument("-t", "--title", help="Title", default=_ROOTTITLE, required=False, type=str, dest="title")
parser.add_argument("--fancyfolders", help="Use fancy folders instead of default apache ones", action="store_true", default=False, required=False, dest="fancyfolders") parser.add_argument("--fancyfolders", help="Use fancy folders instead of default apache ones", action="store_true", default=False, required=False, dest="fancyfolders")
args = parser.parse_args() args = parser.parse_args()
@@ -280,6 +351,9 @@ def main():
if not os.path.exists(os.path.join(args.root, ".previews")): if not os.path.exists(os.path.join(args.root, ".previews")):
os.mkdir(os.path.join(args.root, ".previews")) os.mkdir(os.path.join(args.root, ".previews"))
if args.license:
_cclicense = Template(cclicense.licenseswitch(args.license))
if os.path.exists(os.path.join(args.root, ".lock")): if os.path.exists(os.path.join(args.root, ".lock")):
print("Another instance of this program is running.") print("Another instance of this program is running.")
exit() exit()
@@ -288,7 +362,7 @@ def main():
if args.non_interactive: if args.non_interactive:
print("Generating html files...") print("Generating html files...")
listfolder(args.root, _ROOTTITLE) listfolder(args.root, args.title)
with Pool(os.cpu_count()) as p: with Pool(os.cpu_count()) as p:
print("Generating thumbnails...") print("Generating thumbnails...")
@@ -299,7 +373,7 @@ def main():
pbar.close() pbar.close()
pbar = tqdm(total=total + 1, desc="Generating html files", unit=" files", ascii=True, dynamic_ncols=True) pbar = tqdm(total=total + 1, desc="Generating html files", unit=" files", ascii=True, dynamic_ncols=True)
listfolder(args.root, _ROOTTITLE) listfolder(args.root, args.title)
pbar.close() pbar.close()
with Pool(os.cpu_count()) as p: with Pool(os.cpu_count()) as p: