From 9624351d80ace747d3117fb22d6cbd00fce3eedf Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 29 Sep 2026 13:33:14 +0200 Subject: feat: add an Immich memories card with click and scroll A centred card showing today's "N years ago" photos from Immich, one at a time on a 30-second rotation, with when each was taken and where. bin/immich-fetch.sh runs from conky's execi every 30 minutes: it asks /api/memories for today, converts each preview to PNG (Cairo loads nothing else) and writes an index with the year, local capture time and place. The place needs the key's asset.read permission and reads '--' without it. The key lives in ~/.config/udt/immich.env, like the weather key, because a GPG-backed store would raise a pinentry unattended. The dashboard now registers lua_mouse_hook and hands pointer events to the widget under the pointer. On this card, scrolling steps through the photos and a left click opens the photo in Immich's web UI and hides the dashboard so the browser is not stuck beneath it. Co-Authored-By: Claude Opus 5.5 --- bin/immich-fetch.sh | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100755 bin/immich-fetch.sh (limited to 'bin') diff --git a/bin/immich-fetch.sh b/bin/immich-fetch.sh new file mode 100755 index 0000000..961cd63 --- /dev/null +++ b/bin/immich-fetch.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Fetch today's Immich "N years ago" memories into a cache directory, for +# widgets/immich.lua. Run from conky's ${execi}: prints nothing on success and +# never blocks the dashboard, since the widget only ever reads the cache. +# +# Writes $CACHE_DIR/.png per photo plus index.txt: +# generated +# url +# photo ||| +# An index with no photo lines means "no memories today", which the card shows +# differently from a missing index ("the fetch never ran"). +# +# Leaves any existing cache untouched when the server cannot be reached, rather +# than replacing good data with an error. + +set -u + +ENV_FILE="${IMMICH_ENV:-$HOME/.config/udt/immich.env}" +CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/udt/immich" +INDEX="$CACHE_DIR/index.txt" + +if [ ! -r "$ENV_FILE" ]; then + echo "immich-fetch: no $ENV_FILE (copy immich.env.example)" >&2 + exit 1 +fi + +set -a +# shellcheck source=/dev/null +. "$ENV_FILE" +set +a + +if [ -z "${URL:-}" ] || [ -z "${KEY:-}" ]; then + echo "immich-fetch: URL or KEY is empty in $ENV_FILE" >&2 + exit 1 +fi + +# Photos are personal data: keep the cache private from the first byte. +umask 077 +mkdir -p "$CACHE_DIR" + +TMP="$INDEX.tmp.$$" +MEM="$CACHE_DIR/memories.json.tmp.$$" +trap 'rm -f "$TMP" "$MEM" "$CACHE_DIR"/*.part.$$' EXIT + +api() { + curl -fsS --max-time 20 -H "x-api-key: $KEY" "${URL%/}/api/$1" +} + +if ! api "memories?for=$(date +%F)" > "$MEM" 2>/dev/null \ + || ! jq -e 'type == "array"' "$MEM" > /dev/null 2>&1; then + echo "immich-fetch: memories request failed" >&2 + exit 1 +fi + +echo "generated $(date +%s)" > "$TMP" +# For the card's click-through to the photo in Immich's web UI. +echo "url ${URL%/}" >> "$TMP" + +# One line per asset: id, the memory's year, and when the photo was taken. +# localDateTime is the camera's wall clock, which is what "taken at" means; +# the UTC fileCreatedAt would shift an evening photo into the next day. +jq -r '.[] | select(.type == "on_this_day") | .data.year as $y + | .assets[] | [.id, $y, .localDateTime] | @tsv' "$MEM" | +while IFS=$'\t' read -r id year taken; do + png="$CACHE_DIR/$id.png" + if [ ! -s "$png" ]; then + # The preview is a JPEG (or WebP); cairo only loads PNG. Shrunk here + # so the widget holds a ~1MB surface rather than an 11MB one. + # -auto-orient: previews already come rotated, this is belt and braces. + if ! api "assets/$id/thumbnail?size=preview" 2>/dev/null \ + | magick - -auto-orient -resize '800x800>' "png:$png.part.$$" 2>/dev/null; then + continue + fi + mv -f "$png.part.$$" "$png" + fi + + # Location needs the key's asset.read permission. Without it this 403s and + # the place is simply empty: the card shows '--', the photo still shows. + place=$(api "assets/$id" 2>/dev/null | jq -r '.exifInfo // {} | + ([.city, .country] | map(select(. != null and . != "")) | join(", ")) as $p + | if $p != "" then $p + elif .latitude != null and .longitude != null + then "\(.latitude * 1000 | round / 1000), \(.longitude * 1000 | round / 1000)" + else "" end' 2>/dev/null) + + echo "photo $id|$year|$taken|$place" >> "$TMP" +done + +mv -f "$TMP" "$INDEX" + +# Drop photos no longer in today's memories, so the directory holds one day. +for f in "$CACHE_DIR"/*.png; do + [ -e "$f" ] || continue + id=$(basename "$f" .png) + grep -q "^photo $id|" "$INDEX" || rm -f "$f" +done -- cgit v1.2.3