Fix #15 Support live photos and HEIC images
This commit is contained in:
parent
6608d8ca5b
commit
b5265edb6a
@ -4,7 +4,8 @@
|
||||
"Cache-Control": "public, max-age=2592000"
|
||||
},
|
||||
"singleImageGallery": false,
|
||||
"singleItemAutoOpen": true
|
||||
"singleItemAutoOpen": true,
|
||||
"downloadOriginalPhoto": false
|
||||
},
|
||||
"lightGallery": {
|
||||
"controls": true,
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -32,6 +32,7 @@ export interface SharedLinkResult {
|
||||
|
||||
export enum ImageSize {
|
||||
thumbnail = 'thumbnail',
|
||||
preview = 'preview',
|
||||
original = 'original'
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,8 @@
|
||||
<div class="play-icon"></div>
|
||||
</a>
|
||||
<% } else { %>
|
||||
<a href="<%- item.originalUrl %>">
|
||||
<a href="<%- item.previewUrl %>"<% if (item.downloadUrl) { %>
|
||||
data-download-url="<%- item.downloadUrl %>"<% } %>>
|
||||
<img alt="" src="<%- item.thumbnailUrl %>"/>
|
||||
</a>
|
||||
<% }
|
||||
|
Loading…
Reference in New Issue
Block a user