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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
#!/usr/bin/env bash
# Export all Immich assets for given accounts within a date range.
# Usage: immich-export
# Accounts and API keys are hardcoded below; output goes to DEST_DIR.
#
# Works in two phases per account:
# 1. Discovery — paginate through all search results and collect asset IDs
# 2. Download — fetch each asset with real [X/total] progress
set -euo pipefail
IMMICH_URL="http://immich.f3s.lan.buetow.org"
DEST_DIR="/home/paul/Pictures/Pictures.Processing"
DATE_AFTER="2026-01-01T00:00:00.000Z"
DATE_BEFORE="2026-03-31T23:59:59.000Z"
PAGE_SIZE=100
# Load keys explicitly — declare -A with inline $() substitutions can silently
# drop entries in some bash versions, so we assign after the declaration.
declare -A ACCOUNTS
ACCOUNTS["paul"]="$(cat ~/.immich_paul_key)"
ACCOUNTS["albena"]="$(cat ~/.immich_albena_key)"
mkdir -p "$DEST_DIR"
# Phase 1: paginate through all search results and print "id\tfilename" lines.
# The API's 'total' field only reflects the current page size, not the grand
# total, so we must walk all pages to know how many assets there are.
discover_assets() {
local api_key="$1"
local page=1
while true; do
local response
response=$(curl -sf -X POST "$IMMICH_URL/api/search/metadata" \
-H "x-api-key: $api_key" \
-H "Content-Type: application/json" \
-d "{\"takenAfter\":\"$DATE_AFTER\",\"takenBefore\":\"$DATE_BEFORE\",\"type\":\"IMAGE\",\"size\":$PAGE_SIZE,\"page\":$page}")
# Print id<TAB>filename for each asset on this page
echo "$response" | python3 -c "
import json, sys
d = json.load(sys.stdin)
for item in d['assets']['items']:
print(item['id'] + '\t' + item['originalFileName'])
"
# Use 'or ""' to handle null nextPage — .get() returns None when the key
# exists with a JSON null value, and print(None) would yield "None" (non-empty).
local next_page
next_page=$(echo "$response" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['assets'].get('nextPage') or '')")
echo " Discovery: page $page done..." >&2
[[ -z "$next_page" ]] && break
page="$next_page"
done
}
# Phase 2: download each asset by ID, showing real [X/total] progress.
download_assets() {
local api_key="$1"
local account_dir="$2"
local asset_list="$3" # path to temp file with "id\tfilename" lines
local total
total=$(wc -l < "$asset_list")
local downloaded=0
local skipped=0
local n=0
while IFS=$'\t' read -r asset_id filename; do
((n++)) || true
local dest="$account_dir/$filename"
if [[ -f "$dest" ]]; then
((skipped++)) || true
echo " [$n/$total] skip (exists): $filename"
continue
fi
# Download to a temp file first; rename to final dest only on success.
# This prevents half-downloaded files from being mistaken as complete.
local tmp="$dest.tmp"
if curl -sf -o "$tmp" \
-H "x-api-key: $api_key" \
"$IMMICH_URL/api/assets/$asset_id/original"; then
mv "$tmp" "$dest"
((downloaded++)) || true
echo " [$n/$total] downloaded: $filename"
else
echo " [$n/$total] ERROR: failed to download $asset_id ($filename)" >&2
rm -f "$tmp" # Remove partial temp file on failure
fi
done < "$asset_list"
echo " Done: $downloaded downloaded, $skipped skipped (already exist)"
}
download_assets_for_account() {
local account="$1"
local api_key="$2"
local account_dir="$DEST_DIR/$account"
mkdir -p "$account_dir"
echo "==> Account: $account"
# Collect full asset list into a temp file so we know the total upfront
local asset_list
asset_list=$(mktemp)
trap "rm -f $asset_list" EXIT
echo " Phase 1: discovering assets..."
discover_assets "$api_key" > "$asset_list"
echo " Found $(wc -l < "$asset_list") images total"
# Clean up any leftover .tmp files from a previously aborted run
local stale_tmp
stale_tmp=$(find "$account_dir" -name "*.tmp" 2>/dev/null | wc -l)
if [[ "$stale_tmp" -gt 0 ]]; then
echo " Cleaning up $stale_tmp leftover .tmp file(s) from previous run..."
find "$account_dir" -name "*.tmp" -delete
fi
echo " Phase 2: downloading..."
download_assets "$api_key" "$account_dir" "$asset_list"
rm -f "$asset_list"
trap - EXIT
}
for account in "${!ACCOUNTS[@]}"; do
download_assets_for_account "$account" "${ACCOUNTS[$account]}"
done
echo ""
echo "Export complete -> $DEST_DIR"
|