Skip to content

Instantly share code, notes, and snippets.

@sneumann
Last active January 13, 2026 21:31
Show Gist options
  • Select an option

  • Save sneumann/dbb1de943cd28fb52c8411f027e063b2 to your computer and use it in GitHub Desktop.

Select an option

Save sneumann/dbb1de943cd28fb52c8411f027e063b2 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
"""
immich_external_album.py
Add all external library assets in Immich to a single album.
License: CC0
Features:
- Dry-run mode via --dry-run (no changes made)
- Diagnostic output: counts, sample asset IDs
- Pagination-safe for large libraries
"""
import requests
import argparse
import sys
# ==============================
# CONFIGURATION
# ==============================
IMMICH_URL = "http://immich.athome:2283"
API_KEY = "T80.....................................OM"
ALBUM_NAME = "External Library Name"
PAGE_SIZE = 1000 # Number of assets per page
# ==============================
# ARGPARSE
# ==============================
parser = argparse.ArgumentParser(description="Add all external assets to an Immich album")
parser.add_argument("--dry-run", action="store_true", help="Do not make changes, just show what would happen")
args = parser.parse_args()
DRY_RUN = args.dry_run
HEADERS = {
"x-api-key": API_KEY,
"Content-Type": "application/json",
}
# ==============================
# FUNCTIONS
# ==============================
def get_all_external_assets():
"""Return list of all asset IDs from external library"""
assets = []
page = 1
print("[INFO] Fetching external library assets...")
while True:
r = requests.post(
f"{IMMICH_URL}/api/search/metadata",
headers=HEADERS,
json={
"page": page,
"size": PAGE_SIZE,
"isExternal": True
},
)
if r.status_code != 200:
print(f"[ERROR] Failed to fetch assets (status {r.status_code}): {r.text}")
sys.exit(1)
data = r.json()
items = data.get("assets", {}).get("items", [])
assets.extend(items)
print(f"[DEBUG] Page {page}: {len(items)} assets fetched, total so far: {len(assets)}")
if len(items) < PAGE_SIZE:
break
page += 1
asset_ids = [a["id"] for a in assets]
print(f"[INFO] Total external assets found: {len(asset_ids)}")
if asset_ids:
print(f"[INFO] Sample asset IDs: {asset_ids[:10]}")
return asset_ids
def get_or_create_album():
"""Return album ID; create album if missing (unless dry-run)"""
r = requests.get(f"{IMMICH_URL}/api/albums", headers=HEADERS)
if r.status_code != 200:
print(f"[ERROR] Failed to fetch albums: {r.status_code} {r.text}")
sys.exit(1)
albums = r.json()
for album in albums:
if album["albumName"] == ALBUM_NAME:
print(f"[INFO] Album already exists: {ALBUM_NAME} (ID {album['id']})")
return album["id"]
if DRY_RUN:
print(f"[DRY-RUN] Would create album: {ALBUM_NAME}")
return None
r = requests.post(
f"{IMMICH_URL}/api/albums",
headers=HEADERS,
json={"albumName": ALBUM_NAME},
)
if r.status_code != 200:
print(f"[ERROR] Failed to create album: {r.status_code} {r.text}")
sys.exit(1)
album_id = r.json()["id"]
print(f"[INFO] Created album: {ALBUM_NAME} (ID {album_id})")
return album_id
def add_assets_to_album(album_id, asset_ids):
"""Add asset IDs to album"""
if DRY_RUN:
print(f"[DRY-RUN] Would add {len(asset_ids)} assets to album")
if asset_ids:
print(f"[DRY-RUN] Sample asset IDs: {asset_ids[:10]}")
return
if not asset_ids:
print("[INFO] No assets to add")
return
r = requests.put(
f"{IMMICH_URL}/api/albums/{album_id}/assets",
headers=HEADERS,
json={"ids": asset_ids},
)
if r.status_code != 200:
print(f"[ERROR] Failed to add assets to album: {r.status_code} {r.text}")
sys.exit(1)
print(f"[INFO] Added {len(asset_ids)} assets to album {ALBUM_NAME} (ID {album_id})")
# ==============================
# MAIN
# ==============================
if __name__ == "__main__":
print(f"[INFO] Starting {'DRY-RUN' if DRY_RUN else 'LIVE'} mode...")
asset_ids = get_all_external_assets()
album_id = get_or_create_album()
add_assets_to_album(album_id, asset_ids)
print(f"[INFO] Script completed in {'DRY-RUN' if DRY_RUN else 'LIVE'} mode")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment