summaryrefslogtreecommitdiff
path: root/scripts/immich-export
blob: 2d396159001ac558b7ae236dd7ea456ffedb4a9b (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
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"