140 lines
3.8 KiB
TypeScript
140 lines
3.8 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, renderIframeShell } 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(renderIframeShell({
|
|
title: `${spaceSlug} — Photos | rSpace`,
|
|
moduleId: "photos",
|
|
spaceSlug,
|
|
modules: getModuleInfoList(),
|
|
theme: "dark",
|
|
standaloneDomain: "rphotos.online",
|
|
}));
|
|
});
|
|
|
|
export const photosModule: RSpaceModule = {
|
|
id: "photos",
|
|
name: "rPhotos",
|
|
icon: "📸",
|
|
description: "Community photo commons",
|
|
routes,
|
|
standaloneDomain: "rphotos.online",
|
|
feeds: [
|
|
{
|
|
id: "photos",
|
|
name: "Recent Photos",
|
|
kind: "data",
|
|
description: "Stream of recently uploaded photos",
|
|
emits: ["folk-image"],
|
|
filterable: true,
|
|
},
|
|
],
|
|
acceptsFeeds: ["data"],
|
|
};
|