185 lines
6.1 KiB
Python
185 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""Preview Arivertisements embed locally"""
|
|
|
|
import html
|
|
import http.server
|
|
import os
|
|
import socketserver
|
|
import sys
|
|
import threading
|
|
import typing as t
|
|
import webbrowser
|
|
from warnings import filterwarnings as filter_warnings
|
|
|
|
PORT: t.Final[int] = 8000
|
|
|
|
|
|
def parse_meta(filepath: str) -> t.Dict[str, str]:
|
|
"""Parse metadata file into a dictionary"""
|
|
|
|
meta: t.Dict[str, str] = {}
|
|
|
|
with open(filepath, "r", encoding="utf-8") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line or ":" not in line:
|
|
continue
|
|
key, val = line.split(":", 1)
|
|
meta[key.lower()] = val.strip()
|
|
|
|
for rkey in ("filename", "alt", "author", "contact", "statement"):
|
|
if rkey not in meta:
|
|
raise ValueError(f"{rkey} missing in metadata {filepath!r}!")
|
|
|
|
return meta
|
|
|
|
|
|
def build_embed_html(meta: t.Dict[str, str], image_filename: str) -> str:
|
|
"""Build the exact embed HTML with metadata values injected"""
|
|
|
|
bg: str = meta.get("bg", "#fff")
|
|
fg: str = meta.get("fg", "#000")
|
|
|
|
description: str = f'{meta.get("alt")} (image by {meta.get("author")})'
|
|
license_: str = meta.get("license", "CC-BY-NC-SA 4.0")
|
|
alt_text: str = meta.get("alt", "Alternative text")
|
|
to_link: str = meta.get("to", "#")
|
|
author: str = meta.get("author", "Joe Doe")
|
|
contact: str = meta.get("contact", "example@example.com").replace(" at ", "@")
|
|
# statement: str = meta.get("statement", "")
|
|
source: t.Optional[str] = meta.get("source")
|
|
|
|
description_esc: str = html.escape(description)
|
|
license_esc: str = html.escape(license_)
|
|
alt_text_esc: str = html.escape(alt_text)
|
|
to_link_esc: str = html.escape(to_link)
|
|
author_esc: str = html.escape(author)
|
|
contact_esc: str = "".join(f"&#{ord(c)};" for c in contact)
|
|
# statement_esc = html.escape(statement)
|
|
# source_esc: str = html.escape(source) if source else ""
|
|
|
|
source_html: str = (
|
|
f'(<a href="{source}" rel="noopener noreferrer" target="_blank">source here</a>) '
|
|
if source
|
|
else ""
|
|
)
|
|
|
|
img_src = f"/img/{html.escape(image_filename)}"
|
|
|
|
return f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>{html.escape(image_filename)}</title>
|
|
<base target="_blank">
|
|
<meta name="description" content="{description_esc}">
|
|
{source_html}
|
|
<meta name="license" content="{license_esc}">
|
|
<meta name="theme-color" content="{bg}">
|
|
<meta name="robots" content="noindex,noarchive,noimageindex,follow">
|
|
<meta name="author" content="{author_esc}">
|
|
<meta property="og:locale" content="en_GB">
|
|
<meta property="og:url" content="http://127.0.0.1:{PORT}/">
|
|
<link rel="canonical" href="http://127.0.0.1:{PORT}/">
|
|
<style>
|
|
html,body{{overflow:hidden;margin:0;height:100%;background:{bg};color:{fg};font-family:monospace}}
|
|
body{{display:table;width:100%;text-align:center}}
|
|
.v{{display:table-cell;vertical-align:middle}}
|
|
.b{{display:inline-block}}
|
|
img{{display:block;vertical-align:middle;width:722px;max-width:100%;height:auto;border:0}}
|
|
p{{margin:.4em 0 0;font-size:.75em}}
|
|
a{{color:inherit}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="v">
|
|
<div class="b"><a href="{to_link_esc}" rel="noopener noreferrer"><img referrerpolicy="no-referrer" decoding="async" border="0" src="{img_src}" loading="lazy" width="722" height="84" alt="{alt_text_esc}"></a><p><strong>© {author_esc} <<a href="mailto:{contact_esc}" rel="noopener noreferrer nofollow">contact</a>> under {license_esc} | <a href="https://ad.ari.lt/" rel="noopener noreferrer">Arivertisements</a></strong></p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
|
|
class PreviewHandler(http.server.SimpleHTTPRequestHandler):
|
|
"""HTTP handler serving the preview HTML at /"""
|
|
|
|
def __init__(self, *args: t.Any, html_content: str = "", **kwargs: t.Any) -> None:
|
|
self.html_content = html_content
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def do_GET(self) -> None:
|
|
if self.path in ("/", "/index.html"):
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
self.end_headers()
|
|
self.wfile.write(self.html_content.encode("utf-8"))
|
|
else:
|
|
super().do_GET()
|
|
|
|
|
|
def run_server(server: socketserver.TCPServer) -> None:
|
|
"""Run the HTTP server"""
|
|
server.serve_forever()
|
|
|
|
|
|
def main() -> int:
|
|
"""Entry point"""
|
|
|
|
if len(sys.argv) != 2:
|
|
print("Usage: python3 preview.py image-name.png")
|
|
return 1
|
|
|
|
image_name: str = sys.argv[1]
|
|
img_path: str = os.path.join("img", image_name)
|
|
meta_path: str = os.path.join("meta", os.path.splitext(image_name)[0] + ".txt")
|
|
|
|
if not os.path.isfile(img_path):
|
|
print(f"Image file '{img_path}' not found.")
|
|
return 1
|
|
if not os.path.isfile(meta_path):
|
|
print(f"Meta file '{meta_path}' not found.")
|
|
return 1
|
|
|
|
try:
|
|
meta: t.Dict[str, str] = parse_meta(meta_path)
|
|
except Exception as e:
|
|
print(f"Error parsing meta file: {e}")
|
|
return 1
|
|
|
|
html_content: str = build_embed_html(meta, image_name)
|
|
|
|
socketserver.TCPServer.allow_reuse_address = True
|
|
|
|
handler_class = lambda *args, **kwargs: PreviewHandler(
|
|
*args, html_content=html_content, **kwargs
|
|
)
|
|
with socketserver.TCPServer(("", PORT), handler_class) as httpd:
|
|
print(f"Serving preview at http://127.0.0.1:{PORT}")
|
|
|
|
thread: threading.Thread = threading.Thread(
|
|
target=run_server, args=(httpd,), daemon=True
|
|
)
|
|
thread.start()
|
|
|
|
webbrowser.open_new(f"http://127.0.0.1:{PORT}")
|
|
|
|
try:
|
|
while thread.is_alive():
|
|
thread.join(0.5)
|
|
except KeyboardInterrupt:
|
|
print("\nShutting the server down...")
|
|
httpd.shutdown()
|
|
httpd.server_close()
|
|
return 0
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
assert main.__annotations__.get("return") is int, "main() should return an integer"
|
|
|
|
filter_warnings("error", category=Warning)
|
|
raise SystemExit(main())
|