rspace-online/modules/photos/mod.ts

142 lines
3.9 KiB
TypeScript

/**
* 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";
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: `<folk-photo-gallery space="${spaceSlug}"></folk-photo-gallery>`,
scripts: `<script type="module" src="/modules/photos/folk-photo-gallery.js"></script>`,
styles: `<link rel="stylesheet" href="/modules/photos/photos.css">`,
}));
});
export const photosModule: RSpaceModule = {
id: "rphotos",
name: "rPhotos",
icon: "📸",
description: "Community photo commons",
routes,
standaloneDomain: "rphotos.online",
feeds: [
{
id: "rphotos",
name: "Recent Photos",
kind: "data",
description: "Stream of recently uploaded photos",
emits: ["folk-image"],
filterable: true,
},
],
acceptsFeeds: ["data"],
};