Skill: ninjazan420/immich-api (version 1.0.0)

--- name: immich-api description: | Immich Photo Management API Bridge. Use for interacting with self-hosted Immich instances via REST API. Triggers when: (1) User wants to manage photos, albums, or assets in Immich, (2) Building automation around photo backups, (3) Searching or organizing media, (4) User management in Immich, (5) Upload/download photos via API, (6) Getting album or library information, (7) Working with Immich jobs/queues, (8) Any Immich-related photo tasks --- # Immich API Bridge ## Configuration ### Option 1: Environment Variables **Windows (PowerShell):** ```powershell $env:IMMICH_URL = "https://your-immich-instance.com" $env:IMMICH_API_KEY = "your-api-key-here" ``` **Linux/macOS (bash):** ```bash export IMMICH_URL="https://your-immich-instance.com" export IMMICH_API_KEY="your-api-key-here" ``` **Generate API Key in Immich:** User Profile → API Keys → Create API Key ### Option 2: CLI Arguments ```bash python scripts/upload_photos.py --url "https://your-immich.com" --api-key "your-key" --folder ./photos python scripts/download_album.py --url "https://your-immich.com" --api-key "your-key" --album-id "abc123" --output ./downloads ``` ## Quick Start ### Authentication ```bash # Test connection curl -H "x-[REDACTED_SECRET]" "$IMMICH_URL/api/server-info/ping" ``` ### Base URL ``` {IMMICH_URL}/api ``` ## Common Operations ### Albums ```bash # List albums curl -H "x-[REDACTED_SECRET]" "$IMMICH_URL/api/albums" # Create album curl -X POST -H "x-[REDACTED_SECRET]" -H "Content-Type: application/json" \ -d '{"albumName":"My Album"}' "$IMMICH_URL/api/albums" # Get album assets curl -H "x-[REDACTED_SECRET]" "$IMMICH_URL/api/albums/{albumId}" ``` ### Assets ```bash # Get all assets (paginated) curl -H "x-[REDACTED_SECRET]" "$IMMICH_URL/api/assets?limit=100" # Upload asset curl -X POST -H "x-[REDACTED_SECRET]" \ -F "file=@/path/to/photo.jpg" \ "$IMMICH_URL/api/assets" # Get thumbnail curl -H "x-[REDACTED_SECRET]" \ "$IMMICH_URL/api/assets/{assetId}/thumbnail?format=jpeg" -o thumb.jpg # Get original file curl -H "x-[REDACTED_SECRET]" \ "$IMMICH_URL/api/assets/{assetId}/original" -o original.jpg ``` ### Users ```bash # List users curl -H "x-[REDACTED_SECRET]" "$IMMICH_URL/api/users" # Get current user curl -H "x-[REDACTED_SECRET]" "$IMMICH_URL/api/user/me" ``` ### Search ```bash # Search assets curl -X POST -H "x-[REDACTED_SECRET]" -H "Content-Type: application/json" \ -d '{"query":"beach","take":10}' "$IMMICH_URL/api/search/assets" ``` ### Libraries ```bash # List libraries curl -H "x-[REDACTED_SECRET]" "$IMMICH_URL/api/libraries" # Scan library curl -X POST -H "x-[REDACTED_SECRET]" \ "$IMMICH_URL/api/libraries/{libraryId}/scan" ``` ### Jobs ```bash # Get job statuses curl -H "x-[REDACTED_SECRET]" "$IMMICH_URL/api/jobs" # Trigger job (e.g., thumbnail generation) curl -X POST -H "x-[REDACTED_SECRET]" -H "Content-Type: application/json" \ -d '{"jobName":"thumbnail-generation","force":true}' "$IMMICH_URL/api/jobs" ``` ## Script Usage Use the bundled scripts in `scripts/` for complex operations: - `upload_photos.py` - Bulk upload photos - `download_album.py` - Download entire album - `sync_library.py` - Sync external library See `references/api-endpoints.md` for complete endpoint reference.

path: references/api-endpoints.md
content: # Immich API Endpoints Reference

## Base URL
```
{IMMICH_URL}/api
```

## Authentication
All requests require header: `x-[REDACTED_SECRET]`

## Albums
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/albums` | List all albums |
| POST | `/albums` | Create new album |
| GET | `/albums/{id}` | Get album with assets |
| PUT | `/albums/{id}` | Update album |
| DELETE | `/albums/{id}` | Delete album |
| PUT | `/albums/{id}/assets` | Add assets to album |
| DELETE | `/albums/{id}/assets` | Remove assets from album |
| GET | `/albums/{id}/cover-image` | Get album cover |
| PUT | `/albums/{id}/cover-image` | Set album cover |

## Assets
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/assets` | List assets (paginated) |
| POST | `/assets` | Upload new asset |
| GET | `/assets/{id}` | Get asset info |
| PUT | `/assets/{id}` | Update asset metadata |
| DELETE | `/assets/{id}` | Delete asset |
| GET | `/assets/{id}/thumbnail` | Get thumbnail (format=jpeg/webp) |
| GET | `/assets/{id}/original` | Download original file |
| GET | `/assets/{id}/video/playback` | Get video playback info |
| PUT | `/assets/{id}/restore` | Restore from trash |
| PUT | `/assets/{id}/archive` | Archive asset |
| PUT | `/assets/{id}/favorite` | Favorite asset |

## Users
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/users` | List all users |
| POST | `/users` | Create new user |
| GET | `/users/me` | Get current user |
| GET | `/users/{id}` | Get user by ID |
| PUT | `/users/{id}` | Update user |
| DELETE | `/users/{id}` | Delete user |
| PUT | `/users/{id}/profile-image` | Set profile image |

## Libraries (External)
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/libraries` | List libraries |
| POST | `/libraries` | Create library |
| GET | `/libraries/{id}` | Get library details |
| PUT | `/libraries/{id}` | Update library |
| DELETE | `/libraries/{id}` | Delete library |
| POST | `/libraries/{id}/scan` | Scan library |
| POST | `/libraries/{id}/remove-offline` | Remove offline files |

## Search
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/search/assets` | Search assets |
| GET | `/search/metadata` | Get searchable fields |

## Jobs
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/jobs` | Get all job statuses |
| POST | `/jobs` | Start a job |
| DELETE | `/jobs/{id}` | Cancel a job |

Job names: `thumbnail-generation`, `video-transcoding`, `metadata-extraction`, `face-detection`, `search-indexing`

## Server
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/server-info/ping` | Health check |
| GET | `/server-info/stats` | Get server statistics |
| GET | `/server-info/features` | Get feature flags |

## Tags
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/tags` | List all tags |
| POST | `/tags` | Create tag |
| PUT | `/tags/{id}` | Update tag |
| DELETE | `/tags/{id}` | Delete tag |

## Shared Links
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/shared-links` | List shared links |
| POST | `/shared-links` | Create shared link |
| PUT | `/shared-links/{id}` | Update shared link |
| DELETE | `/shared-links/{id}` | Delete shared link |

## Activity
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/activities` | List activities |
| POST | `/activities` | Create activity |
| DELETE | `/activities/{id}` | Delete activity |

## Download
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/download/info` | Get download archive info |
| GET | `/download/archive` | Download full archive |

## Key: API Key Generation
1. Log into Immich web UI
2. Go to your Profile (top right)
3. Scroll to "API Keys"
4. Click "Create API Key"
5. Copy the key (shown once!)


path: scripts/download_album.py
content: """
Immich API Client - Download Album
Usage: python download_album.py --url URL --api-key KEY --album-id ID --output ./downloads
"""
import argparse
import os
import requests
import sys
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed

def download_file(url: str, api_key: str, output_path: Path, session: requests.Session):
    """Download a single file."""
    headers = {"x-api-key": api_key}
    try:
        resp = session.get(url, headers=headers, stream=True)
        if resp.status_code == 200:
            with open(output_path, "wb") as f:
                for chunk in resp.iter_content(chunk_size=8192):
                    f.write(chunk)
            return True
    except Exception as e:
        print(f"Error downloading {url}: {e}")
    return False

def download_album(url: str, api_key: str, album_id: str, output_dir: str, original: bool = False):
    """Download all assets from an album."""
    headers = {"x-api-key": api_key}
    base_url = url.rstrip("/")
    session = requests.Session()
    
    # Get album info
    resp = session.get(f"{base_url}/api/albums/{album_id}", headers=headers)
    if resp.status_code != 200:
        print(f"Album not found: {album_id}")
        return
        
    album = resp.json()
    album_name = album.get("albumName", "unknown")
    assets = album.get("assets", [])
    
    print(f"Album: {album_name}")
    print(f"Assets: {len(assets)}")
    
    # Create output directory
    output_path = Path(output_dir) / album_name
    output_path.mkdir(parents=True, exist_ok=True)
    
    # Determine endpoint
    endpoint = "original" if original else "thumbnail"
    
    # Download assets
    downloaded = 0
    with ThreadPoolExecutor(max_workers=5) as executor:
        futures = {}
        
        for asset in assets:
            asset_id = asset.get("id")
            if not asset_id:
                continue
                
            # Get original filename or use asset ID
            filename = asset.get("originalFileName", f"{asset_id}.jpg")
            file_path = output_path / filename
            
            if file_path.exists():
                print(f"✓ {filename} (already exists)")
                continue
                
            # Build download URL
            if original:
                download_url = f"{base_url}/api/assets/{asset_id}/original"
            else:
                download_url = f"{base_url}/api/assets/{asset_id}/thumbnail?format=jpeg"
            
            future = executor.submit(download_file, download_url, api_key, file_path, session)
            futures[future] = filename
            
        for future in as_completed(futures):
            filename = futures[future]
            if future.result():
                downloaded += 1
                print(f"✓ {filename}")
            else:
                print(f"✗ {filename}")
    
    print(f"\nDownloaded {downloaded}/{len(assets)} files to {output_path}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Download album from Immich")
    parser.add_argument("--url", required=True, help="Immich URL")
    parser.add_argument("--api-key", required=True, help="API Key")
    parser.add_argument("--album-id", required=True, help="Album ID")
    parser.add_argument("--output", default="./downloads", help="Output directory")
    parser.add_argument("--original", action="store_true", help="Download original files (larger)")
    args = parser.parse_args()
    
    download_album(args.url, args.api_key, args.album_id, args.output, args.original)


path: scripts/upload_photos.py
content: """
Immich API Client - Upload Photos
Usage: python upload_photos.py --url URL --api-key KEY --album "Album Name" --folder /path/to/photos
"""
import argparse
import os
import requests
import sys
from pathlib import Path

def upload_photos(url: str, api_key: str, folder: str, album_id: str = None):
    """Upload all photos from a folder to Immich."""
    headers = {"x-api-key": api_key}
    base_url = url.rstrip("/")
    
    # Get or create album
    if album_id:
        album = {"albumId": album_id}
    else:
        # List albums or create new one
        album_name = Path(folder).name
        resp = requests.get(f"{base_url}/api/albums", headers=headers)
        albums = resp.json()
        album = next((a for a in albums if a.get("albumName") == album_name), None)
        
        if not album:
            resp = requests.post(f"{base_url}/api/albums", 
                                headers=headers,
                                json={"albumName": album_name})
            album = resp.json()
            print(f"Created album: {album['id']}")
        else:
            album = album[0] if album else None
            
    album_id = album.get("id") if album else None
    
    # Upload files
    folder_path = Path(folder)
    image_extensions = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".heic", ".mp4", ".mov"}
    
    files = list(folder_path.glob("*.*"))
    files = [f for f in files if f.suffix.lower() in image_extensions]
    
    print(f"Found {len(files)} media files to upload")
    
    uploaded = 0
    for file_path in files:
        try:
            with open(file_path, "rb") as f:
                files_data = {"file": (file_path.name, f)}
                data = {}
                if album_id:
                    data["albumId"] = album_id
                    
                resp = requests.post(f"{base_url}/api/assets", 
                                   headers=headers,
                                   files=files_data,
                                   data=data)
                
                if resp.status_code in (200, 201):
                    uploaded += 1
                    print(f"✓ {file_path.name}")
                else:
                    print(f"✗ {file_path.name}: {resp.status_code}")
        except Exception as e:
            print(f"✗ {file_path.name}: {e}")
            
    print(f"\nUploaded {uploaded}/{len(files)} files")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Upload photos to Immich")
    parser.add_argument("--url", required=True, help="Immich URL")
    parser.add_argument("--api-key", required=True, help="API Key")
    parser.add_argument("--folder", required=True, help="Folder with photos")
    parser.add_argument("--album-id", help="Album ID (optional)")
    args = parser.parse_args()
    
    upload_photos(args.url, args.api_key, args.folder, args.album_id)


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (MEDIUM)