---
name: photo-search
description: >
  Search and explore an Immich photo library using natural language, GPS locations,
  dates, people, cameras, and AI-powered visual search (CLIP).
  Use when the user says "find photos of", "search my photos", "show me pictures from",
  "where are my photos of", "do I have photos of", "find all screenshots",
  "photos taken with", "photos from 2019", "photos near", "photos of [person]",
  or any variation of searching, browsing, or exploring their photo library.
version: 1.2.0
---

# Photo Search

## Connection Required — ALWAYS CHECK FIRST

**Before doing ANYTHING else in this skill, call `ping` on the Immich MCP server.**

- If `ping` succeeds -> proceed with the skill normally.
- If `ping` fails or the MCP tools are not available -> **STOP. Do not continue.** Tell the user:

> 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:
> 1. Your Immich server URL (e.g., `http://192.168.1.100:2283`)
> 2. An Immich API key ([how to create one](https://immich.app/docs/features/command-line-interface#obtain-the-api-key))
> 3. The MCP server configured (see **/setup-immich-photo-manager**)

**Do NOT skip this check. Do NOT try to run any other tool first. Always ping, always block if it fails.**

---

## CRITICAL RULE: NEVER CREATE TEMPORARY ALBUMS

**This is the most important rule of this skill. NEVER create albums as part of a search workflow.**

The user has a curated library of real albums. Creating temporary albums pollutes their library with junk. Instead:

- If photos belong to a real album -> use that real album directly
- If photos are NOT in any album -> show them directly using `get_thumbnails_batch`
- **NEVER call `create_album` from this skill. Not for "temp" albums, not for "search results", not for any reason.**

---

## Search Workflow (Step by Step)

### Step 1: Parse user intent

Identify what the user is looking for. Determine which search dimensions apply.

### Step 2: Search for matching photos

Use `search_metadata` and/or `search_smart` to find matching assets.

**IMPORTANT — Immich EXIF location quirks:**
- Immich stores cities as **municipalities**, not tourist names. "Tikal" does not exist as a city — it's in the municipality of **"Flores"** (state: **"Petén"**, country: **"Guatemala"**).
- "Lanzarote" does not exist as a city — look for municipalities like "Arrecife", "Yaiza", "Teguise", "Haría", etc. (state: **"Canary Islands"**, country: **"Spain"**).
- When a place-name search returns 0 results, try broader terms: search by **state** or **country** instead of city, then filter. Or use `search_smart` with the place name as a CLIP query.

Search strategy priority:
1. `search_metadata(city=...)` — fastest, most precise if the city name matches
2. `search_metadata(state=...)` or `search_metadata(country=...)` — broader, catches municipalities
3. `search_smart(query="...")` — AI/CLIP semantic search, catches things without GPS

### Step 3: Find REAL matching albums

Call `list_albums()` and **fuzzy-match** album names/descriptions against the user's query.

Examples:
- User asks "photos of Tikal" -> match album "Tikal & Petén" (and possibly "Guatemala")
- User asks "fotos de la Barceloneta" -> match album "Barcelona — Barceloneta / Playa"
- User asks "Valle del Jerte" -> match album "Valle del Jerte & Hervás"
- User asks "Lanzarote" -> match albums containing "Lanzarote" in their name

Matching rules:
- Case-insensitive substring match on album name
- Also check album descriptions
- The query terms can appear anywhere in the album name (e.g., "Tikal" matches "Tikal & Petén")
- Include albums for broader regions if relevant (e.g., for "Tikal" also include "Guatemala")

### Step 4: Collect asset metadata for the gallery

**Two paths depending on whether a real album was found:**

#### Path A — Real album found (preferred)
Use `get_album(album_id)` to get the full asset list. Extract `id`, `originalFileName`, and `fileCreatedAt` from each asset.
This is the best path because the user curated these albums intentionally.

#### Path B — No matching album (orphan photos)
Use the search results directly — they already contain `id`, `originalFileName`, and `fileCreatedAt` for each asset.
**Do NOT create an album. Just show the photos.**

**Fetch thumbnails as base64** using `get_thumbnails_batch(asset_ids=[...], size="thumbnail", limit=50)`. The Cowork viewer runs in an `about:` sandbox that blocks ALL external network requests, so thumbnails MUST be embedded as base64 `data:` URIs.

### Step 5: Build and present the HTML gallery

1. Read the template: `assets/viewer-template.html` from the plugin root
2. Replace all `{{PLACEHOLDERS}}` with actual data
3. Write the HTML to the outputs directory
4. Present via `computer://` link

**For Related Albums ({{ALBUMS_JSON}}):**
- Include ONLY real albums found in Step 3
- If no real albums matched, use an empty array `[]`
- NEVER fabricate album entries

---

## Search Capabilities

| Dimension | MCP Tool / Parameter | Example |
|-----------|---------------------|---------|
| **Visual/semantic** | `search_smart(query=...)` | "sunset at the beach", "birthday cake" |
| **Location (text)** | `search_metadata(city=..., state=..., country=...)` | city="Barcelona" |
| **Date range** | `search_metadata(taken_after=..., taken_before=...)` | 2023-06-01 to 2023-06-30 |
| **Camera/device** | `search_metadata(make=..., model=...)` | make="Apple", model="iPhone 14 Pro" |
| **File type** | `search_metadata(asset_type=...)` | "IMAGE" or "VIDEO" |
| **Favorites** | `search_metadata(is_favorite=true)` | true |

## Query Translation

| User says | Search strategy |
|-----------|----------------|
| "photos from my Italy trip" | `search_metadata(country="Italy")` + `list_albums()` to find Italy albums |
| "sunset photos" | `search_smart(query="sunset")` |
| "photos from last Christmas" | `search_metadata(taken_after="2025-12-20", taken_before="2025-12-31")` |
| "my best photos" | `search_metadata(is_favorite=true)` |
| "photos taken with iPhone" | `search_metadata(make="Apple")` |
| "videos from Barcelona" | `search_metadata(city="Barcelona", asset_type="VIDEO")` |
| "show me Tikal" | `search_metadata(state="Petén")` + `search_smart(query="Tikal")` + match album "Tikal & Petén" |

---

## Gallery HTML Generation

### Template

Use the canonical template at `assets/viewer-template.html`. Read the template file, replace `{{PLACEHOLDERS}}` with actual data, and write the result.

### Placeholder Rules

- **`{{PAGE_SIZE}}`**, **`{{PHOTO_COUNT}}`**, **`{{ALBUM_TOTAL}}`**: Should be plain integers (e.g. `20`). The template uses `parseInt()` with fallbacks, so non-numeric values degrade gracefully (PAGE_SIZE defaults to 6, others to 0).
- **`{{ALBUM_NAME}}`**: Can contain any characters including apostrophes (e.g. "L'Hospitalet"). Safe in HTML contexts. The JS alt-text reads from `document.title` instead of re-injecting this placeholder, so apostrophes won't break JS.
- **`{{SEARCH_QUERY}}`**, **`{{IMMICH_URL}}`**: Can be any string.
- **`{{PHOTO_ENTRIES}}`**: Must be valid JS object literals, comma-separated.
- **`{{ALBUMS_JSON}}`**: JSON album objects. The template wraps them in `[...].flat()`, so you can pass any of these formats:
  - Comma-separated objects: `{"id":"abc","name":"X","total":50},{"id":"def","name":"Y","total":30}`
  - A JSON array: `[{"id":"abc","name":"X","total":50}]`
  - Empty string (no albums): the template produces `[].flat()` → `[]` and hides the section

### Thumbnail Delivery Strategies

There are three strategies for delivering thumbnails to the gallery viewer, with different trade-offs. The strategy used depends on the user's Immich setup and the viewing context.

#### Strategy 1: Base64 Embedded (Default — Always Works)

The Cowork viewer runs in an `about:` protocol sandbox that blocks ALL external network requests (`fetch`, `<img src>`, etc.). Therefore, the **default and always-safe strategy** is to embed thumbnails as base64 `data:` URIs directly in the HTML.

Each photo entry in `{{PHOTO_ENTRIES}}` includes the full thumbnail data:

```javascript
{src:'data:image/jpeg;base64,/9j/4AAQ...',id:'<asset-id>',name:'<filename>',date:'<ISO-date>'}
```

- `src`: Base64 data URI of the thumbnail (from `get_thumbnails_batch`, size=thumbnail, ~250px, ~15-25KB each)
- `id`: The Immich asset ID (for linking to Immich web UI)
- `name`: Original filename (displayed as label)
- `date`: ISO date string from the asset metadata

**Always use `size="thumbnail"` (250px)** — never `preview` (1440px). Thumbnails average ~18KB each, so 50 photos ≈ 0.9MB HTML file.

**How to get thumbnails:** Call `get_thumbnails_batch(asset_ids=[...], size="thumbnail", limit=50)`. If more than 50 photos, call in batches of 50.

**Limitations:** HTML file size grows linearly with photo count (~18KB per photo). Not ideal for albums with hundreds of photos. Maximum practical limit is ~50 thumbnails per gallery file.

#### CORS-Enabled Direct URLs (Optional — Requires User Setup)

If the user has configured CORS headers on their Immich reverse proxy, the gallery HTML can use JavaScript `fetch()` with the `x-api-key` header to load thumbnails on demand, converting responses to blob URLs. This enables full URL-based delivery for **any** photos, not just albums.

**This requires the user to configure their reverse proxy** (Nginx, Caddy, Traefik, etc.) to return CORS headers. See the **Post-Install: CORS Configuration** section in `/setup-immich-photo-manager` for instructions.

**Advantages:** Works for any photos (albums or search results), tiny HTML file, true on-demand pagination, no shared links needed.

**Limitations:** Requires CORS configuration on the server side. Not available out of the box.

#### Strategy Decision Flow

```
Always use Base64 Embedded (Strategy 1) as the default:
  ├─ ≤50 photos → Embed all thumbnails as base64
  └─ >50 photos → Embed first 50 in batches, warn user about file size and total count
```

**Note:** CORS-enabled direct URLs (Strategy 3) are an opt-in enhancement for users who open galleries in a regular browser. They do NOT work inside the Cowork sandbox. Never mention strategy numbers or internal labels in user-facing output — just generate the gallery silently using the correct approach.

### Template lazy loading

The first page (PAGE_SIZE photos) loads immediately. Subsequent pages use IntersectionObserver to set `src` from `dataset.src` only when scrolled into view. Pagination is manual via "Load more" button (no infinite scroll). This works with both base64 and URL-based strategies.

### Albums JSON Format

`{{ALBUMS_JSON}}` — a JSON array of REAL albums:

```
{"id":"abc123","name":"Tikal & Petén","total":169},{"id":"xyz789","name":"Guatemala","total":392}
```

Comma-separated JSON objects — NO outer array brackets (the template adds `[...]`). If no real albums match, use empty string.

### Generation Workflow (Concrete Example)

```
User: "show me photos of Tikal"

1. ping() -> OK
2. search_metadata(state="Petén", country="Guatemala") -> found 200+ assets
3. list_albums() -> scan names -> found "Tikal & Petén" (id: d6dd63d0, 169 photos), "Guatemala" (id: 8dde4bb1, 392 photos)
4. get_album(album_id="d6dd63d0") -> get asset list with IDs, names, dates
5. get_thumbnails_batch(asset_ids=[...], size="thumbnail", limit=50) -> base64 JPEG data for first 50 photos
6. Read assets/viewer-template.html
7. Replace placeholders:
   - {{ALBUM_NAME}} -> "Tikal & Petén"
   - {{ALBUM_TOTAL}} -> 169
   - {{SEARCH_QUERY}} -> "Tikal"
   - {{IMMICH_URL}} -> "https://your-immich-server.com"
   - {{PAGE_SIZE}} -> 20
   - {{PHOTO_COUNT}} -> 50 (limited to 50 for file size)
   - {{PHOTO_ENTRIES}} -> {src:'data:image/jpeg;base64,...',id:"abc",name:"IMG_001",date:"2023-06-15"}, ...
   - {{ALBUMS_JSON}} -> {"id":"d6dd63d0","name":"Tikal & Petén","total":169},{"id":"8dde4bb1","name":"Guatemala","total":392}
8. Write tikal.html to outputs (~0.9MB with 50 base64 thumbnails, total album has 169 photos)
9. Present computer:// link
```

### When photos are NOT in any album

```
User: "show me sunset photos"

1. ping() -> OK
2. search_smart(query="sunset") -> found 35 assets (each has id, originalFileName, fileCreatedAt)
3. list_albums() -> no album name matches "sunset"
4. Read template
5. Replace placeholders:
   - {{ALBUM_NAME}} -> "Sunset Photos"
   - {{ALBUM_TOTAL}} -> 35
   - {{PHOTO_COUNT}} -> 35
   - {{PHOTO_ENTRIES}} -> entries built from get_thumbnails_batch (base64 src + id + name + date)
   - {{ALBUMS_JSON}} ->     <-- empty string, no real albums match
6. Write sunset-photos.html to outputs (~0.7MB with base64 thumbnails)
7. Present computer:// link
```

---

## Result Presentation

When showing search results:

- **Count first**: "Found 147 photos matching your search"
- **Real albums**: "These photos are in your album 'Tikal & Petén' (169 photos)"
- **Date range**: "Spanning from June 2019 to June 2023"
- **Visual preview**: Always generate the HTML gallery viewer
- **Action prompt**: Suggest next steps (see more photos, explore related albums, etc.)

## Pagination

Immich API returns paginated results. For large result sets:
- Fetch first page to get total count
- Report total to user before fetching all pages
- For browsing, show first page thumbnails and offer to load more

## Advanced Search Patterns

### Finding screenshots
No GPS data + screen-resolution dimensions + no lens/focal length EXIF.

### Finding duplicates
Same date range across import sources. Compare by exact hash, timestamp + dimensions, or CLIP similarity.
