diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02c4e51..bfc3c26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,3 +41,4 @@ jobs: # Minimal env vars so Next.js build doesn't bail early on env validation IMMICH_URL: http://localhost:2283 IMMICH_API_KEY: placeholder-key-for-ci + AUTH_SECRET: placeholder-secret-for-ci-builds-must-be-32-chars-long diff --git a/lib/thumbhash.ts b/lib/thumbhash.ts index 03c17cb..20a52b1 100644 --- a/lib/thumbhash.ts +++ b/lib/thumbhash.ts @@ -20,6 +20,18 @@ function setCache(map: Map, key: K, value: V) { map.set(key, value); } +/** + * Fast base64 to Uint8Array decoder. + * Uses native Buffer in Node.js/Edge environments (~10x faster) + * and falls back to atob for browser client. + */ +function decodeBase64(base64: string): Uint8Array { + if (typeof Buffer !== 'undefined') { + return new Uint8Array(Buffer.from(base64, 'base64')); + } + return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); +} + /** * Convert an Immich base64-encoded ThumbHash into a tiny data URL * suitable for next/image's blurDataURL prop. @@ -28,7 +40,7 @@ export function thumbHashToBlurDataUrl(base64: string): string { const cached = blurDataUrlCache.get(base64); if (cached) return cached; - const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); + const bytes = decodeBase64(base64); const url = thumbHashToDataURL(bytes); setCache(blurDataUrlCache, base64, url); return url; @@ -42,7 +54,7 @@ export function thumbHashToDominantHex(base64: string): string { const cached = dominantHexCache.get(base64); if (cached) return cached; - const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); + const bytes = decodeBase64(base64); const { w, h, rgba } = thumbHashToRGBA(bytes); let r = 0, diff --git a/scripts/screenshots.ts b/scripts/screenshots.ts index 4a0ee31..fd7dae9 100644 --- a/scripts/screenshots.ts +++ b/scripts/screenshots.ts @@ -35,7 +35,7 @@ async function main() { const originalYaml = fs.readFileSync(SETTINGS_YAML, 'utf8'); // Dynamic import for Playwright (may not be installed globally) - const { chromium } = await import('playwright'); + const { chromium } = await import('@playwright/test'); const browser = await chromium.launch(); try { @@ -226,7 +226,7 @@ async function waitForServer(url: string, timeoutMs: number): Promise { * Wait for all elements to finish loading their full-resolution images. * This prevents capturing blurhash placeholders instead of real photos. */ -async function waitForImages(page: import('playwright').Page): Promise { +async function waitForImages(page: import('@playwright/test').Page): Promise { await page.waitForTimeout(1000); // Initial settle await page.evaluate((timeout) => {