/** * Server-only Immich API client. * Inspired by immich-public-proxy's approach but uses API key auth * to access specific albums rather than shared link keys. * * API key never leaves the server — all client-facing image * requests go through our proxy route. */ import { getConfig, slugify, type SubpageConfig } from './config'; import { cache } from './cache'; // ── Types ────────────────────────────────────────────────────── export interface ImmichAlbum { id: string; slug: string; albumName: string; description: string; albumThumbnailAssetId: string | null; assetCount: number; assets: ImmichAsset[]; createdAt: string; updatedAt: string; order: 'asc' | 'desc'; } export interface ImmichAsset { id: string; type: 'IMAGE' | 'VIDEO'; originalFileName: string; originalMimeType: string; thumbhash: string | null; fileCreatedAt: string; exifInfo?: ImmichExifInfo; isTrashed: boolean; } export interface ImmichExifInfo { make: string | null; model: string | null; lensModel: string | null; focalLength: number | null; fNumber: number | null; exposureTime: string | null; iso: number | null; exifImageWidth: number | null; exifImageHeight: number | null; latitude: number | null; longitude: number | null; city: string | null; state: string | null; country: string | null; dateTimeOriginal: string | null; description: string | null; } export type ImageSize = 'thumbnail' | 'preview' | 'original'; /** Enriched subpage with album metadata (for rendering cards). */ export interface SubpageSummary { name: string; slug: string; albumCount: number; totalAssetCount: number; coverAssetId: string | null; } // Note: Album summary logging state is kept in the client instance. // ── API Client ───────────────────────────────────────────────── class ImmichClient { private hasLoggedAlbums = false; private pendingAlbumsPromise: Promise | null = null; private pendingAlbumPromises = new Map>(); private pendingAssetPromises = new Map>(); private get config() { return getConfig(); } private async request(endpoint: string): Promise { if (this.config.needsSetup) return null; const url = `${this.config.immich.apiUrl}${endpoint}`; try { const res = await fetch(url, { headers: { 'x-api-key': this.config.immich.apiKey, Accept: 'application/json', }, }); if (!res.ok) { console.error(`[Immich] ${res.status} ${res.statusText} for ${endpoint}`); return null; } const contentType = res.headers.get('Content-Type') || ''; if (contentType.includes('application/json')) { return (await res.json()) as T; } return null; } catch (error) { console.error(`[Immich] Failed to reach ${url}:`, error); return null; } } /** * Stream a video from Immich with optional Range header forwarding. * Range support is required for `