mirror of
https://github.com/alangrainger/immich-public-proxy.git
synced 2024-12-28 03:41:58 +00:00
Add #10 - Single item as a gallery
This commit is contained in:
parent
0afc820f7b
commit
71feba9ce8
10 changed files with 87 additions and 54 deletions
|
@ -1,3 +0,0 @@
|
||||||
IMMICH_URL=http://localhost:2283
|
|
||||||
PORT=3000
|
|
||||||
CACHE_AGE=2592000
|
|
19
README.md
19
README.md
|
@ -48,17 +48,7 @@ Here is an example setup for [securing Immich behind mTLS](./docs/securing-immic
|
||||||
|
|
||||||
1. Download the [docker-compose.yml](https://github.com/alangrainger/immich-public-proxy/blob/main/docker-compose.yml) file.
|
1. Download the [docker-compose.yml](https://github.com/alangrainger/immich-public-proxy/blob/main/docker-compose.yml) file.
|
||||||
|
|
||||||
2. Create a `.env` file to configure the app:
|
2. Update the value for `IMMICH_URL` in your docker-compose file to point to your local URL for Immich. This should not be a public URL.
|
||||||
|
|
||||||
```
|
|
||||||
IMMICH_URL=http://localhost:2283
|
|
||||||
PORT=3000
|
|
||||||
CACHE_AGE=2592000
|
|
||||||
```
|
|
||||||
|
|
||||||
- `IMMICH_URL` is the URL to access Immich in your local network. This is not your public URL.
|
|
||||||
- `PORT` is the external port you want for the docker container.
|
|
||||||
- `CACHE_AGE` this is setting the Cache-Control header, to tell the visitor's browser to cache the assets. Set to 0 to disable caching. By default this is 30 days.
|
|
||||||
|
|
||||||
3. Start the docker container:
|
3. Start the docker container:
|
||||||
|
|
||||||
|
@ -102,7 +92,7 @@ If the shared link has expired or any of the assets have been put in the Immich
|
||||||
|
|
||||||
## Additional configuration
|
## Additional configuration
|
||||||
|
|
||||||
The gallery is created using [lightGallery](https://github.com/sachinchoolur/lightGallery). You can adjust various settings to customise how your gallery displays.
|
There are some additional configuration options you can change, for example the way the gallery is set up.
|
||||||
|
|
||||||
1. Make a copy of [config.json](https://github.com/alangrainger/immich-public-proxy/blob/main/app/config.json) in the same folder as your `docker-compose.yml`.
|
1. Make a copy of [config.json](https://github.com/alangrainger/immich-public-proxy/blob/main/app/config.json) in the same folder as your `docker-compose.yml`.
|
||||||
|
|
||||||
|
@ -115,10 +105,13 @@ The gallery is created using [lightGallery](https://github.com/sachinchoolur/lig
|
||||||
|
|
||||||
3. Restart your container and your custom configuration should be active.
|
3. Restart your container and your custom configuration should be active.
|
||||||
|
|
||||||
|
### lightGallery
|
||||||
|
|
||||||
|
The gallery is created using [lightGallery](https://github.com/sachinchoolur/lightGallery).
|
||||||
You can find all of lightGallery's settings here:
|
You can find all of lightGallery's settings here:
|
||||||
https://www.lightgalleryjs.com/docs/settings/
|
https://www.lightgalleryjs.com/docs/settings/
|
||||||
|
|
||||||
For example, to disable the download button for images, you would change `download` to `false`:
|
For example, to disable the download button for images, you would edit the `lightGallery` section and change `download` to `false`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
{
|
{
|
||||||
|
"ipp": {
|
||||||
|
"responseHeaders": {
|
||||||
|
"Cache-Control": "public, max-age=2592000"
|
||||||
|
},
|
||||||
|
"singleImageGallery": false,
|
||||||
|
"singleItemAutoOpen": true
|
||||||
|
},
|
||||||
"lightGallery": {
|
"lightGallery": {
|
||||||
"controls": true,
|
"controls": true,
|
||||||
"download": true,
|
"download": true,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "immich-public-proxy",
|
"name": "immich-public-proxy",
|
||||||
"version": "1.3.6",
|
"version": "1.3.7",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "ts-node src/index.ts",
|
"dev": "ts-node src/index.ts",
|
||||||
"build": "npx tsc",
|
"build": "npx tsc",
|
||||||
|
|
54
app/src/functions.ts
Normal file
54
app/src/functions.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { Request, Response } from 'express-serve-static-core'
|
||||||
|
import { ImageSize } from './types'
|
||||||
|
|
||||||
|
let config = {}
|
||||||
|
try {
|
||||||
|
const configJson = require('../config.json')
|
||||||
|
if (typeof configJson === 'object') config = configJson
|
||||||
|
} catch (e) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a configuration option using dotted notation.
|
||||||
|
*
|
||||||
|
* @param path
|
||||||
|
* @param [defaultOption] - Specify a default option to return if no configuation value is found
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* getConfigOption('ipp.singleImageGallery')
|
||||||
|
*/
|
||||||
|
export const getConfigOption = (path: string, defaultOption?: unknown) => {
|
||||||
|
const value = path.split('.').reduce((obj: { [key: string]: unknown }, key) => (obj || {})[key], config)
|
||||||
|
if (value === undefined) {
|
||||||
|
return defaultOption
|
||||||
|
} else {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output a console.log message with timestamp
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toString (value: unknown) {
|
||||||
|
return typeof value === 'string' ? value : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add response headers from config.json
|
||||||
|
*/
|
||||||
|
export function addResponseHeaders (res: Response) {
|
||||||
|
Object.entries(getConfigOption('ipp.responseHeaders', {}) as { [key: string]: string })
|
||||||
|
.forEach(([header, value]) => {
|
||||||
|
res.set(header, value)
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { Asset, AssetType, ImageSize, IncomingShareRequest, SharedLink, SharedLinkResult } from './types'
|
import { Asset, AssetType, ImageSize, IncomingShareRequest, SharedLink, SharedLinkResult } from './types'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { log } from './index'
|
import { getConfigOption, log } from './functions'
|
||||||
import render from './render'
|
import render from './render'
|
||||||
import { Response } from 'express-serve-static-core'
|
import { Response } from 'express-serve-static-core'
|
||||||
import { encrypt } from './encrypt'
|
import { encrypt } from './encrypt'
|
||||||
|
@ -70,12 +70,13 @@ class Immich {
|
||||||
// This is an individual item (not a gallery)
|
// This is an individual item (not a gallery)
|
||||||
log('Serving link ' + request.key)
|
log('Serving link ' + request.key)
|
||||||
const asset = link.assets[0]
|
const asset = link.assets[0]
|
||||||
if (asset.type === AssetType.image) {
|
if (asset.type === AssetType.image && !getConfigOption('ipp.singleImageGallery')) {
|
||||||
// For photos, output the image directly
|
// For photos, output the image directly unless configured to show a gallery
|
||||||
await render.assetBuffer(res, link.assets[0], request.size)
|
await render.assetBuffer(res, link.assets[0], request.size)
|
||||||
} else if (asset.type === AssetType.video) {
|
} else {
|
||||||
// For videos, show the video as a web player
|
// Show a gallery page
|
||||||
await render.gallery(res, link, 1)
|
const openItem = getConfigOption('ipp.singleItemAutoOpen', true) ? 1 : 0
|
||||||
|
await render.gallery(res, link, openItem)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Multiple images - render as a gallery
|
// Multiple images - render as a gallery
|
||||||
|
|
|
@ -2,19 +2,19 @@ import express from 'express'
|
||||||
import immich from './immich'
|
import immich from './immich'
|
||||||
import render from './render'
|
import render from './render'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { AssetType, ImageSize } from './types'
|
import { AssetType } from './types'
|
||||||
import { Request } from 'express-serve-static-core'
|
|
||||||
import { decrypt } from './encrypt'
|
import { decrypt } from './encrypt'
|
||||||
|
import { log, getSize, toString, addResponseHeaders } from './functions'
|
||||||
|
|
||||||
require('dotenv').config()
|
require('dotenv').config()
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
// Add the EJS view engine, to render the gallery page
|
// Add the EJS view engine, to render the gallery page
|
||||||
app.set('view engine', 'ejs')
|
app.set('view engine', 'ejs')
|
||||||
// Serve static assets from the /public folder
|
|
||||||
app.use(express.static('public'))
|
|
||||||
// For parsing the password unlock form
|
// For parsing the password unlock form
|
||||||
app.use(express.json())
|
app.use(express.json())
|
||||||
|
// Serve static assets from the /public folder
|
||||||
|
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) => {
|
||||||
|
@ -34,7 +34,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', async (req, res) => {
|
||||||
res.set('Cache-Control', 'public, max-age=' + process.env.CACHE_AGE)
|
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)) {
|
||||||
let password
|
let password
|
||||||
|
@ -83,23 +83,6 @@ app.get('*', (req, res) => {
|
||||||
res.status(404).send()
|
res.status(404).send()
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Output a console.log message with timestamp
|
|
||||||
*/
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
const getSize = (req: Request) => {
|
|
||||||
return req.query?.size === 'thumbnail' ? ImageSize.thumbnail : ImageSize.original
|
|
||||||
}
|
|
||||||
|
|
||||||
const toString = (value: unknown) => {
|
|
||||||
return typeof value === 'string' ? value : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
app.listen(3000, () => {
|
app.listen(3000, () => {
|
||||||
console.log(dayjs().format() + ' Server started')
|
console.log(dayjs().format() + ' Server started')
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
import immich from './immich'
|
import immich from './immich'
|
||||||
import { Response } from 'express-serve-static-core'
|
import { Response } from 'express-serve-static-core'
|
||||||
import { Asset, AssetType, ImageSize, SharedLink } from './types'
|
import { Asset, AssetType, ImageSize, SharedLink } from './types'
|
||||||
|
import { getConfigOption } from './functions'
|
||||||
|
|
||||||
class Render {
|
class Render {
|
||||||
lgConfig = {}
|
lgConfig
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
try {
|
this.lgConfig = getConfigOption('lightGallery', {})
|
||||||
// Import user-provided lightGallery config (if exists)
|
|
||||||
const config = require('../config.json')
|
|
||||||
if (typeof config === 'object' && config.lightGallery) this.lgConfig = config.lightGallery
|
|
||||||
} catch (e) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async assetBuffer (res: Response, asset: Asset, size?: ImageSize) {
|
async assetBuffer (res: Response, asset: Asset, size?: ImageSize) {
|
||||||
|
@ -61,7 +58,7 @@ class Render {
|
||||||
items,
|
items,
|
||||||
openItem,
|
openItem,
|
||||||
title: this.title(share),
|
title: this.title(share),
|
||||||
lgConfig: this.lgConfig
|
lgConfig: getConfigOption('lightGallery', {})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,9 @@ services:
|
||||||
container_name: immich-public-proxy
|
container_name: immich-public-proxy
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- ${PORT}:3000
|
- "3000:3000"
|
||||||
env_file: .env
|
environment:
|
||||||
|
- IMMICH_URL=http://localhost:2283
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: wget -q http://localhost:3000/healthcheck || exit 1
|
test: wget -q http://localhost:3000/healthcheck || exit 1
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
|
Loading…
Reference in a new issue