Add Books to Org-Mode File

End of 2020 I migrated all books I own and/or read until then from Goodreads to Org-Mode files. Since then I added all books I have read manually to that file. Over the christmas holidays I had to add a few books and wasn't willing to write these entries manually anymore. So I looked for a source I could get information about books from: Openlibrary API. The endpoint I decided to use is /api/books?bibkeys=ISBN:...&jscmd=data which is older, but gets title and authors in one call.

With the help from Claude I now have a script that queries the Openlibrary API, asks if the book is from the local library and if it is fiction or non-fiction. Then it searches for the alphabetically best place in either the fiction or the non-fiction section and places the entry there. I still have to edit the entry after that to add a rating and a short personal summary of the book.

For building simple text UIs I nowadays use rich a lot. Easy to use and good looking functional UIs.

The current interface for the add-book.py looks like this:

$ ./add-book.py
╭──────────╮
│ Add Book │
╰──────────╯
ISBN13: 9781401208417
Looking up...
Found: V for Vendetta by Moore, Alan
Library? [y/n] (y):
Category [fiction/non-fiction] (non-fiction): fiction
Added!

This short script is already a major time saver. I should have done that years ago.

Plain Text Acounting and Ionity Subscription Calculations

I use an electric car more than once a month. The car is not charged at a wallbox at home, so one of the cheaper ways to charge the car is with an Ionity subscription.

There are two subscription plans that reduce the price a lot: Power and Motion. Example calculation for the Power subscription (€11.99):

# (price without subscription) - (price with power subscription)
€0.75/kWh - €0.39/kWh = €0.36/kWh
# subscription price divided by savings
€11.99 / 0.36 = 33kWh

So the break-even is at 33kWh in a month. When regular charging at Ionity this is worth it really fast.

The cheaper Subscription (Motion) has a break-even at 23kWh, but the Power subscription with €0.10/kWh less, is obviously better after 33kWh.

I track all my expenses with the plain text accounting tool hledger. So for every subscription and charge I have records.

Some example records, exported with `hledger register Expenses:Car:Ionity -O csv > expenses-ionity.csv`:

"txnidx","date","code","description","account","amount","total"
"7260","2025-10-21","","Ionity Power Monthly","Expenses:Car:Ionity:SubPower","€11.99","€11.99"
"7262","2025-10-21","","Ionity Charge","Expenses:Car:Ionity:Charge39","€6.96","€19.87"
"7274","2025-10-27","","Ionity Charge","Expenses:Car:Ionity:Charge39","€11.67","€31.54"
"7293","2025-11-04","","Ionity Charge","Expenses:Car:Ionity:Charge39","€9.52","€63.44"
"7312","2025-11-11","","Ionity Charge","Expenses:Car:Ionity:Charge39","€12.67","€76.11"
"7329","2025-11-18","","Ionity Charge","Expenses:Car:Ionity:Charge39","€9.09","€85.20"
"7335","2025-11-20","","Ionity Power Monthly","Expenses:Car:Ionity:SubPower","€11.99","€97.19"
"7364","2025-12-01","","Ionity Charge","Expenses:Car:Ionity:Charge39","€8.58","€105.77"
"7365","2025-12-01","","Ionity Charge","Expenses:Car:Ionity:Charge39","€8.63","€114.40"
"7366","2025-12-01","","Ionity Charge","Expenses:Car:Ionity:Charge39","€7.41","€121.81"
"7397","2025-12-16","","Ionity Charge","Expenses:Car:Ionity:Charge39","€6.49","€132.25"
"7412","2025-12-22","","Ionity Power Monthly","Expenses:Car:Ionity:SubPower","€11.99","€144.24"
"7420","2025-12-29","","Ionity Charge","Expenses:Car:Ionity:Charge39","€14.93","€159.17"

As you can see I named the accounts with the name of the subscription (SubPower) and with the costs per kWh (Charge39). I did the same for the charges with EnBW and Stadtwerke Stuttgart (but with different paths, i.e. Expenses:Car:EnBW:Charge59). I haven't subscribed to either, and I don't plan to. I've only used each once, and I'll use them again only if convenience outweighs cost.

Looking at the data it is obvious that the Ionity subscription is worth it. But I would actually like to know what I paid per kWh. For this we need to establish one other detail. The date in the ledger is not the charging date, but the transaction date on my banking account. For example the Power subscription starts every 17th, but the transaction is between the 20th and the 22nd. And for the charges it is the same. They have happened in the days before and depending if weekends were between them this could be 2-5 days. So some inaccuracies are expected and would be hard to remove completly (i.e. with parsing the pdf invoices from Ionity).

Code to calculate the actual costs:

import csv
from datetime import datetime
from decimal import Decimal

POWER_MONTHLY, POWER_RATE = Decimal("11.99"), Decimal("0.39")

# update csv with: hledger register Expenses:Car:Ionity -O csv > expenses-ionity.csv
rows = [
    (
        datetime.strptime(r["date"], "%Y-%m-%d").date(),
        Decimal(r["amount"].replace("€", "").replace(",", ".")),
        r["account"],
    )
    for r in csv.DictReader(open("expenses-ionity.csv"))
]
rows.sort()

sub_dates = [d for d, _, a in rows if "SubPower" in a]
periods = {d: [] for d in sub_dates}

for date, amount, account in rows:
    if "Charge39" in account:
        periods[max(d for d in sub_dates if d <= date)].append(
            (date, amount, amount / POWER_RATE)
        )
    elif "SubPower" not in account:
        raise ValueError(f"only Charge39/SubPower supported, got {account}")

total_kwh, total_cost = Decimal(0), Decimal(0)
for charges in periods.values():
    period_kwh = sum(kwh for _, _, kwh in charges)
    for date, cost, kwh in charges:
        sub_share = POWER_MONTHLY * kwh / period_kwh
        print(
            f"{date}  {kwh:6.2f} kWh  €{cost:6.2f} + €{sub_share:5.2f} sub  -> €{(cost+sub_share)/kwh:.3f}/kWh"
        )
    total_kwh += period_kwh
    total_cost += sum(c for _, c, _ in charges) + POWER_MONTHLY

print("-" * 70)
print(
    f"Total:    {total_kwh:6.2f} kWh  €{total_cost:6.2f}  -> €{total_cost/total_kwh:.3f}/kWh average"
)

This results in a table with all charges and adds the actual cost per kWh for every subscription period:

2025-10-21   17.85 kWh  €  6.96 + € 1.67 sub  -> €0.484/kWh
2025-10-27   29.92 kWh  € 11.67 + € 2.80 sub  -> €0.484/kWh
2025-11-04   24.41 kWh  €  9.52 + € 2.29 sub  -> €0.484/kWh
2025-11-11   32.49 kWh  € 12.67 + € 3.04 sub  -> €0.484/kWh
2025-11-18   23.31 kWh  €  9.09 + € 2.18 sub  -> €0.484/kWh
2025-12-01   19.00 kWh  €  7.41 + € 2.86 sub  -> €0.540/kWh
2025-12-01   22.00 kWh  €  8.58 + € 3.31 sub  -> €0.540/kWh
2025-12-01   22.13 kWh  €  8.63 + € 3.33 sub  -> €0.540/kWh
2025-12-16   16.64 kWh  €  6.49 + € 2.50 sub  -> €0.540/kWh
2025-12-29   38.28 kWh  € 14.93 + €11.99 sub  -> €0.703/kWh
----------------------------------------------------------------------
Total:    246.03 kWh  €131.92  -> €0.536/kWh average

The last month is ongoing which skews the total value a bit.

I have a Forgejo action running a version of this script that generates a report per subscription period and commits it to my plain text accounting repository.

GPX Viewer with Streamlit

In March last year I exported all my Komoot planned routes and I already cycled some of them. With the new year starting today I try to find routes I still want to cycle. So a map with all my old Komoot planned routes would be nice.

I used Claude to build me a simple GPX viewer with Streamlit. A few iterations were needed to figure out the caching for the main loop, but then the viewer is exactly what I wanted:

img1

The map is interactive: zooming, panning and selecting one track to get the filename is possible.

Full code:

import random
from pathlib import Path

import folium
import gpxpy
import streamlit as st
from streamlit_folium import st_folium

@st.cache_resource
def create_map(folder: Path) -> folium.Map:
    gpx_files: list[Path] = list(folder.glob("*.gpx"))
    m: folium.Map = folium.Map(
        location=[48.5, 9.0], zoom_start=7, tiles="CartoDB Positron"
    )

    for gpx_file in gpx_files:
        with open(gpx_file) as f:
            gpx = gpxpy.parse(f)

        for track in gpx.tracks:
            points = [
                (seg.points[i].latitude, seg.points[i].longitude)
                for seg in track.segments
                for i in range(len(seg.points))
            ]
            folium.PolyLine(
                points,
                color=f"#{random.randint(0, 0xFFFFFF):06x}",
                weight=3,
                opacity=0.7,
                popup=gpx_file.name,
                tooltip=gpx_file.name,
            ).add_to(m)
    return m

st.set_page_config(layout="wide")
st.title("GPX Map Viewer")

st_folium(
    create_map(folder=Path("files")),
    width=1400,
    height=800,
    returned_objects=[],
)

To run the code (saved as app.py) without any other files I used:

uv run --with streamlit --with folium --with gpxpy --with streamlit-folium streamlit run app.py

Some decisions I made while figuring out how I want this tool to use: Random colors for the tracks are good enough -- no need to give a list of colors. For this to work out, I switched the map tiles to a black and white map: CartoDB Positron.

Overall I am happy with this. After I find a file I'd like to base a future cycling trip on, I import it into RideWithGPS and begin the actual planning.