/** * Photos module — community photo commons powered by Immich. * * Provides a gallery UI within the rSpace shell that connects to * the Immich instance at {space}.rphotos.online. Proxies API requests * for albums and thumbnails to avoid CORS issues. */ import { Hono } from "hono"; import { renderShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; import { renderLanding } from "./landing"; const routes = new Hono(); const IMMICH_BASE = process.env.RPHOTOS_IMMICH_URL || "http://localhost:2284"; const IMMICH_API_KEY = process.env.RPHOTOS_API_KEY || ""; // ── Proxy: list shared albums ── routes.get("/api/albums", async (c) => { try { const res = await fetch(`${IMMICH_BASE}/api/albums?shared=true`, { headers: { "x-api-key": IMMICH_API_KEY }, }); if (!res.ok) return c.json({ albums: [] }); const albums = await res.json(); return c.json({ albums }); } catch { return c.json({ albums: [] }); } }); // ── Proxy: album detail with assets ── routes.get("/api/albums/:id", async (c) => { const id = c.req.param("id"); try { const res = await fetch(`${IMMICH_BASE}/api/albums/${id}`, { headers: { "x-api-key": IMMICH_API_KEY }, }); if (!res.ok) return c.json({ error: "Album not found" }, 404); return c.json(await res.json()); } catch { return c.json({ error: "Failed to load album" }, 500); } }); // ── Proxy: recent assets ── routes.get("/api/assets", async (c) => { const size = c.req.query("size") || "50"; try { const res = await fetch(`${IMMICH_BASE}/api/search/metadata`, { method: "POST", headers: { "x-api-key": IMMICH_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ size: parseInt(size), order: "desc", type: "IMAGE", }), }); if (!res.ok) return c.json({ assets: [] }); const data = await res.json(); return c.json({ assets: data.assets?.items || [] }); } catch { return c.json({ assets: [] }); } }); // ── Proxy: asset thumbnail ── routes.get("/api/assets/:id/thumbnail", async (c) => { const id = c.req.param("id"); const size = c.req.query("size") || "thumbnail"; try { const res = await fetch(`${IMMICH_BASE}/api/assets/${id}/thumbnail?size=${size}`, { headers: { "x-api-key": IMMICH_API_KEY }, }); if (!res.ok) return c.body(null, 404); const body = await res.arrayBuffer(); return c.body(body, 200, { "Content-Type": res.headers.get("Content-Type") || "image/jpeg", "Cache-Control": "public, max-age=86400", }); } catch { return c.body(null, 500); } }); // ── Proxy: full-size asset ── routes.get("/api/assets/:id/original", async (c) => { const id = c.req.param("id"); try { const res = await fetch(`${IMMICH_BASE}/api/assets/${id}/original`, { headers: { "x-api-key": IMMICH_API_KEY }, }); if (!res.ok) return c.body(null, 404); const body = await res.arrayBuffer(); return c.body(body, 200, { "Content-Type": res.headers.get("Content-Type") || "image/jpeg", "Cache-Control": "public, max-age=86400", }); } catch { return c.body(null, 500); } }); // ── Page route ── routes.get("/", (c) => { const spaceSlug = c.req.param("space") || "demo"; return c.html(renderShell({ title: `${spaceSlug} — Photos | rSpace`, moduleId: "rphotos", spaceSlug, modules: getModuleInfoList(), theme: "dark", body: ``, scripts: ``, styles: ``, })); }); export const photosModule: RSpaceModule = { id: "rphotos", name: "rPhotos", icon: "📸", description: "Community photo commons", routes, landingPage: renderLanding, standaloneDomain: "rphotos.online", feeds: [ { id: "rphotos", name: "Recent Photos", kind: "data", description: "Stream of recently uploaded photos", emits: ["folk-image"], filterable: true, }, ], acceptsFeeds: ["data"], };