aboutsummaryrefslogtreecommitdiffstats
path: root/bin/immich-fetch.sh
blob: 0fb5b418faa8d838429560cd21bd248db0ab04e7 (plain)
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/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>
#   scope week        (only when the past week's memories stand in for today's)
#   photo <asset id>|<memory year>|<localDateTime>|<place>
# 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