Update site content, styling, and review layout
Revise about page, hello world post, and The Compound review. Add book cover support to review template, favicon, typographer config, and cover image enrichment script. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
133
enrich.py
Normal file
133
enrich.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user