Add video player
This commit is contained in:
parent
7be2508eac
commit
65eeba2df6
BIN
public/images/play.png
Normal file
BIN
public/images/play.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
8
public/lg-video.min.js
vendored
Normal file
8
public/lg-video.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -2,13 +2,32 @@ html {
|
|||||||
background: #191919;
|
background: #191919;
|
||||||
}
|
}
|
||||||
|
|
||||||
#lightgallery a {
|
|
||||||
text-decoration: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#lightgallery img {
|
#lightgallery img {
|
||||||
height: 250px;
|
height: 250px;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#lightgallery a {
|
||||||
|
position: relative; /* Establishes a positioning context */
|
||||||
|
display: inline-block; /* Allows the container to wrap around the image */
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background-image: url('/images/play.png');
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
#lightgallery a:has(.play-icon):hover .play-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { Asset, AssetType, ImageSize, SharedLink } from './types'
|
import { Asset, AssetType, ImageSize, SharedLink } from './types'
|
||||||
|
|
||||||
class Immich {
|
class Immich {
|
||||||
async request (endpoint: string, json = true) {
|
async request (endpoint: string) {
|
||||||
const res = await fetch(process.env.IMMICH_URL + '/api' + endpoint, {
|
const res = await fetch(process.env.IMMICH_URL + '/api' + endpoint, {
|
||||||
headers: {
|
headers: {
|
||||||
'x-api-key': process.env.API_KEY || ''
|
'x-api-key': process.env.API_KEY || ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
if (json) {
|
const contentType = res.headers.get('Content-Type') || ''
|
||||||
|
if (contentType.includes('application/json')) {
|
||||||
return res.json()
|
return res.json()
|
||||||
} else {
|
} else {
|
||||||
return res
|
return res
|
||||||
@ -17,25 +18,38 @@ class Immich {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getShareByKey (key: string) {
|
async getShareByKey (key: string) {
|
||||||
const links = await this.request('/shared-links') as SharedLink[]
|
const res = (await this.request('/shared-links') || []) as SharedLink[]
|
||||||
return links.find(x => x.key === key)
|
return res?.find(x => x.key === key)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAssetBuffer (asset: Asset, size?: ImageSize) {
|
async getAssetBuffer (asset: Asset, size?: ImageSize) {
|
||||||
switch (asset.type) {
|
switch (asset.type) {
|
||||||
case AssetType.image:
|
case AssetType.image:
|
||||||
size = size === ImageSize.thumbnail ? ImageSize.thumbnail : ImageSize.original
|
size = size === ImageSize.thumbnail ? ImageSize.thumbnail : ImageSize.original
|
||||||
return this.request('/assets/' + asset.id + '/' + size, false)
|
return this.request('/assets/' + asset.id + '/' + size)
|
||||||
case AssetType.video:
|
case AssetType.video:
|
||||||
return this.request('/assets/' + asset.id + '/video/playback', false)
|
return this.request('/assets/' + asset.id + '/video/playback')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
imageUrl (id: string, size?: ImageSize) {
|
async getContentType (asset: Asset) {
|
||||||
|
const assetBuffer = await this.getAssetBuffer(asset)
|
||||||
|
return assetBuffer.headers.get('Content-Type')
|
||||||
|
}
|
||||||
|
|
||||||
|
photoUrl (id: string, size?: ImageSize) {
|
||||||
return `${process.env.SERVER_URL}/photo/${id}` + (size ? `?size=${size}` : '')
|
return `${process.env.SERVER_URL}/photo/${id}` + (size ? `?size=${size}` : '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
videoUrl (id: string) {
|
||||||
|
return `${process.env.SERVER_URL}/video/${id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
isId (id: string) {
|
||||||
|
return !!id.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = new Immich()
|
const immich = new Immich()
|
||||||
|
|
||||||
export default api
|
export default immich
|
||||||
|
59
src/index.ts
59
src/index.ts
@ -1,9 +1,9 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import api from './immich'
|
|
||||||
import immich from './immich'
|
import immich from './immich'
|
||||||
|
import render from './render'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { AssetType, ImageSize } from './types'
|
import { AssetType, ImageSize } from './types'
|
||||||
import { Request, Response } from 'express-serve-static-core'
|
import { Request } from 'express-serve-static-core'
|
||||||
|
|
||||||
require('dotenv').config()
|
require('dotenv').config()
|
||||||
|
|
||||||
@ -11,22 +11,6 @@ const app = express()
|
|||||||
app.set('view engine', 'ejs')
|
app.set('view engine', 'ejs')
|
||||||
app.use(express.static('public'))
|
app.use(express.static('public'))
|
||||||
|
|
||||||
async function serveImage (res: Response, id: string, size?: ImageSize) {
|
|
||||||
const image = await api.getAssetBuffer({
|
|
||||||
id,
|
|
||||||
type: AssetType.image
|
|
||||||
}, size)
|
|
||||||
if (image) {
|
|
||||||
for (const header of ['content-type', 'content-length']) {
|
|
||||||
res.set(header, image.headers[header])
|
|
||||||
}
|
|
||||||
console.log(`${dayjs().format()} Serving image ${id}`)
|
|
||||||
res.send(Buffer.from(await image.arrayBuffer()))
|
|
||||||
} else {
|
|
||||||
res.status(404).send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSize = (req: Request) => {
|
const getSize = (req: Request) => {
|
||||||
return req?.query?.size === 'thumbnail' ? ImageSize.thumbnail : ImageSize.original
|
return req?.query?.size === 'thumbnail' ? ImageSize.thumbnail : ImageSize.original
|
||||||
}
|
}
|
||||||
@ -36,33 +20,42 @@ app.get('/share/:key', async (req, res) => {
|
|||||||
// Invalid characters in the incoming URL
|
// Invalid characters in the incoming URL
|
||||||
res.status(404).send()
|
res.status(404).send()
|
||||||
} else {
|
} else {
|
||||||
const share = await api.getShareByKey(req.params.key)
|
const share = await immich.getShareByKey(req.params.key)
|
||||||
if (!share || !share.assets.length) {
|
if (!share || !share.assets.length) {
|
||||||
res.status(404).send()
|
res.status(404).send()
|
||||||
} else if (share.assets.length === 1) {
|
} else if (share.assets.length === 1) {
|
||||||
// This is an individual item (not a gallery)
|
// This is an individual item (not a gallery)
|
||||||
await serveImage(res, share.assets[0].id, getSize(req))
|
const asset = share.assets[0]
|
||||||
|
if (asset.type === AssetType.image) {
|
||||||
|
// Output the image directly
|
||||||
|
await render.assetBuffer(res, share.assets[0], getSize(req))
|
||||||
|
} else if (asset.type === AssetType.video) {
|
||||||
|
// Show the video as a web player
|
||||||
|
await render.gallery(res, share.assets, 1)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Multiple images - render as a gallery
|
// Multiple images - render as a gallery
|
||||||
res.render('gallery', {
|
await render.gallery(res, share.assets, 1)
|
||||||
photos: share.assets.map(photo => {
|
|
||||||
return {
|
|
||||||
originalUrl: immich.imageUrl(photo.id),
|
|
||||||
thumbnailUrl: immich.imageUrl(photo.id, ImageSize.thumbnail)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/photo/:id', (req, res) => {
|
// Output the buffer data for an photo or video
|
||||||
if (req.params.id.match(/^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$/)) {
|
app.get('/:type(photo|video)/:id', (req, res) => {
|
||||||
// Check for photo
|
if (!immich.isId(req.params.id)) {
|
||||||
serveImage(res, req.params.id, getSize(req)).then()
|
|
||||||
} else {
|
|
||||||
// Invalid characters in the incoming URL
|
// Invalid characters in the incoming URL
|
||||||
res.status(404).send()
|
res.status(404).send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const asset = {
|
||||||
|
id: req.params.id,
|
||||||
|
type: req.params.type === 'video' ? AssetType.video : AssetType.image
|
||||||
|
}
|
||||||
|
switch (req.params.type) {
|
||||||
|
case 'photo':
|
||||||
|
case 'video':
|
||||||
|
render.assetBuffer(res, asset, getSize(req)).then()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
54
src/render.ts
Normal file
54
src/render.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import immich from './immich'
|
||||||
|
import { Response } from 'express-serve-static-core'
|
||||||
|
import { Asset, AssetType, ImageSize } from './types'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
class Render {
|
||||||
|
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[header])
|
||||||
|
}
|
||||||
|
console.log(`${dayjs().format()} Serving asset ${asset.id}`)
|
||||||
|
res.send(Buffer.from(await data.arrayBuffer()))
|
||||||
|
} else {
|
||||||
|
res.status(404).send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async gallery (res: Response, assets: Asset[], openItem?: number) {
|
||||||
|
const items = []
|
||||||
|
for (const asset of assets) {
|
||||||
|
let video
|
||||||
|
if (asset.type === AssetType.video) {
|
||||||
|
// Populate the data-video property
|
||||||
|
video = JSON.stringify({
|
||||||
|
source: [
|
||||||
|
{
|
||||||
|
src: immich.videoUrl(asset.id),
|
||||||
|
type: await immich.getContentType(asset)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
attributes: {
|
||||||
|
preload: false,
|
||||||
|
controls: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
items.push({
|
||||||
|
originalUrl: immich.photoUrl(asset.id),
|
||||||
|
thumbnailUrl: immich.photoUrl(asset.id, ImageSize.thumbnail),
|
||||||
|
video
|
||||||
|
})
|
||||||
|
}
|
||||||
|
res.render('gallery', {
|
||||||
|
items,
|
||||||
|
openItem
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const render = new Render()
|
||||||
|
|
||||||
|
export default render
|
@ -7,21 +7,36 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="lightgallery">
|
<div id="lightgallery">
|
||||||
<% photos.forEach(photo => { %>
|
<% items.forEach(item => {
|
||||||
<a href="<%- photo.originalUrl %>">
|
if (item.video) { %>
|
||||||
<img alt="" src="<%- photo.thumbnailUrl %>" />
|
<a data-video='<%- item.video %>'>
|
||||||
|
<img alt="" src="<%- item.thumbnailUrl %>"/>
|
||||||
|
<div class="play-icon"></div>
|
||||||
</a>
|
</a>
|
||||||
<% }) %>
|
<% } else { %>
|
||||||
|
<a href="<%- item.originalUrl %>">
|
||||||
|
<img alt="" src="<%- item.thumbnailUrl %>"/>
|
||||||
|
</a>
|
||||||
|
<% }
|
||||||
|
}) %>
|
||||||
</div>
|
</div>
|
||||||
<script src="/lightgallery.min.js"></script>
|
<script src="/lightgallery.min.js"></script>
|
||||||
<script src="/lg-thumbnail.min.js"></script>
|
<script src="/lg-thumbnail.min.js"></script>
|
||||||
|
<script src="/lg-video.min.js"></script>
|
||||||
<script src="/lg-zoom.min.js"></script>
|
<script src="/lg-zoom.min.js"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
lightGallery(document.getElementById('lightgallery'), {
|
lightGallery(document.getElementById('lightgallery'), {
|
||||||
plugins: [lgZoom, lgThumbnail],
|
plugins: [lgZoom, lgThumbnail, lgVideo],
|
||||||
licenseKey: '0000-0000-000-0000',
|
licenseKey: '0000-0000-000-0000',
|
||||||
speed: 500
|
speed: 500
|
||||||
});
|
})
|
||||||
|
<% if (openItem) { %>
|
||||||
|
const openItem = <%- openItem %>
|
||||||
|
const thumbs = document.querySelectorAll('#lightgallery a')
|
||||||
|
if (thumbs.length >= openItem) {
|
||||||
|
thumbs[openItem - 1].click()
|
||||||
|
}
|
||||||
|
<% } %>
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
Reference in New Issue
Block a user