From b5265edb6a3d559ea611e7b338a710c7aaefced1 Mon Sep 17 00:00:00 2001 From: Alan Grainger Date: Tue, 12 Nov 2024 09:48:03 +0100 Subject: [PATCH] Fix #15 Support live photos and HEIC images --- app/config.json | 3 ++- app/package.json | 2 +- app/src/functions.ts | 11 +---------- app/src/immich.ts | 24 +++++++++++++++++------- app/src/index.ts | 9 ++++----- app/src/render.ts | 27 +++++++++++++++++++++++---- app/src/types.ts | 1 + app/views/gallery.ejs | 3 ++- 8 files changed, 51 insertions(+), 29 deletions(-) diff --git a/app/config.json b/app/config.json index d519c94..24696fb 100644 --- a/app/config.json +++ b/app/config.json @@ -4,7 +4,8 @@ "Cache-Control": "public, max-age=2592000" }, "singleImageGallery": false, - "singleItemAutoOpen": true + "singleItemAutoOpen": true, + "downloadOriginalPhoto": false }, "lightGallery": { "controls": true, diff --git a/app/package.json b/app/package.json index 8dd7d09..86d63e6 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "immich-public-proxy", - "version": "1.3.10", + "version": "1.4.0", "scripts": { "dev": "ts-node src/index.ts", "build": "npx tsc", diff --git a/app/src/functions.ts b/app/src/functions.ts index 2604010..8e55c64 100644 --- a/app/src/functions.ts +++ b/app/src/functions.ts @@ -1,6 +1,5 @@ import dayjs from 'dayjs' -import { Request, Response } from 'express-serve-static-core' -import { ImageSize } from './types' +import { Response } from 'express-serve-static-core' let config = {} try { @@ -31,14 +30,6 @@ export const getConfigOption = (path: string, defaultOption?: unknown) => { */ export const log = (message: string) => console.log(dayjs().format() + ' ' + message) -/** - * Sanitise the data for an incoming query string `size` parameter - * e.g. https://example.com/share/abc...xyz?size=thumbnail - */ -export function getSize (req: Request) { - return req.query?.size === 'thumbnail' ? ImageSize.thumbnail : ImageSize.original -} - /** * Force a value to be a string */ diff --git a/app/src/immich.ts b/app/src/immich.ts index b75a126..35d7033 100644 --- a/app/src/immich.ts +++ b/app/src/immich.ts @@ -63,7 +63,10 @@ class Immich { // Password required - show the visitor the password page // `req.params.key` is already sanitised at this point, but it never hurts to be explicit const key = request.key.replace(/[^\w-]/g, '') - res.render('password', { key, lgConfig: render.lgConfig }) + res.render('password', { + key, + lgConfig: render.lgConfig + }) } else if (sharedLinkRes.link) { // Valid shared link const link = sharedLinkRes.link @@ -77,7 +80,7 @@ class Immich { const asset = link.assets[0] if (asset.type === AssetType.image && !getConfigOption('ipp.singleImageGallery')) { // For photos, output the image directly unless configured to show a gallery - await render.assetBuffer(request, res, link.assets[0], request.size) + await render.assetBuffer(request, res, link.assets[0], ImageSize.preview) } else { // Show a gallery page const openItem = getConfigOption('ipp.singleItemAutoOpen', true) ? 1 : 0 @@ -175,11 +178,10 @@ class Immich { * Return the image data URL for a photo */ photoUrl (key: string, id: string, size?: ImageSize, password?: string) { - const params = { key, size } - if (password) { - Object.assign(params, this.encryptPassword(password)) - } - return this.buildUrl(`/photo/${key}/${id}`, params) + const path = ['photo', key, id] + if (size) path.push(size) + const params = password ? this.encryptPassword(password) : {} + return this.buildUrl('/' + path.join('/'), params) } /** @@ -223,6 +225,14 @@ class Immich { async accessible () { return !!(await immich.request('/server/ping')) } + + validateImageSize (size: unknown) { + if (!size || !Object.values(ImageSize).includes(size as ImageSize)) { + return ImageSize.preview + } else { + return size as ImageSize + } + } } const immich = new Immich() diff --git a/app/src/index.ts b/app/src/index.ts index 477bc51..e690069 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -4,7 +4,7 @@ import render from './render' import dayjs from 'dayjs' import { AssetType } from './types' import { decrypt } from './encrypt' -import { log, getSize, toString, addResponseHeaders } from './functions' +import { log, toString, addResponseHeaders } from './functions' require('dotenv').config() @@ -19,8 +19,7 @@ app.use(express.static('public', { setHeaders: addResponseHeaders })) // An incoming request for a shared link app.get('/share/:key', async (req, res) => { await immich.handleShareRequest({ - key: req.params.key, - size: getSize(req) + key: req.params.key }, res) }) @@ -33,7 +32,7 @@ app.post('/unlock', async (req, res) => { }) // Output the buffer data for a photo or video -app.get('/:type(photo|video)/:key/:id', async (req, res) => { +app.get('/:type(photo|video)/:key/:id/:size?', async (req, res) => { addResponseHeaders(res) // Check for valid key and ID if (immich.isKey(req.params.key) && immich.isId(req.params.id)) { @@ -60,7 +59,7 @@ app.get('/:type(photo|video)/:key/:id', async (req, res) => { const asset = sharedLink.assets.find(x => x.id === req.params.id) if (asset) { asset.type = req.params.type === 'video' ? AssetType.video : AssetType.image - render.assetBuffer(request, res, asset, getSize(req)).then() + render.assetBuffer(request, res, asset, immich.validateImageSize(req.params.size)).then() return } } diff --git a/app/src/render.ts b/app/src/render.ts index 8e83cc7..4b817de 100644 --- a/app/src/render.ts +++ b/app/src/render.ts @@ -16,8 +16,22 @@ class Render { async assetBuffer (req: IncomingShareRequest, res: Response, asset: Asset, size?: ImageSize) { // Prepare the request const headerList = ['content-type', 'content-length', 'last-modified', 'etag'] - size = size === ImageSize.thumbnail ? ImageSize.thumbnail : ImageSize.original - const subpath = asset.type === AssetType.video ? '/video/playback' : '/' + size + size = immich.validateImageSize(size) + let subpath, sizeQueryParam + if (asset.type === AssetType.video) { + subpath = '/video/playback' + } else if (asset.type === AssetType.image) { + // For images, there are three combinations of path + query string, depending on image size + if (size === ImageSize.original && getConfigOption('ipp.downloadOriginalPhoto', false)) { + subpath = '/original' + } else if (size === ImageSize.preview || size === ImageSize.original) { + // IPP is configured in config.json to send the preview size instead of the original size + subpath = '/thumbnail' + sizeQueryParam = 'preview' + } else { + subpath = '/' + size + } + } const headers = { range: '' } // For videos, request them in 2.5MB chunks rather than the entire video @@ -33,6 +47,7 @@ class Render { // Request data from Immich const url = immich.buildUrl(immich.apiUrl() + '/assets/' + encodeURIComponent(asset.id) + subpath, { key: asset.key, + size: sizeQueryParam, password: asset.password }) const data = await fetch(url, { headers }) @@ -66,7 +81,7 @@ class Render { async gallery (res: Response, share: SharedLink, openItem?: number) { const items = [] for (const asset of share.assets) { - let video + let video, downloadUrl if (asset.type === AssetType.video) { // Populate the data-video property video = JSON.stringify({ @@ -81,9 +96,13 @@ class Render { controls: true } }) + } else if (asset.type === AssetType.image && getConfigOption('ipp.downloadOriginalPhoto', false)) { + // Add a download link for the original-size image, if configured in config.json + downloadUrl = immich.photoUrl(share.key, asset.id, ImageSize.original, asset.password) } items.push({ - originalUrl: immich.photoUrl(share.key, asset.id, undefined, asset.password), + previewUrl: immich.photoUrl(share.key, asset.id, ImageSize.preview, asset.password), + downloadUrl, thumbnailUrl: immich.photoUrl(share.key, asset.id, ImageSize.thumbnail, asset.password), video }) diff --git a/app/src/types.ts b/app/src/types.ts index 3eb93b8..8825be1 100644 --- a/app/src/types.ts +++ b/app/src/types.ts @@ -32,6 +32,7 @@ export interface SharedLinkResult { export enum ImageSize { thumbnail = 'thumbnail', + preview = 'preview', original = 'original' } diff --git a/app/views/gallery.ejs b/app/views/gallery.ejs index 3b90d65..28549b1 100644 --- a/app/views/gallery.ejs +++ b/app/views/gallery.ejs @@ -15,7 +15,8 @@
<% } else { %> - + + data-download-url="<%- item.downloadUrl %>"<% } %>> <% }