#!/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 # scope week (only when the past week's memories stand in for today's) # photo ||| # When today has none, the past six days' memories are used instead. An # index with no photo lines means "no memories this week", 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.$$ "$CACHE_DIR"/week.json.tmp.$$*' EXIT api() { curl -fsS --max-time 20 -H "x-api-key: $KEY" "${URL%/}/api/$1" } memories() { api "memories?for=$1" > "$MEM" 2>/dev/null \ && jq -e 'type == "array"' "$MEM" > /dev/null 2>&1 } if ! memories "$(date +%F)"; then echo "immich-fetch: memories request failed" >&2 exit 1 fi # No memories today: fall back to the past week's, so the card still shows # something. Each day's memories go in one merged array; a day that fails is # skipped rather than failing the whole fetch. if ! jq -e 'any(.[]; .type == "on_this_day")' "$MEM" > /dev/null; then WEEK="$CACHE_DIR/week.json.tmp.$$" echo '[]' > "$WEEK" for d in 1 2 3 4 5 6; do memories "$(date -d "$d days ago" +%F)" || continue jq -s 'add' "$WEEK" "$MEM" > "$WEEK.new" && mv -f "$WEEK.new" "$WEEK" done mv -f "$WEEK" "$MEM" SCOPE=week fi echo "generated $(date +%s)" > "$TMP" # For the card's click-through to the photo in Immich's web UI. echo "url ${URL%/}" >> "$TMP" [ -n "${SCOPE:-}" ] && echo "scope $SCOPE" >> "$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 the index, so the directory holds one fetch's worth. for f in "$CACHE_DIR"/*.png; do [ -e "$f" ] || continue id=$(basename "$f" .png) grep -q "^photo $id|" "$INDEX" || rm -f "$f" done