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;
|
||||
}
|
||||
|
||||
#lightgallery a {
|
||||
text-decoration: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#lightgallery img {
|
||||
height: 250px;
|
||||
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'
|
||||
|
||||
class Immich {
|
||||
async request (endpoint: string, json = true) {
|
||||
async request (endpoint: string) {
|
||||
const res = await fetch(process.env.IMMICH_URL + '/api' + endpoint, {
|
||||
headers: {
|
||||
'x-api-key': process.env.API_KEY || ''
|
||||
}
|
||||
})
|
||||
if (res.status === 200) {
|
||||
if (json) {
|
||||
const contentType = res.headers.get('Content-Type') || ''
|
||||
if (contentType.includes('application/json')) {
|
||||
return res.json()
|
||||
} else {
|
||||
return res
|
||||
@ -17,25 +18,38 @@ class Immich {
|
||||
}
|
||||
|
||||
async getShareByKey (key: string) {
|
||||
const links = await this.request('/shared-links') as SharedLink[]
|
||||
return links.find(x => x.key === key)
|
||||
const res = (await this.request('/shared-links') || []) as SharedLink[]
|
||||
return res?.find(x => x.key === key)
|
||||
}
|
||||
|
||||
async getAssetBuffer (asset: Asset, size?: ImageSize) {
|
||||
switch (asset.type) {
|
||||
case AssetType.image:
|
||||
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:
|
||||
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}` : '')
|
||||
}
|
||||
|
||||
videoUrl (id: string) {
|
||||
return `${process.env.SERVER_URL}/video/${id}`
|
||||
}
|
||||
|
||||
const api = new Immich()
|
||||
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}$/)
|
||||
}
|
||||
}
|
||||
|
||||
export default api
|
||||
const immich = new Immich()
|
||||
|
||||
export default immich
|
||||
|
59
src/index.ts
59
src/index.ts
@ -1,9 +1,9 @@
|
||||
import express from 'express'
|
||||
import api from './immich'
|
||||
import immich from './immich'
|
||||
import render from './render'
|
||||
import dayjs from 'dayjs'
|
||||
import { AssetType, ImageSize } from './types'
|
||||
import { Request, Response } from 'express-serve-static-core'
|
||||
import { Request } from 'express-serve-static-core'
|
||||
|
||||
require('dotenv').config()
|
||||
|
||||
@ -11,22 +11,6 @@ const app = express()
|
||||
app.set('view engine', 'ejs')
|
||||
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) => {
|
||||
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
|
||||
res.status(404).send()
|
||||
} else {
|
||||
const share = await api.getShareByKey(req.params.key)
|
||||
const share = await immich.getShareByKey(req.params.key)
|
||||
if (!share || !share.assets.length) {
|
||||
res.status(404).send()
|
||||
} else if (share.assets.length === 1) {
|
||||
// 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 {
|
||||
// Multiple images - render as a gallery
|
||||
res.render('gallery', {
|
||||
photos: share.assets.map(photo => {
|
||||
return {
|
||||
originalUrl: immich.imageUrl(photo.id),
|
||||
thumbnailUrl: immich.imageUrl(photo.id, ImageSize.thumbnail)
|
||||
}
|
||||
})
|
||||
})
|
||||
await render.gallery(res, share.assets, 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/photo/:id', (req, res) => {
|
||||
if (req.params.id.match(/^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$/)) {
|
||||
// Check for photo
|
||||
serveImage(res, req.params.id, getSize(req)).then()
|
||||
} else {
|
||||
// Output the buffer data for an photo or video
|
||||
app.get('/:type(photo|video)/:id', (req, res) => {
|
||||
if (!immich.isId(req.params.id)) {
|
||||
// Invalid characters in the incoming URL
|
||||
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>
|
||||
<body>
|
||||
<div id="lightgallery">
|
||||
<% photos.forEach(photo => { %>
|
||||
<a href="<%- photo.originalUrl %>">
|
||||
<img alt="" src="<%- photo.thumbnailUrl %>" />
|
||||
<% items.forEach(item => {
|
||||
if (item.video) { %>
|
||||
<a data-video='<%- item.video %>'>
|
||||
<img alt="" src="<%- item.thumbnailUrl %>"/>
|
||||
<div class="play-icon"></div>
|
||||
</a>
|
||||
<% }) %>
|
||||
<% } else { %>
|
||||
<a href="<%- item.originalUrl %>">
|
||||
<img alt="" src="<%- item.thumbnailUrl %>"/>
|
||||
</a>
|
||||
<% }
|
||||
}) %>
|
||||
</div>
|
||||
<script src="/lightgallery.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 type="text/javascript">
|
||||
lightGallery(document.getElementById('lightgallery'), {
|
||||
plugins: [lgZoom, lgThumbnail],
|
||||
plugins: [lgZoom, lgThumbnail, lgVideo],
|
||||
licenseKey: '0000-0000-000-0000',
|
||||
speed: 500
|
||||
});
|
||||
})
|
||||
<% if (openItem) { %>
|
||||
const openItem = <%- openItem %>
|
||||
const thumbs = document.querySelectorAll('#lightgallery a')
|
||||
if (thumbs.length >= openItem) {
|
||||
thumbs[openItem - 1].click()
|
||||
}
|
||||
<% } %>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
Reference in New Issue
Block a user