diff --git a/server.js b/server.js index e618f05..d434698 100644 --- a/server.js +++ b/server.js @@ -3,6 +3,7 @@ const fetch = require('node-fetch'); const path = require('path'); require('dotenv').config(); +const VERSION = '1.2.2'; const app = express(); const PORT = process.env.PORT || 3000; @@ -25,11 +26,31 @@ function immichHeaders() { return { 'x-api-key': API_KEY, 'Accept': 'application/json', 'Content-Type': 'application/json' }; } -app.use(express.static(path.join(__dirname, 'public'))); +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(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'); + } + } +})); 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, @@ -38,21 +59,29 @@ app.get('/api/config', (_req, res) => { }); }); +// --- 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}`); - res.json({ ok: true, version: await r.json() }); - } catch (err) { res.status(502).json({ ok: false, error: err.message }); } + 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) { res.status(502).json({ error: err.message }); } + } catch (err) { logErr('Albums list failed: ' + err.message); res.status(502).json({ error: err.message }); } }); function mapAsset(a) { @@ -64,12 +93,14 @@ function mapAsset(a) { 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 }); - } catch (err) { res.status(502).json({ error: err.message }); } + } catch (err) { logErr('Album fetch failed: ' + err.message); res.status(502).json({ error: err.message }); } }); app.get('/api/people', async (_req, res) => { @@ -77,17 +108,22 @@ app.get('/api/people', async (_req, res) => { 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 }); } + 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(); - res.json((Array.isArray(assets) ? assets : []).filter(a => a.type === 'IMAGE').map(mapAsset)); - } catch (err) { res.status(502).json({ error: err.message }); } + const images = (Array.isArray(assets) ? assets : []).filter(a => a.type === 'IMAGE').map(mapAsset); + log('Person returned ' + images.length + ' images'); + res.json(images); + } catch (err) { logErr('Person fetch failed: ' + err.message); res.status(502).json({ error: err.message }); } }); app.get('/api/people/:id/thumbnail', async (req, res) => { @@ -105,8 +141,10 @@ app.get('/api/assets/random', async (req, res) => { 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 }); } + const images = (await r.json()).filter(a => a.type === 'IMAGE').map(mapAsset); + log('Random returned ' + images.length + ' images'); + res.json(images); + } catch (err) { logErr('Random fetch failed: ' + err.message); res.status(502).json({ error: err.message }); } }); app.get('/api/assets/favorites', async (_req, res) => { @@ -114,8 +152,10 @@ app.get('/api/assets/favorites', async (_req, res) => { 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 }); } + const images = (data.assets?.items || []).map(a => ({ ...mapAsset(a), isFavorite: true })); + log('Favorites returned ' + images.length + ' images'); + res.json(images); + } catch (err) { logErr('Favorites fetch failed: ' + err.message); res.status(502).json({ error: err.message }); } }); app.get('/api/assets/:id/thumbnail', async (req, res) => { @@ -142,9 +182,12 @@ app.get('/api/assets/:id/original', async (req, res) => { 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`); + log('--- Frambe v' + VERSION + ' ---'); + log('Server listening on port ' + PORT); + 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'); + if (ALBUM_ID) log('Default album: ' + ALBUM_ID); + if (SHOW_FAVORITES_ONLY) log('Auto-start: favorites only'); + log('Waiting for requests...'); });