diff --git a/enrich.py b/enrich.py new file mode 100644 index 0000000..bee609b --- /dev/null +++ b/enrich.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "python-frontmatter", +# ] +# /// +"""Enrich book reviews with ISBN and cover images from OpenLibrary.""" + +import json +import sys +import time +import urllib.request +import urllib.parse +from pathlib import Path + +import frontmatter + +REVIEWS_DIR = Path(__file__).parent / "site" / "content" / "reviews" +COVERS_DIR = Path(__file__).parent / "site" / "static" / "covers" + +OL_SEARCH = "https://openlibrary.org/search.json" +OL_COVER = "https://covers.openlibrary.org/b/isbn/{isbn}-L.jpg" + + +def search_isbn(title: str, author: str) -> str | None: + """Search OpenLibrary for an ISBN by title and author.""" + params = {"title": title, "author": author, "limit": "3", "fields": "isbn"} + url = f"{OL_SEARCH}?{urllib.parse.urlencode(params)}" + req = urllib.request.Request(url, headers={"User-Agent": "monotrope-enrich/1.0"}) + with urllib.request.urlopen(req, timeout=15) as resp: + data = json.loads(resp.read()) + for doc in data.get("docs", []): + for isbn in doc.get("isbn", []): + if len(isbn) == 13: + return isbn + # fall back to ISBN-10 if no 13 + for doc in data.get("docs", []): + for isbn in doc.get("isbn", []): + if len(isbn) == 10: + return isbn + return None + + +def fetch_cover(isbn: str, dest: Path) -> bool: + """Download a cover image for the given ISBN. Returns True on success.""" + url = OL_COVER.format(isbn=isbn) + req = urllib.request.Request(url, headers={"User-Agent": "monotrope-enrich/1.0"}) + with urllib.request.urlopen(req, timeout=15) as resp: + data = resp.read() + # OpenLibrary returns a tiny 1x1 placeholder when no cover exists + if len(data) < 1000: + return False + dest.write_bytes(data) + return True + + +def enrich(path: Path, dry_run: bool = False) -> None: + """Enrich a single review file with ISBN and cover.""" + post = frontmatter.load(path) + title = post.get("title", "") + author = post.get("book_author", "") + + has_isbn = bool(post.get("isbn")) + has_cover = bool(post.get("cover")) + + if has_isbn and has_cover: + print(f" skip {path.name} (already enriched)") + return + + # ── ISBN lookup ────────────────────────────────── + isbn = post.get("isbn", "") + if not isbn: + print(f" search title={title!r} author={author!r}") + isbn = search_isbn(title, author) + if not isbn: + print(f" ✗ no ISBN found for {path.name}") + return + print(f" found ISBN {isbn}") + + # ── Cover download ────────────────────────────── + slug = path.stem + cover_file = COVERS_DIR / f"{slug}.jpg" + if not has_cover and not cover_file.exists(): + print(f" fetch cover → {cover_file.relative_to(Path(__file__).parent)}") + if not dry_run: + COVERS_DIR.mkdir(parents=True, exist_ok=True) + if not fetch_cover(isbn, cover_file): + print(f" ✗ no cover image available for ISBN {isbn}") + cover_file = None + else: + cover_file = None + elif cover_file.exists(): + print(f" ok cover already exists") + + # ── Update frontmatter ────────────────────────── + changed = False + if not has_isbn: + post["isbn"] = isbn + changed = True + if not has_cover and cover_file and cover_file.exists(): + post["cover"] = f"/covers/{slug}.jpg" + changed = True + + if changed and not dry_run: + path.write_text(frontmatter.dumps(post) + "\n") + print(f" ✓ updated {path.name}") + elif changed: + print(f" (dry run) would update {path.name}") + + +def main() -> None: + dry_run = "--dry-run" in sys.argv + + reviews = sorted(REVIEWS_DIR.glob("*.md")) + reviews = [r for r in reviews if r.name != "_index.md"] + + if not reviews: + print("No reviews found.") + return + + print(f"Enriching {len(reviews)} review(s)...\n") + for path in reviews: + print(f" ── {path.stem} ──") + try: + enrich(path, dry_run=dry_run) + except Exception as e: + print(f" ✗ error: {e}") + print() + + +if __name__ == "__main__": + main() diff --git a/site/content/about.md b/site/content/about.md index fb35488..0fea392 100644 --- a/site/content/about.md +++ b/site/content/about.md @@ -3,8 +3,8 @@ title: About type: page --- -Monotrope is a play on the idea of [monotropism](https://en.wikipedia.org/wiki/Monotropism). I created this site as an experiment in writing regularly, and also in trying to own things instead of putting them on corporate platforms. Monotropism relates to the autistic experience, but I also just like the idea of deep singular focus and flow states. Modern work, especially with the use of AI, is increasingly fragmented and alienating. +Monotrope is a play on the idea of [monotropism](https://en.wikipedia.org/wiki/Monotropism) -- the theory of autistic cognition as deep, singular focus. I created this site as an experiment in writing regularly, and also in trying to own my content instead of putting it on corporate platforms. -I read across all genres, and post my reviews here because I want to own them. +I read across all genres, and post my reviews here. I live on Djadjawurrung and Taungurong land. \ No newline at end of file diff --git a/site/content/posts/hello-world.md b/site/content/posts/hello-world.md index 2165d50..eeca651 100644 --- a/site/content/posts/hello-world.md +++ b/site/content/posts/hello-world.md @@ -4,4 +4,4 @@ date: 2026-04-08 draft: false --- -This is the first post on Monotrope. My intent is to write here as frequently as possible. In part this is to practice the craft of writing directly, and in part it is to ensure I own the stuff I'm writing. \ No newline at end of file +This is the first post on Monotrope. My intent is to write here as frequently as possible. In part this is to practice the craft of writing directly, and in part as an experiment in direct ownership of my writing. \ No newline at end of file diff --git a/site/content/reviews/the-compound.md b/site/content/reviews/the-compound.md index b7d8dc4..d77b3d2 100644 --- a/site/content/reviews/the-compound.md +++ b/site/content/reviews/the-compound.md @@ -1,26 +1,26 @@ --- -title: "The Compound" -book_author: "Aisling Rawle" -date: 2026-03-01 -date_read: "March 2026" +book_author: Aisling Rawle +cover: /covers/the-compound.jpg +date: 2026-04-08 +date_read: March 2026 +isbn: '9780008710088' rating: 4 -tags: ["dystopia", "satire"] +tags: +- dystopia +- satire +title: The Compound --- -I liked this book _a lot_. I'm giving it 4 stars because I'm not sure I can fully decipher what it was trying to say, or if it was trying to say anything. +_The Compound_ is a hell of a vibe. It's compulsively readable while also being creepy as fuck. -The pitch is "Love Island meets Lord of the Flies", and that probably about sums it up. Its setting is a realtiy TV show in a very ambiguous future dystopia (there are references to "the wars", but really we get no detail of the world outside the show). +It takes place in an indeterminate dystopian near-future, where we get only the tiniest glimpses of life outside the eponymous compound. Characters make reference to "the wars," and there is general sense that life as a whole has gone downhill. But it's entirely possible to imagine that these characters live in our world; nothing they describe about the dystopia is wholly incompatible with life in 2026, it's just a bit magnified or brought into focus. -In a sense maybe it's a bit of a mess, it's a satire but it's not obvious exactly what it's satirising. There's an ambient theme of consumerism and late-stage capitalism, but it's never really a plot point, it's just there. It doesn't really make you think so much as make you vaguely uneasy. As an example, the contestants on the show get rewards for completing tasks, and when they get the reward they go to the camera and thank the brand that provided the reward. Nothing else happens about this, and the narrator (whose name is Lilly) doesn't really reflect on it much either. But it creates this sense of the absurd that contrasts with the darkness of what's going on between the characters. +The setting is a reality TV show set in an isolated compound in the desert, a bit of Love Island meets Survivor. The characters are paired off for romantic drama, but also pushed to extremes by the producers who withhold food or water in the intrest of creating tension or pushing specific tasks or challenges. -The narrator is an interesting point of view as well, because she's deliberately written as a bit dumb, which isn't something I've encountered before in a first person narrative. +The satire is more ambient than ever directly stated. To give one example, the contestants on the show get rewards for completing tasks, after which they're expected to walk up to a camera and thank the brand that provided the reward. This is never really commented on or used to make a point, it's simply there, an absurd reflection of late-stage consumer capitalism contrasted with the darkness of what the characters are being put through. -Most of the time all of the internal monologue just relates to the immediate situation in the compound, but there are a small number of times when we get a bit more reflection. There's a bit I highlighted where Lilly talks about dreading leaving the show and going home, and it ends with: +The protagonist and narrator, Lilly, is at once extremely shallow and more than a little stupid, while also having good intuitions about people and their motivations, and more self-awareness than you'd give her credit for were you a viewer of the show. Most of her inner monologue is in the moment, focused on the people and situations immediately in front of her, but very occasionally she dips into reflection. Here's the final few sentences of a longer passage I highlighted, in which Lilly talks about her dread of leaving the show and going home: > What did it matter to wake up at the same time every morning and wear the same clothes and try to eat more protein but less sugar, when an earthquake or a tsunami or a bomb might end it all at any minute? Or maybe we would all continue to boil, slowly but surely, in the mess that we pretended was an acceptable place to live. -I loved the writing style: tight pace, not overly flowery, but it had this creeping sense of unease or dread that made me want to keep reading, even when not much was happening. I haven't read that many thriller type books but I think this is something I really enjoy, when something manages to be a page turner without relying on plot twists or crazy stuff happening all the time. - -Didn't overstay its welcome, the ending was ambiguous and open ended but I thought it was everything it needed to be. - -In writing this I've _very nearly_ convinced myself to bump it up to five stars. Maybe I'll come back to this one. \ No newline at end of file +The style isn't flowery but it is sharp and expressive, and personally I found the pacing to be perfect: tense without the need for plot twists or constant action. \ No newline at end of file diff --git a/site/hugo.toml b/site/hugo.toml index 8f90578..9dfdc3f 100644 --- a/site/hugo.toml +++ b/site/hugo.toml @@ -2,6 +2,9 @@ baseURL = "https://monotrope.au" languageCode = "en-au" title = "Monotrope" +[markup.goldmark.extensions.typographer] + disable = false + [markup.goldmark.renderer] unsafe = false @@ -10,4 +13,4 @@ title = "Monotrope" section = ["HTML", "RSS"] [params] - description = "writing on software, books, and ideas." + description = "writing on technology, books, and ideas." diff --git a/site/layouts/_default/baseof.html b/site/layouts/_default/baseof.html index d1d9b94..1ee4621 100644 --- a/site/layouts/_default/baseof.html +++ b/site/layouts/_default/baseof.html @@ -9,6 +9,7 @@ + {{ range .AlternativeOutputFormats -}} {{ printf `` .Rel .MediaType.Type .Permalink $.Site.Title | safeHTML }} diff --git a/site/layouts/reviews/single.html b/site/layouts/reviews/single.html index e382feb..9924a67 100644 --- a/site/layouts/reviews/single.html +++ b/site/layouts/reviews/single.html @@ -17,8 +17,13 @@ {{- strings.Repeat $rating "▓" -}}{{- strings.Repeat (sub 5 $rating) "░" -}} {{ $rating }}/5 + + {{ with .Params.cover }} + Cover of {{ $.Title }} + {{ end }} + {{ .Content }} {{ with .Params.tags }} diff --git a/site/static/covers/the-compound.jpg b/site/static/covers/the-compound.jpg new file mode 100644 index 0000000..8709d30 Binary files /dev/null and b/site/static/covers/the-compound.jpg differ diff --git a/site/static/css/main.css b/site/static/css/main.css index 875a0a6..37ed7b0 100644 --- a/site/static/css/main.css +++ b/site/static/css/main.css @@ -336,6 +336,14 @@ article hr { } /* ── Book review ───────────────────────────────── */ +.book-cover { + float: right; + width: 180px; + margin: 0 0 1rem 1.5rem; + border: 1px solid var(--border); + border-radius: 2px; +} + .book-meta { font-family: var(--font-mono); font-size: 0.75rem; @@ -528,6 +536,13 @@ article hr { padding-top: 0; } + .book-cover { + float: none; + display: block; + width: 100px; + margin: 0 0 1.25rem 0; + } + .book-meta { grid-template-columns: 1fr; gap: 0.5rem; diff --git a/site/static/favicon.svg b/site/static/favicon.svg new file mode 100644 index 0000000..2965c77 --- /dev/null +++ b/site/static/favicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + +