1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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/<asset id>.png per photo plus index.txt:
# generated <epoch>
# url <server URL>
# photo <asset id>|<memory year>|<localDateTime>|<place>
# 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
|