from sys import argv from subprocess import Popen, PIPE, run, call from PIL import Image from jinja2 import Environment, FileSystemLoader from markdown import markdown from glob import glob from json import dump, dumps, load from os import path from feedgen.feed import FeedGenerator import re import datetime numbers_re = re.compile("[0-9]+") name_from_url_re = re.compile(r"https:\/\/helixnebula.space/(.*)/") out_dir = "/var/www/html/" gemini_out_dir = "/var/gemini/" base_url = "https://helixnebula.space/" md_dir = "/var/stories/" template_environment = Environment( loader=FileSystemLoader("templates/")) with open("metadata.json", "r") as f: metadata = load(f) story_names = [ path.basename(post).split('/')[-1][:-3] for post in glob(md_dir + "*") ] story_names.sort() for placename in metadata.keys(): read_path = f"{out_dir}{placename}/" photos = [ path.basename(photo).split('/')[-1] for photo in glob(read_path + "*") ] try: photos.remove("index.html") except ValueError: pass metadata[placename]["count"] = len(photos) metadata[placename]["photos"] = photos metadata[placename]["photos"].sort(key=lambda path: int(numbers_re.search(path).group(0))) with open(f"{out_dir}photos.json", "w") as f: dump(metadata, f) def header_from_code(code): path = f"{md_dir}{code}.md" with open(path, "r") as f: md = f.read() header = "" for char in md[2:]: if char == "\n": break header += char return header def covers(overwrite=False): print("running covers") for placename, info in metadata.items(): read_path = f"{out_dir}{placename}/{info['cover']}" write_path = f"{out_dir}cover/cover_{info['cover']}" if not overwrite and path.exists(write_path): continue command = ["convert", read_path, "-strip", "-interlace", "Plane", "-gaussian-blur", "0.05", "-auto-orient", "-resize", "700x525>", "-quality", "80%", write_path] run(command) def thumbnails(overwrite=False): print("running thumbnails") for placename in metadata.keys(): for photo in metadata[placename]["photos"]: read_path = f"{out_dir}{placename}/{photo}" write_path = f"{out_dir}thumbnail/thumbnail_{photo}" if not overwrite and path.exists(write_path): continue command = ["convert", read_path, "-strip", "-interlace", "Plane", "-gaussian-blur", "0.05", "-auto-orient", "-resize", "300x200>", "-quality", "65%", write_path] run(command) def compressed(overwrite=False): print("running compressed") for placename in metadata.keys(): for photo in metadata[placename]["photos"]: read_path = f"{out_dir}{placename}/{photo}" write_path = f"{out_dir}compressed/compressed_{photo}" if not overwrite and path.exists(write_path): continue command = ["convert", read_path, "-auto-orient", "-strip", "-resize", "1200>", "-quality", "90%", write_path] run(command) def render_index(): template = template_environment.get_template("main") data = {key: value for key, value in sorted(metadata.items(), key=lambda item: item[1]['title'])} photo_counts = {placename: metadata[placename]["count"] for placename in metadata.keys()} total_count = 0 posts = sorted(story_names, key=lambda x: metadata[x]["story_time"], reverse=True) for _, info in metadata.items(): total_count = info["count"] + total_count with open(out_dir + "index.html", "w") as f: f.write(template.render({ "metadata": data, "album_count": len(metadata.keys()), "photo_counts": photo_counts, "total_count": total_count, "posts": posts })) def render_places(): template = template_environment.get_template("place") for placename, info in metadata.items(): working_path = out_dir + placename photos = metadata[placename]["photos"] photos_json = dumps(photos) count = len(photos) widths = [] heights = [] for photo in photos: photo_path = out_dir + "thumbnail/thumbnail_" + photo img = Image.open(photo_path) width, height = img.size widths.append(width) heights.append(height) photo_specs = zip(widths, heights, photos) try: with open(md_dir + placename + ".md", "r") as f: md = markdown(f.read()) except FileNotFoundError: md = "

No story (yet)

" with open(working_path + "/index.html", "w") as f: f.write(template.render({ "count": count, "placename": placename, "cover": info["cover"], "info": info, "markdown": md, "photos": photo_specs, "photos_json": photos_json })) def sub_http_local_urls(match): name = match.group(1) if name in story_names: return name + ".gmi" else: return match.group(0) def render_gemini_index(): template = template_environment.get_template("gemini_main") stories = [(post_code, metadata[post_code]) for post_code in story_names] stories.sort(key=lambda i: i[1]["story_time"], reverse=True) with open(gemini_out_dir + "index.gmi", "w") as f: f.write(template.render({ "story_data": stories })) def render_gemini_places(): post_paths = glob(md_dir + "*") template = template_environment.get_template("gemini_place") for post_path in post_paths: post_name = path.basename(post_path).split('/')[-1][:-3] with open(post_path, "r") as f: content = f.read() process = Popen(["gemgen"], stdout=PIPE, stdin=PIPE, stderr=PIPE, text=True) gemtext = process.communicate(input=content)[0] gemtext_with_local_urls = name_from_url_re.sub(sub_http_local_urls, gemtext) with open(gemini_out_dir + post_name + ".gmi", "w") as f: f.write(template.render({ "post_code": post_name, "post_body": gemtext_with_local_urls, "post_title": metadata[post_name]["title"], "state": metadata[post_name]["state"] })) def md_from_code(code): with open(f"{md_dir}{code}.md", "r") as f: return markdown(f.read()) def path_to_story_name(story_path): return path.basename(story_path).split('/')[-1][:-3] def render_atom(): posts_sorted = story_names.copy() posts_sorted.sort(key=lambda i: metadata[i]["story_time"]) feed = FeedGenerator() feed.id("helixnebula.space") feed.title("helixnebula.space") feed.updated(datetime.datetime.fromtimestamp(metadata[posts_sorted[-1]]["story_time"], datetime.timezone.utc)) feed.author({"name":"~nebula","email":"nebula@tilde.town"}) feed.link(href="https://helixnebula.space/", rel="alternate") feed.link(href="https://helixnebula.space/feed.atom", rel="self") feed.subtitle("Exploring America the Beautiful") feed.icon("https://helixnebula.space/favicon.ico") feed.language("en") for post_code in posts_sorted: post_metadata = metadata[post_code] header = header_from_code(post_code) title = f"{post_metadata['title']}, {post_metadata['state']}: {header}" entry = feed.add_entry() entry.id(post_code) entry.title(title) entry.updated(datetime.datetime.fromtimestamp(metadata[post_code]["story_time"], datetime.timezone.utc)) entry.link(href=f"https://helixnebula.space/{post_code}/") entry.summary(header) entry.description(header) entry.content(md_from_code(post_code), type="html") feed.atom_file(out_dir + "feed_html.atom") template = template_environment.get_template("atom_page") with open(f"{out_dir}feeds.html", "w") as f: f.write(template.render({ "last_post_code": posts_sorted[-1], "metadata": metadata })) def copy_files(): call(f"cp js/* {out_dir}js/", shell=True) call(f"cp style.css {out_dir}style.css", shell=True) if __name__ == "__main__": try: overwrite = "overwrite" in argv covers(overwrite=overwrite) thumbnails(overwrite=overwrite) compressed(overwrite=overwrite) except KeyboardInterrupt: exit() render_index() render_places() copy_files() render_gemini_index() render_gemini_places() render_atom()