Tweaks to make images + shelves look good on retina displays
[books.alexwlchan.net] / scripts / render_html.py
index 8f98231..c9f11e3 100755 (executable)
@@ -1,6 +1,7 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 import datetime
 
 import datetime
+import hashlib
 import itertools
 import os
 import pathlib
 import itertools
 import os
 import pathlib
@@ -15,6 +16,9 @@ import markdown
 from markdown.extensions.smarty import SmartyExtension
 import smartypants
 
 from markdown.extensions.smarty import SmartyExtension
 import smartypants
 
+from generate_bookshelf import create_shelf_data_uri
+from tint_colors import get_tint_colors, store_tint_color
+
 
 def rsync(dir1, dir2):
     subprocess.check_call(["rsync", "--recursive", "--delete", dir1, dir2])
 
 def rsync(dir1, dir2):
     subprocess.check_call(["rsync", "--recursive", "--delete", dir1, dir2])
@@ -25,7 +29,7 @@ class Book:
     title = attr.ib()
     author = attr.ib()
     publication_year = attr.ib()
     title = attr.ib()
     author = attr.ib()
     publication_year = attr.ib()
-    cover_image = attr.ib()
+    cover_image = attr.ib(default="")
     cover_desc = attr.ib(default="")
 
     isbn10 = attr.ib(default="")
     cover_desc = attr.ib(default="")
 
     isbn10 = attr.ib(default="")
@@ -35,9 +39,9 @@ class Book:
 @attr.s
 class Review:
     date_read = attr.ib()
 @attr.s
 class Review:
     date_read = attr.ib()
-    rating = attr.ib()
     text = attr.ib()
     text = attr.ib()
-    format = attr.ib()
+    format = attr.ib(default=None)
+    rating = attr.ib(default=None)
     did_not_finish = attr.ib(default=False)
 
 
     did_not_finish = attr.ib(default=False)
 
 
@@ -55,7 +59,15 @@ class ReviewEntry:
 def get_review_entry_from_path(path):
     post = frontmatter.load(path)
 
 def get_review_entry_from_path(path):
     post = frontmatter.load(path)
 
-    book = Book(**post["book"])
+    kwargs = {}
+    for attr_name in Book.__attrs_attrs__:
+        try:
+            kwargs[attr_name.name] = post["book"][attr_name.name]
+        except KeyError:
+            pass
+
+    book = Book(**kwargs)
+
     review = Review(**post["review"], text=post.content)
 
     return ReviewEntry(path=path, book=book, review=review)
     review = Review(**post["review"], text=post.content)
 
     return ReviewEntry(path=path, book=book, review=review)
@@ -82,9 +94,17 @@ def get_reading_entry_from_path(path):
     return CurrentlyReadingEntry(path=path, book=book, reading=reading)
 
 
     return CurrentlyReadingEntry(path=path, book=book, reading=reading)
 
 
+def _parse_date(value):
+    if isinstance(value, datetime.date):
+        return value
+    else:
+        return datetime.datetime.strptime(value, "%Y-%m-%d").date()
+
+
 @attr.s
 class Plan:
     text = attr.ib()
 @attr.s
 class Plan:
     text = attr.ib()
+    date_added = attr.ib(converter=_parse_date)
 
 
 @attr.s
 
 
 @attr.s
@@ -98,7 +118,7 @@ def get_plan_entry_from_path(path):
     post = frontmatter.load(path)
 
     book = Book(**post["book"])
     post = frontmatter.load(path)
 
     book = Book(**post["book"])
-    plan = Plan(text=post.content)
+    plan = Plan(date_added=post["plan"]["date_added"], text=post.content)
 
     return PlanEntry(path=path, book=book, plan=plan)
 
 
     return PlanEntry(path=path, book=book, plan=plan)
 
@@ -124,7 +144,7 @@ def render_markdown(text):
 
 def render_date(date_value):
     if isinstance(date_value, datetime.date):
 
 def render_date(date_value):
     if isinstance(date_value, datetime.date):
-        return date_value.strftime("%d %B %Y")
+        return date_value.strftime("%-d %B %Y")
 
     date_match = re.match(
         r"^(?P<year>\d{4})-(?P<month>\d{2})(?:-(?P<day>\d{2}))?$", date_value
 
     date_match = re.match(
         r"^(?P<year>\d{4})-(?P<month>\d{2})(?:-(?P<day>\d{2}))?$", date_value
@@ -138,15 +158,17 @@ def render_date(date_value):
     )
 
     if date_match.group("day"):
     )
 
     if date_match.group("day"):
-        return date_obj.strftime("%-d %B %Y")
+        return render_date(date_obj)
     else:
         return date_obj.strftime("%B %Y")
 
 
     else:
         return date_obj.strftime("%B %Y")
 
 
-def render_individual_review(env, *, review_entry):
+def render_individual_review(env, *, review_entry, **kwargs):
     template = env.get_template("review.html")
     html = template.render(
     template = env.get_template("review.html")
     html = template.render(
-        review_entry=review_entry, title=f"My review of {review_entry.book.title}"
+        review_entry=review_entry,
+        title=f"My review of {review_entry.book.title}",
+        **kwargs
     )
 
     out_name = review_entry.out_path() / "index.html"
     )
 
     out_name = review_entry.out_path() / "index.html"
@@ -155,7 +177,60 @@ def render_individual_review(env, *, review_entry):
     out_path.write_text(html)
 
 
     out_path.write_text(html)
 
 
-if __name__ == "__main__":
+def _create_new_thumbnail(src_path, dst_path):
+    dst_path.parent.mkdir(exist_ok=True, parents=True)
+
+    # Thumbnails are 240x240 max, then 2x for retina displays
+    subprocess.check_call([
+        "convert", src_path, "-resize", "480x480>", dst_path
+    ])
+
+
+def thumbnail_1x(name):
+    pth = pathlib.Path(name)
+    return pth.stem + "_1x" + pth.suffix
+
+
+def _create_new_square(src_path, square_path):
+    square_path.parent.mkdir(exist_ok=True, parents=True)
+
+    subprocess.check_call([
+        "convert",
+        src_path, "-resize", "240x240", "-gravity", "center", "-background", "white", "-extent", "240x240", square_path
+    ])
+
+
+def create_thumbnails():
+    for image_name in os.listdir("src/covers"):
+        if image_name == ".DS_Store":
+            continue
+
+        src_path = pathlib.Path("src/covers") / image_name
+        dst_path = pathlib.Path("_html/thumbnails") / image_name
+
+        if not dst_path.exists():
+            _create_new_thumbnail(src_path, dst_path)
+        elif src_path.stat().st_mtime > dst_path.stat().st_mtime:
+            _create_new_thumbnail(src_path, dst_path)
+
+        square_path = pathlib.Path("_html/squares") / image_name
+
+        if not square_path.exists():
+            _create_new_square(src_path, square_path)
+        elif src_path.stat().st_mtime > square_path.stat().st_mtime:
+            _create_new_square(src_path, square_path)
+
+        store_tint_color(dst_path)
+
+
+CSS_HASH = hashlib.md5(open('static/style.css', 'rb').read()).hexdigest()
+
+
+def css_hash(_):
+    return f"md5:{CSS_HASH}"
+
+
+def main():
     env = Environment(
         loader=FileSystemLoader("templates"),
         autoescape=select_autoescape(["html", "xml"]),
     env = Environment(
         loader=FileSystemLoader("templates"),
         autoescape=select_autoescape(["html", "xml"]),
@@ -164,6 +239,13 @@ if __name__ == "__main__":
     env.filters["render_markdown"] = render_markdown
     env.filters["render_date"] = render_date
     env.filters["smartypants"] = smartypants.smartypants
     env.filters["render_markdown"] = render_markdown
     env.filters["render_date"] = render_date
     env.filters["smartypants"] = smartypants.smartypants
+    env.filters["thumbnail_1x"] = thumbnail_1x
+    env.filters["css_hash"] = css_hash
+    env.filters["create_shelf_data_uri"] = create_shelf_data_uri
+
+    create_thumbnails()
+
+    tint_colors = get_tint_colors()
 
     rsync("src/covers/", "_html/covers/")
     rsync("static/", "_html/static/")
 
     rsync("src/covers/", "_html/covers/")
     rsync("static/", "_html/static/")
@@ -178,7 +260,11 @@ if __name__ == "__main__":
     )
 
     for review_entry in all_reviews:
     )
 
     for review_entry in all_reviews:
-        render_individual_review(env, review_entry=review_entry)
+        render_individual_review(
+            env,
+            review_entry=review_entry,
+            tint_colors=tint_colors
+        )
 
     template = env.get_template("list_reviews.html")
     html = template.render(
 
     template = env.get_template("list_reviews.html")
     html = template.render(
@@ -190,6 +276,7 @@ if __name__ == "__main__":
         ],
         title="books i’ve read",
         this_year=str(datetime.datetime.now().year),
         ],
         title="books i’ve read",
         this_year=str(datetime.datetime.now().year),
+        tint_colors=tint_colors,
     )
 
     out_path = pathlib.Path("_html") / "reviews/index.html"
     )
 
     out_path = pathlib.Path("_html") / "reviews/index.html"
@@ -204,7 +291,11 @@ if __name__ == "__main__":
     )
 
     template = env.get_template("list_reading.html")
     )
 
     template = env.get_template("list_reading.html")
-    html = template.render(all_reading=all_reading, title="books i’m currently reading")
+    html = template.render(
+        all_reading=all_reading,
+        title="books i’m currently reading",
+        tint_colors=tint_colors
+    )
 
     out_path = pathlib.Path("_html") / "reading/index.html"
     out_path.parent.mkdir(exist_ok=True, parents=True)
 
     out_path = pathlib.Path("_html") / "reading/index.html"
     out_path.parent.mkdir(exist_ok=True, parents=True)
@@ -216,19 +307,54 @@ if __name__ == "__main__":
         get_entries(dirpath="src/plans", constructor=get_plan_entry_from_path)
     )
 
         get_entries(dirpath="src/plans", constructor=get_plan_entry_from_path)
     )
 
+    all_plans = sorted(all_plans, key=lambda plan: plan.plan.date_added, reverse=True)
+
     template = env.get_template("list_plans.html")
     template = env.get_template("list_plans.html")
-    html = template.render(all_plans=all_plans, title="books i want to read")
+    html = template.render(
+        all_plans=all_plans,
+        title="books i want to read",
+        tint_colors=tint_colors,
+    )
 
     out_path = pathlib.Path("_html") / "to-read/index.html"
     out_path.parent.mkdir(exist_ok=True, parents=True)
     out_path.write_text(html)
 
 
     out_path = pathlib.Path("_html") / "to-read/index.html"
     out_path.parent.mkdir(exist_ok=True, parents=True)
     out_path.write_text(html)
 
+    # Render the "never going to read this page"
+
+    all_retired = list(
+        get_entries(dirpath="src/will_never_read", constructor=get_plan_entry_from_path)
+    )
+
+    all_retired = sorted(
+        all_retired, key=lambda plan: plan.plan.date_added, reverse=True
+    )
+
+    template = env.get_template("list_will_never_read.html")
+    html = template.render(
+        all_retired=all_retired,
+        title="books i&rsquo;m never going to read",
+        tint_colors=tint_colors
+    )
+
+    out_path = pathlib.Path("_html") / "will-never-read/index.html"
+    out_path.parent.mkdir(exist_ok=True, parents=True)
+    out_path.write_text(html)
+
     # Render the front page
 
     index_template = env.get_template("index.html")
     # Render the front page
 
     index_template = env.get_template("index.html")
-    html = index_template.render(text=open("src/index.md").read())
+    html = index_template.render(
+        text=open("src/index.md").read(),
+        reviews=all_reviews[:5],
+        tint_colors=tint_colors
+    )
 
     index_path = pathlib.Path("_html") / "index.html"
     index_path.write_text(html)
 
     print("✨ Rendered HTML files to _html ✨")
 
     index_path = pathlib.Path("_html") / "index.html"
     index_path.write_text(html)
 
     print("✨ Rendered HTML files to _html ✨")
+
+
+if __name__ == "__main__":
+    main()