From 89c484e0f264d08bd774d7760c877218f740ef5c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Feb 2026 04:10:17 +0000 Subject: [PATCH 1/3] Fully automate Immich import: account creation, API key, and upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The import-photos.sh helper script now handles the entire setup flow without requiring any manual web UI interaction: 1. Checks if Immich is initialized via /api/server/config 2. If uninitialized: creates admin account via /api/auth/admin-sign-up (prompts for email, password, name) 3. If already initialized: prompts for existing credentials 4. Logs in via /api/auth/login to get bearer token 5. Creates API key via /api/api-keys automatically 6. Configures storage template via /api/system-config (PUT) 7. Installs immich-cli (checks immich → npx → npm → offers apt install) 8. Runs recursive upload with EXIF date preservation Also accepts an API key as argument to skip account setup for re-runs. Fixes: - Removed -f flag from curl calls that suppressed error details - Fixed $? check after login (now uses if ! command pattern) - Variable naming collision (INSTALL_NODE → INSTALL_NODE_YN) - Post-setup instructions now just say "run the import script" instead of listing manual steps https://claude.ai/code/session_01NAtxAkC5t6YVb3gcP1VcX8 --- ubuntu-post-install.sh | 222 ++++++++++++++++++++++++++++++----------- 1 file changed, 165 insertions(+), 57 deletions(-) diff --git a/ubuntu-post-install.sh b/ubuntu-post-install.sh index f1e0a03..1c63acd 100644 --- a/ubuntu-post-install.sh +++ b/ubuntu-post-install.sh @@ -3123,6 +3123,17 @@ IMMICH_ENV # EXIF date preservation. Photos are uploaded through the API, which triggers # Immich's metadata extraction pipeline — dates, GPS, camera info are all # read from the original files. +# +# What this script does: +# 1. Creates admin account (if first run) or logs in +# 2. Generates an API key automatically +# 3. Configures the storage template (date-based organization) +# 4. Installs the Immich CLI (if needed) +# 5. Uploads all photos with EXIF metadata preserved +# +# Usage: +# ./import-photos.sh # interactive (prompts for everything) +# ./import-photos.sh # skip account setup, use existing key ################################################################################ IMPORT_SCRIPT_HEAD @@ -3130,6 +3141,7 @@ IMPORT_SCRIPT_HEAD cat >> "$IMMICH_DIR/import-photos.sh" << IMPORT_SCRIPT_VARS IMMICH_URL="http://localhost:2283" SOURCE_DIR="$EXISTING_PHOTOS_SOURCE" +IMMICH_DIR="$IMMICH_DIR" IMPORT_SCRIPT_VARS cat >> "$IMMICH_DIR/import-photos.sh" << 'IMPORT_SCRIPT_BODY' @@ -3141,12 +3153,11 @@ echo "└─────────────────────── echo "" # ── Preflight checks ──────────────────────────────────────────── -# Check if Immich is running echo "Checking Immich server..." -if ! curl -sf "$IMMICH_URL/api/server/ping" > /dev/null 2>&1; then +if ! curl -s "$IMMICH_URL/api/server/ping" > /dev/null 2>&1; then echo "" echo " ✗ Immich is not running at $IMMICH_URL" - echo " Start it with: cd $(dirname "$0") && docker compose up -d" + echo " Start it with: cd $IMMICH_DIR && docker compose up -d" echo "" exit 1 fi @@ -3164,41 +3175,137 @@ fi PHOTO_COUNT=$(find "$SOURCE_DIR" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.heic" -o -iname "*.heif" -o -iname "*.webp" -o -iname "*.gif" -o -iname "*.tiff" -o -iname "*.bmp" -o -iname "*.mp4" -o -iname "*.mov" -o -iname "*.avi" -o -iname "*.mkv" -o -iname "*.webm" \) 2>/dev/null | wc -l) echo " ✓ Found ~$PHOTO_COUNT photos/videos in $SOURCE_DIR" -# ── Get API key ───────────────────────────────────────────────── -echo "" -echo " Before continuing, you need an API key from Immich:" -echo " 1. Open $IMMICH_URL in your browser" -echo " 2. Log in (create admin account if first visit)" -echo " 3. Click your avatar → Account Settings → API Keys" -echo " 4. Create a new key and copy it" -echo "" - +# ── Get or create API key ─────────────────────────────────────── API_KEY="${1:-}" -if [ -z "$API_KEY" ]; then - read -p " Enter your Immich API key: " API_KEY -fi if [ -z "$API_KEY" ]; then - echo " ✗ API key is required" - exit 1 -fi + echo "" + # Check if server needs initial setup + SERVER_CONFIG=$(curl -s "$IMMICH_URL/api/server/config" 2>/dev/null) + IS_INITIALIZED=$(echo "$SERVER_CONFIG" | python3 -c "import sys,json; print(json.load(sys.stdin).get('isInitialized', True))" 2>/dev/null) -# Verify API key works -echo "" -echo " Verifying API key..." -HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" -H "x-api-key: $API_KEY" "$IMMICH_URL/api/users/me" 2>/dev/null) -if [ "$HTTP_CODE" != "200" ]; then - echo " ✗ Invalid API key (server returned $HTTP_CODE)" - echo " Double-check the key and try again." - exit 1 + if [ "$IS_INITIALIZED" = "False" ]; then + # ── First-time setup: create admin account ────────────── + echo "┌─────────────────────────────────────────────────────────────────┐" + echo "│ FIRST-TIME SETUP — Creating admin account │" + echo "└─────────────────────────────────────────────────────────────────┘" + echo "" + echo " Immich needs an admin account. Enter your details:" + echo "" + read -p " Admin email: " ADMIN_EMAIL + while [ -z "$ADMIN_EMAIL" ]; do + read -p " Admin email (required): " ADMIN_EMAIL + done + read -sp " Admin password: " ADMIN_PASS + echo "" + while [ ${#ADMIN_PASS} -lt 8 ]; do + echo " Password must be at least 8 characters." + read -sp " Admin password: " ADMIN_PASS + echo "" + done + read -p " Your name [Admin]: " ADMIN_NAME + ADMIN_NAME="${ADMIN_NAME:-Admin}" + + echo "" + echo " Creating admin account..." + SIGNUP_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ + -H "Content-Type: application/json" \ + "$IMMICH_URL/api/auth/admin-sign-up" \ + -d "{\"email\":\"$ADMIN_EMAIL\",\"password\":\"$ADMIN_PASS\",\"name\":\"$ADMIN_NAME\"}" 2>/dev/null) + SIGNUP_CODE=$(echo "$SIGNUP_RESPONSE" | tail -1) + SIGNUP_BODY=$(echo "$SIGNUP_RESPONSE" | sed '$d') + + if [ "$SIGNUP_CODE" = "201" ]; then + echo " ✓ Admin account created" + else + echo " ✗ Failed to create admin account (HTTP $SIGNUP_CODE)" + echo " Response: $SIGNUP_BODY" + echo "" + echo " Create your account manually at $IMMICH_URL" + echo " Then re-run this script with your API key:" + echo " $0 " + exit 1 + fi + else + # Server already initialized, need credentials to log in + echo " Immich is already set up. Log in to generate an API key." + echo "" + read -p " Admin email: " ADMIN_EMAIL + while [ -z "$ADMIN_EMAIL" ]; do + read -p " Admin email (required): " ADMIN_EMAIL + done + read -sp " Admin password: " ADMIN_PASS + echo "" + fi + + # ── Login to get bearer token ─────────────────────────────── + echo " Logging in..." + LOGIN_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ + -H "Content-Type: application/json" \ + "$IMMICH_URL/api/auth/login" \ + -d "{\"email\":\"$ADMIN_EMAIL\",\"password\":\"$ADMIN_PASS\"}" 2>/dev/null) + LOGIN_CODE=$(echo "$LOGIN_RESPONSE" | tail -1) + LOGIN_BODY=$(echo "$LOGIN_RESPONSE" | sed '$d') + + if [ "$LOGIN_CODE" != "201" ]; then + echo " ✗ Login failed (HTTP $LOGIN_CODE)" + echo "" + echo " Check your email/password and try again, or pass an API key:" + echo " $0 " + exit 1 + fi + + ACCESS_TOKEN=$(echo "$LOGIN_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin)['accessToken'])" 2>/dev/null) + if [ -z "$ACCESS_TOKEN" ]; then + echo " ✗ Could not extract access token from login response" + exit 1 + fi + echo " ✓ Logged in" + + # ── Create API key ────────────────────────────────────────── + echo " Creating API key..." + APIKEY_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + "$IMMICH_URL/api/api-keys" \ + -d '{"name":"import-photos-script"}' 2>/dev/null) + APIKEY_CODE=$(echo "$APIKEY_RESPONSE" | tail -1) + APIKEY_BODY=$(echo "$APIKEY_RESPONSE" | sed '$d') + + if [ "$APIKEY_CODE" = "201" ]; then + API_KEY=$(echo "$APIKEY_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin)['secret'])" 2>/dev/null) + if [ -n "$API_KEY" ]; then + echo " ✓ API key created" + else + echo " ✗ Could not extract API key from response" + echo " Create one manually: $IMMICH_URL → Account Settings → API Keys" + echo " Then re-run: $0 " + exit 1 + fi + else + echo " ✗ Failed to create API key (HTTP $APIKEY_CODE)" + echo " Create one manually: $IMMICH_URL → Account Settings → API Keys" + echo " Then re-run: $0 " + exit 1 + fi +else + # API key provided as argument — verify it + echo "" + echo " Verifying API key..." + VERIFY_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "x-api-key: $API_KEY" "$IMMICH_URL/api/users/me" 2>/dev/null) + if [ "$VERIFY_CODE" != "200" ]; then + echo " ✗ Invalid API key (HTTP $VERIFY_CODE)" + echo " Double-check the key and try again." + exit 1 + fi + echo " ✓ API key valid" fi -echo " ✓ API key valid" # ── Configure storage template via API ────────────────────────── echo "" echo " Configuring storage template ({{y}}/{{MM}}/{{filename}})..." -# Get current config, enable storage template -CURRENT_CONFIG=$(curl -sf -H "x-api-key: $API_KEY" "$IMMICH_URL/api/system-config" 2>/dev/null) +CURRENT_CONFIG=$(curl -s -H "x-api-key: $API_KEY" "$IMMICH_URL/api/system-config" 2>/dev/null) if [ -n "$CURRENT_CONFIG" ] && command -v python3 &> /dev/null; then UPDATED_CONFIG=$(echo "$CURRENT_CONFIG" | python3 -c " import sys, json @@ -3208,16 +3315,16 @@ config['storageTemplate']['template'] = '{{y}}/{{MM}}/{{filename}}' json.dump(config, sys.stdout) " 2>/dev/null) if [ -n "$UPDATED_CONFIG" ]; then - RESULT=$(curl -sf -o /dev/null -w "%{http_code}" -X PUT \ + RESULT=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \ -H "x-api-key: $API_KEY" \ -H "Content-Type: application/json" \ "$IMMICH_URL/api/system-config" \ -d "$UPDATED_CONFIG" 2>/dev/null) if [ "$RESULT" = "200" ]; then - echo " ✓ Storage template configured automatically" + echo " ✓ Storage template configured" else - echo " ⚠ Could not set storage template via API" - echo " Set it manually: Admin → Settings → Storage Template → Enable" + echo " ⚠ Could not set storage template (HTTP $RESULT)" + echo " Set manually: Admin → Settings → Storage Template → Enable" echo " Template: {{y}}/{{MM}}/{{filename}}" fi else @@ -3226,7 +3333,7 @@ json.dump(config, sys.stdout) echo " Template: {{y}}/{{MM}}/{{filename}}" fi else - echo " ⚠ Set storage template manually before importing:" + echo " ⚠ python3 not found. Set storage template manually:" echo " Admin → Settings → Storage Template → Enable" echo " Template: {{y}}/{{MM}}/{{filename}}" fi @@ -3237,45 +3344,47 @@ IMMICH_CMD="" if command -v immich &> /dev/null; then IMMICH_CMD="immich" - echo " ✓ Immich CLI already installed" + echo " ✓ Immich CLI found" elif command -v npx &> /dev/null; then + echo " Immich CLI not installed. Will use npx (downloads temporarily)." IMMICH_CMD="npx --yes @immich/cli" - echo " ✓ Using npx to run Immich CLI (no global install needed)" elif command -v npm &> /dev/null; then - echo " Installing Immich CLI..." - npm install -g @immich/cli 2>/dev/null && IMMICH_CMD="immich" + echo " Installing Immich CLI globally..." + if npm install -g @immich/cli 2>/dev/null; then + IMMICH_CMD="immich" + echo " ✓ Immich CLI installed" + else + echo " ✗ npm install failed" + fi fi if [ -z "$IMMICH_CMD" ]; then echo " Node.js is required for the Immich CLI." - read -p " Install Node.js now? (y/n): " INSTALL_NODE - if [ "$INSTALL_NODE" = "y" ] || [ "$INSTALL_NODE" = "Y" ]; then + read -p " Install Node.js now? (y/n): " INSTALL_NODE_YN + if [ "$INSTALL_NODE_YN" = "y" ] || [ "$INSTALL_NODE_YN" = "Y" ]; then + echo " Installing nodejs and npm..." sudo apt-get update -qq && sudo apt-get install -y -qq nodejs npm 2>/dev/null if command -v npx &> /dev/null; then IMMICH_CMD="npx --yes @immich/cli" echo " ✓ Node.js installed" else - echo " ✗ Node.js installation failed" + echo " ✗ Installation failed. Install manually: sudo apt install nodejs npm" exit 1 fi else echo "" - echo " You can install Node.js later and run this script again," - echo " or install manually:" + echo " Install Node.js later and re-run this script:" echo " sudo apt install nodejs npm" - echo " npx @immich/cli login $IMMICH_URL/api " - echo " npx @immich/cli upload --recursive \"$SOURCE_DIR\"" + echo " $0 $API_KEY" exit 0 fi fi # ── Run the import ────────────────────────────────────────────── echo "" -echo " Logging into Immich..." -$IMMICH_CMD login "$IMMICH_URL/api" "$API_KEY" - -if [ $? -ne 0 ]; then - echo " ✗ Login failed" +echo " Authenticating CLI with Immich..." +if ! $IMMICH_CMD login "$IMMICH_URL/api" "$API_KEY"; then + echo " ✗ CLI login failed" exit 1 fi @@ -3338,15 +3447,14 @@ IMPORT_SCRIPT_BODY echo " New uploads from the mobile app or web UI are stored" echo " separately in: $UPLOAD_LOCATION" elif [ -n "$EXISTING_PHOTOS_SOURCE" ]; then - echo " 2. Get your API key:" - echo " Click avatar → Account Settings → API Keys → New API Key" - echo "" - echo " 3. Run the import script:" + echo " 2. Run the import script:" echo " $IMMICH_DIR/import-photos.sh" echo "" - echo " The script handles everything: configures storage template," - echo " installs the CLI tool, and uploads all photos from" - echo " $EXISTING_PHOTOS_SOURCE with EXIF dates preserved." + echo " The script handles everything automatically:" + echo " creates your admin account, generates an API key," + echo " configures the storage template, installs the CLI," + echo " and imports all photos from $EXISTING_PHOTOS_SOURCE" + echo " with EXIF dates preserved." else echo " 2. Enable storage template (keeps files organized by date):" echo " Admin → Settings → Storage Template → Enable" From 5c37d23788c04a40d79516caaf38c5da752cef3b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Feb 2026 04:14:37 +0000 Subject: [PATCH 2/3] Clean up post-setup instructions: one step for import path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The import-photos.sh script creates the admin account via API, so "Open browser and create admin account" is no longer needed for the import path. Post-setup is now just: Import path (Strategy 1 + existing photos): "Run ~/docker/immich/import-photos.sh" — one step, no browser External library path (Strategy 2): Still requires web UI for library creation (3 steps) No existing photos: Still requires web UI for storage template (2 steps) https://claude.ai/code/session_01NAtxAkC5t6YVb3gcP1VcX8 --- ubuntu-post-install.sh | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/ubuntu-post-install.sh b/ubuntu-post-install.sh index 1c63acd..a2168c7 100644 --- a/ubuntu-post-install.sh +++ b/ubuntu-post-install.sh @@ -3431,10 +3431,21 @@ IMPORT_SCRIPT_BODY echo "│ IMMICH SETUP STEPS │" echo "└─────────────────────────────────────────────────────────────────┘" echo "" - echo " 1. Open http://localhost:2283 and create your admin account" - echo "" - if [ "$IMMICH_STRATEGY" = "2" ]; then + if [ -n "$EXISTING_PHOTOS_SOURCE" ] && [ "$IMMICH_STRATEGY" != "2" ]; then + echo " Import your existing photos:" + echo " $IMMICH_DIR/import-photos.sh" + echo "" + echo " The script handles everything automatically:" + echo " creates your admin account, generates an API key," + echo " configures the storage template, installs the CLI," + echo " and imports all photos from $EXISTING_PHOTOS_SOURCE" + echo " with EXIF dates preserved." + echo "" + echo " After import, open http://localhost:2283 to browse your photos." + elif [ "$IMMICH_STRATEGY" = "2" ]; then + echo " 1. Open http://localhost:2283 and create your admin account" + echo "" echo " 2. Enable storage template (keeps new uploads organized):" echo " Admin → Settings → Storage Template → Enable" echo " Template: {{y}}/{{MM}}/{{filename}}" @@ -3446,16 +3457,9 @@ IMPORT_SCRIPT_BODY echo "" echo " New uploads from the mobile app or web UI are stored" echo " separately in: $UPLOAD_LOCATION" - elif [ -n "$EXISTING_PHOTOS_SOURCE" ]; then - echo " 2. Run the import script:" - echo " $IMMICH_DIR/import-photos.sh" - echo "" - echo " The script handles everything automatically:" - echo " creates your admin account, generates an API key," - echo " configures the storage template, installs the CLI," - echo " and imports all photos from $EXISTING_PHOTOS_SOURCE" - echo " with EXIF dates preserved." else + echo " 1. Open http://localhost:2283 and create your admin account" + echo "" echo " 2. Enable storage template (keeps files organized by date):" echo " Admin → Settings → Storage Template → Enable" echo " Template: {{y}}/{{MM}}/{{filename}}" From 99a6843010100ca6e675b3712bde0c6c72be1ecb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Feb 2026 04:24:12 +0000 Subject: [PATCH 3/3] Ask strategy before photo location in Immich setup The user should decide *what* they want (import vs external library) before being asked *where* their photos are. The "what" frames the context for the "where" question. https://claude.ai/code/session_01NAtxAkC5t6YVb3gcP1VcX8 --- ubuntu-post-install.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ubuntu-post-install.sh b/ubuntu-post-install.sh index a2168c7..402697d 100644 --- a/ubuntu-post-install.sh +++ b/ubuntu-post-install.sh @@ -2833,13 +2833,6 @@ else EXISTING_PHOTOS_SOURCE="" if [ "$HAS_EXISTING_PHOTOS" = "y" ] || [ "$HAS_EXISTING_PHOTOS" = "Y" ]; then - echo "" - echo " Where are your existing photos now?" - echo " Default: $PHOTOS_DIR" - prompt_text " Existing photos path:" "$PHOTOS_DIR" EXISTING_PHOTOS_SOURCE 2>/dev/null || EXISTING_PHOTOS_SOURCE="$PHOTOS_DIR" - EXISTING_PHOTOS_SOURCE="${EXISTING_PHOTOS_SOURCE/#\~/$ACTUAL_HOME}" - EXISTING_PHOTOS_SOURCE="${EXISTING_PHOTOS_SOURCE%/}" - echo "" echo " How should Immich handle your existing photos?" echo "" @@ -2862,6 +2855,13 @@ else read -p " Choose [1/2]: " IMMICH_STRATEGY IMMICH_STRATEGY="${IMMICH_STRATEGY:-1}" fi + + echo "" + echo " Where are your existing photos now?" + echo " Default: $PHOTOS_DIR" + prompt_text " Existing photos path:" "$PHOTOS_DIR" EXISTING_PHOTOS_SOURCE 2>/dev/null || EXISTING_PHOTOS_SOURCE="$PHOTOS_DIR" + EXISTING_PHOTOS_SOURCE="${EXISTING_PHOTOS_SOURCE/#\~/$ACTUAL_HOME}" + EXISTING_PHOTOS_SOURCE="${EXISTING_PHOTOS_SOURCE%/}" else IMMICH_STRATEGY="1" fi