Immich-photo-manager storage-optimizer
git clone https://github.com/drolosoft/immich-photo-manager
T=$(mktemp -d) && git clone --depth=1 https://github.com/drolosoft/immich-photo-manager "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/storage-optimizer" ~/.claude/skills/drolosoft-immich-photo-manager-storage-optimizer && rm -rf "$T"
skills/storage-optimizer/SKILL.mdStorage Optimizer
⚠️ Connection Required — ALWAYS CHECK FIRST
Before doing ANYTHING else in this skill, call
on the Immich MCP server.ping
- If
succeeds → proceed with the skill normally.ping - If
fails or the MCP tools are not available → STOP. Do not continue. Tell the user:ping
❌ Immich is not connected. This plugin needs a running Immich MCP server to work.
Run /setup-immich-photo-manager to configure your Immich connection. You'll need:
- Your Immich server URL (e.g.,
)http://192.168.1.100:2283- An Immich API key (how to create one)
- The MCP server configured (see /setup-immich-photo-manager)
Nothing in this plugin will work until the connection is configured.
Do NOT skip this check. Do NOT try to run any other tool first. Always ping, always block if it fails.
Analyze storage usage in an Immich photo library, identify the biggest consumers, and recommend strategies to reclaim space without losing important content.
When to Use
- Disk space running low on the Immich server
- After bulk imports to assess storage impact
- Planning storage capacity (how long until disk is full?)
- Deciding whether to keep RAW files, original videos, etc.
Analysis Workflow
Step 1: Storage Overview
-- Total storage by asset type SELECT type, count(*) as files, pg_size_pretty(sum(("exifInfo"->>'fileSizeInByte')::bigint)) as total_size, pg_size_pretty(avg(("exifInfo"->>'fileSizeInByte')::bigint)) as avg_size, pg_size_pretty(max(("exifInfo"->>'fileSizeInByte')::bigint)) as max_size FROM asset WHERE "deletedAt" IS NULL GROUP BY type; -- Trash storage (reclaimable immediately) SELECT count(*) as trashed_files, pg_size_pretty(sum(("exifInfo"->>'fileSizeInByte')::bigint)) as trash_size FROM asset WHERE "deletedAt" IS NOT NULL;
Step 2: Format Breakdown
-- File formats by count and size SELECT upper(split_part("originalPath", '.', -1)) as format, count(*) as files, pg_size_pretty(sum(("exifInfo"->>'fileSizeInByte')::bigint)) as total_size, pg_size_pretty(avg(("exifInfo"->>'fileSizeInByte')::bigint)) as avg_size, round(100.0 * sum(("exifInfo"->>'fileSizeInByte')::bigint) / (SELECT sum(("exifInfo"->>'fileSizeInByte')::bigint) FROM asset WHERE "deletedAt" IS NULL), 1) as pct_of_total FROM asset WHERE "deletedAt" IS NULL GROUP BY format ORDER BY sum(("exifInfo"->>'fileSizeInByte')::bigint) DESC;
Step 3: Identify Top Storage Consumers
Large files (>50MB):
SELECT "id", "originalPath", type, pg_size_pretty(("exifInfo"->>'fileSizeInByte')::bigint) as size, "exifInfo"->>'make' as camera, "localDateTime" FROM asset WHERE "deletedAt" IS NULL AND ("exifInfo"->>'fileSizeInByte')::bigint > 52428800 -- 50MB ORDER BY ("exifInfo"->>'fileSizeInByte')::bigint DESC LIMIT 50;
RAW+JPEG pairs:
-- Find RAW files that have a matching JPEG WITH raws AS ( SELECT "originalFileName", regexp_replace("originalFileName", '\.[^.]+$', '') as base_name, ("exifInfo"->>'fileSizeInByte')::bigint as size FROM asset WHERE "deletedAt" IS NULL AND upper(split_part("originalPath", '.', -1)) IN ('ARW', 'CR2', 'CR3', 'NEF', 'RAF', 'DNG', 'ORF', 'RW2') ), jpegs AS ( SELECT regexp_replace("originalFileName", '\.[^.]+$', '') as base_name FROM asset WHERE "deletedAt" IS NULL AND upper(split_part("originalPath", '.', -1)) IN ('JPG', 'JPEG') ) SELECT count(*) as raw_with_jpeg, pg_size_pretty(sum(r.size)) as raw_size_reclaimable FROM raws r INNER JOIN jpegs j ON r.base_name = j.base_name;
Videos by size tier:
SELECT CASE WHEN ("exifInfo"->>'fileSizeInByte')::bigint > 1073741824 THEN '>1GB' WHEN ("exifInfo"->>'fileSizeInByte')::bigint > 524288000 THEN '500MB-1GB' WHEN ("exifInfo"->>'fileSizeInByte')::bigint > 104857600 THEN '100-500MB' WHEN ("exifInfo"->>'fileSizeInByte')::bigint > 52428800 THEN '50-100MB' ELSE '<50MB' END as size_tier, count(*) as videos, pg_size_pretty(sum(("exifInfo"->>'fileSizeInByte')::bigint)) as total FROM asset WHERE "deletedAt" IS NULL AND type = 'VIDEO' GROUP BY 1 ORDER BY min(("exifInfo"->>'fileSizeInByte')::bigint) DESC;
Step 4: Growth Projection
-- Monthly storage growth SELECT date_trunc('month', "localDateTime") as month, count(*) as new_assets, pg_size_pretty(sum(("exifInfo"->>'fileSizeInByte')::bigint)) as monthly_growth FROM asset WHERE "deletedAt" IS NULL AND "localDateTime" > now() - interval '12 months' GROUP BY 1 ORDER BY 1;
Calculate: at current growth rate, how many months until disk is full?
Step 5: Generate Report
STORAGE ANALYSIS ═══════════════════════════════════════ OVERVIEW Total storage: 182.4 GB Photos: 94.2 GB (39,596 files, avg 2.4 MB) Videos: 85.1 GB (4,983 files, avg 17.1 MB) Trash: 12.3 GB (8,433 files) ← RECLAIMABLE NOW TOP CONSUMERS 1. Videos >100MB: 43 files, 18.2 GB 2. RAW files: 892 files, 22.4 GB (614 have matching JPEGs) 3. HEIC photos: 7,890 files, 31.2 GB 4. Large screenshots: 234 files, 1.8 GB RECLAIMABLE SPACE Empty trash: 12.3 GB Remove RAW where JPEG exists: 15.8 GB (614 RAW files) Remove large screenshots: 1.8 GB (234 files) TOTAL POTENTIAL: 29.9 GB (16.4% of library) GROWTH RATE Last 12 months: 2.1 GB/month average At current rate: ~24 months until 250 GB disk full RECOMMENDATIONS 1. Empty trash immediately → 12.3 GB freed 2. Review RAW+JPEG pairs → keep JPEGs, remove RAWs for 15.8 GB 3. Review 43 videos >100MB — any worth compressing? 4. 234 large screenshots — worth keeping?
Optimization Actions (User-Approved)
- Empty trash → Immich API bulk delete with
force: true - Remove RAW duplicates → Delete RAW where JPEG exists (keep JPEG)
- Flag large videos → List for user review (no auto-action on videos)
- Screenshot removal → Integrate with photo-cleanup skill
NEVER auto-delete. Always present findings and wait for approval.
Important Notes
- Storage calculations use EXIF fileSizeInByte which reflects the original file, not Immich's generated thumbnails/previews
- Immich also stores thumbnails, preview images, and ML embeddings — these are NOT included in the analysis but do consume disk space
- RAW+JPEG detection uses filename matching (base name without extension)
- Video transcoding recommendations depend on the user's quality preferences — always ask
- Growth projection assumes linear growth, which may not hold for seasonal photographers