🚀 Initial commit
This commit is contained in:
commit
371989c0cb
15 changed files with 1612 additions and 0 deletions
32
.dockerignore
Normal file
32
.dockerignore
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Node modules
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# Local environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# TypeScript build output
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Docker files
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*.yml
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# IDE/editor files
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
32
Dockerfile
Normal file
32
Dockerfile
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Build stage
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY pnpm-lock.yaml ./
|
||||||
|
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
RUN pnpm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:20-alpine AS production
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY pnpm-lock.yaml ./
|
||||||
|
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
RUN pnpm install --prod
|
||||||
|
|
||||||
|
COPY --from=build /app/dist ./dist
|
||||||
|
COPY --from=build /app/public ./public
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "."]
|
||||||
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
services:
|
||||||
|
immich-manager:
|
||||||
|
build: .
|
||||||
|
container_name: immich-manager
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- CLIENT_ID
|
||||||
|
- IMMICH_URL
|
||||||
|
- FRAME_URL
|
||||||
|
- IMMICH_TOKEN
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
restart: unless-stopped
|
||||||
26
package.json
Normal file
26
package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "immich-assets-manager",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm --package=typescript dlx tsc",
|
||||||
|
"start": "node ."
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "nathan-lamy",
|
||||||
|
"packageManager": "pnpm@10.13.1",
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/static": "^8.2.0",
|
||||||
|
"fastify": "^5.5.0",
|
||||||
|
"fastify-sse-v2": "^4.2.1",
|
||||||
|
"jose": "^6.1.0",
|
||||||
|
"undici": "^7.15.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"dotenv": "^17.2.1",
|
||||||
|
"@types/node": "^24.3.0",
|
||||||
|
"typescript": "^5.9.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
872
pnpm-lock.yaml
generated
Normal file
872
pnpm-lock.yaml
generated
Normal file
|
|
@ -0,0 +1,872 @@
|
||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
dependencies:
|
||||||
|
'@fastify/static':
|
||||||
|
specifier: ^8.2.0
|
||||||
|
version: 8.2.0
|
||||||
|
fastify:
|
||||||
|
specifier: ^5.5.0
|
||||||
|
version: 5.5.0
|
||||||
|
fastify-sse-v2:
|
||||||
|
specifier: ^4.2.1
|
||||||
|
version: 4.2.1(fastify@5.5.0)
|
||||||
|
jose:
|
||||||
|
specifier: ^6.1.0
|
||||||
|
version: 6.1.0
|
||||||
|
undici:
|
||||||
|
specifier: ^7.15.0
|
||||||
|
version: 7.15.0
|
||||||
|
devDependencies:
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^24.3.0
|
||||||
|
version: 24.3.0
|
||||||
|
dotenv:
|
||||||
|
specifier: ^17.2.1
|
||||||
|
version: 17.2.1
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.9.2
|
||||||
|
version: 5.9.2
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@fastify/accept-negotiator@2.0.1':
|
||||||
|
resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==}
|
||||||
|
|
||||||
|
'@fastify/ajv-compiler@4.0.2':
|
||||||
|
resolution: {integrity: sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==}
|
||||||
|
|
||||||
|
'@fastify/error@4.2.0':
|
||||||
|
resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==}
|
||||||
|
|
||||||
|
'@fastify/fast-json-stringify-compiler@5.0.3':
|
||||||
|
resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==}
|
||||||
|
|
||||||
|
'@fastify/forwarded@3.0.0':
|
||||||
|
resolution: {integrity: sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==}
|
||||||
|
|
||||||
|
'@fastify/merge-json-schemas@0.2.1':
|
||||||
|
resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==}
|
||||||
|
|
||||||
|
'@fastify/proxy-addr@5.0.0':
|
||||||
|
resolution: {integrity: sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==}
|
||||||
|
|
||||||
|
'@fastify/send@4.1.0':
|
||||||
|
resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==}
|
||||||
|
|
||||||
|
'@fastify/static@8.2.0':
|
||||||
|
resolution: {integrity: sha512-PejC/DtT7p1yo3p+W7LiUtLMsV8fEvxAK15sozHy9t8kwo5r0uLYmhV/inURmGz1SkHZFz/8CNtHLPyhKcx4SQ==}
|
||||||
|
|
||||||
|
'@isaacs/balanced-match@4.0.1':
|
||||||
|
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
'@isaacs/brace-expansion@5.0.0':
|
||||||
|
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
'@isaacs/cliui@8.0.2':
|
||||||
|
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
'@lukeed/ms@2.0.2':
|
||||||
|
resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
'@types/node@24.3.0':
|
||||||
|
resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==}
|
||||||
|
|
||||||
|
abstract-logging@2.0.1:
|
||||||
|
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
|
||||||
|
|
||||||
|
ajv-formats@3.0.1:
|
||||||
|
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
|
||||||
|
peerDependencies:
|
||||||
|
ajv: ^8.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
ajv:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
ajv@8.17.1:
|
||||||
|
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
||||||
|
|
||||||
|
ansi-regex@5.0.1:
|
||||||
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
ansi-regex@6.2.0:
|
||||||
|
resolution: {integrity: sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
ansi-styles@4.3.0:
|
||||||
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
ansi-styles@6.2.1:
|
||||||
|
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
atomic-sleep@1.0.0:
|
||||||
|
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
|
avvio@9.1.0:
|
||||||
|
resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==}
|
||||||
|
|
||||||
|
base64-js@1.5.1:
|
||||||
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
|
|
||||||
|
buffer@6.0.3:
|
||||||
|
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||||
|
|
||||||
|
color-convert@2.0.1:
|
||||||
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
|
engines: {node: '>=7.0.0'}
|
||||||
|
|
||||||
|
color-name@1.1.4:
|
||||||
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
content-disposition@0.5.4:
|
||||||
|
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
cookie@1.0.2:
|
||||||
|
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
cross-spawn@7.0.6:
|
||||||
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
depd@2.0.0:
|
||||||
|
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
dequal@2.0.3:
|
||||||
|
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
dotenv@17.2.1:
|
||||||
|
resolution: {integrity: sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
eastasianwidth@0.2.0:
|
||||||
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
|
emoji-regex@8.0.0:
|
||||||
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
|
emoji-regex@9.2.2:
|
||||||
|
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||||
|
|
||||||
|
escape-html@1.0.3:
|
||||||
|
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||||
|
|
||||||
|
fast-decode-uri-component@1.0.1:
|
||||||
|
resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
|
||||||
|
|
||||||
|
fast-deep-equal@3.1.3:
|
||||||
|
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||||
|
|
||||||
|
fast-fifo@1.3.2:
|
||||||
|
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
|
||||||
|
|
||||||
|
fast-json-stringify@6.0.1:
|
||||||
|
resolution: {integrity: sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg==}
|
||||||
|
|
||||||
|
fast-querystring@1.1.2:
|
||||||
|
resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==}
|
||||||
|
|
||||||
|
fast-redact@3.5.0:
|
||||||
|
resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
fast-uri@3.1.0:
|
||||||
|
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
|
||||||
|
|
||||||
|
fastify-plugin@4.5.1:
|
||||||
|
resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==}
|
||||||
|
|
||||||
|
fastify-plugin@5.0.1:
|
||||||
|
resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==}
|
||||||
|
|
||||||
|
fastify-sse-v2@4.2.1:
|
||||||
|
resolution: {integrity: sha512-VDUgAUpu+v+WsbAjcdiG7yB9zHhL64gHSN3mneiXB12nsC2QlCw+e0W97BfLApbSYcEgEz4J+1dqO2YmVqRbRw==}
|
||||||
|
peerDependencies:
|
||||||
|
fastify: '>=4'
|
||||||
|
|
||||||
|
fastify@5.5.0:
|
||||||
|
resolution: {integrity: sha512-ZWSWlzj3K/DcULCnCjEiC2zn2FBPdlZsSA/pnPa/dbUfLvxkD/Nqmb0XXMXLrWkeM4uQPUvjdJpwtXmTfriXqw==}
|
||||||
|
|
||||||
|
fastq@1.19.1:
|
||||||
|
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
|
||||||
|
|
||||||
|
find-my-way@9.3.0:
|
||||||
|
resolution: {integrity: sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
foreground-child@3.3.1:
|
||||||
|
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
get-iterator@1.0.2:
|
||||||
|
resolution: {integrity: sha512-v+dm9bNVfOYsY1OrhaCrmyOcYoSeVvbt+hHZ0Au+T+p1y+0Uyj9aMaGIeUTT6xdpRbWzDeYKvfOslPhggQMcsg==}
|
||||||
|
|
||||||
|
glob@11.0.3:
|
||||||
|
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
http-errors@2.0.0:
|
||||||
|
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
ieee754@1.2.1:
|
||||||
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
|
|
||||||
|
inherits@2.0.4:
|
||||||
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
|
||||||
|
ipaddr.js@2.2.0:
|
||||||
|
resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0:
|
||||||
|
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
isexe@2.0.0:
|
||||||
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
|
it-pushable@1.4.2:
|
||||||
|
resolution: {integrity: sha512-vVPu0CGRsTI8eCfhMknA7KIBqqGFolbRx+1mbQ6XuZ7YCz995Qj7L4XUviwClFunisDq96FdxzF5FnAbw15afg==}
|
||||||
|
|
||||||
|
it-to-stream@1.0.0:
|
||||||
|
resolution: {integrity: sha512-pLULMZMAB/+vbdvbZtebC0nWBTbG581lk6w8P7DfIIIKUfa8FbY7Oi0FxZcFPbxvISs7A9E+cMpLDBc1XhpAOA==}
|
||||||
|
|
||||||
|
jackspeak@4.1.1:
|
||||||
|
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
jose@6.1.0:
|
||||||
|
resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==}
|
||||||
|
|
||||||
|
json-schema-ref-resolver@2.0.1:
|
||||||
|
resolution: {integrity: sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q==}
|
||||||
|
|
||||||
|
json-schema-traverse@1.0.0:
|
||||||
|
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||||
|
|
||||||
|
light-my-request@6.6.0:
|
||||||
|
resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
|
||||||
|
|
||||||
|
lru-cache@11.1.0:
|
||||||
|
resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
mime@3.0.0:
|
||||||
|
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
minimatch@10.0.3:
|
||||||
|
resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
minipass@7.1.2:
|
||||||
|
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||||
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
|
on-exit-leak-free@2.1.2:
|
||||||
|
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
|
p-defer@3.0.0:
|
||||||
|
resolution: {integrity: sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
p-fifo@1.0.0:
|
||||||
|
resolution: {integrity: sha512-IjoCxXW48tqdtDFz6fqo5q1UfFVjjVZe8TC1QRflvNUJtNfCUhxOUw6MOVZhDPjqhSzc26xKdugsO17gmzd5+A==}
|
||||||
|
|
||||||
|
package-json-from-dist@1.0.1:
|
||||||
|
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||||
|
|
||||||
|
path-key@3.1.1:
|
||||||
|
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
path-scurry@2.0.0:
|
||||||
|
resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
pino-abstract-transport@2.0.0:
|
||||||
|
resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
|
||||||
|
|
||||||
|
pino-std-serializers@7.0.0:
|
||||||
|
resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
|
||||||
|
|
||||||
|
pino@9.9.0:
|
||||||
|
resolution: {integrity: sha512-zxsRIQG9HzG+jEljmvmZupOMDUQ0Jpj0yAgE28jQvvrdYTlEaiGwelJpdndMl/MBuRr70heIj83QyqJUWaU8mQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
process-warning@4.0.1:
|
||||||
|
resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==}
|
||||||
|
|
||||||
|
process-warning@5.0.0:
|
||||||
|
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
|
||||||
|
|
||||||
|
quick-format-unescaped@4.0.4:
|
||||||
|
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||||
|
|
||||||
|
readable-stream@3.6.2:
|
||||||
|
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
real-require@0.2.0:
|
||||||
|
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
||||||
|
engines: {node: '>= 12.13.0'}
|
||||||
|
|
||||||
|
require-from-string@2.0.2:
|
||||||
|
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
ret@0.5.0:
|
||||||
|
resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
reusify@1.1.0:
|
||||||
|
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||||
|
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||||
|
|
||||||
|
rfdc@1.4.1:
|
||||||
|
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||||
|
|
||||||
|
safe-buffer@5.2.1:
|
||||||
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
|
||||||
|
safe-regex2@5.0.0:
|
||||||
|
resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==}
|
||||||
|
|
||||||
|
safe-stable-stringify@2.5.0:
|
||||||
|
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
secure-json-parse@4.0.0:
|
||||||
|
resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==}
|
||||||
|
|
||||||
|
semver@7.7.2:
|
||||||
|
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
set-cookie-parser@2.7.1:
|
||||||
|
resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
|
||||||
|
|
||||||
|
setprototypeof@1.2.0:
|
||||||
|
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||||
|
|
||||||
|
shebang-command@2.0.0:
|
||||||
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
shebang-regex@3.0.0:
|
||||||
|
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
signal-exit@4.1.0:
|
||||||
|
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
sonic-boom@4.2.0:
|
||||||
|
resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
|
||||||
|
|
||||||
|
split2@4.2.0:
|
||||||
|
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||||
|
engines: {node: '>= 10.x'}
|
||||||
|
|
||||||
|
statuses@2.0.1:
|
||||||
|
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
string-width@5.1.2:
|
||||||
|
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
string_decoder@1.3.0:
|
||||||
|
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||||
|
|
||||||
|
strip-ansi@6.0.1:
|
||||||
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
strip-ansi@7.1.0:
|
||||||
|
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
thread-stream@3.1.0:
|
||||||
|
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
||||||
|
|
||||||
|
toad-cache@3.7.0:
|
||||||
|
resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
toidentifier@1.0.1:
|
||||||
|
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||||
|
engines: {node: '>=0.6'}
|
||||||
|
|
||||||
|
typescript@5.9.2:
|
||||||
|
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
undici-types@7.10.0:
|
||||||
|
resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==}
|
||||||
|
|
||||||
|
undici@7.15.0:
|
||||||
|
resolution: {integrity: sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==}
|
||||||
|
engines: {node: '>=20.18.1'}
|
||||||
|
|
||||||
|
util-deprecate@1.0.2:
|
||||||
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
|
which@2.0.2:
|
||||||
|
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
wrap-ansi@7.0.0:
|
||||||
|
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
wrap-ansi@8.1.0:
|
||||||
|
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@fastify/accept-negotiator@2.0.1': {}
|
||||||
|
|
||||||
|
'@fastify/ajv-compiler@4.0.2':
|
||||||
|
dependencies:
|
||||||
|
ajv: 8.17.1
|
||||||
|
ajv-formats: 3.0.1(ajv@8.17.1)
|
||||||
|
fast-uri: 3.1.0
|
||||||
|
|
||||||
|
'@fastify/error@4.2.0': {}
|
||||||
|
|
||||||
|
'@fastify/fast-json-stringify-compiler@5.0.3':
|
||||||
|
dependencies:
|
||||||
|
fast-json-stringify: 6.0.1
|
||||||
|
|
||||||
|
'@fastify/forwarded@3.0.0': {}
|
||||||
|
|
||||||
|
'@fastify/merge-json-schemas@0.2.1':
|
||||||
|
dependencies:
|
||||||
|
dequal: 2.0.3
|
||||||
|
|
||||||
|
'@fastify/proxy-addr@5.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@fastify/forwarded': 3.0.0
|
||||||
|
ipaddr.js: 2.2.0
|
||||||
|
|
||||||
|
'@fastify/send@4.1.0':
|
||||||
|
dependencies:
|
||||||
|
'@lukeed/ms': 2.0.2
|
||||||
|
escape-html: 1.0.3
|
||||||
|
fast-decode-uri-component: 1.0.1
|
||||||
|
http-errors: 2.0.0
|
||||||
|
mime: 3.0.0
|
||||||
|
|
||||||
|
'@fastify/static@8.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@fastify/accept-negotiator': 2.0.1
|
||||||
|
'@fastify/send': 4.1.0
|
||||||
|
content-disposition: 0.5.4
|
||||||
|
fastify-plugin: 5.0.1
|
||||||
|
fastq: 1.19.1
|
||||||
|
glob: 11.0.3
|
||||||
|
|
||||||
|
'@isaacs/balanced-match@4.0.1': {}
|
||||||
|
|
||||||
|
'@isaacs/brace-expansion@5.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@isaacs/balanced-match': 4.0.1
|
||||||
|
|
||||||
|
'@isaacs/cliui@8.0.2':
|
||||||
|
dependencies:
|
||||||
|
string-width: 5.1.2
|
||||||
|
string-width-cjs: string-width@4.2.3
|
||||||
|
strip-ansi: 7.1.0
|
||||||
|
strip-ansi-cjs: strip-ansi@6.0.1
|
||||||
|
wrap-ansi: 8.1.0
|
||||||
|
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||||
|
|
||||||
|
'@lukeed/ms@2.0.2': {}
|
||||||
|
|
||||||
|
'@types/node@24.3.0':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 7.10.0
|
||||||
|
|
||||||
|
abstract-logging@2.0.1: {}
|
||||||
|
|
||||||
|
ajv-formats@3.0.1(ajv@8.17.1):
|
||||||
|
optionalDependencies:
|
||||||
|
ajv: 8.17.1
|
||||||
|
|
||||||
|
ajv@8.17.1:
|
||||||
|
dependencies:
|
||||||
|
fast-deep-equal: 3.1.3
|
||||||
|
fast-uri: 3.1.0
|
||||||
|
json-schema-traverse: 1.0.0
|
||||||
|
require-from-string: 2.0.2
|
||||||
|
|
||||||
|
ansi-regex@5.0.1: {}
|
||||||
|
|
||||||
|
ansi-regex@6.2.0: {}
|
||||||
|
|
||||||
|
ansi-styles@4.3.0:
|
||||||
|
dependencies:
|
||||||
|
color-convert: 2.0.1
|
||||||
|
|
||||||
|
ansi-styles@6.2.1: {}
|
||||||
|
|
||||||
|
atomic-sleep@1.0.0: {}
|
||||||
|
|
||||||
|
avvio@9.1.0:
|
||||||
|
dependencies:
|
||||||
|
'@fastify/error': 4.2.0
|
||||||
|
fastq: 1.19.1
|
||||||
|
|
||||||
|
base64-js@1.5.1: {}
|
||||||
|
|
||||||
|
buffer@6.0.3:
|
||||||
|
dependencies:
|
||||||
|
base64-js: 1.5.1
|
||||||
|
ieee754: 1.2.1
|
||||||
|
|
||||||
|
color-convert@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
color-name: 1.1.4
|
||||||
|
|
||||||
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
content-disposition@0.5.4:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
cookie@1.0.2: {}
|
||||||
|
|
||||||
|
cross-spawn@7.0.6:
|
||||||
|
dependencies:
|
||||||
|
path-key: 3.1.1
|
||||||
|
shebang-command: 2.0.0
|
||||||
|
which: 2.0.2
|
||||||
|
|
||||||
|
depd@2.0.0: {}
|
||||||
|
|
||||||
|
dequal@2.0.3: {}
|
||||||
|
|
||||||
|
dotenv@17.2.1: {}
|
||||||
|
|
||||||
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
||||||
|
emoji-regex@8.0.0: {}
|
||||||
|
|
||||||
|
emoji-regex@9.2.2: {}
|
||||||
|
|
||||||
|
escape-html@1.0.3: {}
|
||||||
|
|
||||||
|
fast-decode-uri-component@1.0.1: {}
|
||||||
|
|
||||||
|
fast-deep-equal@3.1.3: {}
|
||||||
|
|
||||||
|
fast-fifo@1.3.2: {}
|
||||||
|
|
||||||
|
fast-json-stringify@6.0.1:
|
||||||
|
dependencies:
|
||||||
|
'@fastify/merge-json-schemas': 0.2.1
|
||||||
|
ajv: 8.17.1
|
||||||
|
ajv-formats: 3.0.1(ajv@8.17.1)
|
||||||
|
fast-uri: 3.1.0
|
||||||
|
json-schema-ref-resolver: 2.0.1
|
||||||
|
rfdc: 1.4.1
|
||||||
|
|
||||||
|
fast-querystring@1.1.2:
|
||||||
|
dependencies:
|
||||||
|
fast-decode-uri-component: 1.0.1
|
||||||
|
|
||||||
|
fast-redact@3.5.0: {}
|
||||||
|
|
||||||
|
fast-uri@3.1.0: {}
|
||||||
|
|
||||||
|
fastify-plugin@4.5.1: {}
|
||||||
|
|
||||||
|
fastify-plugin@5.0.1: {}
|
||||||
|
|
||||||
|
fastify-sse-v2@4.2.1(fastify@5.5.0):
|
||||||
|
dependencies:
|
||||||
|
fastify: 5.5.0
|
||||||
|
fastify-plugin: 4.5.1
|
||||||
|
it-pushable: 1.4.2
|
||||||
|
it-to-stream: 1.0.0
|
||||||
|
|
||||||
|
fastify@5.5.0:
|
||||||
|
dependencies:
|
||||||
|
'@fastify/ajv-compiler': 4.0.2
|
||||||
|
'@fastify/error': 4.2.0
|
||||||
|
'@fastify/fast-json-stringify-compiler': 5.0.3
|
||||||
|
'@fastify/proxy-addr': 5.0.0
|
||||||
|
abstract-logging: 2.0.1
|
||||||
|
avvio: 9.1.0
|
||||||
|
fast-json-stringify: 6.0.1
|
||||||
|
find-my-way: 9.3.0
|
||||||
|
light-my-request: 6.6.0
|
||||||
|
pino: 9.9.0
|
||||||
|
process-warning: 5.0.0
|
||||||
|
rfdc: 1.4.1
|
||||||
|
secure-json-parse: 4.0.0
|
||||||
|
semver: 7.7.2
|
||||||
|
toad-cache: 3.7.0
|
||||||
|
|
||||||
|
fastq@1.19.1:
|
||||||
|
dependencies:
|
||||||
|
reusify: 1.1.0
|
||||||
|
|
||||||
|
find-my-way@9.3.0:
|
||||||
|
dependencies:
|
||||||
|
fast-deep-equal: 3.1.3
|
||||||
|
fast-querystring: 1.1.2
|
||||||
|
safe-regex2: 5.0.0
|
||||||
|
|
||||||
|
foreground-child@3.3.1:
|
||||||
|
dependencies:
|
||||||
|
cross-spawn: 7.0.6
|
||||||
|
signal-exit: 4.1.0
|
||||||
|
|
||||||
|
get-iterator@1.0.2: {}
|
||||||
|
|
||||||
|
glob@11.0.3:
|
||||||
|
dependencies:
|
||||||
|
foreground-child: 3.3.1
|
||||||
|
jackspeak: 4.1.1
|
||||||
|
minimatch: 10.0.3
|
||||||
|
minipass: 7.1.2
|
||||||
|
package-json-from-dist: 1.0.1
|
||||||
|
path-scurry: 2.0.0
|
||||||
|
|
||||||
|
http-errors@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
depd: 2.0.0
|
||||||
|
inherits: 2.0.4
|
||||||
|
setprototypeof: 1.2.0
|
||||||
|
statuses: 2.0.1
|
||||||
|
toidentifier: 1.0.1
|
||||||
|
|
||||||
|
ieee754@1.2.1: {}
|
||||||
|
|
||||||
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
|
ipaddr.js@2.2.0: {}
|
||||||
|
|
||||||
|
is-fullwidth-code-point@3.0.0: {}
|
||||||
|
|
||||||
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
|
it-pushable@1.4.2:
|
||||||
|
dependencies:
|
||||||
|
fast-fifo: 1.3.2
|
||||||
|
|
||||||
|
it-to-stream@1.0.0:
|
||||||
|
dependencies:
|
||||||
|
buffer: 6.0.3
|
||||||
|
fast-fifo: 1.3.2
|
||||||
|
get-iterator: 1.0.2
|
||||||
|
p-defer: 3.0.0
|
||||||
|
p-fifo: 1.0.0
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
|
jackspeak@4.1.1:
|
||||||
|
dependencies:
|
||||||
|
'@isaacs/cliui': 8.0.2
|
||||||
|
|
||||||
|
jose@6.1.0: {}
|
||||||
|
|
||||||
|
json-schema-ref-resolver@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
dequal: 2.0.3
|
||||||
|
|
||||||
|
json-schema-traverse@1.0.0: {}
|
||||||
|
|
||||||
|
light-my-request@6.6.0:
|
||||||
|
dependencies:
|
||||||
|
cookie: 1.0.2
|
||||||
|
process-warning: 4.0.1
|
||||||
|
set-cookie-parser: 2.7.1
|
||||||
|
|
||||||
|
lru-cache@11.1.0: {}
|
||||||
|
|
||||||
|
mime@3.0.0: {}
|
||||||
|
|
||||||
|
minimatch@10.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@isaacs/brace-expansion': 5.0.0
|
||||||
|
|
||||||
|
minipass@7.1.2: {}
|
||||||
|
|
||||||
|
on-exit-leak-free@2.1.2: {}
|
||||||
|
|
||||||
|
p-defer@3.0.0: {}
|
||||||
|
|
||||||
|
p-fifo@1.0.0:
|
||||||
|
dependencies:
|
||||||
|
fast-fifo: 1.3.2
|
||||||
|
p-defer: 3.0.0
|
||||||
|
|
||||||
|
package-json-from-dist@1.0.1: {}
|
||||||
|
|
||||||
|
path-key@3.1.1: {}
|
||||||
|
|
||||||
|
path-scurry@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
lru-cache: 11.1.0
|
||||||
|
minipass: 7.1.2
|
||||||
|
|
||||||
|
pino-abstract-transport@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
split2: 4.2.0
|
||||||
|
|
||||||
|
pino-std-serializers@7.0.0: {}
|
||||||
|
|
||||||
|
pino@9.9.0:
|
||||||
|
dependencies:
|
||||||
|
atomic-sleep: 1.0.0
|
||||||
|
fast-redact: 3.5.0
|
||||||
|
on-exit-leak-free: 2.1.2
|
||||||
|
pino-abstract-transport: 2.0.0
|
||||||
|
pino-std-serializers: 7.0.0
|
||||||
|
process-warning: 5.0.0
|
||||||
|
quick-format-unescaped: 4.0.4
|
||||||
|
real-require: 0.2.0
|
||||||
|
safe-stable-stringify: 2.5.0
|
||||||
|
sonic-boom: 4.2.0
|
||||||
|
thread-stream: 3.1.0
|
||||||
|
|
||||||
|
process-warning@4.0.1: {}
|
||||||
|
|
||||||
|
process-warning@5.0.0: {}
|
||||||
|
|
||||||
|
quick-format-unescaped@4.0.4: {}
|
||||||
|
|
||||||
|
readable-stream@3.6.2:
|
||||||
|
dependencies:
|
||||||
|
inherits: 2.0.4
|
||||||
|
string_decoder: 1.3.0
|
||||||
|
util-deprecate: 1.0.2
|
||||||
|
|
||||||
|
real-require@0.2.0: {}
|
||||||
|
|
||||||
|
require-from-string@2.0.2: {}
|
||||||
|
|
||||||
|
ret@0.5.0: {}
|
||||||
|
|
||||||
|
reusify@1.1.0: {}
|
||||||
|
|
||||||
|
rfdc@1.4.1: {}
|
||||||
|
|
||||||
|
safe-buffer@5.2.1: {}
|
||||||
|
|
||||||
|
safe-regex2@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
ret: 0.5.0
|
||||||
|
|
||||||
|
safe-stable-stringify@2.5.0: {}
|
||||||
|
|
||||||
|
secure-json-parse@4.0.0: {}
|
||||||
|
|
||||||
|
semver@7.7.2: {}
|
||||||
|
|
||||||
|
set-cookie-parser@2.7.1: {}
|
||||||
|
|
||||||
|
setprototypeof@1.2.0: {}
|
||||||
|
|
||||||
|
shebang-command@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
shebang-regex: 3.0.0
|
||||||
|
|
||||||
|
shebang-regex@3.0.0: {}
|
||||||
|
|
||||||
|
signal-exit@4.1.0: {}
|
||||||
|
|
||||||
|
sonic-boom@4.2.0:
|
||||||
|
dependencies:
|
||||||
|
atomic-sleep: 1.0.0
|
||||||
|
|
||||||
|
split2@4.2.0: {}
|
||||||
|
|
||||||
|
statuses@2.0.1: {}
|
||||||
|
|
||||||
|
string-width@4.2.3:
|
||||||
|
dependencies:
|
||||||
|
emoji-regex: 8.0.0
|
||||||
|
is-fullwidth-code-point: 3.0.0
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
string-width@5.1.2:
|
||||||
|
dependencies:
|
||||||
|
eastasianwidth: 0.2.0
|
||||||
|
emoji-regex: 9.2.2
|
||||||
|
strip-ansi: 7.1.0
|
||||||
|
|
||||||
|
string_decoder@1.3.0:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
strip-ansi@6.0.1:
|
||||||
|
dependencies:
|
||||||
|
ansi-regex: 5.0.1
|
||||||
|
|
||||||
|
strip-ansi@7.1.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-regex: 6.2.0
|
||||||
|
|
||||||
|
thread-stream@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
real-require: 0.2.0
|
||||||
|
|
||||||
|
toad-cache@3.7.0: {}
|
||||||
|
|
||||||
|
toidentifier@1.0.1: {}
|
||||||
|
|
||||||
|
typescript@5.9.2: {}
|
||||||
|
|
||||||
|
undici-types@7.10.0: {}
|
||||||
|
|
||||||
|
undici@7.15.0: {}
|
||||||
|
|
||||||
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
|
which@2.0.2:
|
||||||
|
dependencies:
|
||||||
|
isexe: 2.0.0
|
||||||
|
|
||||||
|
wrap-ansi@7.0.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
|
wrap-ansi@8.1.0:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 6.2.1
|
||||||
|
string-width: 5.1.2
|
||||||
|
strip-ansi: 7.1.0
|
||||||
72
public/index.html
Normal file
72
public/index.html
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Immich Asset Manager</title>
|
||||||
|
<script
|
||||||
|
defer
|
||||||
|
src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.13.3/cdn.min.js"
|
||||||
|
></script>
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
</head>
|
||||||
|
<body x-data="photoManager()">
|
||||||
|
<div class="header">
|
||||||
|
<h1>📸 Immich Asset Manager</h1>
|
||||||
|
<div class="controls">
|
||||||
|
<button
|
||||||
|
class="control-btn"
|
||||||
|
:class="{ 'active': isPlaying }"
|
||||||
|
@click="togglePlayback()"
|
||||||
|
:title="isPlaying ? 'Pause slideshow' : 'Play slideshow'"
|
||||||
|
>
|
||||||
|
<span x-show="!isPlaying">▶️</span>
|
||||||
|
<span x-show="isPlaying">⏸️</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="photo-grid">
|
||||||
|
<template x-if="photos.length === 0">
|
||||||
|
<div class="empty-state">
|
||||||
|
<h2>No photos yet</h2>
|
||||||
|
<p>Photos will appear here when displayed on your Immich Frame</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-for="photo in getVisiblePhotos()" :key="photo.id">
|
||||||
|
<div class="photo-card">
|
||||||
|
<div class="photo-container">
|
||||||
|
<img
|
||||||
|
:src="photo.url"
|
||||||
|
:alt="'Photo from ' + photo.displayDate"
|
||||||
|
@error="handleImageError($event)"
|
||||||
|
/>
|
||||||
|
<div class="photo-overlay">
|
||||||
|
<div class="photo-actions">
|
||||||
|
<button
|
||||||
|
class="action-btn"
|
||||||
|
@click="openInImmich(photo.immichUrl)"
|
||||||
|
>
|
||||||
|
📱 Open in Immich
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="action-btn delete-btn"
|
||||||
|
@click="deletePhoto(photo.id)"
|
||||||
|
>
|
||||||
|
🗑️ Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="photo-info">
|
||||||
|
<div class="photo-date">
|
||||||
|
<span>🖼️ Displayed on</span> <span x-text="photo.date"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
59
public/main.js
Normal file
59
public/main.js
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
function photoManager() {
|
||||||
|
return {
|
||||||
|
photos: [],
|
||||||
|
isPlaying: false,
|
||||||
|
// Default with immich frame
|
||||||
|
hiddenPhotosCount: 5,
|
||||||
|
maxPhotosToShow: 25,
|
||||||
|
|
||||||
|
getVisiblePhotos() {
|
||||||
|
return this.photos.length > this.hiddenPhotosCount
|
||||||
|
? this.photos.slice(this.hiddenPhotosCount, this.photos.length)
|
||||||
|
: this.photos;
|
||||||
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const eventSource = new EventSource("/api/photos");
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
if (event.data.includes("Heartbeat")) return;
|
||||||
|
// Parse the incoming photo data
|
||||||
|
const newPhoto = JSON.parse(event.data);
|
||||||
|
this.photos.push(newPhoto);
|
||||||
|
this.photos.sort((a, b) => new Date(b.rawDate) - new Date(a.rawDate));
|
||||||
|
// Limit the number of photos to maxPhotosToShow + hiddenPhotosCount
|
||||||
|
const max = this.maxPhotosToShow + this.hiddenPhotosCount;
|
||||||
|
if (this.photos.length > max) {
|
||||||
|
// Remove oldest photos
|
||||||
|
this.photos.splice(max, this.photos.length - max);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
togglePlayback() {
|
||||||
|
fetch("/api/pause", {
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to toggle playback", err);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.isPlaying = !this.isPlaying;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deletePhoto(photoId) {
|
||||||
|
if (confirm("Are you sure you want to delete this photo?")) {
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openInImmich(photoUrl) {
|
||||||
|
window.open(photoUrl, "_blank");
|
||||||
|
},
|
||||||
|
|
||||||
|
handleImageError(event) {
|
||||||
|
event.target.src =
|
||||||
|
"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjBmMGYwIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtc2l6ZT0iMTQiIGZpbGw9IiM5OTkiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj5JbWFnZSBub3QgZm91bmQ8L3RleHQ+PC9zdmc+";
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
200
public/styles.css
Normal file
200
public/styles.css
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
||||||
|
Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 300;
|
||||||
|
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.active {
|
||||||
|
background: rgba(16, 172, 132, 0.8);
|
||||||
|
border-color: #10ac84;
|
||||||
|
box-shadow: 0 4px 15px rgba(16, 172, 132, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-card {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-container {
|
||||||
|
position: relative;
|
||||||
|
height: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-container img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-container:hover img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
rgba(102, 126, 234, 0.8),
|
||||||
|
rgba(118, 75, 162, 0.8)
|
||||||
|
);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-container:hover .photo-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 2px solid white;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
background: white;
|
||||||
|
color: #667eea;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
border-color: #ff6b6b;
|
||||||
|
background: rgba(255, 107, 107, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-info {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-date {
|
||||||
|
color: #2c3e50;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
padding: 60px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.photo-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/constants.ts
Normal file
13
src/constants.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
export const APP_PORT = 3000;
|
||||||
|
|
||||||
|
export const CLIENT_ID = process.env.CLIENT_ID;
|
||||||
|
export const EVENT_NAME = "ImageRequestedNotification";
|
||||||
|
|
||||||
|
export const MAX_QUEUE_SIZE = 25;
|
||||||
|
export const SIGNATURE_EXPIRY = "1h";
|
||||||
|
|
||||||
|
export const IMMICH_URL = process.env.IMMICH_URL;
|
||||||
|
export const IMMICH_TOKEN = process.env.IMMICH_TOKEN;
|
||||||
|
export const FRAME_URL = process.env.FRAME_URL;
|
||||||
|
|
||||||
|
export const LOCALE = process.env.LOCALE || "en-US";
|
||||||
165
src/index.ts
Normal file
165
src/index.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import Fastify from "fastify";
|
||||||
|
import fastifyStatic from "@fastify/static";
|
||||||
|
import { FastifySSEPlugin } from "fastify-sse-v2";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
import FixedQueue from "./queue.js";
|
||||||
|
import {
|
||||||
|
__dirname,
|
||||||
|
fetchPhoto,
|
||||||
|
formatDate,
|
||||||
|
generatePhotoUrl,
|
||||||
|
generateSignature,
|
||||||
|
togglePause,
|
||||||
|
verifySignature,
|
||||||
|
} from "./utils.js";
|
||||||
|
import {
|
||||||
|
APP_PORT,
|
||||||
|
CLIENT_ID,
|
||||||
|
EVENT_NAME,
|
||||||
|
MAX_QUEUE_SIZE,
|
||||||
|
} from "./constants.js";
|
||||||
|
import { Photo } from "./types.js";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
|
const app = Fastify();
|
||||||
|
const queue = new FixedQueue<Photo>(MAX_QUEUE_SIZE);
|
||||||
|
|
||||||
|
// Keep track of SSE clients
|
||||||
|
let clients: Array<{
|
||||||
|
reply: Fastify.FastifyReply;
|
||||||
|
id: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
app.register(fastifyStatic, {
|
||||||
|
root: join(__dirname, "..", "public"),
|
||||||
|
prefix: "/", // Serve at root
|
||||||
|
decorateReply: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
app.register(FastifySSEPlugin);
|
||||||
|
|
||||||
|
app.post("/api", async (req, res) => {
|
||||||
|
const { Name, ClientIdentifier, RequestedImageId, DateTime } =
|
||||||
|
req.body as any;
|
||||||
|
// Send empty response for other events or clients
|
||||||
|
if (Name !== EVENT_NAME || ClientIdentifier !== CLIENT_ID) {
|
||||||
|
return res.send({});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the RequestedImageId to the queue
|
||||||
|
const newPhoto: Photo = {
|
||||||
|
id: RequestedImageId,
|
||||||
|
date: new Date(DateTime),
|
||||||
|
};
|
||||||
|
|
||||||
|
queue.push(newPhoto);
|
||||||
|
|
||||||
|
// Notify all SSE clients
|
||||||
|
const signature = await generateSignature(newPhoto.id);
|
||||||
|
const data = JSON.stringify({
|
||||||
|
...newPhoto,
|
||||||
|
url: `/photos/${newPhoto.id}?signature=${signature}`,
|
||||||
|
immichUrl: generatePhotoUrl(newPhoto.id),
|
||||||
|
date: formatDate(newPhoto.date),
|
||||||
|
rawDate: newPhoto.date.toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const client of clients) {
|
||||||
|
try {
|
||||||
|
client.reply.sse({ data });
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to send to client ${client.id}`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Serve photos with a signed URL
|
||||||
|
app.get("/photos/:immichId", async (req, res) => {
|
||||||
|
const { immichId } = req.params as { immichId: string };
|
||||||
|
const signature = (req.query as any).signature as string;
|
||||||
|
|
||||||
|
if (!signature) {
|
||||||
|
return res.status(400).send("Missing signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the signature
|
||||||
|
const verifiedId = await verifySignature(signature);
|
||||||
|
if (verifiedId !== immichId) {
|
||||||
|
return res.status(403).send("Invalid signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to Immich server
|
||||||
|
const photo = await fetchPhoto(immichId);
|
||||||
|
// Set caching headers for 1 hour
|
||||||
|
res.header("Cache-Control", "public, max-age=3600, immutable");
|
||||||
|
res.header("Content-Type", "image/jpeg");
|
||||||
|
res.send(photo);
|
||||||
|
});
|
||||||
|
|
||||||
|
// SSE endpoint for photo updates
|
||||||
|
app.get("/api/photos", async (req, reply) => {
|
||||||
|
// Add client to SSE clients array
|
||||||
|
const clientId = randomUUID();
|
||||||
|
clients.push({
|
||||||
|
reply,
|
||||||
|
id: clientId,
|
||||||
|
});
|
||||||
|
console.log(`Client ${clientId} connected. Total clients: ${clients.length}`);
|
||||||
|
|
||||||
|
req.raw.on("close", () => removeClient(clientId));
|
||||||
|
req.raw.on("error", () => removeClient(clientId));
|
||||||
|
|
||||||
|
reply.sse(
|
||||||
|
(async function* () {
|
||||||
|
// Send initial list of photos
|
||||||
|
const initialPhotos = await Promise.all(
|
||||||
|
queue
|
||||||
|
.items()
|
||||||
|
.sort((a, b) => b.date.getTime() - a.date.getTime())
|
||||||
|
.map(async (photo) => {
|
||||||
|
const s = await generateSignature(photo.id);
|
||||||
|
return {
|
||||||
|
...photo,
|
||||||
|
url: `/photos/${photo.id}?signature=${s}`,
|
||||||
|
immichUrl: generatePhotoUrl(photo.id),
|
||||||
|
date: formatDate(photo.date),
|
||||||
|
rawDate: photo.date.toISOString(),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
for (const photo of initialPhotos) {
|
||||||
|
yield { data: JSON.stringify(photo) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep connection alive and handle cleanup
|
||||||
|
try {
|
||||||
|
// This generator will stay open until client disconnects
|
||||||
|
while (true) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 30000)); // 30 second heartbeat
|
||||||
|
yield { data: `Heartbeat for client ${clientId}` };
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
removeClient(clientId);
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/pause", async (_, res) => {
|
||||||
|
try {
|
||||||
|
await togglePause();
|
||||||
|
res.send({ success: true });
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to toggle pause", e);
|
||||||
|
res.status(500).send({ success: false, error: "Failed to toggle pause" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeClient = (clientId: string) => {
|
||||||
|
clients = clients.filter((c) => c.id !== clientId);
|
||||||
|
};
|
||||||
|
|
||||||
|
app.listen({ port: APP_PORT, host: "0.0.0.0" });
|
||||||
24
src/queue.ts
Normal file
24
src/queue.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
export default class FixedQueue<T> {
|
||||||
|
limit: number;
|
||||||
|
queue: T[];
|
||||||
|
|
||||||
|
constructor(limit: number) {
|
||||||
|
this.limit = limit; // max number of items to keep
|
||||||
|
this.queue = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
push(item: T) {
|
||||||
|
if (this.queue.length >= this.limit) {
|
||||||
|
this.queue.shift(); // remove oldest
|
||||||
|
}
|
||||||
|
this.queue.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
items() {
|
||||||
|
return this.queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
size() {
|
||||||
|
return this.queue.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/types.ts
Normal file
4
src/types.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface Photo {
|
||||||
|
date: Date;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
82
src/utils.ts
Normal file
82
src/utils.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { dirname } from "path";
|
||||||
|
|
||||||
|
// recreate __filename and __dirname
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
export { __dirname, __filename };
|
||||||
|
|
||||||
|
// Signed URL
|
||||||
|
import { generateKeyPair, SignJWT, jwtVerify } from "jose";
|
||||||
|
import {
|
||||||
|
FRAME_URL,
|
||||||
|
IMMICH_TOKEN,
|
||||||
|
IMMICH_URL,
|
||||||
|
LOCALE,
|
||||||
|
SIGNATURE_EXPIRY,
|
||||||
|
} from "./constants.js";
|
||||||
|
|
||||||
|
const { privateKey, publicKey } = await generateKeyPair("ES256");
|
||||||
|
|
||||||
|
export async function generateSignature(immichId: string) {
|
||||||
|
const jwt = await new SignJWT({ immichId })
|
||||||
|
.setProtectedHeader({ alg: "ES256" })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime(SIGNATURE_EXPIRY)
|
||||||
|
.sign(privateKey);
|
||||||
|
|
||||||
|
return jwt;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifySignature(token: string) {
|
||||||
|
try {
|
||||||
|
const { payload } = await jwtVerify(token, publicKey);
|
||||||
|
return payload.immichId as string;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Invalid signature", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Immich API
|
||||||
|
import { request } from "undici";
|
||||||
|
|
||||||
|
// Download a photo from Immich (with its id) and return its buffer.
|
||||||
|
// from /api/assets/ID/thumbnail?size=preview
|
||||||
|
export async function fetchPhoto(immichId: string) {
|
||||||
|
const url = `${IMMICH_URL}/api/assets/${immichId}/thumbnail?size=preview`;
|
||||||
|
const res = await request(url, {
|
||||||
|
headers: {
|
||||||
|
"x-api-key": IMMICH_TOKEN,
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
console.log(await res.body.text());
|
||||||
|
throw new Error(`Failed to fetch photo: ${res.statusCode}`);
|
||||||
|
}
|
||||||
|
const arrayBuffer = await res.body.arrayBuffer();
|
||||||
|
return Buffer.from(arrayBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generatePhotoUrl(immichId: string) {
|
||||||
|
return `${IMMICH_URL}/photos/${immichId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format date to a more readable format
|
||||||
|
export function formatDate(date: Date) {
|
||||||
|
return date.toLocaleDateString(LOCALE, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle pause state
|
||||||
|
export async function togglePause() {
|
||||||
|
await request(FRAME_URL + "/pause");
|
||||||
|
}
|
||||||
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2024",
|
||||||
|
"module": "nodenext",
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue