Files
mirva/mirva/mirva.py
2023-09-28 19:32:34 +03:00

473 lines
15 KiB
Python
Executable File

import configparser
import os
import sys
import re
import shutil
import subprocess
import random
import urllib.parse
from argparse import ArgumentParser, HelpFormatter
from mirva import get_version
from tqdm import tqdm
class Mirva:
def __init__(self):
self.resource_src = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "resources"
)
self.resource_dir = ".mirva"
self.medium_dir = os.path.join(self.resource_dir, "med")
self.config_file = os.path.join(self.resource_dir, "config.cfg")
self.config_backup = os.path.join(self.resource_dir, "config.cfg.bkp")
self.image_match = re.compile(
".*\.jpg$|.*\.jpeg$|.*\.png$|.*\.gif$|.*\.tif$", re.I
)
self.video_match = re.compile(".*\.mp4$", re.I)
self.site_defaults = {
"title": {"default": "", "help": "Title of the site"},
"sub_title": {
"default": "",
"help": "Subtitle of the site, shown under the title",
},
"intro": {
"default": "",
"help": "Intro text shown before first image",
},
"image_size": {
"default": "1920",
"help": "Resize images for faster loading. Use 'link' for symbolic links without resizing.",
},
"scroll": {
"default": "smooth",
"help": "Transition to next image with keyboard: smooth or auto",
},
}
## Init ##
self.get_options()
os.chdir(self.options.folder)
self.file_list = self.get_files()
if self.run_commands["config"]:
self.write_resources()
updated = self.create_config()
if updated:
print(
"Config created or updated: Check config contents: {}".format(
self.config_file
)
)
if self.run_commands["build"]:
self.get_config()
self.create_posts()
self.write_index()
self.write_mediums()
print("Gallery written.")
def create_config(self):
self.config = configparser.RawConfigParser()
self.config.read(self.config_file)
config_changed = False
if not "SITE" in self.config:
self.config["SITE"] = {}
config_changed = True
for key in self.site_defaults:
if not key in self.config["SITE"]:
config_changed = True
self.config["SITE"][key] = self.site_defaults[key]["default"]
if self.options.set:
for key, value in self.options.set:
if key not in self.site_defaults:
raise KeyError("Key '{}' is not a config keyword".format(key))
self.config["SITE"][key] = value
config_changed = True
for f in self.file_list:
if not f in self.config:
title, _ = os.path.splitext(f)
title = title.replace("_", " ")
self.config[f] = {"title": title, "description": ""}
config_changed = True
print("Added {}".format(f))
if self.options.purge:
unnecessary = []
for f in self.config:
if f in ("SITE", "DEFAULT"):
continue
if f not in self.file_list:
print("{} not found in files".format(f))
unnecessary.append(f)
for f in unnecessary:
del self.config[f]
config_changed = True
if self.options.exif:
self.append_exif()
config_changed = True
if config_changed:
self.write_config()
return config_changed
def create_posts(self):
self.posts = []
for c in self.config:
if c in self.file_list:
post = self.get_post(
c, self.config[c]["title"], self.config[c]["description"]
)
self.posts.append(post)
def get_config(self):
self.config = configparser.RawConfigParser()
self.config.read(self.config_file)
def write_config(self):
print(
"Modified config: {}".format(
os.path.join(self.options.folder, self.config_file)
)
)
if os.path.exists(self.config_file):
with open(self.config_file, "rt") as reader:
with open(self.config_backup, "wt") as writer:
writer.write(reader.read())
with open(self.config_file, "wt") as fp:
self.config.write(fp)
def get_files(self):
files = []
for f in sorted(os.listdir(".")):
if f.startswith("."):
continue
if not (self.image_match.match(f) or self.video_match.match(f)):
continue
files.append(f)
return files
def get_options(self):
parser = ArgumentParser(prog="mirva", formatter_class=SmartFormatter)
parser.add_argument(
"--version",
action="version",
version="%(prog)s {version}".format(version=get_version()),
)
parser.add_argument(
"--folder",
type=str,
default=".",
help="Folder for gallery. Default current folder.",
)
parser.add_argument(
"--purge",
default=False,
action="store_true",
help="Remove non-existent files in image list. Required if images are removed.",
)
parser.add_argument(
"--exif",
default=False,
action="store_true",
help="Append EXIF information to image descriptions",
)
parser.add_argument(
"--set",
"-s",
default=None,
nargs=2,
action="append",
metavar=("key", "value"),
help="smart|Configurable options: \n"
+ "\n".join(
["{}: {}".format(k, v["help"]) for k, v in self.site_defaults.items()]
),
)
parser.add_argument(
"--force",
default=False,
action="store_true",
help="Force regeneration of middle sized images",
)
parser.add_argument(
default="all",
dest="command",
action="store",
nargs="?",
choices=["all", "config", "build"],
help="Run only part of the process. Defaults to all. Config is run always if it doesn't exist.",
)
self.options = parser.parse_args()
self.run_commands = {
"config": self.options.command == "config"
or self.options.command == "all"
or not os.path.exists(self.config_file),
"build": self.options.command == "build" or self.options.command == "all",
}
def get_index(self, posts):
return """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!--
Design by TEMPLATED
http://templated.co
Released for free under the Creative Commons Attribution License
Name : Green Forest
Description: A two-column, fixed-width design with dark color scheme.
Version : 1.0
Released : 20110306
-->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="generator" content="Mirva" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<link rel="shortcut icon" href="{resource}/mirva.ico"/>
<title>{page_title}</title>
<link href="{resource}/mirva.css" rel="stylesheet" type="text/css" media="screen" />
<script src="{resource}/mirva.js"></script>
</head>
<body data-scroll="{scroll}">
<div id="wrapper">
<div id="header">
<div id="logo">
<div id="page_title">
<h1>{page_title}</h1>
<p>{sub_title}</p>
</div>
</div>
</div>
<!-- end #header -->
<div id="page">
<div id="page-bgtop">
<div id="page-bgbtm">
<div id="content">
<div class="post" id="post_intro">
<div class="entry intro">
{intro}
</div>
</div>
{posts}
<div style="clear: both;">&nbsp;</div>
</div>
<!-- end #content -->
<div style="clear: both;">&nbsp;</div>
</div>
</div>
<!-- end #page -->
</div>
</body>
</html>""".format(
page_title=self.config["SITE"]["title"],
sub_title=self.config["SITE"]["sub_title"],
intro=self.config["SITE"]["intro"],
scroll=self.config["SITE"]["scroll"],
posts="\n".join(posts),
resource=self.resource_dir,
)
def get_post(self, image, title, content):
image = urllib.parse.quote(image)
if self.video_match.match(image):
return """
<div class="post" id="post_{image}">
<div class="navigation">&nbsp;</div>
<div class="image_wrapper center"><a href="{image}">
<video class=post_image controls preload="metadata">
<source src="{image}" type="video/mp4" >
</video>
</a></div>
<div class="meta"><div class="post_title" id="title_{image}">{title}</div></div>
<div style="clear: both;">&nbsp;</div>
<div class="entry">{content}</div>
</div>""".format(
image=image, title=title, content=content
)
return """
<div class="post" id="post_{image}">
<div class="navigation">&nbsp;</div>
<div class="image_wrapper center"><a href="{image}">
<img loading=lazy class=post_image src="{med_dir}/{image}.jpg">
</a></div>
<div class="meta"><div class="post_title" id="title_{image}">{title}</div></div>
<div style="clear: both;">&nbsp;</div>
<div class="entry">{content}</div>
</div>""".format(
image=image, title=title, content=content, med_dir=self.medium_dir
)
def is_created_with_mirva(self):
with open("index.html", "rt") as fp:
for line in fp.readlines():
if line.strip() == '<meta name="generator" content="Mirva" />':
return True
return False
def write_index(self):
if os.path.exists("index.html"):
if not self.is_created_with_mirva():
print(
"index.html exists, and it's not written with Mirva. Not overwriting."
)
sys.exit(1)
with open("index.html", "wt") as fp:
fp.write(self.get_index(self.posts))
def write_resources(self):
try:
os.makedirs(self.resource_dir)
except Exception:
pass
for f in (
"mirva.css",
"arrow_up.png",
"arrow_down.png",
"mirva.ico",
"mirva.js",
):
if os.path.exists(os.path.join(self.resource_dir, f)):
continue
shutil.copy(
os.path.join(self.resource_src, f),
os.path.join(self.resource_dir, f),
)
if not os.path.exists(os.path.join(self.resource_dir, "banner.jpg")):
try:
f = random.choice(self.file_list)
outfile = os.path.join(self.resource_dir, "banner.jpg")
res = "1920x"
convargs = [
"convert",
"{}[0]".format(f),
"-background",
"white",
"-flatten",
"-resize",
res,
"+contrast",
"+contrast",
"+contrast",
"-quality",
"85",
outfile,
]
subprocess.run(convargs)
print("Random banner written to {}".format(outfile))
except Exception:
shutil.copy(
os.path.join(self.resource_src, "banner.jpg"),
os.path.join(self.resource_dir, "banner.jpg"),
)
def write_mediums(self):
try:
os.makedirs(self.medium_dir)
except Exception:
pass
r = self.config["SITE"].get("image_size", 1920)
link = str(r).lower() == "link"
if link:
r = 0
res = "{:d}x{:d}>".format(int(r), int(r))
force = self.options.force
for f in tqdm(self.file_list):
if self.video_match.match(f):
continue
outfile = os.path.join(self.medium_dir, "{}.jpg".format(f))
if force:
try:
os.remove(outfile)
except FileNotFoundError:
pass
if not os.path.exists(outfile):
if link:
os.symlink("../../{}".format(f), outfile)
continue
convargs = [
"convert",
"{}[0]".format(f),
"-background",
"white",
"-flatten",
"-resize",
res,
"-quality",
"85",
outfile,
]
subprocess.run(convargs)
sys.stdout.write("\n")
def append_exif(self):
exif_format = """
<ul>
<li>Date: %[EXIF:DateTimeOriginal]
<li>Camera: %[EXIF:Make] %[EXIF:Model]
<li>Parameters: %[EXIF:ExposureTime]s / F%[EXIF:FNumber] / Focal %[EXIF:FocalLength]
<li>Size: %w x %h / {size}
</ul>
"""
for f in self.config:
if f in self.file_list:
sys.stdout.write(".")
sys.stdout.flush()
file_size = human_size(f)
p = subprocess.run(
[
"identify",
"-format",
exif_format.format(size=file_size),
"{}[0]".format(f),
],
capture_output=True,
)
self.config[f]["description"] += p.stdout.decode("utf-8")
sys.stdout.write("\n")
def human_size(file_name, precision=1):
size = os.path.getsize(file_name)
if size == None:
return "nan"
sign = ""
if size < 0:
sign = "-"
size = -size
suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"]
suffixIndex = 0
defPrecision = 0
while size > 1024:
suffixIndex += 1
size = size / 1024.0
defPrecision = precision
return "%s%.*f%s" % (sign, defPrecision, size, suffixes[suffixIndex])
class SmartFormatter(HelpFormatter):
def _split_lines(self, help, width):
if help.startswith("smart|"):
return help[6:].splitlines()
return HelpFormatter._split_lines(self, help, width)