From 00bfa926e425fe296f046a0d363af21048422fbe Mon Sep 17 00:00:00 2001 From: jessikitty Date: Tue, 19 May 2026 15:56:43 +1000 Subject: [PATCH] feat: port 3030, person API endpoints, refresh interval config, mapAsset helper --- server.js | 257 +++++++++++++++++------------------------------------- 1 file changed, 79 insertions(+), 178 deletions(-) diff --git a/server.js b/server.js index 0c8702c..0d5caf0 100644 --- a/server.js +++ b/server.js @@ -4,9 +4,8 @@ const path = require('path'); require('dotenv').config(); const app = express(); -const PORT = process.env.PORT || 3000; +const PORT = process.env.PORT || 3030; -// --- Configuration --- const IMMICH_URL = (process.env.IMMICH_URL || 'http://localhost:2283').replace(/\/+$/, ''); const API_KEY = process.env.IMMICH_API_KEY || ''; const SLIDESHOW_INTERVAL = parseInt(process.env.SLIDESHOW_INTERVAL, 10) || 30; @@ -20,230 +19,132 @@ const BACKGROUND_BLUR = process.env.BACKGROUND_BLUR !== 'false'; const SHUFFLE = process.env.SHUFFLE !== 'false'; const ALBUM_ID = process.env.ALBUM_ID || ''; const SHOW_FAVORITES_ONLY = process.env.SHOW_FAVORITES_ONLY === 'true'; +const REFRESH_INTERVAL = parseInt(process.env.REFRESH_INTERVAL, 10) || 300; -// --- Shared headers for Immich API --- function immichHeaders() { - return { - 'x-api-key': API_KEY, - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }; + return { 'x-api-key': API_KEY, 'Accept': 'application/json', 'Content-Type': 'application/json' }; } -// --- Middleware --- app.use(express.static(path.join(__dirname, 'public'))); app.use(express.json()); -// --- API: Config endpoint (sends safe config to frontend) --- app.get('/api/config', (_req, res) => { res.json({ - slideshowInterval: SLIDESHOW_INTERVAL, - transitionDuration: TRANSITION_DURATION, - showClock: SHOW_CLOCK, - showDate: SHOW_DATE, - showExif: SHOW_EXIF, - showProgress: SHOW_PROGRESS, - imageFit: IMAGE_FIT, - backgroundBlur: BACKGROUND_BLUR, - shuffle: SHUFFLE, - albumId: ALBUM_ID, - showFavoritesOnly: SHOW_FAVORITES_ONLY, + slideshowInterval: SLIDESHOW_INTERVAL, transitionDuration: TRANSITION_DURATION, + showClock: SHOW_CLOCK, showDate: SHOW_DATE, showExif: SHOW_EXIF, showProgress: SHOW_PROGRESS, + imageFit: IMAGE_FIT, backgroundBlur: BACKGROUND_BLUR, shuffle: SHUFFLE, + albumId: ALBUM_ID, showFavoritesOnly: SHOW_FAVORITES_ONLY, refreshInterval: REFRESH_INTERVAL, connected: !!API_KEY, }); }); -// --- API: Server info / connectivity check --- app.get('/api/server-info', async (_req, res) => { try { - const response = await fetch(`${IMMICH_URL}/api/server/version`, { - headers: immichHeaders(), - }); - if (!response.ok) throw new Error(`Immich returned ${response.status}`); - const data = await response.json(); - res.json({ ok: true, version: data }); - } catch (err) { - res.status(502).json({ ok: false, error: err.message }); - } + const r = await fetch(`${IMMICH_URL}/api/server/version`, { headers: immichHeaders() }); + if (!r.ok) throw new Error(`Immich returned ${r.status}`); + res.json({ ok: true, version: await r.json() }); + } catch (err) { res.status(502).json({ ok: false, error: err.message }); } }); -// --- API: List albums --- app.get('/api/albums', async (_req, res) => { try { - const response = await fetch(`${IMMICH_URL}/api/albums`, { - headers: immichHeaders(), - }); - if (!response.ok) throw new Error(`Immich returned ${response.status}`); - const albums = await response.json(); - // Return simplified album list - const simplified = albums.map((a) => ({ - id: a.id, - albumName: a.albumName, - assetCount: a.assetCount, - albumThumbnailAssetId: a.albumThumbnailAssetId, - updatedAt: a.updatedAt, - })); - res.json(simplified); - } catch (err) { - res.status(502).json({ error: err.message }); - } + const r = await fetch(`${IMMICH_URL}/api/albums`, { headers: immichHeaders() }); + if (!r.ok) throw new Error(`Immich returned ${r.status}`); + const albums = await r.json(); + res.json(albums.map(a => ({ id: a.id, albumName: a.albumName, assetCount: a.assetCount, albumThumbnailAssetId: a.albumThumbnailAssetId, updatedAt: a.updatedAt }))); + } catch (err) { res.status(502).json({ error: err.message }); } }); -// --- API: Get album assets --- +function mapAsset(a) { + return { + id: a.id, originalFileName: a.originalFileName, fileCreatedAt: a.fileCreatedAt, isFavorite: a.isFavorite, + exifInfo: a.exifInfo ? { make: a.exifInfo.make, model: a.exifInfo.model, city: a.exifInfo.city, state: a.exifInfo.state, country: a.exifInfo.country, description: a.exifInfo.description, dateTimeOriginal: a.exifInfo.dateTimeOriginal } : null, + }; +} + app.get('/api/albums/:id', async (req, res) => { try { - const response = await fetch(`${IMMICH_URL}/api/albums/${req.params.id}`, { - headers: immichHeaders(), - }); - if (!response.ok) throw new Error(`Immich returned ${response.status}`); - const album = await response.json(); - // Filter to images only, return simplified asset data - const assets = (album.assets || []) - .filter((a) => a.type === 'IMAGE') - .map((a) => ({ - id: a.id, - originalFileName: a.originalFileName, - fileCreatedAt: a.fileCreatedAt, - exifInfo: a.exifInfo - ? { - make: a.exifInfo.make, - model: a.exifInfo.model, - city: a.exifInfo.city, - state: a.exifInfo.state, - country: a.exifInfo.country, - description: a.exifInfo.description, - dateTimeOriginal: a.exifInfo.dateTimeOriginal, - } - : null, - })); - res.json({ - id: album.id, - albumName: album.albumName, - assetCount: assets.length, - assets, - }); - } catch (err) { - res.status(502).json({ error: err.message }); - } + const r = await fetch(`${IMMICH_URL}/api/albums/${req.params.id}`, { headers: immichHeaders() }); + if (!r.ok) throw new Error(`Immich returned ${r.status}`); + const album = await r.json(); + const assets = (album.assets || []).filter(a => a.type === 'IMAGE').map(mapAsset); + res.json({ id: album.id, albumName: album.albumName, assetCount: assets.length, assets }); + } catch (err) { res.status(502).json({ error: err.message }); } +}); + +app.get('/api/people', async (_req, res) => { + try { + const r = await fetch(`${IMMICH_URL}/api/people`, { headers: immichHeaders() }); + if (!r.ok) throw new Error(`Immich returned ${r.status}`); + const data = await r.json(); + res.json((data.people || data || []).map(p => ({ id: p.id, name: p.name, thumbnailPath: p.thumbnailPath }))); + } catch (err) { res.status(502).json({ error: err.message }); } +}); + +app.get('/api/people/:id', async (req, res) => { + try { + const r = await fetch(`${IMMICH_URL}/api/people/${req.params.id}/assets`, { headers: immichHeaders() }); + if (!r.ok) throw new Error(`Immich returned ${r.status}`); + const assets = await r.json(); + res.json((Array.isArray(assets) ? assets : []).filter(a => a.type === 'IMAGE').map(mapAsset)); + } catch (err) { res.status(502).json({ error: err.message }); } +}); + +app.get('/api/people/:id/thumbnail', async (req, res) => { + try { + const r = await fetch(`${IMMICH_URL}/api/people/${req.params.id}/thumbnail`, { headers: { 'x-api-key': API_KEY } }); + if (!r.ok) throw new Error(`Immich returned ${r.status}`); + res.set('Content-Type', r.headers.get('content-type') || 'image/jpeg'); + res.set('Cache-Control', 'public, max-age=86400'); + r.body.pipe(res); + } catch (err) { res.status(502).json({ error: err.message }); } }); -// --- API: Get random assets (when no album selected) --- app.get('/api/assets/random', async (req, res) => { try { const count = Math.min(parseInt(req.query.count, 10) || 50, 250); - const response = await fetch(`${IMMICH_URL}/api/assets/random?count=${count}`, { - headers: immichHeaders(), - }); - if (!response.ok) throw new Error(`Immich returned ${response.status}`); - const assets = await response.json(); - const images = assets - .filter((a) => a.type === 'IMAGE') - .map((a) => ({ - id: a.id, - originalFileName: a.originalFileName, - fileCreatedAt: a.fileCreatedAt, - isFavorite: a.isFavorite, - exifInfo: a.exifInfo - ? { - make: a.exifInfo.make, - model: a.exifInfo.model, - city: a.exifInfo.city, - state: a.exifInfo.state, - country: a.exifInfo.country, - description: a.exifInfo.description, - dateTimeOriginal: a.exifInfo.dateTimeOriginal, - } - : null, - })); - res.json(images); - } catch (err) { - res.status(502).json({ error: err.message }); - } + const r = await fetch(`${IMMICH_URL}/api/assets/random?count=${count}`, { headers: immichHeaders() }); + if (!r.ok) throw new Error(`Immich returned ${r.status}`); + res.json((await r.json()).filter(a => a.type === 'IMAGE').map(mapAsset)); + } catch (err) { res.status(502).json({ error: err.message }); } }); -// --- API: Get favorites --- app.get('/api/assets/favorites', async (_req, res) => { try { - const body = JSON.stringify({ - isFavorite: true, - type: 'IMAGE', - size: 250, - page: 1, - }); - const response = await fetch(`${IMMICH_URL}/api/search/metadata`, { - method: 'POST', - headers: immichHeaders(), - body, - }); - if (!response.ok) throw new Error(`Immich returned ${response.status}`); - const data = await response.json(); - const images = (data.assets?.items || []).map((a) => ({ - id: a.id, - originalFileName: a.originalFileName, - fileCreatedAt: a.fileCreatedAt, - isFavorite: true, - exifInfo: a.exifInfo - ? { - make: a.exifInfo.make, - model: a.exifInfo.model, - city: a.exifInfo.city, - state: a.exifInfo.state, - country: a.exifInfo.country, - description: a.exifInfo.description, - dateTimeOriginal: a.exifInfo.dateTimeOriginal, - } - : null, - })); - res.json(images); - } catch (err) { - res.status(502).json({ error: err.message }); - } + const r = await fetch(`${IMMICH_URL}/api/search/metadata`, { method: 'POST', headers: immichHeaders(), body: JSON.stringify({ isFavorite: true, type: 'IMAGE', size: 250, page: 1 }) }); + if (!r.ok) throw new Error(`Immich returned ${r.status}`); + const data = await r.json(); + res.json((data.assets?.items || []).map(a => ({ ...mapAsset(a), isFavorite: true }))); + } catch (err) { res.status(502).json({ error: err.message }); } }); -// --- API: Proxy image (thumbnail) --- app.get('/api/assets/:id/thumbnail', async (req, res) => { try { - const size = req.query.size || 'preview'; // 'thumbnail' or 'preview' - const response = await fetch( - `${IMMICH_URL}/api/assets/${req.params.id}/thumbnail?size=${size}`, - { headers: { 'x-api-key': API_KEY } } - ); - if (!response.ok) throw new Error(`Immich returned ${response.status}`); - const contentType = response.headers.get('content-type'); - res.set('Content-Type', contentType || 'image/jpeg'); + const size = req.query.size || 'preview'; + const r = await fetch(`${IMMICH_URL}/api/assets/${req.params.id}/thumbnail?size=${size}`, { headers: { 'x-api-key': API_KEY } }); + if (!r.ok) throw new Error(`Immich returned ${r.status}`); + res.set('Content-Type', r.headers.get('content-type') || 'image/jpeg'); res.set('Cache-Control', 'public, max-age=86400'); - response.body.pipe(res); - } catch (err) { - res.status(502).json({ error: err.message }); - } + r.body.pipe(res); + } catch (err) { res.status(502).json({ error: err.message }); } }); -// --- API: Proxy full-size image --- app.get('/api/assets/:id/original', async (req, res) => { try { - const response = await fetch( - `${IMMICH_URL}/api/assets/${req.params.id}/original`, - { headers: { 'x-api-key': API_KEY } } - ); - if (!response.ok) throw new Error(`Immich returned ${response.status}`); - const contentType = response.headers.get('content-type'); - res.set('Content-Type', contentType || 'image/jpeg'); + const r = await fetch(`${IMMICH_URL}/api/assets/${req.params.id}/original`, { headers: { 'x-api-key': API_KEY } }); + if (!r.ok) throw new Error(`Immich returned ${r.status}`); + res.set('Content-Type', r.headers.get('content-type') || 'image/jpeg'); res.set('Cache-Control', 'public, max-age=86400'); - response.body.pipe(res); - } catch (err) { - res.status(502).json({ error: err.message }); - } + r.body.pipe(res); + } catch (err) { res.status(502).json({ error: err.message }); } }); -// --- Fallback: serve index.html for SPA --- -app.get('*', (_req, res) => { - res.sendFile(path.join(__dirname, 'public', 'index.html')); -}); +app.get('*', (_req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); -// --- Start --- app.listen(PORT, '0.0.0.0', () => { console.log(`🖼️ Frambe running on http://0.0.0.0:${PORT}`); console.log(`📡 Immich server: ${IMMICH_URL}`); console.log(`🔑 API Key: ${API_KEY ? '***configured***' : '⚠️ NOT SET'}`); if (ALBUM_ID) console.log(`📁 Default album: ${ALBUM_ID}`); - console.log(`⏱️ Slideshow interval: ${SLIDESHOW_INTERVAL}s`); + console.log(`⏱️ Slideshow: ${SLIDESHOW_INTERVAL}s | Refresh: ${REFRESH_INTERVAL}s`); });