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"
|
"Cache-Control": "public, max-age=2592000"
|
||||||
},
|
},
|
||||||
"singleImageGallery": false,
|
"singleImageGallery": false,
|
||||||
"singleItemAutoOpen": true
|
"singleItemAutoOpen": true,
|
||||||
|
"downloadOriginalPhoto": false
|
||||||
},
|
},
|
||||||
"lightGallery": {
|
"lightGallery": {
|
||||||
"controls": true,
|
"controls": true,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-public-proxy",
|
"name": "immich-public-proxy",
|
||||||
"version": "1.3.10",
|
"version": "1.4.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "ts-node src/index.ts",
|
"dev": "ts-node src/index.ts",
|
||||||
"build": "npx tsc",
|
"build": "npx tsc",
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { Request, Response } from 'express-serve-static-core'
|
import { Response } from 'express-serve-static-core'
|
||||||
import { ImageSize } from './types'
|
|
||||||
|
|
||||||
let config = {}
|
let config = {}
|
||||||
try {
|
try {
|
||||||
@ -31,14 +30,6 @@ export const getConfigOption = (path: string, defaultOption?: unknown) => {
|
|||||||
*/
|
*/
|
||||||
export const log = (message: string) => console.log(dayjs().format() + ' ' + message)
|
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
|
* Force a value to be a string
|
||||||
*/
|
*/
|
||||||
|
@ -63,7 +63,10 @@ class Immich {
|
|||||||
// Password required - show the visitor the password page
|
// Password required - show the visitor the password page
|
||||||
// `req.params.key` is already sanitised at this point, but it never hurts to be explicit
|
// `req.params.key` is already sanitised at this point, but it never hurts to be explicit
|
||||||
const key = request.key.replace(/[^\w-]/g, '')
|
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) {
|
} else if (sharedLinkRes.link) {
|
||||||
// Valid shared link
|
// Valid shared link
|
||||||
const link = sharedLinkRes.link
|
const link = sharedLinkRes.link
|
||||||
@ -77,7 +80,7 @@ class Immich {
|
|||||||
const asset = link.assets[0]
|
const asset = link.assets[0]
|
||||||
if (asset.type === AssetType.image && !getConfigOption('ipp.singleImageGallery')) {
|
if (asset.type === AssetType.image && !getConfigOption('ipp.singleImageGallery')) {
|
||||||
// For photos, output the image directly unless configured to show a gallery
|
// 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 {
|
} else {
|
||||||
// Show a gallery page
|
// Show a gallery page
|
||||||
const openItem = getConfigOption('ipp.singleItemAutoOpen', true) ? 1 : 0
|
const openItem = getConfigOption('ipp.singleItemAutoOpen', true) ? 1 : 0
|
||||||
@ -175,11 +178,10 @@ class Immich {
|
|||||||
* Return the image data URL for a photo
|
* Return the image data URL for a photo
|
||||||
*/
|
*/
|
||||||
photoUrl (key: string, id: string, size?: ImageSize, password?: string) {
|
photoUrl (key: string, id: string, size?: ImageSize, password?: string) {
|
||||||
const params = { key, size }
|
const path = ['photo', key, id]
|
||||||
if (password) {
|
if (size) path.push(size)
|
||||||
Object.assign(params, this.encryptPassword(password))
|
const params = password ? this.encryptPassword(password) : {}
|
||||||
}
|
return this.buildUrl('/' + path.join('/'), params)
|
||||||
return this.buildUrl(`/photo/${key}/${id}`, params)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -223,6 +225,14 @@ class Immich {
|
|||||||
async accessible () {
|
async accessible () {
|
||||||
return !!(await immich.request('/server/ping'))
|
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()
|
const immich = new Immich()
|
||||||
|
@ -4,7 +4,7 @@ import render from './render'
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { AssetType } from './types'
|
import { AssetType } from './types'
|
||||||
import { decrypt } from './encrypt'
|
import { decrypt } from './encrypt'
|
||||||
import { log, getSize, toString, addResponseHeaders } from './functions'
|
import { log, toString, addResponseHeaders } from './functions'
|
||||||
|
|
||||||
require('dotenv').config()
|
require('dotenv').config()
|
||||||
|
|
||||||
@ -19,8 +19,7 @@ app.use(express.static('public', { setHeaders: addResponseHeaders }))
|
|||||||
// An incoming request for a shared link
|
// An incoming request for a shared link
|
||||||
app.get('/share/:key', async (req, res) => {
|
app.get('/share/:key', async (req, res) => {
|
||||||
await immich.handleShareRequest({
|
await immich.handleShareRequest({
|
||||||
key: req.params.key,
|
key: req.params.key
|
||||||
size: getSize(req)
|
|
||||||
}, res)
|
}, res)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -33,7 +32,7 @@ app.post('/unlock', async (req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Output the buffer data for a photo or video
|
// 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)
|
addResponseHeaders(res)
|
||||||
// Check for valid key and ID
|
// Check for valid key and ID
|
||||||
if (immich.isKey(req.params.key) && immich.isId(req.params.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)
|
const asset = sharedLink.assets.find(x => x.id === req.params.id)
|
||||||
if (asset) {
|
if (asset) {
|
||||||
asset.type = req.params.type === 'video' ? AssetType.video : AssetType.image
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,22 @@ class Render {
|
|||||||
async assetBuffer (req: IncomingShareRequest, res: Response, asset: Asset, size?: ImageSize) {
|
async assetBuffer (req: IncomingShareRequest, res: Response, asset: Asset, size?: ImageSize) {
|
||||||
// Prepare the request
|
// Prepare the request
|
||||||
const headerList = ['content-type', 'content-length', 'last-modified', 'etag']
|
const headerList = ['content-type', 'content-length', 'last-modified', 'etag']
|
||||||
size = size === ImageSize.thumbnail ? ImageSize.thumbnail : ImageSize.original
|
size = immich.validateImageSize(size)
|
||||||
const subpath = asset.type === AssetType.video ? '/video/playback' : '/' + 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: '' }
|
const headers = { range: '' }
|
||||||
|
|
||||||
// For videos, request them in 2.5MB chunks rather than the entire video
|
// For videos, request them in 2.5MB chunks rather than the entire video
|
||||||
@ -33,6 +47,7 @@ class Render {
|
|||||||
// Request data from Immich
|
// Request data from Immich
|
||||||
const url = immich.buildUrl(immich.apiUrl() + '/assets/' + encodeURIComponent(asset.id) + subpath, {
|
const url = immich.buildUrl(immich.apiUrl() + '/assets/' + encodeURIComponent(asset.id) + subpath, {
|
||||||
key: asset.key,
|
key: asset.key,
|
||||||
|
size: sizeQueryParam,
|
||||||
password: asset.password
|
password: asset.password
|
||||||
})
|
})
|
||||||
const data = await fetch(url, { headers })
|
const data = await fetch(url, { headers })
|
||||||
@ -66,7 +81,7 @@ class Render {
|
|||||||
async gallery (res: Response, share: SharedLink, openItem?: number) {
|
async gallery (res: Response, share: SharedLink, openItem?: number) {
|
||||||
const items = []
|
const items = []
|
||||||
for (const asset of share.assets) {
|
for (const asset of share.assets) {
|
||||||
let video
|
let video, downloadUrl
|
||||||
if (asset.type === AssetType.video) {
|
if (asset.type === AssetType.video) {
|
||||||
// Populate the data-video property
|
// Populate the data-video property
|
||||||
video = JSON.stringify({
|
video = JSON.stringify({
|
||||||
@ -81,9 +96,13 @@ class Render {
|
|||||||
controls: true
|
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({
|
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),
|
thumbnailUrl: immich.photoUrl(share.key, asset.id, ImageSize.thumbnail, asset.password),
|
||||||
video
|
video
|
||||||
})
|
})
|
||||||
|
@ -32,6 +32,7 @@ export interface SharedLinkResult {
|
|||||||
|
|
||||||
export enum ImageSize {
|
export enum ImageSize {
|
||||||
thumbnail = 'thumbnail',
|
thumbnail = 'thumbnail',
|
||||||
|
preview = 'preview',
|
||||||
original = 'original'
|
original = 'original'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,8 @@
|
|||||||
<div class="play-icon"></div>
|
<div class="play-icon"></div>
|
||||||
</a>
|
</a>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<a href="<%- item.originalUrl %>">
|
<a href="<%- item.previewUrl %>"<% if (item.downloadUrl) { %>
|
||||||
|
data-download-url="<%- item.downloadUrl %>"<% } %>>
|
||||||
<img alt="" src="<%- item.thumbnailUrl %>"/>
|
<img alt="" src="<%- item.thumbnailUrl %>"/>
|
||||||
</a>
|
</a>
|
||||||
<% }
|
<% }
|
||||||
|
Loading…
Reference in New Issue
Block a user