feat(1.3.0): video support, INCLUDE_VIDEOS config, video streaming endpoint, asset type in mapAsset
This commit is contained in:
@@ -3,7 +3,7 @@ const fetch = require('node-fetch');
|
||||
const path = require('path');
|
||||
require('dotenv').config();
|
||||
|
||||
const VERSION = '1.2.2';
|
||||
const VERSION = '1.3.0';
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
@@ -21,161 +21,92 @@ 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;
|
||||
const INCLUDE_VIDEOS = process.env.INCLUDE_VIDEOS !== 'false';
|
||||
|
||||
function immichHeaders() {
|
||||
return { 'x-api-key': API_KEY, 'Accept': 'application/json', 'Content-Type': 'application/json' };
|
||||
}
|
||||
|
||||
function log(msg) { console.log('[Frambe] ' + msg); }
|
||||
function logErr(msg) { console.error('[Frambe] ERROR: ' + msg); }
|
||||
|
||||
// --- Request logging for API calls ---
|
||||
app.use('/api', (req, _res, next) => {
|
||||
log('API ' + req.method + ' ' + req.originalUrl);
|
||||
next();
|
||||
});
|
||||
|
||||
// --- Static files with no-cache on HTML/JS/CSS (prevents stale browser cache) ---
|
||||
app.use('/api', (req, _res, next) => { log('API ' + req.method + ' ' + req.originalUrl); next(); });
|
||||
app.use(express.static(path.join(__dirname, 'public'), {
|
||||
setHeaders: (res, filePath) => {
|
||||
if (filePath.endsWith('.html') || filePath.endsWith('.js') || filePath.endsWith('.css')) {
|
||||
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||
res.setHeader('Pragma', 'no-cache');
|
||||
res.setHeader('Expires', '0');
|
||||
res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0');
|
||||
}
|
||||
}
|
||||
}));
|
||||
app.use(express.json());
|
||||
|
||||
// --- API: Config ---
|
||||
app.get('/api/config', (_req, res) => {
|
||||
res.json({
|
||||
version: VERSION,
|
||||
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 ---
|
||||
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}`);
|
||||
const v = await r.json();
|
||||
log('Immich connection OK, version ' + v.major + '.' + v.minor + '.' + v.patch);
|
||||
res.json({ ok: true, version: v });
|
||||
} catch (err) {
|
||||
logErr('Immich connection failed: ' + err.message);
|
||||
res.status(502).json({ ok: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// --- API: Albums ---
|
||||
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();
|
||||
log('Listed ' + albums.length + ' albums');
|
||||
res.json(albums.map(a => ({ id: a.id, albumName: a.albumName, assetCount: a.assetCount, albumThumbnailAssetId: a.albumThumbnailAssetId, updatedAt: a.updatedAt })));
|
||||
} catch (err) { logErr('Albums list failed: ' + err.message); res.status(502).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
function mapAsset(a) {
|
||||
return {
|
||||
id: a.id, originalFileName: a.originalFileName, fileCreatedAt: a.fileCreatedAt, isFavorite: a.isFavorite,
|
||||
id: a.id, type: a.type, 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,
|
||||
};
|
||||
}
|
||||
function filterAssets(assets) {
|
||||
if (INCLUDE_VIDEOS) return assets.filter(a => a.type === 'IMAGE' || a.type === 'VIDEO');
|
||||
return assets.filter(a => a.type === 'IMAGE');
|
||||
}
|
||||
|
||||
app.get('/api/config', (_req, res) => {
|
||||
res.json({ version: VERSION, 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, includeVideos: INCLUDE_VIDEOS, 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}`); const v = await r.json(); log('Immich connection OK, version ' + v.major + '.' + v.minor + '.' + v.patch); res.json({ ok: true, version: v });
|
||||
} catch (err) { logErr('Immich connection failed: ' + err.message); 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(); log('Listed ' + albums.length + ' albums'); res.json(albums.map(a => ({ id: a.id, albumName: a.albumName, assetCount: a.assetCount, albumThumbnailAssetId: a.albumThumbnailAssetId, updatedAt: a.updatedAt })));
|
||||
} catch (err) { logErr('Albums list failed: ' + err.message); res.status(502).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.get('/api/albums/:id', async (req, res) => {
|
||||
try {
|
||||
log('Fetching album ' + req.params.id);
|
||||
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);
|
||||
log('Album "' + album.albumName + '" returned ' + assets.length + ' images');
|
||||
res.json({ id: album.id, albumName: album.albumName, assetCount: assets.length, assets });
|
||||
try { log('Fetching album ' + req.params.id); 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 = filterAssets(album.assets || []).map(mapAsset); const vids = assets.filter(a => a.type === 'VIDEO').length; log('Album "' + album.albumName + '" returned ' + assets.length + ' assets (' + (assets.length - vids) + ' photos, ' + vids + ' videos)'); res.json({ id: album.id, albumName: album.albumName, assetCount: assets.length, assets });
|
||||
} catch (err) { logErr('Album fetch failed: ' + err.message); 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();
|
||||
const people = (data.people || data || []).map(p => ({ id: p.id, name: p.name, thumbnailPath: p.thumbnailPath }));
|
||||
log('Listed ' + people.length + ' people');
|
||||
res.json(people);
|
||||
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(); const people = (data.people || data || []).map(p => ({ id: p.id, name: p.name, thumbnailPath: p.thumbnailPath })); log('Listed ' + people.length + ' people'); res.json(people);
|
||||
} catch (err) { logErr('People list failed: ' + err.message); res.status(502).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
app.get('/api/people/:id', async (req, res) => {
|
||||
try {
|
||||
log('Fetching person ' + req.params.id);
|
||||
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();
|
||||
const images = (Array.isArray(assets) ? assets : []).filter(a => a.type === 'IMAGE').map(mapAsset);
|
||||
log('Person returned ' + images.length + ' images');
|
||||
res.json(images);
|
||||
try { log('Fetching person ' + req.params.id); 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 raw = await r.json(); const assets = filterAssets(Array.isArray(raw) ? raw : []).map(mapAsset); log('Person returned ' + assets.length + ' assets'); res.json(assets);
|
||||
} catch (err) { logErr('Person fetch failed: ' + err.message); 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);
|
||||
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}`);
|
||||
const images = (await r.json()).filter(a => a.type === 'IMAGE').map(mapAsset);
|
||||
log('Random returned ' + images.length + ' images');
|
||||
res.json(images);
|
||||
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}`); const assets = filterAssets(await r.json()).map(mapAsset); log('Random returned ' + assets.length + ' assets'); res.json(assets);
|
||||
} catch (err) { logErr('Random fetch failed: ' + err.message); 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();
|
||||
const images = (data.assets?.items || []).map(a => ({ ...mapAsset(a), isFavorite: true }));
|
||||
log('Favorites returned ' + images.length + ' images');
|
||||
res.json(images);
|
||||
try { const r = await fetch(`${IMMICH_URL}/api/search/metadata`, { method: 'POST', headers: immichHeaders(), body: JSON.stringify({ isFavorite: true, size: 250, page: 1 }) }); if (!r.ok) throw new Error(`Immich returned ${r.status}`); const data = await r.json(); const assets = filterAssets(data.assets?.items || []).map(a => ({ ...mapAsset(a), isFavorite: true })); log('Favorites returned ' + assets.length + ' assets'); res.json(assets);
|
||||
} catch (err) { logErr('Favorites fetch failed: ' + err.message); 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);
|
||||
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/video', async (req, res) => {
|
||||
try { log('Streaming video ' + req.params.id); const r = await fetch(`${IMMICH_URL}/api/assets/${req.params.id}/video/playback`, { 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') || 'video/mp4'); res.set('Cache-Control', 'public, max-age=86400'); const cl = r.headers.get('content-length'); if (cl) res.set('Content-Length', cl); r.body.pipe(res);
|
||||
} catch (err) { logErr('Video stream failed: ' + err.message); 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);
|
||||
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 }); }
|
||||
});
|
||||
|
||||
@@ -187,6 +118,7 @@ app.listen(PORT, '0.0.0.0', () => {
|
||||
log('Immich URL: ' + IMMICH_URL);
|
||||
log('API key: ' + (API_KEY ? 'configured (' + API_KEY.substring(0, 8) + '...)' : 'NOT SET'));
|
||||
log('Slideshow: ' + SLIDESHOW_INTERVAL + 's interval, ' + TRANSITION_DURATION + 's transition, refresh every ' + REFRESH_INTERVAL + 's');
|
||||
log('Videos: ' + (INCLUDE_VIDEOS ? 'enabled' : 'disabled'));
|
||||
if (ALBUM_ID) log('Default album: ' + ALBUM_ID);
|
||||
if (SHOW_FAVORITES_ONLY) log('Auto-start: favorites only');
|
||||
log('Waiting for requests...');
|
||||
|
||||
Reference in New Issue
Block a user