diff --git a/app/package.json b/app/package.json index 401c900..348b164 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "immich-public-proxy", - "version": "1.3.7", + "version": "1.3.8", "scripts": { "dev": "ts-node src/index.ts", "build": "npx tsc", diff --git a/app/src/immich.ts b/app/src/immich.ts index c115b05..efb5696 100644 --- a/app/src/immich.ts +++ b/app/src/immich.ts @@ -12,7 +12,7 @@ class Immich { */ async request (endpoint: string) { try { - const res = await fetch(process.env.IMMICH_URL + '/api' + endpoint) + const res = await fetch(this.apiUrl() + endpoint) if (res.status === 200) { const contentType = res.headers.get('Content-Type') || '' if (contentType.includes('application/json')) { @@ -29,6 +29,10 @@ class Immich { } } + apiUrl () { + return process.env.IMMICH_URL + '/api' + } + /** * Handle an incoming request for a shared link `key`. This is the main function which * communicates with Immich and returns the output back to the visitor. @@ -72,7 +76,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(res, link.assets[0], request.size) + await render.assetBuffer(request, res, link.assets[0], request.size) } else { // Show a gallery page const openItem = getConfigOption('ipp.singleItemAutoOpen', true) ? 1 : 0 diff --git a/app/src/index.ts b/app/src/index.ts index 03e9133..477bc51 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -54,12 +54,13 @@ app.get('/:type(photo|video)/:key/:id', async (req, res) => { } // Check if the key is a valid share link const sharedLink = (await immich.getShareByKey(req.params.key, password))?.link + const request = { key: req.params.key, range: req.headers.range || '' } if (sharedLink?.assets.length) { // Check that the requested asset exists in this share 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(res, asset, getSize(req)).then() + render.assetBuffer(request, res, asset, getSize(req)).then() return } } diff --git a/app/src/render.ts b/app/src/render.ts index 4a83e56..5ab937b 100644 --- a/app/src/render.ts +++ b/app/src/render.ts @@ -1,6 +1,6 @@ import immich from './immich' import { Response } from 'express-serve-static-core' -import { Asset, AssetType, ImageSize, SharedLink } from './types' +import { Asset, AssetType, ImageSize, IncomingShareRequest, SharedLink } from './types' import { getConfigOption } from './functions' class Render { @@ -10,13 +10,44 @@ class Render { this.lgConfig = getConfigOption('lightGallery', {}) } - async assetBuffer (res: Response, asset: Asset, size?: ImageSize) { - const data = await immich.getAssetBuffer(asset, size) - if (data) { - for (const header of ['content-type', 'content-length']) { - res.set(header, data.headers.get(header)) - } - res.send(Buffer.from(await data.arrayBuffer())) + /** + * Stream data from Immich back to the client + */ + async assetBuffer (req: IncomingShareRequest, res: Response, asset: Asset, size?: ImageSize) { + // Prepare the request + size = size === ImageSize.thumbnail ? ImageSize.thumbnail : ImageSize.original + const subpath = asset.type === AssetType.video ? '/video/playback' : '/' + size + const headers = { range: '' } + if (asset.type === AssetType.video && req.range) { + const start = req.range.replace(/bytes=/, '').split('-')[0] + const startByte = parseInt(start, 10) || 0 + const endByte = startByte + 2499999 + headers.range = `bytes=${startByte}-${endByte}` + } + const url = immich.buildUrl(immich.apiUrl() + '/assets/' + encodeURIComponent(asset.id) + subpath, { + key: asset.key, + password: asset.password + }) + const data = await fetch(url, { headers }) + + // Return the response to the client + if (data.status >= 200 && data.status < 300) { + // Populate the response headers + ['content-type', 'content-length', 'last-modified', 'etag', 'content-range'] + .forEach(header => { + const value = data.headers.get(header) + if (value) res.setHeader(header, value) + }) + if (headers.range) res.status(206) // Partial Content + // Return the body + await data.body?.pipeTo( + new WritableStream({ + write (chunk) { + res.write(chunk) + } + }) + ) + res.end() } else { res.status(404).send() } diff --git a/app/src/types.ts b/app/src/types.ts index 8270a73..3eb93b8 100644 --- a/app/src/types.ts +++ b/app/src/types.ts @@ -39,4 +39,5 @@ export interface IncomingShareRequest { key: string; password?: string; size?: ImageSize; + range?: string; }