switched to Jinja2 templating

This commit is contained in:
2024-06-28 11:38:13 +02:00
committed by Flo Greistorfer
parent b1dbfd9c14
commit a3748af71c
9 changed files with 866 additions and 256 deletions

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.12

View File

@@ -17,13 +17,15 @@
- Python 3.x - Python 3.x
- `numpy` library - `numpy` library
- `tqdm` library - `tqdm` library
- `Jinja2` library
- `ImageMagick`
## Installation ## Installation
Install the required libraries using pip: Install the required libraries using pip:
```sh ```sh
pip install numpy tqdm pip install numpy tqdm Jinja2
``` ```
## Usage ## Usage
@@ -71,7 +73,7 @@ To include a license, author, and custom title:
- 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.
- Ensure that ImageMagick is installed and accessible in your system for thumbnail generation. - Ensure that ImageMagick is installed and accessible in your system for thumbnail generation.
- The script generates the preview thumbnails in a `.previews` subdirectory within the root folder. - The script generates the preview thumbnails in a `.thumbnails` subdirectory within the root folder.
## License ## License

View File

@@ -1,4 +1,4 @@
def licenseswitch(cclicense: str): def licenseswitch(cclicense: str) -> str:
switch = { switch = {
"cc-zero": """ "cc-zero": """
<div class="license" xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/"> <div class="license" xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/">
@@ -124,7 +124,7 @@ def licenseswitch(cclicense: str):
return switch.get(cclicense, "") return switch.get(cclicense, "")
def licenseurlswitch(cclicense: str): def licenseurlswitch(cclicense: str) -> str:
switch = { switch = {
"cc-zero": "https://creativecommons.org/publicdomain/zero/1.0/", "cc-zero": "https://creativecommons.org/publicdomain/zero/1.0/",
"cc-by": "https://creativecommons.org/licenses/by/4.0/", "cc-by": "https://creativecommons.org/licenses/by/4.0/",
@@ -136,3 +136,58 @@ def licenseurlswitch(cclicense: str):
} }
return switch.get(cclicense, "") return switch.get(cclicense, "")
def licensenameswitch(cclicense: str) -> str:
switch = {
"cc-zero": "CC0 1.0",
"cc-by": "CC BY 4.0",
"cc-by-sa": "CC BY-SA 4.0",
"cc-by-nd": "CC BY-ND 4.0",
"cc-by-nc": "CC BY-NC 4.0",
"cc-by-nc-sa": "CC BY-NC-SA 4.0",
"cc-by-nc-nd": "CC BY-NC-ND 4.0",
}
return switch.get(cclicense, "")
def licensepicswitch(cclicense: str) -> list[str]:
switch = {
"cc-zero": [
"https://mirrors.creativecommons.org/presskit/icons/cc.svg",
"https://mirrors.creativecommons.org/presskit/icons/zero.svg",
],
"cc-by": [
"https://mirrors.creativecommons.org/presskit/icons/cc.svg",
"https://mirrors.creativecommons.org/presskit/icons/by.svg",
],
"cc-by-sa": [
"https://mirrors.creativecommons.org/presskit/icons/cc.svg",
"https://mirrors.creativecommons.org/presskit/icons/by.svg",
"https://mirrors.creativecommons.org/presskit/icons/sa.svg",
],
"cc-by-nd": [
"https://mirrors.creativecommons.org/presskit/icons/cc.svg",
"https://mirrors.creativecommons.org/presskit/icons/by.svg",
"https://mirrors.creativecommons.org/presskit/icons/nd.svg",
],
"cc-by-nc": [
"https://mirrors.creativecommons.org/presskit/icons/cc.svg",
"https://mirrors.creativecommons.org/presskit/icons/by.svg",
"https://mirrors.creativecommons.org/presskit/icons/nc.svg",
],
"cc-by-nc-sa": [
"https://mirrors.creativecommons.org/presskit/icons/cc.svg",
"https://mirrors.creativecommons.org/presskit/icons/by.svg",
"https://mirrors.creativecommons.org/presskit/icons/nc.svg",
"https://mirrors.creativecommons.org/presskit/icons/sa.svg",
],
"cc-by-nc-nd": [
"https://mirrors.creativecommons.org/presskit/icons/cc.svg",
"https://mirrors.creativecommons.org/presskit/icons/by.svg",
"https://mirrors.creativecommons.org/presskit/icons/nd.svg",
],
}
return switch.get(cclicense, "")

BIN
files/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

180
files/global.css Normal file
View File

@@ -0,0 +1,180 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
margin-top: 32px;
margin-bottom: 56px;
font-family: Arial;
}
.folders {
text-align: center;
display: -ms-flexbox;
/* IE10 */
display: flex;
-ms-flex-wrap: wrap;
/* IE10 */
flex-wrap: wrap;
justify-content: space-evenly;
overflow: hidden;
}
.folders figure {
margin-bottom: 32px;
margin-top: 50px;
}
.header h1 {
font-size: 2.5em;
font-weight: bold;
text-align: center;
}
.folders img {
width: 100px;
vertical-align: middle;
}
.folders figcaption {
width: 120px;
font-size: smaller;
text-align: center;
}
.row {
display: -ms-flexbox;
/* IE10 */
display: flex;
-ms-flex-wrap: wrap;
/* IE10 */
flex-wrap: wrap;
padding: 0 2px;
}
figure {
margin: 0;
}
/* Create four equal columns that sits next to each other */
.column {
-ms-flex: 12.5%;
/* IE10 */
flex: 12.5%;
max-width: 12.5%;
padding: 0 4px;
}
.column img {
margin-top: 20px;
vertical-align: middle;
width: 100%;
}
/* Responsive layout - makes a four column-layout instead of eight columns */
@media screen and (max-width: 1000px) {
.column {
-ms-flex: 25%;
flex: 25%;
max-width: 25%;
}
.folders img {
width: 80px;
}
.folders figcaption {
width: 100px;
font-size: small;
}
}
/* Responsive layout - makes a two column-layout instead of four columns */
@media screen and (max-width: 800px) {
.column {
-ms-flex: 50%;
flex: 50%;
max-width: 50%;
}
.folders img {
width: 60px;
}
.folders figcaption {
width: 80px;
font-size: x-small;
}
}
/* Responsive layout - makes the two columns stack on top of each other instead of next to each other */
@media screen and (max-width: 600px) {
.column {
-ms-flex: 100%;
flex: 100%;
max-width: 100%;
}
.folders img {
width: 40px;
}
.folders figcaption {
width: 60px;
font-size: xx-small;
}
}
.caption {
padding-top: 4px;
text-align: center;
font-style: italic;
font-size: 12px;
width: 100%;
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;
}

392
generate_html.old.py Executable file
View File

@@ -0,0 +1,392 @@
#!/usr/bin/env python3
import os
import argparse
import urllib.parse
import shutil
from multiprocessing import Pool
from string import Template
from pathlib import Path
import numpy as np
from tqdm.auto import tqdm
import cclicense
environment = Environment(loader=FileSystemLoader("templates/"))
_ROOT = "/data/pictures/"
_WEBROOT = "https://pictures.example.com/"
_FOLDERICON = "https://www.svgrepo.com/show/400249/folder.svg"
_ROOTTITLE = "Pictures"
_FAVICON = "favicon.ico"
_AUTHOR = "Author"
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"]
excludes = [".lock", _FAVICON, "index.html", ".previews"]
notlist = ["Galleries", "Archives"]
thumbnails: list[tuple[str, str]] = []
HTMLHEADER = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>$title</title>
<link rel="icon" type="image/x-icon" href="$favicon">
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
margin-top: 32px;
margin-bottom: 56px;
font-family: Arial;
}
.folders {
text-align: center;
display: -ms-flexbox; /* IE10 */
display: flex;
-ms-flex-wrap: wrap; /* IE10 */
flex-wrap: wrap;
justify-content: space-evenly;
overflow: hidden;
}
.folders figure {
margin-bottom: 32px;
margin-top: 50px;
}
.header h1 {
font-size: 2.5em;
font-weight: bold;
text-align: center;
}
.folders img {
width: 100px;
vertical-align: middle;
}
.folders figcaption {
width: 120px;
font-size: smaller;
text-align: center;
}
.row {
display: -ms-flexbox; /* IE10 */
display: flex;
-ms-flex-wrap: wrap; /* IE10 */
flex-wrap: wrap;
padding: 0 2px;
}
figure {
margin: 0;
}
/* Create four equal columns that sits next to each other */
.column {
-ms-flex: 12.5%; /* IE10 */
flex: 12.5%;
max-width: 12.5%;
padding: 0 4px;
}
.column img {
margin-top: 20px;
vertical-align: middle;
width: 100%;
}
/* Responsive layout - makes a four column-layout instead of eight columns */
@media screen and (max-width: 1000px) {
.column {
-ms-flex: 25%;
flex: 25%;
max-width: 25%;
}
.folders img {
width: 80px;
}
.folders figcaption {
width: 100px;
font-size: small;
}
}
/* Responsive layout - makes a two column-layout instead of four columns */
@media screen and (max-width: 800px) {
.column {
-ms-flex: 50%;
flex: 50%;
max-width: 50%;
}
.folders img {
width: 60px;
}
.folders figcaption {
width: 80px;
font-size: x-small;
}
}
/* Responsive layout - makes the two columns stack on top of each other instead of next to each other */
@media screen and (max-width: 600px) {
.column {
-ms-flex: 100%;
flex: 100%;
max-width: 100%;
}
.folders img {
width: 40px;
}
.folders figcaption {
width: 60px;
font-size: xx-small;
}
}
.caption {
padding-top: 4px;
text-align: center;
font-style: italic;
font-size: 12px;
width: 100%;
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>
</head>
<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]):
folder, item = arguments
if not os.path.exists(os.path.join(args.root, ".previews", folder.removeprefix(args.root), os.path.splitext(item)[0]) + ".jpg") or args.regenerate:
if shutil.which("magick"):
os.system(f'magick "{os.path.join(folder, item)}" -quality 75% -define jpeg:size=1024x1024 -define jpeg:extent=100kb -thumbnail 512x512 -auto-orient "{os.path.join(args.root, ".previews", folder.removeprefix(args.root), os.path.splitext(item)[0])}.jpg"')
else:
os.system(f'convert "{os.path.join(folder, item)}" -quality 75% -define jpeg:size=1024x1024 -define jpeg:extent=100kb -thumbnail 512x512 -auto-orient "{os.path.join(args.root, ".previews", folder.removeprefix(args.root), os.path.splitext(item)[0])}.jpg"')
def listfolder(folder: str, title: str):
if not args.non_interactive:
pbar.desc = f"Generating html files - {folder}"
pbar.update(0)
items: list[str] = os.listdir(folder)
items.sort()
images: list[str] = []
subfolders: list[str] = []
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)))
body = Template(HTMLHEADER)
navbar = Template(NAVBAR)
contains_files = False
for item in items:
if item not in excludes:
if os.path.isdir(os.path.join(folder, item)):
subfolders.extend([f'<figure><a href="{args.webroot}{urllib.parse.quote(folder.removeprefix(args.root))}/{urllib.parse.quote(item)}"><img src="{args.foldericon}" alt="Folder icon"/></a><figcaption><a href="{args.webroot}{urllib.parse.quote(folder.removeprefix(args.root))}/{urllib.parse.quote(item)}">{item}</a></figcaption></figure>'])
if item not in notlist:
listfolder(os.path.join(folder, item), os.path.join(folder, item).removeprefix(args.root))
else:
if not args.non_interactive:
pbar.desc = f"Generating html files - {folder}"
pbar.update(0)
contains_files = True
if os.path.splitext(item)[1].lower() in imgext:
image = f'<figure><a href="{args.webroot}{urllib.parse.quote(folder.removeprefix(args.root))}/{urllib.parse.quote(item)}"><img src="{args.webroot}.previews/{urllib.parse.quote(folder.removeprefix(args.root))}/{urllib.parse.quote(os.path.splitext(item)[0])}.jpg" alt="{item}"/></a><figcaption class="caption">{item}'
if not os.path.exists(os.path.join(args.root, ".previews", folder.removeprefix(args.root), item)):
thumbnails.append((folder, item))
for raw in rawext:
if os.path.exists(os.path.join(folder, os.path.splitext(item)[0] + raw)):
if raw in (".tif", ".tiff"):
image += f': <a href="{args.webroot}{urllib.parse.quote(folder.removeprefix(args.root))}/{urllib.parse.quote(os.path.splitext(item)[0])}{raw}">TIFF</a>'
else:
image += f': <a href="{args.webroot}{urllib.parse.quote(folder.removeprefix(args.root))}/{urllib.parse.quote(os.path.splitext(item)[0])}{raw}">RAW</a>'
elif os.path.exists(os.path.join(folder, os.path.splitext(item)[0] + raw.upper())):
if raw in (".tif", ".tiff"):
image += f': <a href="{args.webroot}{urllib.parse.quote(folder.removeprefix(args.root))}/{urllib.parse.quote(os.path.splitext(item)[0])}{raw.upper()}">TIFF</a>'
else:
image += f': <a href="{args.webroot}{urllib.parse.quote(folder.removeprefix(args.root))}/{urllib.parse.quote(os.path.splitext(item)[0])}{raw.upper()}">RAW</a>'
image += "</figcaption></figure>"
images.extend([image])
if not args.non_interactive:
pbar.desc = f"Generating html files - {folder}"
pbar.update(0)
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:
f.write(body.substitute(title=title, favicon=f"{args.webroot}{_FAVICON}"))
f.write(' <div class="header">\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')
for subfolder in subfolders:
f.write(subfolder)
f.write("\n")
f.write(" </div>\n")
f.write(" </div>\n")
if len(images) > 0:
f.write(' <div class="row">\n')
for chunk in np.array_split(images, 8):
f.write(' <div class="column">\n')
for image in chunk:
f.write(f" {image}\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.close()
else:
if os.path.exists(os.path.join(folder, "index.html")):
os.remove(os.path.join(folder, "index.html"))
if not args.non_interactive:
pbar.update(1)
def gettotal(folder):
global total
if not args.non_interactive:
pbar.desc = f"Traversing filesystem - {folder}"
pbar.update(0)
items: list[str] = os.listdir(folder)
items.sort()
for item in items:
if item not in excludes:
if os.path.isdir(os.path.join(folder, item)):
total += 1
if not args.non_interactive:
pbar.update(1)
if item not in notlist:
gettotal(os.path.join(folder, item))
def main():
global total
global args
global pbar
global _cclicense
total = 0
# Parse command-line arguments
parser = argparse.ArgumentParser(description="Generate html files for static image host.")
parser.add_argument("-p", "--root", help="Root folder", default=_ROOT, required=False, type=str, dest="root")
parser.add_argument("-w", "--webroot", help="Webroot url", default=_WEBROOT, required=False, type=str, dest="webroot")
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("-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")
args = parser.parse_args()
if not args.root.endswith("/"):
args.root += "/"
if not args.webroot.endswith("/"):
args.webroot += "/"
if not os.path.exists(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")):
print("Another instance of this program is running.")
exit()
try:
Path(os.path.join(args.root, ".lock")).touch()
if args.non_interactive:
print("Generating html files...")
listfolder(args.root, args.title)
with Pool(os.cpu_count()) as p:
print("Generating thumbnails...")
p.map(thumbnail_convert, thumbnails)
else:
pbar = tqdm(desc="Traversing filesystem", unit=" folders", ascii=True, dynamic_ncols=True)
gettotal(args.root)
pbar.close()
pbar = tqdm(total=total + 1, desc="Generating html files", unit=" files", ascii=True, dynamic_ncols=True)
listfolder(args.root, args.title)
pbar.close()
with Pool(os.cpu_count()) as p:
for r in tqdm(p.imap_unordered(thumbnail_convert, thumbnails), total=len(thumbnails), desc="Generating thumbnails", unit=" files", ascii=True, dynamic_ncols=True):
pass
finally:
os.remove(os.path.join(args.root, ".lock"))
if __name__ == "__main__":
main()

View File

@@ -4,9 +4,9 @@ import argparse
import urllib.parse import urllib.parse
import shutil import shutil
from multiprocessing import Pool from multiprocessing import Pool
from string import Template
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
from jinja2 import Environment, FileSystemLoader
from tqdm.auto import tqdm from tqdm.auto import tqdm
import cclicense import cclicense
@@ -17,219 +17,32 @@ _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" _STATICFILES = os.path.join(os.path.abspath(os.path.dirname(__file__)), "files")
_FAVICON = ".static/favicon.ico"
_STYLE = ".static/global.css"
_AUTHOR = "Author" _AUTHOR = "Author"
imgext = [".jpg", ".jpeg"] # fmt: off
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 = [ imgext = [".jpg", ".jpeg"]
".lock", excludes = [".lock", "index.html", ".thumbnails", ".static"]
_FAVICON,
"index.html",
".previews",
]
notlist = ["Galleries", "Archives"] notlist = ["Galleries", "Archives"]
# fmt: on
thumbnails: list[tuple[str, str]] = [] thumbnails: list[tuple[str, str]] = []
HTMLHEADER = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>$title</title>
<link rel="icon" type="image/x-icon" href="$favicon">
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
margin-top: 32px;
margin-bottom: 56px;
font-family: Arial;
}
.folders {
text-align: center;
display: -ms-flexbox; /* IE10 */
display: flex;
-ms-flex-wrap: wrap; /* IE10 */
flex-wrap: wrap;
justify-content: space-evenly;
overflow: hidden;
}
.folders figure {
margin-bottom: 32px;
margin-top: 50px;
}
.header h1 {
font-size: 2.5em;
font-weight: bold;
text-align: center;
}
.folders img {
width: 100px;
vertical-align: middle;
}
.folders figcaption {
width: 120px;
font-size: smaller;
text-align: center;
}
.row {
display: -ms-flexbox; /* IE10 */
display: flex;
-ms-flex-wrap: wrap; /* IE10 */
flex-wrap: wrap;
padding: 0 2px;
}
figure {
margin: 0;
}
/* Create four equal columns that sits next to each other */
.column {
-ms-flex: 12.5%; /* IE10 */
flex: 12.5%;
max-width: 12.5%;
padding: 0 4px;
}
.column img {
margin-top: 20px;
vertical-align: middle;
width: 100%;
}
/* Responsive layout - makes a four column-layout instead of eight columns */
@media screen and (max-width: 1000px) {
.column {
-ms-flex: 25%;
flex: 25%;
max-width: 25%;
}
.folders img {
width: 80px;
}
.folders figcaption {
width: 100px;
font-size: small;
}
}
/* Responsive layout - makes a two column-layout instead of four columns */
@media screen and (max-width: 800px) {
.column {
-ms-flex: 50%;
flex: 50%;
max-width: 50%;
}
.folders img {
width: 60px;
}
.folders figcaption {
width: 80px;
font-size: x-small;
}
}
/* Responsive layout - makes the two columns stack on top of each other instead of next to each other */
@media screen and (max-width: 600px) {
.column {
-ms-flex: 100%;
flex: 100%;
max-width: 100%;
}
.folders img {
width: 40px;
}
.folders figcaption {
width: 60px;
font-size: xx-small;
}
}
.caption {
padding-top: 4px;
text-align: center;
font-style: italic;
font-size: 12px;
width: 100%;
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>
</head>
<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
if not os.path.exists(os.path.join(args.root, ".previews", folder.removeprefix(args.root), os.path.splitext(item)[0]) + ".jpg") or args.regenerate: path = os.path.join(args.root, ".thumbnails", folder.removeprefix(args.root), os.path.splitext(item)[0]) + ".jpg"
if not os.path.exists(path) or args.regenerate:
if shutil.which("magick"): if shutil.which("magick"):
os.system(f'magick "{os.path.join(folder, item)}" -quality 75% -define jpeg:size=1024x1024 -define jpeg:extent=100kb -thumbnail 512x512 -auto-orient "{os.path.join(args.root, ".previews", folder.removeprefix(args.root), os.path.splitext(item)[0])}.jpg"') os.system(
f'magick "{os.path.join(folder, item)}" -quality 75% -define jpeg:size=1024x1024 -define jpeg:extent=100kb -thumbnail 512x512 -auto-orient "{path}"'
)
else: else:
os.system(f'convert "{os.path.join(folder, item)}" -quality 75% -define jpeg:size=1024x1024 -define jpeg:extent=100kb -thumbnail 512x512 -auto-orient "{os.path.join(args.root, ".previews", folder.removeprefix(args.root), os.path.splitext(item)[0])}.jpg"') os.system(
f'convert "{os.path.join(folder, item)}" -quality 75% -define jpeg:size=1024x1024 -define jpeg:extent=100kb -thumbnail 512x512 -auto-orient "{path}"'
)
def listfolder(folder: str, title: str): def listfolder(folder: str, title: str):
@@ -238,74 +51,86 @@ def listfolder(folder: str, title: str):
pbar.update(0) pbar.update(0)
items: list[str] = os.listdir(folder) items: list[str] = os.listdir(folder)
items.sort() items.sort()
images: list[str] = [] images: list[dict] = []
subfolders: list[str] = [] subfolders: list[dict] = []
if not os.path.exists(os.path.join(args.root, ".previews", folder.removeprefix(args.root))): foldername = folder.removeprefix(args.root)
os.mkdir(os.path.join(args.root, ".previews", folder.removeprefix(args.root)))
if not os.path.exists(os.path.join(args.root, ".thumbnails", foldername)):
os.mkdir(os.path.join(args.root, ".thumbnails", foldername))
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:
if os.path.isdir(os.path.join(folder, item)): if os.path.isdir(os.path.join(folder, item)):
subfolders.extend([f'<figure><a href="{args.webroot}{urllib.parse.quote(folder.removeprefix(args.root))}/{urllib.parse.quote(item)}"><img src="{args.foldericon}" alt="Folder icon"/></a><figcaption><a href="{args.webroot}{urllib.parse.quote(folder.removeprefix(args.root))}/{urllib.parse.quote(item)}">{item}</a></figcaption></figure>']) subfolder = {"url": f"{args.webroot}{urllib.parse.quote(foldername)}/{urllib.parse.quote(item)}", "name": item}
subfolders.extend([subfolder])
if item not in notlist: if item not in notlist:
listfolder(os.path.join(folder, item), os.path.join(folder, item).removeprefix(args.root)) listfolder(os.path.join(folder, item), os.path.join(folder, item).removeprefix(args.root))
else: else:
baseurl = urllib.parse.quote(foldername) + "/"
extsplit = os.path.splitext(item)
if not args.non_interactive: if not args.non_interactive:
pbar.desc = f"Generating html files - {folder}" pbar.desc = f"Generating html files - {folder}"
pbar.update(0) pbar.update(0)
contains_files = True contains_files = True
if os.path.splitext(item)[1].lower() in imgext: if extsplit[1].lower() in imgext:
image = f'<figure><a href="{args.webroot}{urllib.parse.quote(folder.removeprefix(args.root))}/{urllib.parse.quote(item)}"><img src="{args.webroot}.previews/{urllib.parse.quote(folder.removeprefix(args.root))}/{urllib.parse.quote(os.path.splitext(item)[0])}.jpg" alt="{item}"/></a><figcaption class="caption">{item}' image = {
if not os.path.exists(os.path.join(args.root, ".previews", folder.removeprefix(args.root), item)): "url": f"{args.webroot}{baseurl}{urllib.parse.quote(item)}",
"thumbnail": f"{args.webroot}.thumbnails/{baseurl}{urllib.parse.quote(extsplit[0])}.jpg",
"name": item,
}
if not os.path.exists(os.path.join(args.root, ".thumbnails", foldername, item)):
thumbnails.append((folder, item)) thumbnails.append((folder, item))
for raw in rawext: for raw in rawext:
if os.path.exists(os.path.join(folder, os.path.splitext(item)[0] + raw)): if os.path.exists(os.path.join(folder, extsplit[0] + raw)):
url = urllib.parse.quote(extsplit[0]) + raw
if raw in (".tif", ".tiff"): if raw in (".tif", ".tiff"):
image += f': <a href="{args.webroot}{urllib.parse.quote(folder.removeprefix(args.root))}/{urllib.parse.quote(os.path.splitext(item)[0])}{raw}">TIFF</a>' image["tiff"] = f"{args.webroot}{baseurl}{url}"
else: else:
image += f': <a href="{args.webroot}{urllib.parse.quote(folder.removeprefix(args.root))}/{urllib.parse.quote(os.path.splitext(item)[0])}{raw}">RAW</a>' image["raw"] = f"{args.webroot}{baseurl}{url}"
elif os.path.exists(os.path.join(folder, os.path.splitext(item)[0] + raw.upper())):
if raw in (".tif", ".tiff"):
image += f': <a href="{args.webroot}{urllib.parse.quote(folder.removeprefix(args.root))}/{urllib.parse.quote(os.path.splitext(item)[0])}{raw.upper()}">TIFF</a>'
else:
image += f': <a href="{args.webroot}{urllib.parse.quote(folder.removeprefix(args.root))}/{urllib.parse.quote(os.path.splitext(item)[0])}{raw.upper()}">RAW</a>'
image += "</figcaption></figure>"
images.extend([image]) images.extend([image])
if not args.non_interactive: if not args.non_interactive:
pbar.desc = f"Generating html files - {folder}" pbar.desc = f"Generating html files - {folder}"
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):
imagechunks = []
if len(images) > 0:
for chunk in np.array_split(images, 8):
imagechunks.append(chunk)
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(body.substitute(title=title, favicon=f"{args.webroot}{_FAVICON}")) header = os.path.basename(folder)
f.write(' <div class="header">\n') if header == "":
if folder == args.root: header = title
f.write(f" <h1>{os.path.basename(folder)}</h1>\n") if foldername == "":
parent = None
else: else:
if args.license: parent = f"{args.webroot}{urllib.parse.quote(foldername.removesuffix(folder.split('/')[-1]))}"
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')
for subfolder in subfolders:
f.write(subfolder)
f.write("\n")
f.write(" </div>\n")
f.write(" </div>\n")
if len(images) > 0:
f.write(' <div class="row">\n')
for chunk in np.array_split(images, 8):
f.write(' <div class="column">\n')
for image in chunk:
f.write(f" {image}\n")
f.write(" </div>\n")
f.write(" </div>\n")
if args.license: if args.license:
f.write(_cclicense.substitute(webroot=args.webroot, title=args.title, author=args.author)) _license = {
f.write(" </body>\n</html>") "project": args.title,
"author": args.author,
"type": cclicense.licensenameswitch(args.license),
"url": cclicense.licenseurlswitch(args.license),
"pics": cclicense.licensepicswitch(args.license),
}
else:
_license = None
html = environment.get_template("index.html.j2")
content = html.render(
title=title,
favicon=f"{args.webroot}{_FAVICON}",
stylesheet=f"{args.webroot}{_STYLE}",
theme=None,
root=args.webroot,
parent=parent,
header=header,
license=_license,
subdirectories=subfolders,
images=imagechunks,
)
f.write(content)
f.close() f.close()
else: else:
if os.path.exists(os.path.join(folder, "index.html")): if os.path.exists(os.path.join(folder, "index.html")):
@@ -335,12 +160,14 @@ def gettotal(folder):
def main(): def main():
global rawext
global total global total
global args global args
global pbar global pbar
global _cclicense global _cclicense
total = 0 total = 0
# fmt: off
# Parse command-line arguments # Parse command-line arguments
parser = argparse.ArgumentParser(description="Generate html files for static image host.") parser = argparse.ArgumentParser(description="Generate html files for static image host.")
parser.add_argument("-p", "--root", help="Root folder", default=_ROOT, required=False, type=str, dest="root") parser.add_argument("-p", "--root", help="Root folder", default=_ROOT, required=False, type=str, dest="root")
@@ -353,16 +180,19 @@ def main():
parser.add_argument("-t", "--title", help="Title", default=_ROOTTITLE, required=False, type=str, dest="title") 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()
# fmt: on
if not args.root.endswith("/"): if not args.root.endswith("/"):
args.root += "/" args.root += "/"
if not args.webroot.endswith("/"): if not args.webroot.endswith("/"):
args.webroot += "/" args.webroot += "/"
if not os.path.exists(os.path.join(args.root, ".previews")): if not os.path.exists(os.path.join(args.root, ".thumbnails")):
os.mkdir(os.path.join(args.root, ".previews")) os.mkdir(os.path.join(args.root, ".thumbnails"))
tmprawext = []
if args.license: for raw in rawext:
_cclicense = Template(cclicense.licenseswitch(args.license)) tmprawext.append(raw)
tmprawext.append(raw.upper())
rawext = tmprawext
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.")
@@ -370,6 +200,9 @@ def main():
try: try:
Path(os.path.join(args.root, ".lock")).touch() Path(os.path.join(args.root, ".lock")).touch()
print("Copying static files...")
shutil.copytree(_STATICFILES, os.path.join(args.root, ".static"), dirs_exist_ok=True)
if args.non_interactive: if args.non_interactive:
print("Generating html files...") print("Generating html files...")
listfolder(args.root, args.title) listfolder(args.root, args.title)
@@ -387,7 +220,14 @@ def main():
pbar.close() pbar.close()
with Pool(os.cpu_count()) as p: with Pool(os.cpu_count()) as p:
for r in tqdm(p.imap_unordered(thumbnail_convert, thumbnails), total=len(thumbnails), desc="Generating thumbnails", unit=" files", ascii=True, dynamic_ncols=True): for r in tqdm(
p.imap_unordered(thumbnail_convert, thumbnails),
total=len(thumbnails),
desc="Generating thumbnails",
unit=" files",
ascii=True,
dynamic_ncols=True,
):
pass pass
finally: finally:
os.remove(os.path.join(args.root, ".lock")) os.remove(os.path.join(args.root, ".lock"))

View File

@@ -0,0 +1,62 @@
{
"folders": [
{
"path": "./",
"name": "Simple Picture Server"
}
],
"settings": {
"files.associations": {
"**/*.html.j2": "jinja-html",
"**/*.css.j2": "jinja-css",
"**/*.css": "css",
},
"python.analysis.inlayHints.callArgumentNames": "off",
"python.analysis.inlayHints.functionReturnTypes": false,
"python.analysis.inlayHints.variableTypes": false,
"pylint.args": [
"--disable=C0111",
"--disable=C0301",
"--good-names-rgxs=^[_a-z][_a-z0-9]?$"
],
"editor.formatOnSave": false,
"black-formatter.interpreter": [
"/usr/bin/python3"
],
"black-formatter.args": [
"-l 140"
],
"gitblame.inlineMessageEnabled": true,
"gitblame.inlineMessageFormat": "${author.name}, ${time.ago} • ${commit.summary}",
"gitblame.statusBarMessageFormat": "${author.name} (${time.ago})",
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"[css]": {
"editor.defaultFormatter": "vscode.css-language-features"
},
"[jinja-html]": {
"editor.defaultFormatter": "vscode.html-language-features"
},
"[jinja-css]": {
"editor.defaultFormatter": "vscode.css-language-features"
},
"html.format.templating": true,
"html.format.wrapAttributes": "preserve",
"html.format.wrapLineLength": 200,
"html.format.indentHandlebars": true
},
"extensions": {
"recommendations": [
"ms-python.black-formatter",
"ms-python.python",
"ms-python.vscode-pylance",
"ms-python.debugpy",
"ms-python.pylint",
"samuelcolvin.jinjahtml",
"vscode.html-language-features",
"vscode.css-language-features",
"waderyan.gitblame",
]
}
}

78
templates/index.html.j2 Normal file
View File

@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<link rel="icon" type="image/x-icon" href="{{ favicon }}">
<link rel="stylesheet" href="{{ stylesheet }}">
{%- if theme %}
<link rel="stylesheet" href="{{ theme }}">
{%- endif %}
</head>
<body>
<div class="header">
<ul class="navbar">
<li><a href="{{ root }}">Home</a></li>
{%- if parent %}
<li><a href="{{ parent }}">Parent Directory</a></li>
{%- endif %}
<li style="position: absolute; left: 50%; transform: translateX(-50%);"><span>{{ header }}</span></li>
{%- if license %}
<li style="float:right"><a href="{{ license.url }}" target="_blank">License</a></li>
{%- endif %}
</ul>
{% if subdirectories %}
<div class="folders">
{%- for subdirectory in subdirectories %}
<figure>
<a href="{{ subdirectory.url }}"><img src="{{ foldericon }}" alt="Folder icon" /></a>
<figcaption><a href="{{ subdirectory.url }}">{{ subdirectory.name }}</a></figcaption>
</figure>
{%- endfor %}
</div>
{%- endif %}
</div>
{% if images %}
<div class="row">
{%- for imageblock in images %}
<div class="column">
{%- for image in imageblock %}
<figure>
<a href="{{ image.url }}"><img src="{{ image.thumbnail }}" alt="{{ image.name }}" /></a>
<figcaption class="caption">{{ image.name }}{% if image.tiff %}
<a href="{{ image.tiff }}">TIFF</a>{% endif %}{% if image.raw %}
<a href="{{ image.raw }}">RAW</a>{% endif %}
</figcaption>
</figure>
{%- endfor %}
</div>
{%- endfor %}
</div>
{%- endif %}
{% if license %}
{%- if 'CC' in license.type %}
<div class="license" xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/">
{%- if license.type == 'CC0 1.0' %}
<a property="dct:title" rel="cc:attributionURL" href="{{ root }}">{{ license.project }}</a> by <span property="cc:attributionName">{{ license.author }}</span> is marked with
<a href="{{ license.url }}" target="_blank" rel="license noopener noreferrer" style="display: inline-block">CC0 1.0
{%- for pic in license.pics %}
<img style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom" src="{{ pic }}" alt="" />
{%- endfor %}
</a>
{%- else %}
<a property="dct:title" rel="cc:attributionURL" href="{{ root }}">{{ license.project }}</a> by <span property="cc:attributionName">{{ license.author }}</span> is licensed under
<a href="{{ license.url }}" target="_blank" rel="license noopener noreferrer" style="display: inline-block">{{ license.type }}
{%- for pic in license.pics %}
<img style="height: 22px !important; margin-left: 3px; vertical-align: text-bottom" src="{{ pic }}" alt="" />
{%- endfor %}
</a>
{%- endif %}
</div>
{%- endif %}
{%- endif %}
</body>
</html>