Assorted shell and Python scripts
1#!/usr/bin/env -S uv run --script
2# /// script
3# dependencies = [
4# "feedgen",
5# "feedparser",
6# "md2gemini",
7# ]
8# ///
9
10# 1. Take a markdown blog post as input, convert it to gemtext.
11# 2. Update gemlog index.
12# 3. Update the gemlog Atom feed.
13
14import sys
15from datetime import datetime
16from pathlib import Path
17from zoneinfo import ZoneInfo
18
19import feedparser
20from feedgen.feed import FeedGenerator
21from md2gemini import md2gemini
22
23# This is so that Python doesn't yell at me if I forget an argument. Not
24# likely to happen, but still.
25if len(sys.argv) != 3:
26 print('Usage: blog2gemlog /path/to/blog/post.md "Blog Post Title"')
27 exit(1)
28
29# Set the absolute path to the gemini content directory
30gemini_dir = Path.home().joinpath(
31 "repos/tildegit.org/hyperreal/hyperreal.coffee/gemini"
32)
33
34# Get the current date in YYYY-MM-DD format
35date_now = datetime.now().strftime("%Y-%m-%d")
36
37# Read blog post path from sys.argv[1] and ensure it is an absolute path
38blog_post_path = Path(sys.argv[1])
39if not blog_post_path.is_absolute():
40 print("Supply absolute path to blog post.")
41 exit(1)
42
43# Convert the markdown blog post to gemtext
44with open(blog_post_path, "r") as md_f:
45 content = md2gemini(md_f.read(), frontmatter=True, links="paragraph", md_links=True)
46
47# Set the absolute path to the gemlog post
48gemlog_post_path = gemini_dir.joinpath(f"gemlog/{blog_post_path.stem}.gmi")
49
50# Write the gemtext content to the gemlog post path
51with open(gemlog_post_path, "w") as gmi_f:
52 gmi_f.write(content)
53
54# Set the string for the END section of the gemlog post
55gemlog_end = f"\n\n## END\nLast updated: {date_now}\n\n=> ../gemlog Gemlog archive\n=> ../ hyperreal.coffee"
56
57# Append gemlog_end to the end of the gemlog post
58with open(gemlog_post_path, "a") as gmi_f:
59 gmi_f.write(gemlog_end)
60
61# Read the gemlog post file lines into a list
62with open(gemlog_post_path, "r") as gmi_f:
63 contents = gmi_f.readlines()
64
65# Get the gemlog post title from sys.argv[2]
66gemlog_post_title = str(sys.argv[2])
67
68# Insert the gemlog post title as the level 1 heading on line 1
69contents.insert(0, f"# {gemlog_post_title}\n\n")
70
71# Write the new contents as a string to the gemlog file
72with open(gemlog_post_path, "w") as gmi_f:
73 contents = "".join(contents)
74 gmi_f.write(contents)
75
76# Read the lines of the gemlog index into a list
77with open(gemini_dir.joinpath("gemlog/index.gmi"), "r") as index_f:
78 contents = index_f.readlines()
79
80# Set the content of the gemlog index entry line
81gemlog_index_line = f"=> ./{gemlog_post_path.name} {date_now} {gemlog_post_title}\n"
82
83# Insert the new gemlog index line into the list on line 6
84contents.insert(5, gemlog_index_line)
85
86# Write the new contents as a string to the gemlog index file
87with open(gemini_dir.joinpath("gemlog/index.gmi"), "w") as index_f:
88 contents = "".join(contents)
89 index_f.write(contents)
90
91# Get a timezone-aware datetime object from a timestamp of the present moment
92aware_ts = datetime.fromtimestamp(
93 datetime.timestamp(datetime.now()), tz=ZoneInfo("America/Chicago")
94)
95
96# Format the timezone-aware datetime object for the <updated> element of the
97# Atom feed
98updated_ts = aware_ts.strftime("%Y-%m-%dT%H:%M:%S%z")
99
100# Instantiate a FeedParserDict object
101d = feedparser.parse(gemini_dir.joinpath("gemlog/atom.xml"))
102
103# Update the <updated> element's value to the current timestamp
104d["updated"] = updated_ts
105
106# Define a dictionary for the new Atom feed entry
107new_entry_dict = {
108 "id": f"gemini://hyperreal.coffee/gemlog/{gemlog_post_path.name}",
109 "title": gemlog_post_title,
110 "updated": updated_ts,
111 "links": [
112 {
113 "href": f"gemini://hyperreal.coffee/gemlog/{gemlog_post_path.name}",
114 "rel": "alternate",
115 "type": "text/gemini",
116 }
117 ],
118}
119
120# Insert the new Atom feed entry into the FeedParserDict
121d["entries"].insert(0, new_entry_dict)
122
123# Instantiate a FeedGenerator object and set the methods for the feed
124fg = FeedGenerator()
125fg.id(d["feed"]["id"])
126fg.title(d["feed"]["title"])
127fg.updated(d["feed"]["updated"])
128fg.link(d["feed"]["links"])
129
130# Reverse the order of d["entries"] so that they are written to the file in
131# the correct order
132d["entries"].reverse()
133
134# For each entry, add a new entry to the FeedGenerator object
135for entry in d["entries"]:
136 fe = fg.add_entry()
137 fe.id(entry["id"])
138 fe.title(entry["title"])
139 fe.updated(entry["updated"])
140 fe.link(entry["links"])
141
142# Finally, render the FeedGenerator object as an Atom feed and write it to
143# the atom.xml file
144fg.atom_file(gemini_dir.joinpath("gemlog/atom.xml"), pretty=True)
145
146# vim: ai et ft=python sts=4 sw=4 ts=4