summaryrefslogtreecommitdiff
path: root/src/lib/metadata-cache.source.sh
blob: 45edd3cb221678c123332546ae5c0aefd514e92a (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
# Shared EXIF identify cache primitive. Promoted out of album-metadata.source.sh
# (task pn0) because the cached `identify -verbose` reader is a low-level
# metadata primitive consumed by BOTH the album (tooltips, details tables) and
# the stats aggregator (leaderboard tallies). Keeping it inside the album module
# forced stats to reach across a module boundary into album internals; moving it
# here gives both consumers a shared, stable dependency that is sourced before
# either of them (see LIB_SOURCES in the Justfile, ordered right after
# metadata-label.source.sh, the sibling shared metadata helper). All library
# modules are sourced before any code runs, so source order documents the
# dependency, it does not affect availability. Behaviour and signatures are
# unchanged by the move.

# Build the cache signature line ("<photo>:<size>:<mtime>") used to decide
# whether a cache entry is still valid for the source file. Kept private to this
# module alongside its only consumers, plus the stats test that pre-seeds caches.
photo_cache_signature() {
    local -r photo="$1"; shift
    local -r photo_path="$1"; shift
    local stat_output

    stat_output=$(stat -c '%s:%Y' "$photo_path")
    printf '%s:%s\n' "$photo" "$stat_output"
}

# Print a cache file's payload (everything after the leading signature line).
print_cached_photo_identify_output() {
    local -r cache_file="$1"; shift
    local line
    local skipped_signature=no

    while IFS= read -r line || [ -n "$line" ]; do
        if [ "$skipped_signature" = no ]; then
            skipped_signature=yes
            continue
        fi
        printf '%s\n' "$line"
    done < "$cache_file"
}

# Return cached ImageMagick `identify -verbose` output for a photo, rebuilding
# the cache when missing or stale. Public shared primitive: album-metadata and
# stats-aggregate both call this rather than running identify themselves.
cached_photo_identify_output() {
    local -r photo="$1"; shift
    local -r photo_path="$1"; shift
    local cache_dir
    local cache_file
    local cached_signature=''
    local current_signature
    local identify_status

    # Persist the EXIF cache in a volatile ./cache directory parallel to ./dist
    # (the staging dir is a sibling of the final dist, so dirname "$DIST_DIR" is
    # the working dir in both staging and direct contexts). Keeping it outside
    # dist means it survives a fresh/cleared dist and is never deployed, so an
    # unchanged photo skips the slow `identify -verbose` on every regenerate.
    cache_dir="$(dirname "$DIST_DIR")/cache/exif"
    cache_file="$cache_dir/$photo.txt"
    current_signature=$(photo_cache_signature "$photo" "$photo_path")

    # Reuse the cache when its signature still matches the source file. --force
    # is handled once up front by clear_exif_cache (which empties this directory),
    # so the first call per photo then rebuilds it and the rest of the run reuses
    # it -- exactly one identify per photo even under force.
    if [ -f "$cache_file" ]; then
        IFS= read -r cached_signature < "$cache_file" || true
        if [ "$cached_signature" = "$current_signature" ]; then
            print_cached_photo_identify_output "$cache_file"
            return
        fi
    fi

    mkdir -p "$cache_dir"
    printf '%s\n' "$current_signature" > "$cache_file"

    # Capture the identify exit status instead of swallowing it with `|| true`.
    # Errors are still hidden from stdout (so a corrupt photo does not pollute
    # the EXIF output), but a non-zero status now drives a warning + no-cache
    # rather than silently leaving a signature-only cache entry behind.
    identify_status=0
    imagemagick_identify -verbose "$photo_path" >> "$cache_file" 2>/dev/null \
        || identify_status=$?

    if [ "$identify_status" -ne 0 ]; then
        # Failed identify (corrupt photo, timeout, missing binary, ...): warn
        # naming the photo and remove the cache file. Removing it is essential:
        # a file holding only the signature line is a valid-looking cache hit,
        # so the next run would silently reuse the empty result forever -- never
        # retrying identify and never warning again (the original data-loss bug).
        # Deleting it makes the next run retry and warn.
        #
        # We deliberately do NOT abort: this runs inside backgrounded render jobs
        # under `set -euo pipefail`, and one unreadable photo must not kill the
        # whole generation. The photo still renders, just with empty tooltip and
        # stats, now accompanied by a warning.
        rm -f "$cache_file"
        log_warning \
            "could not read EXIF for $photo (ImageMagick identify failed);" \
            "tooltip/stats will be missing"
        return 0
    fi

    print_cached_photo_identify_output "$cache_file"
}