151 lines
7.1 KiB
JavaScript
151 lines
7.1 KiB
JavaScript
const express = require('express');
|
|
const fetch = require('node-fetch');
|
|
const path = require('path');
|
|
require('dotenv').config();
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3030;
|
|
|
|
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;
|
|
const TRANSITION_DURATION = parseInt(process.env.TRANSITION_DURATION, 10) || 2;
|
|
const SHOW_CLOCK = process.env.SHOW_CLOCK !== 'false';
|
|
const SHOW_DATE = process.env.SHOW_DATE !== 'false';
|
|
const SHOW_EXIF = process.env.SHOW_EXIF !== 'false';
|
|
const SHOW_PROGRESS = process.env.SHOW_PROGRESS !== 'false';
|
|
const IMAGE_FIT = process.env.IMAGE_FIT || 'contain';
|
|
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;
|
|
|
|
function immichHeaders() {
|
|
return { 'x-api-key': API_KEY, 'Accept': 'application/json', 'Content-Type': 'application/json' };
|
|
}
|
|
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
app.use(express.json());
|
|
|
|
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, refreshInterval: REFRESH_INTERVAL,
|
|
connected: !!API_KEY,
|
|
});
|
|
});
|
|
|
|
app.get('/api/server-info', async (_req, res) => {
|
|
try {
|
|
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 }); }
|
|
});
|
|
|
|
app.get('/api/albums', async (_req, res) => {
|
|
try {
|
|
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 }); }
|
|
});
|
|
|
|
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 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 }); }
|
|
});
|
|
|
|
app.get('/api/assets/random', async (req, res) => {
|
|
try {
|
|
const count = Math.min(parseInt(req.query.count, 10) || 50, 250);
|
|
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 }); }
|
|
});
|
|
|
|
app.get('/api/assets/favorites', async (_req, res) => {
|
|
try {
|
|
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 }); }
|
|
});
|
|
|
|
app.get('/api/assets/:id/thumbnail', async (req, res) => {
|
|
try {
|
|
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');
|
|
r.body.pipe(res);
|
|
} catch (err) { res.status(502).json({ error: err.message }); }
|
|
});
|
|
|
|
app.get('/api/assets/:id/original', async (req, res) => {
|
|
try {
|
|
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');
|
|
r.body.pipe(res);
|
|
} catch (err) { res.status(502).json({ error: err.message }); }
|
|
});
|
|
|
|
app.get('*', (_req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); });
|
|
|
|
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: ${SLIDESHOW_INTERVAL}s | Refresh: ${REFRESH_INTERVAL}s`);
|
|
});
|