forked from ari.lt/arivertisements
203 lines
5.8 KiB
Python
203 lines
5.8 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.0" />
|
|
<title>{html.escape(image_filename)}</title>
|
|
<meta name="description" content="{description_esc}" />
|
|
<meta name="author" content="{author_esc}" />
|
|
<meta name="license" content="{license_esc}" />
|
|
<meta name="theme-color" content="{bg}" />
|
|
<style>
|
|
* {{
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
background-color: {bg};
|
|
color: {fg};
|
|
}}
|
|
html {{
|
|
overflow: hidden;
|
|
}}
|
|
body {{
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100vh;
|
|
font-family: monospace;
|
|
}}
|
|
p {{
|
|
font-size: 0.75em;
|
|
padding: 0.5ch 0 0 1ch;
|
|
height: 14px;
|
|
width: 100%;
|
|
text-align: center;
|
|
}}
|
|
img {{
|
|
display: block;
|
|
max-width: 100%;
|
|
width: 722px;
|
|
height: auto;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="{to_link_esc}" rel="noopener noreferrer" target="_blank">
|
|
<img src="{img_src}" loading="lazy" width="722" height="84" alt="{alt_text_esc}" />
|
|
</a>
|
|
<p><strong>© <strong>{author_esc}</strong> <<a href="mailto:{contact_esc}">contact</a>> under {license_esc}. {source_html}| <a href="https://ad.ari.lt/" rel="noopener noreferrer" target="_blank">Arivertisements</a></strong></p>
|
|
</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 down server...")
|
|
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())
|