import httpx import os from datetime import datetime from collections import defaultdict from typing import List, Dict, Any # Get environment variables IMMICH_API_KEY = os.environ.get("IMMICH_API_KEY") IMMICH_SERVER_URL = os.environ.get("IMMICH_SERVER_URL") if not IMMICH_API_KEY or not IMMICH_SERVER_URL: raise ValueError("IMMICH_API_KEY and IMMICH_SERVER_URL environment variables must be set") # Setup HTTP client with auth headers headers = {"Content-Type": "application/json", "Accept": "application/json", "x-api-key": IMMICH_API_KEY} # Global HTTP client client = httpx.Client(headers=headers) def get_albums() -> List[Dict[str, Any]]: """Fetch all albums from Immich API""" response = client.get(f"{IMMICH_SERVER_URL}/api/albums") response.raise_for_status() return response.json() def get_album_assets(album_id: str) -> List[str]: """Fetch all asset IDs from a specific album""" response = client.get(f"{IMMICH_SERVER_URL}/api/albums/{album_id}?withoutAssets=false") response.raise_for_status() album_data = response.json() return [asset["id"] for asset in album_data.get("assets", [])] def add_assets_to_album(album_id: str, asset_ids: List[str]) -> None: """Add assets to an album""" if not asset_ids: return response = client.put(f"{IMMICH_SERVER_URL}/api/albums/{album_id}/assets", json={"ids": asset_ids}) response.raise_for_status() def delete_album(album_id: str) -> None: """Delete an album""" response = client.delete(f"{IMMICH_SERVER_URL}/api/albums/{album_id}") response.raise_for_status() def deduplicate_albums(): """Main function to deduplicate albums""" print("Fetching albums...") albums = get_albums() # Group albums by name (case-insensitive) album_groups = defaultdict(list) for album in albums: album_name = album["albumName"].lower().strip() album_groups[album_name].append(album) # Process groups with duplicates for album_name, album_list in album_groups.items(): if len(album_list) <= 1: continue # Skip groups with only one album print(f"\nProcessing duplicate albums for: '{album_name}' ({len(album_list)} albums)") # Sort by creation date to find the oldest album_list.sort(key=lambda x: datetime.fromisoformat(x["createdAt"].replace("Z", "+00:00"))) oldest_album = album_list[0] duplicate_albums = album_list[1:] print(f" Oldest album: {oldest_album['id']} (created: {oldest_album['createdAt']})") # Collect assets from duplicate albums all_asset_ids = [] for dup_album in duplicate_albums: print(f" Processing duplicate: {dup_album['id']} (created: {dup_album['createdAt']})") asset_ids = get_album_assets(dup_album["id"]) all_asset_ids.extend(asset_ids) print(f" Found {len(asset_ids)} assets") # Add assets to oldest album if all_asset_ids: print(f" Adding {len(all_asset_ids)} assets to oldest album...") add_assets_to_album(oldest_album["id"], all_asset_ids) # Delete duplicate albums for dup_album in duplicate_albums: print(f" Deleting duplicate album: {dup_album['id']}") delete_album(dup_album["id"]) print(f" āœ“ Merged {len(duplicate_albums)} duplicate albums into {oldest_album['id']}") if __name__ == "__main__": try: deduplicate_albums() print("\nāœ“ Album deduplication completed successfully!") except Exception as e: print(f"\nāœ— Error: {e}") raise finally: client.close()