Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45d4984fde | |||
| ef2f605635 | |||
| 4532db552e | |||
| b9d438006c | |||
| 1eb96fa9e9 | |||
| c2694996e5 | |||
| 4d60133504 | |||
| c0ef300040 | |||
| b05d4fa7d3 | |||
| 84cd91bbbe | |||
| 718c258a07 |
@@ -58,7 +58,7 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@v3.0.0
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.1.0
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@v3.0.0
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.1.0
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
# Workaround to fix error:
|
# Workaround to fix error:
|
||||||
# failed to push: failed to copy: io: read/write on closed pipe
|
# failed to push: failed to copy: io: read/write on closed pipe
|
||||||
# See https://github.com/docker/build-push-action/issues/761
|
# See https://github.com/docker/build-push-action/issues/761
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
server-e2e-api:
|
server-e2e-api:
|
||||||
name: Server (e2e-api)
|
name: Server (e2e-api)
|
||||||
runs-on: ubuntu-latest
|
runs-on: mich
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
|
|
||||||
server-e2e-jobs:
|
server-e2e-jobs:
|
||||||
name: Server (e2e-jobs)
|
name: Server (e2e-jobs)
|
||||||
runs-on: ubuntu-latest
|
runs-on: mich
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
Generated
+404
-414
File diff suppressed because it is too large
Load Diff
@@ -60,12 +60,12 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: registry.hub.docker.com/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
POSTGRES_USER: ${DB_USERNAME}
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ services:
|
|||||||
- /dev/dri:/dev/dri
|
- /dev/dri:/dev/dri
|
||||||
- /dev/dma_heap:/dev/dma_heap
|
- /dev/dma_heap:/dev/dma_heap
|
||||||
- /dev/mpp_service:/dev/mpp_service
|
- /dev/mpp_service:/dev/mpp_service
|
||||||
|
volumes:
|
||||||
|
- /usr/bin/ffmpeg:/usr/bin/ffmpeg_mpp:ro
|
||||||
|
- /lib/aarch64-linux-gnu:/lib/ffmpeg-mpp:ro
|
||||||
|
- /lib/aarch64-linux-gnu/libblas.so.3:/lib/ffmpeg-mpp/libblas.so.3:ro # symlink is resolved by mounting
|
||||||
|
- /lib/aarch64-linux-gnu/liblapack.so.3:/lib/ffmpeg-mpp/liblapack.so.3:ro # symlink is resolved by mounting
|
||||||
|
- /lib/aarch64-linux-gnu/pulseaudio/libpulsecommon-15.99.so:/lib/ffmpeg-mpp/libpulsecommon-15.99.so:ro
|
||||||
|
|
||||||
vaapi:
|
vaapi:
|
||||||
devices:
|
devices:
|
||||||
|
|||||||
@@ -88,7 +88,10 @@ Some basic examples:
|
|||||||
|
|
||||||
This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. Deleted assets are, as always, marked as offline and can be removed with the "Remove offline files" button.
|
This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. Deleted assets are, as always, marked as offline and can be removed with the "Remove offline files" button.
|
||||||
|
|
||||||
If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes.
|
If your photos are on a network drive you will likely have to enable filesystem polling. The performance hit for polling large libraries is currently unknown, feel free to test this feature and report back. In addition to the boolean feature flag, the configuration file allows customization of the following parameters, please see the [chokidar documentation](https://github.com/paulmillr/chokidar?tab=readme-ov-file#performance) for reference.
|
||||||
|
|
||||||
|
- `usePolling` (default: `false`).
|
||||||
|
- `interval`. (default: 10000). When using polling, this is how often (in milliseconds) the filesystem is polled.
|
||||||
|
|
||||||
### Nightly job
|
### Nightly job
|
||||||
|
|
||||||
|
|||||||
@@ -50,22 +50,12 @@ import {
|
|||||||
mdiVectorCombine,
|
mdiVectorCombine,
|
||||||
mdiVideo,
|
mdiVideo,
|
||||||
mdiWeb,
|
mdiWeb,
|
||||||
mdiScaleBalance,
|
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Timeline, { DateType, Item } from '../components/timeline';
|
import Timeline, { DateType, Item } from '../components/timeline';
|
||||||
|
|
||||||
const items: Item[] = [
|
const items: Item[] = [
|
||||||
{
|
|
||||||
icon: mdiScaleBalance,
|
|
||||||
description: 'Immich switches to AGPLv3 license',
|
|
||||||
title: 'AGPL License',
|
|
||||||
release: 'v1.95.0',
|
|
||||||
tag: 'v1.95.0',
|
|
||||||
date: new Date(2024, 1, 20),
|
|
||||||
dateType: DateType.RELEASE,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: mdiEyeRefreshOutline,
|
icon: mdiEyeRefreshOutline,
|
||||||
description: 'Automatically import files in external libraries when the operating system detects changes.',
|
description: 'Automatically import files in external libraries when the operating system detects changes.',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ name: immich-e2e
|
|||||||
|
|
||||||
x-server-build: &server-common
|
x-server-build: &server-common
|
||||||
image: immich-server:latest
|
image: immich-server:latest
|
||||||
|
container_name: immich-e2e-server
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: server/Dockerfile
|
dockerfile: server/Dockerfile
|
||||||
@@ -22,16 +23,14 @@ x-server-build: &server-common
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich-e2e-server
|
|
||||||
command: [ "./start.sh", "immich" ]
|
command: [ "./start.sh", "immich" ]
|
||||||
<<: *server-common
|
<<: *server-common
|
||||||
ports:
|
ports:
|
||||||
- 2283:3001
|
- 2283:3001
|
||||||
|
|
||||||
immich-microservices:
|
# immich-microservices:
|
||||||
container_name: immich-e2e-microservices
|
# command: [ "./start.sh", "microservices" ]
|
||||||
command: [ "./start.sh", "microservices" ]
|
# <<: *server-common
|
||||||
<<: *server-common
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
||||||
|
|||||||
Generated
+3
-169
@@ -12,15 +12,11 @@
|
|||||||
"@immich/cli": "file:../cli",
|
"@immich/cli": "file:../cli",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@playwright/test": "^1.41.2",
|
"@playwright/test": "^1.41.2",
|
||||||
"@types/luxon": "^3.4.2",
|
|
||||||
"@types/node": "^20.11.17",
|
"@types/node": "^20.11.17",
|
||||||
"@types/pg": "^8.11.0",
|
"@types/pg": "^8.11.0",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@vitest/coverage-v8": "^1.3.0",
|
"@vitest/coverage-v8": "^1.3.0",
|
||||||
"exiftool-vendored": "^24.5.0",
|
|
||||||
"luxon": "^3.4.4",
|
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"socket.io-client": "^4.7.4",
|
|
||||||
"supertest": "^6.3.4",
|
"supertest": "^6.3.4",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vitest": "^1.3.0"
|
"vitest": "^1.3.0"
|
||||||
@@ -595,12 +591,6 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@photostructure/tz-lookup": {
|
|
||||||
"version": "9.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-9.0.2.tgz",
|
|
||||||
"integrity": "sha512-H8+tTt7ilJNkFyb+QgPnLEGUjQzGwiMb9n7lwRZNBgSKL3VZs9AkjI1E//FcwPjNafwAH932U92+xTqJiF3Bbw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.41.2",
|
"version": "1.41.2",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz",
|
||||||
@@ -791,12 +781,6 @@
|
|||||||
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
|
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@socket.io/component-emitter": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/cookiejar": {
|
"node_modules/@types/cookiejar": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
|
||||||
@@ -815,12 +799,6 @@
|
|||||||
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
|
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/luxon": {
|
|
||||||
"version": "3.4.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
|
|
||||||
"integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/methods": {
|
"node_modules/@types/methods": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
||||||
@@ -828,9 +806,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.11.20",
|
"version": "20.11.19",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
|
||||||
"integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
|
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
@@ -1081,15 +1059,6 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/batch-cluster": {
|
|
||||||
"version": "13.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-13.0.0.tgz",
|
|
||||||
"integrity": "sha512-EreW0Vi8TwovhYUHBXXRA5tthuU2ynGsZFlboyMJHCCUXYa2AjgwnE3ubBOJs2xJLcuXFJbi6c/8pH5+FVj8Og==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
@@ -1294,28 +1263,6 @@
|
|||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/engine.io-client": {
|
|
||||||
"version": "6.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
|
|
||||||
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@socket.io/component-emitter": "~3.1.0",
|
|
||||||
"debug": "~4.3.1",
|
|
||||||
"engine.io-parser": "~5.2.1",
|
|
||||||
"ws": "~8.11.0",
|
|
||||||
"xmlhttprequest-ssl": "~2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/engine.io-parser": {
|
|
||||||
"version": "5.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz",
|
|
||||||
"integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/es-define-property": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||||
@@ -1407,43 +1354,6 @@
|
|||||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/exiftool-vendored": {
|
|
||||||
"version": "24.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.5.0.tgz",
|
|
||||||
"integrity": "sha512-uLGYfeshak3mYn2ucCsebXfNFdOpeAULlMb84wiJv+4B236n+ypgK/vr8bJgAcsIPSRJXFSz9WonvjjQYYqR3w==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@photostructure/tz-lookup": "^9.0.1",
|
|
||||||
"@types/luxon": "^3.4.2",
|
|
||||||
"batch-cluster": "^13.0.0",
|
|
||||||
"he": "^1.2.0",
|
|
||||||
"luxon": "^3.4.4"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"exiftool-vendored.exe": "12.76.0",
|
|
||||||
"exiftool-vendored.pl": "12.76.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/exiftool-vendored.exe": {
|
|
||||||
"version": "12.76.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.76.0.tgz",
|
|
||||||
"integrity": "sha512-lbKPPs31qpjnhFiMRaVxJX+iNcJ+p0NrRSFLHHaX6KTsfMba6e5i6NykSvU3wMiafzUTef1Fen3XQ+8n1tjjNw==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/exiftool-vendored.pl": {
|
|
||||||
"version": "12.76.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.76.0.tgz",
|
|
||||||
"integrity": "sha512-4DxqgnvL71YziVoY27ZMgVfLAWDH3pQLljuV5+ffTnTPvz/BWeV+/bVFwRvDqCD3lkCWds0YfVcsycfJgbQ5fA==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"!win32"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/fast-safe-stringify": {
|
"node_modules/fast-safe-stringify": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
|
||||||
@@ -1637,15 +1547,6 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/he": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"he": "bin/he"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hexoid": {
|
"node_modules/hexoid": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
|
||||||
@@ -1803,15 +1704,6 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/luxon": {
|
|
||||||
"version": "3.4.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
|
|
||||||
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.7",
|
"version": "0.30.7",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
|
||||||
@@ -2454,34 +2346,6 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io-client": {
|
|
||||||
"version": "4.7.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz",
|
|
||||||
"integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@socket.io/component-emitter": "~3.1.0",
|
|
||||||
"debug": "~4.3.2",
|
|
||||||
"engine.io-client": "~6.5.2",
|
|
||||||
"socket.io-parser": "~4.2.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/socket.io-parser": {
|
|
||||||
"version": "4.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
|
||||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@socket.io/component-emitter": "~3.1.0",
|
|
||||||
"debug": "~4.3.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
@@ -2879,36 +2743,6 @@
|
|||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
|
||||||
"version": "8.11.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
|
||||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bufferutil": "^4.0.1",
|
|
||||||
"utf-8-validate": "^5.0.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"bufferutil": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"utf-8-validate": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/xmlhttprequest-ssl": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|||||||
@@ -16,15 +16,11 @@
|
|||||||
"@immich/cli": "file:../cli",
|
"@immich/cli": "file:../cli",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@playwright/test": "^1.41.2",
|
"@playwright/test": "^1.41.2",
|
||||||
"@types/luxon": "^3.4.2",
|
|
||||||
"@types/node": "^20.11.17",
|
"@types/node": "^20.11.17",
|
||||||
"@types/pg": "^8.11.0",
|
"@types/pg": "^8.11.0",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@vitest/coverage-v8": "^1.3.0",
|
"@vitest/coverage-v8": "^1.3.0",
|
||||||
"exiftool-vendored": "^24.5.0",
|
|
||||||
"luxon": "^3.4.4",
|
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"socket.io-client": "^4.7.4",
|
|
||||||
"supertest": "^6.3.4",
|
"supertest": "^6.3.4",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vitest": "^1.3.0"
|
"vitest": "^1.3.0"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ActivityCreateDto,
|
ActivityCreateDto,
|
||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
AssetFileUploadResponseDto,
|
AssetResponseDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
ReactionType,
|
ReactionType,
|
||||||
createActivity as create,
|
createActivity as create,
|
||||||
@@ -16,13 +16,13 @@ import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|||||||
describe('/activity', () => {
|
describe('/activity', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let nonOwner: LoginResponseDto;
|
let nonOwner: LoginResponseDto;
|
||||||
let asset: AssetFileUploadResponseDto;
|
let asset: AssetResponseDto;
|
||||||
let album: AlbumResponseDto;
|
let album: AlbumResponseDto;
|
||||||
|
|
||||||
const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
|
const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
|
||||||
create(
|
create(
|
||||||
{ activityCreateDto: dto },
|
{ activityCreateDto: dto },
|
||||||
{ headers: asBearerAuth(accessToken || admin.accessToken) },
|
{ headers: asBearerAuth(accessToken || admin.accessToken) }
|
||||||
);
|
);
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@@ -40,7 +40,7 @@ describe('/activity', () => {
|
|||||||
sharedWithUserIds: [nonOwner.userId],
|
sharedWithUserIds: [nonOwner.userId],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ describe('/activity', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
|
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ describe('/activity', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
|
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ describe('/activity', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])),
|
errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID']))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ describe('/activity', () => {
|
|||||||
assetIds: [asset.id],
|
assetIds: [asset.id],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
);
|
);
|
||||||
|
|
||||||
const [reaction] = await Promise.all([
|
const [reaction] = await Promise.all([
|
||||||
@@ -216,7 +216,7 @@ describe('/activity', () => {
|
|||||||
.send({ albumId: uuidDto.invalid });
|
.send({ albumId: uuidDto.invalid });
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
|
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -230,7 +230,7 @@ describe('/activity', () => {
|
|||||||
errorDto.badRequest([
|
errorDto.badRequest([
|
||||||
'comment must be a string',
|
'comment must be a string',
|
||||||
'comment should not be empty',
|
'comment should not be empty',
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -357,7 +357,7 @@ describe('/activity', () => {
|
|||||||
describe('DELETE /activity/:id', () => {
|
describe('DELETE /activity/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).delete(
|
const { status, body } = await request(app).delete(
|
||||||
`/activity/${uuidDto.notFound}`,
|
`/activity/${uuidDto.notFound}`
|
||||||
);
|
);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@@ -421,7 +421,7 @@ describe('/activity', () => {
|
|||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
errorDto.badRequest('Not found or no activity.delete access'),
|
errorDto.badRequest('Not found or no activity.delete access')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -432,7 +432,7 @@ describe('/activity', () => {
|
|||||||
type: ReactionType.Comment,
|
type: ReactionType.Comment,
|
||||||
comment: 'This is a test comment',
|
comment: 'This is a test comment',
|
||||||
},
|
},
|
||||||
nonOwner.accessToken,
|
nonOwner.accessToken
|
||||||
);
|
);
|
||||||
|
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
AssetFileUploadResponseDto,
|
AssetResponseDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
@@ -21,8 +21,8 @@ const user2NotShared = 'user2NotShared';
|
|||||||
describe('/album', () => {
|
describe('/album', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let user1: LoginResponseDto;
|
let user1: LoginResponseDto;
|
||||||
let user1Asset1: AssetFileUploadResponseDto;
|
let user1Asset1: AssetResponseDto;
|
||||||
let user1Asset2: AssetFileUploadResponseDto;
|
let user1Asset2: AssetResponseDto;
|
||||||
let user1Albums: AlbumResponseDto[];
|
let user1Albums: AlbumResponseDto[];
|
||||||
let user2: LoginResponseDto;
|
let user2: LoginResponseDto;
|
||||||
let user2Albums: AlbumResponseDto[];
|
let user2Albums: AlbumResponseDto[];
|
||||||
@@ -95,7 +95,7 @@ describe('/album', () => {
|
|||||||
|
|
||||||
await deleteUser(
|
await deleteUser(
|
||||||
{ id: user3.userId },
|
{ id: user3.userId },
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ describe('/album', () => {
|
|||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
expect(status).toEqual(400);
|
expect(status).toEqual(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
errorDto.badRequest(['shared must be a boolean value']),
|
errorDto.badRequest(['shared must be a boolean value'])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ describe('/album', () => {
|
|||||||
albumName: user2SharedUser,
|
albumName: user2SharedUser,
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ describe('/album', () => {
|
|||||||
albumName: user1NotShared,
|
albumName: user1NotShared,
|
||||||
shared: false,
|
shared: false,
|
||||||
}),
|
}),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ describe('/album', () => {
|
|||||||
albumName: user2SharedUser,
|
albumName: user2SharedUser,
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ describe('/album', () => {
|
|||||||
albumName: user1NotShared,
|
albumName: user1NotShared,
|
||||||
shared: false,
|
shared: false,
|
||||||
}),
|
}),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ describe('/album', () => {
|
|||||||
describe('GET /album/:id', () => {
|
describe('GET /album/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(
|
const { status, body } = await request(app).get(
|
||||||
`/album/${user1Albums[0].id}`,
|
`/album/${user1Albums[0].id}`
|
||||||
);
|
);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@@ -361,7 +361,7 @@ describe('/album', () => {
|
|||||||
describe('PUT /album/:id/assets', () => {
|
describe('PUT /album/:id/assets', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).put(
|
const { status, body } = await request(app).put(
|
||||||
`/album/${user1Albums[0].id}/assets`,
|
`/album/${user1Albums[0].id}/assets`
|
||||||
);
|
);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
@@ -519,7 +519,7 @@ describe('/album', () => {
|
|||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
sharedUsers: [expect.objectContaining({ id: user2.userId })],
|
sharedUsers: [expect.objectContaining({ id: user2.userId })],
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,808 +0,0 @@
|
|||||||
import {
|
|
||||||
AssetFileUploadResponseDto,
|
|
||||||
AssetResponseDto,
|
|
||||||
AssetTypeEnum,
|
|
||||||
LoginResponseDto,
|
|
||||||
SharedLinkType,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { exiftool } from 'exiftool-vendored';
|
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import { createHash } from 'node:crypto';
|
|
||||||
import { readFile, writeFile } from 'node:fs/promises';
|
|
||||||
import { basename, join } from 'node:path';
|
|
||||||
import { Socket } from 'socket.io-client';
|
|
||||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
|
||||||
import { errorDto } from 'src/responses';
|
|
||||||
import {
|
|
||||||
apiUtils,
|
|
||||||
app,
|
|
||||||
dbUtils,
|
|
||||||
tempDir,
|
|
||||||
testAssetDir,
|
|
||||||
wsUtils,
|
|
||||||
} from 'src/utils';
|
|
||||||
import request from 'supertest';
|
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
|
|
||||||
|
|
||||||
const sha1 = (bytes: Buffer) =>
|
|
||||||
createHash('sha1').update(bytes).digest('base64');
|
|
||||||
|
|
||||||
const readTags = async (bytes: Buffer, filename: string) => {
|
|
||||||
const filepath = join(tempDir, filename);
|
|
||||||
await writeFile(filepath, bytes);
|
|
||||||
return exiftool.read(filepath);
|
|
||||||
};
|
|
||||||
|
|
||||||
const today = DateTime.fromObject({
|
|
||||||
year: 2023,
|
|
||||||
month: 11,
|
|
||||||
day: 3,
|
|
||||||
}) as DateTime<true>;
|
|
||||||
const yesterday = today.minus({ days: 1 });
|
|
||||||
|
|
||||||
describe('/asset', () => {
|
|
||||||
let admin: LoginResponseDto;
|
|
||||||
let user1: LoginResponseDto;
|
|
||||||
let user2: LoginResponseDto;
|
|
||||||
let userStats: LoginResponseDto;
|
|
||||||
let user1Assets: AssetFileUploadResponseDto[];
|
|
||||||
let user2Assets: AssetFileUploadResponseDto[];
|
|
||||||
let assetLocation: AssetFileUploadResponseDto;
|
|
||||||
let ws: Socket;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
apiUtils.setup();
|
|
||||||
await dbUtils.reset();
|
|
||||||
admin = await apiUtils.adminSetup({ onboarding: false });
|
|
||||||
|
|
||||||
[ws, user1, user2, userStats] = await Promise.all([
|
|
||||||
wsUtils.connect(admin.accessToken),
|
|
||||||
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
|
|
||||||
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
|
|
||||||
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// asset location
|
|
||||||
assetLocation = await apiUtils.createAsset(
|
|
||||||
admin.accessToken,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
filename: 'thompson-springs.jpg',
|
|
||||||
bytes: await readFile(locationAssetFilepath),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await wsUtils.waitForEvent({ event: 'upload', assetId: assetLocation.id });
|
|
||||||
|
|
||||||
user1Assets = await Promise.all([
|
|
||||||
apiUtils.createAsset(user1.accessToken),
|
|
||||||
apiUtils.createAsset(user1.accessToken),
|
|
||||||
apiUtils.createAsset(
|
|
||||||
user1.accessToken,
|
|
||||||
{
|
|
||||||
isFavorite: true,
|
|
||||||
isExternal: true,
|
|
||||||
isReadOnly: true,
|
|
||||||
fileCreatedAt: yesterday.toISO(),
|
|
||||||
fileModifiedAt: yesterday.toISO(),
|
|
||||||
},
|
|
||||||
{ filename: 'example.mp4' },
|
|
||||||
),
|
|
||||||
apiUtils.createAsset(user1.accessToken),
|
|
||||||
apiUtils.createAsset(user1.accessToken),
|
|
||||||
]);
|
|
||||||
|
|
||||||
user2Assets = await Promise.all([apiUtils.createAsset(user2.accessToken)]);
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
// stats
|
|
||||||
apiUtils.createAsset(userStats.accessToken),
|
|
||||||
apiUtils.createAsset(userStats.accessToken, { isFavorite: true }),
|
|
||||||
apiUtils.createAsset(userStats.accessToken, { isArchived: true }),
|
|
||||||
apiUtils.createAsset(
|
|
||||||
userStats.accessToken,
|
|
||||||
{
|
|
||||||
isArchived: true,
|
|
||||||
isFavorite: true,
|
|
||||||
},
|
|
||||||
{ filename: 'example.mp4' },
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const person1 = await apiUtils.createPerson(user1.accessToken, {
|
|
||||||
name: 'Test Person',
|
|
||||||
});
|
|
||||||
await dbUtils.createFace({
|
|
||||||
assetId: user1Assets[0].id,
|
|
||||||
personId: person1.id,
|
|
||||||
});
|
|
||||||
}, 30_000);
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
wsUtils.disconnect(ws);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /asset/:id', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get(
|
|
||||||
`/asset/${uuidDto.notFound}`,
|
|
||||||
);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
expect(status).toBe(401);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require a valid id', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get(`/asset/${uuidDto.invalid}`)
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require access', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get(`/asset/${user2Assets[0].id}`)
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.noPermission);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get the asset info', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get(`/asset/${user1Assets[0].id}`)
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toMatchObject({ id: user1Assets[0].id });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should work with a shared link', async () => {
|
|
||||||
const sharedLink = await apiUtils.createSharedLink(user1.accessToken, {
|
|
||||||
type: SharedLinkType.Individual,
|
|
||||||
assetIds: [user1Assets[0].id],
|
|
||||||
});
|
|
||||||
|
|
||||||
const { status, body } = await request(app).get(
|
|
||||||
`/asset/${user1Assets[0].id}?key=${sharedLink.key}`,
|
|
||||||
);
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toMatchObject({ id: user1Assets[0].id });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not send people data for shared links for un-authenticated users', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get(`/asset/${user1Assets[0].id}`)
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toEqual(200);
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
id: user1Assets[0].id,
|
|
||||||
isFavorite: false,
|
|
||||||
people: [
|
|
||||||
{
|
|
||||||
birthDate: null,
|
|
||||||
id: expect.any(String),
|
|
||||||
isHidden: false,
|
|
||||||
name: 'Test Person',
|
|
||||||
thumbnailPath: '/my/awesome/thumbnail.jpg',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const sharedLink = await apiUtils.createSharedLink(user1.accessToken, {
|
|
||||||
type: SharedLinkType.Individual,
|
|
||||||
assetIds: [user1Assets[0].id],
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await request(app).get(
|
|
||||||
`/asset/${user1Assets[0].id}?key=${sharedLink.key}`,
|
|
||||||
);
|
|
||||||
expect(data.status).toBe(200);
|
|
||||||
expect(data.body).toMatchObject({ people: [] });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /asset/statistics', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/asset/statistics');
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return stats of all assets', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/statistics')
|
|
||||||
.set('Authorization', `Bearer ${userStats.accessToken}`);
|
|
||||||
|
|
||||||
expect(body).toEqual({ images: 3, videos: 1, total: 4 });
|
|
||||||
expect(status).toBe(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return stats of all favored assets', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/statistics')
|
|
||||||
.set('Authorization', `Bearer ${userStats.accessToken}`)
|
|
||||||
.query({ isFavorite: true });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual({ images: 1, videos: 1, total: 2 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return stats of all archived assets', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/statistics')
|
|
||||||
.set('Authorization', `Bearer ${userStats.accessToken}`)
|
|
||||||
.query({ isArchived: true });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual({ images: 1, videos: 1, total: 2 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return stats of all favored and archived assets', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/statistics')
|
|
||||||
.set('Authorization', `Bearer ${userStats.accessToken}`)
|
|
||||||
.query({ isFavorite: true, isArchived: true });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual({ images: 0, videos: 1, total: 1 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return stats of all assets neither favored nor archived', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/statistics')
|
|
||||||
.set('Authorization', `Bearer ${userStats.accessToken}`)
|
|
||||||
.query({ isFavorite: false, isArchived: false });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual({ images: 1, videos: 0, total: 1 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /asset/random', () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
await Promise.all([
|
|
||||||
apiUtils.createAsset(user1.accessToken),
|
|
||||||
apiUtils.createAsset(user1.accessToken),
|
|
||||||
apiUtils.createAsset(user1.accessToken),
|
|
||||||
apiUtils.createAsset(user1.accessToken),
|
|
||||||
apiUtils.createAsset(user1.accessToken),
|
|
||||||
apiUtils.createAsset(user1.accessToken),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/asset/random');
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each(Array(10))('should return 1 random assets', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/random')
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
|
|
||||||
const assets: AssetResponseDto[] = body;
|
|
||||||
expect(assets.length).toBe(1);
|
|
||||||
expect(assets[0].ownerId).toBe(user1.userId);
|
|
||||||
|
|
||||||
// assets owned by user1
|
|
||||||
expect([user1Assets.map(({ id }) => id)]).toContain(assets[0].id);
|
|
||||||
// assets owned by user2
|
|
||||||
expect([user1Assets.map(({ id }) => id)]).not.toContain(assets[0].id);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each(Array(10))('should return 2 random assets', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/random?count=2')
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
|
|
||||||
const assets: AssetResponseDto[] = body;
|
|
||||||
expect(assets.length).toBe(2);
|
|
||||||
|
|
||||||
for (const asset of assets) {
|
|
||||||
expect(asset.ownerId).toBe(user1.userId);
|
|
||||||
// assets owned by user1
|
|
||||||
expect([user1Assets.map(({ id }) => id)]).toContain(asset.id);
|
|
||||||
// assets owned by user2
|
|
||||||
expect([user2Assets.map(({ id }) => id)]).not.toContain(asset.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each(Array(10))(
|
|
||||||
'should return 1 asset if there are 10 assets in the database but user 2 only has 1',
|
|
||||||
async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/[]asset/random')
|
|
||||||
.set('Authorization', `Bearer ${user2.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual([
|
|
||||||
expect.objectContaining({ id: user2Assets[0].id }),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it('should return error', async () => {
|
|
||||||
const { status } = await request(app)
|
|
||||||
.get('/asset/random?count=ABC')
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('PUT /asset/:id', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).put(
|
|
||||||
`/asset/:${uuidDto.notFound}`,
|
|
||||||
);
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require a valid id', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/asset/${uuidDto.invalid}`)
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require access', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/asset/${user2Assets[0].id}`)
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.noPermission);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should favorite an asset', async () => {
|
|
||||||
const before = await apiUtils.getAssetInfo(
|
|
||||||
user1.accessToken,
|
|
||||||
user1Assets[0].id,
|
|
||||||
);
|
|
||||||
expect(before.isFavorite).toBe(false);
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/asset/${user1Assets[0].id}`)
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
.send({ isFavorite: true });
|
|
||||||
expect(body).toMatchObject({ id: user1Assets[0].id, isFavorite: true });
|
|
||||||
expect(status).toEqual(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should archive an asset', async () => {
|
|
||||||
const before = await apiUtils.getAssetInfo(
|
|
||||||
user1.accessToken,
|
|
||||||
user1Assets[0].id,
|
|
||||||
);
|
|
||||||
expect(before.isArchived).toBe(false);
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/asset/${user1Assets[0].id}`)
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
.send({ isArchived: true });
|
|
||||||
expect(body).toMatchObject({ id: user1Assets[0].id, isArchived: true });
|
|
||||||
expect(status).toEqual(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update date time original', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/asset/${user1Assets[0].id}`)
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
.send({ dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' });
|
|
||||||
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
id: user1Assets[0].id,
|
|
||||||
exifInfo: expect.objectContaining({
|
|
||||||
dateTimeOriginal: '2023-11-20T01:11:00.000Z',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(status).toEqual(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject invalid gps coordinates', async () => {
|
|
||||||
for (const test of [
|
|
||||||
{ latitude: 12 },
|
|
||||||
{ longitude: 12 },
|
|
||||||
{ latitude: 12, longitude: 'abc' },
|
|
||||||
{ latitude: 'abc', longitude: 12 },
|
|
||||||
{ latitude: null, longitude: 12 },
|
|
||||||
{ latitude: 12, longitude: null },
|
|
||||||
{ latitude: 91, longitude: 12 },
|
|
||||||
{ latitude: -91, longitude: 12 },
|
|
||||||
{ latitude: 12, longitude: -181 },
|
|
||||||
{ latitude: 12, longitude: 181 },
|
|
||||||
]) {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/asset/${user1Assets[0].id}`)
|
|
||||||
.send(test)
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update gps data', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/asset/${user1Assets[0].id}`)
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
.send({ latitude: 12, longitude: 12 });
|
|
||||||
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
id: user1Assets[0].id,
|
|
||||||
exifInfo: expect.objectContaining({ latitude: 12, longitude: 12 }),
|
|
||||||
});
|
|
||||||
expect(status).toEqual(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set the description', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/asset/${user1Assets[0].id}`)
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
.send({ description: 'Test asset description' });
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
id: user1Assets[0].id,
|
|
||||||
exifInfo: expect.objectContaining({
|
|
||||||
description: 'Test asset description',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(status).toEqual(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return tagged people', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/asset/${user1Assets[0].id}`)
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
.send({ isFavorite: true });
|
|
||||||
expect(status).toEqual(200);
|
|
||||||
expect(body).toMatchObject({
|
|
||||||
id: user1Assets[0].id,
|
|
||||||
isFavorite: true,
|
|
||||||
people: [
|
|
||||||
{
|
|
||||||
birthDate: null,
|
|
||||||
id: expect.any(String),
|
|
||||||
isHidden: false,
|
|
||||||
name: 'Test Person',
|
|
||||||
thumbnailPath: '/my/awesome/thumbnail.jpg',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('DELETE /asset', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.delete(`/asset`)
|
|
||||||
.send({ ids: [uuidDto.notFound] });
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require a valid uuid', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.delete(`/asset`)
|
|
||||||
.send({ ids: [uuidDto.invalid] })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(
|
|
||||||
errorDto.badRequest(['each value in ids must be a UUID']),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error when the id is not found', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.delete(`/asset`)
|
|
||||||
.send({ ids: [uuidDto.notFound] })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(
|
|
||||||
errorDto.badRequest('Not found or no asset.delete access'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should move an asset to the trash', async () => {
|
|
||||||
const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
|
|
||||||
|
|
||||||
const before = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
|
||||||
expect(before.isTrashed).toBe(false);
|
|
||||||
|
|
||||||
const { status } = await request(app)
|
|
||||||
.delete('/asset')
|
|
||||||
.send({ ids: [assetId] })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
const after = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
|
||||||
expect(after.isTrashed).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('POST /asset/upload', () => {
|
|
||||||
const tests = [
|
|
||||||
{
|
|
||||||
input: 'formats/jpg/el_torcal_rocks.jpg',
|
|
||||||
expected: {
|
|
||||||
type: AssetTypeEnum.Image,
|
|
||||||
originalFileName: 'el_torcal_rocks',
|
|
||||||
resized: true,
|
|
||||||
exifInfo: {
|
|
||||||
dateTimeOriginal: '2012-08-05T11:39:59.000Z',
|
|
||||||
exifImageWidth: 512,
|
|
||||||
exifImageHeight: 341,
|
|
||||||
latitude: null,
|
|
||||||
longitude: null,
|
|
||||||
focalLength: 75,
|
|
||||||
iso: 200,
|
|
||||||
fNumber: 11,
|
|
||||||
exposureTime: '1/160',
|
|
||||||
fileSizeInByte: 53_493,
|
|
||||||
make: 'SONY',
|
|
||||||
model: 'DSLR-A550',
|
|
||||||
orientation: null,
|
|
||||||
description: 'SONY DSC',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'formats/heic/IMG_2682.heic',
|
|
||||||
expected: {
|
|
||||||
type: AssetTypeEnum.Image,
|
|
||||||
originalFileName: 'IMG_2682',
|
|
||||||
resized: true,
|
|
||||||
fileCreatedAt: '2019-03-21T16:04:22.348Z',
|
|
||||||
exifInfo: {
|
|
||||||
dateTimeOriginal: '2019-03-21T16:04:22.348Z',
|
|
||||||
exifImageWidth: 4032,
|
|
||||||
exifImageHeight: 3024,
|
|
||||||
latitude: 41.2203,
|
|
||||||
longitude: -96.071_625,
|
|
||||||
make: 'Apple',
|
|
||||||
model: 'iPhone 7',
|
|
||||||
lensModel: 'iPhone 7 back camera 3.99mm f/1.8',
|
|
||||||
fileSizeInByte: 880_703,
|
|
||||||
exposureTime: '1/887',
|
|
||||||
iso: 20,
|
|
||||||
focalLength: 3.99,
|
|
||||||
fNumber: 1.8,
|
|
||||||
timeZone: 'America/Chicago',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'formats/png/density_plot.png',
|
|
||||||
expected: {
|
|
||||||
type: AssetTypeEnum.Image,
|
|
||||||
originalFileName: 'density_plot',
|
|
||||||
resized: true,
|
|
||||||
exifInfo: {
|
|
||||||
exifImageWidth: 800,
|
|
||||||
exifImageHeight: 800,
|
|
||||||
latitude: null,
|
|
||||||
longitude: null,
|
|
||||||
fileSizeInByte: 25_408,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'formats/raw/Nikon/D80/glarus.nef',
|
|
||||||
expected: {
|
|
||||||
type: AssetTypeEnum.Image,
|
|
||||||
originalFileName: 'glarus',
|
|
||||||
resized: true,
|
|
||||||
fileCreatedAt: '2010-07-20T17:27:12.000Z',
|
|
||||||
exifInfo: {
|
|
||||||
make: 'NIKON CORPORATION',
|
|
||||||
model: 'NIKON D80',
|
|
||||||
exposureTime: '1/200',
|
|
||||||
fNumber: 10,
|
|
||||||
focalLength: 18,
|
|
||||||
iso: 100,
|
|
||||||
fileSizeInByte: 9_057_784,
|
|
||||||
dateTimeOriginal: '2010-07-20T17:27:12.000Z',
|
|
||||||
latitude: null,
|
|
||||||
longitude: null,
|
|
||||||
orientation: '1',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: 'formats/raw/Nikon/D700/philadelphia.nef',
|
|
||||||
expected: {
|
|
||||||
type: AssetTypeEnum.Image,
|
|
||||||
originalFileName: 'philadelphia',
|
|
||||||
resized: true,
|
|
||||||
fileCreatedAt: '2016-09-22T22:10:29.060Z',
|
|
||||||
exifInfo: {
|
|
||||||
make: 'NIKON CORPORATION',
|
|
||||||
model: 'NIKON D700',
|
|
||||||
exposureTime: '1/400',
|
|
||||||
fNumber: 11,
|
|
||||||
focalLength: 85,
|
|
||||||
iso: 200,
|
|
||||||
fileSizeInByte: 15_856_335,
|
|
||||||
dateTimeOriginal: '2016-09-22T22:10:29.060Z',
|
|
||||||
latitude: null,
|
|
||||||
longitude: null,
|
|
||||||
orientation: '1',
|
|
||||||
timeZone: 'UTC-5',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const { input, expected } of tests) {
|
|
||||||
it(`should generate a thumbnail for ${input}`, async () => {
|
|
||||||
const filepath = join(testAssetDir, input);
|
|
||||||
const { id, duplicate } = await apiUtils.createAsset(
|
|
||||||
admin.accessToken,
|
|
||||||
{},
|
|
||||||
{ bytes: await readFile(filepath), filename: basename(filepath) },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(duplicate).toBe(false);
|
|
||||||
|
|
||||||
await wsUtils.waitForEvent({ event: 'upload', assetId: id });
|
|
||||||
|
|
||||||
const asset = await apiUtils.getAssetInfo(admin.accessToken, id);
|
|
||||||
|
|
||||||
expect(asset.exifInfo).toBeDefined();
|
|
||||||
expect(asset.exifInfo).toMatchObject(expected.exifInfo);
|
|
||||||
expect(asset).toMatchObject(expected);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
it('should handle a duplicate', async () => {
|
|
||||||
const filepath = 'formats/jpeg/el_torcal_rocks.jpeg';
|
|
||||||
const { duplicate } = await apiUtils.createAsset(
|
|
||||||
admin.accessToken,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
bytes: await readFile(join(testAssetDir, filepath)),
|
|
||||||
filename: basename(filepath),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(duplicate).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// These hashes were created by copying the image files to a Samsung phone,
|
|
||||||
// exporting the video from Samsung's stock Gallery app, and hashing them locally.
|
|
||||||
// This ensures that immich+exiftool are extracting the videos the same way Samsung does.
|
|
||||||
// DO NOT assume immich+exiftool are doing things correctly and just copy whatever hash it gives
|
|
||||||
// into the test here.
|
|
||||||
const motionTests = [
|
|
||||||
{
|
|
||||||
filepath: 'formats/motionphoto/Samsung One UI 5.jpg',
|
|
||||||
checksum: 'fr14niqCq6N20HB8rJYEvpsUVtI=',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filepath: 'formats/motionphoto/Samsung One UI 6.jpg',
|
|
||||||
checksum: 'lT9Uviw/FFJYCjfIxAGPTjzAmmw=',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filepath: 'formats/motionphoto/Samsung One UI 6.heic',
|
|
||||||
checksum: '/ejgzywvgvzvVhUYVfvkLzFBAF0=',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const { filepath, checksum } of motionTests) {
|
|
||||||
it(`should extract motionphoto video from ${filepath}`, async () => {
|
|
||||||
const response = await apiUtils.createAsset(
|
|
||||||
admin.accessToken,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
bytes: await readFile(join(testAssetDir, filepath)),
|
|
||||||
filename: basename(filepath),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await wsUtils.waitForEvent({ event: 'upload', assetId: response.id });
|
|
||||||
|
|
||||||
expect(response.duplicate).toBe(false);
|
|
||||||
|
|
||||||
const asset = await apiUtils.getAssetInfo(
|
|
||||||
admin.accessToken,
|
|
||||||
response.id,
|
|
||||||
);
|
|
||||||
expect(asset.livePhotoVideoId).toBeDefined();
|
|
||||||
|
|
||||||
const video = await apiUtils.getAssetInfo(
|
|
||||||
admin.accessToken,
|
|
||||||
asset.livePhotoVideoId as string,
|
|
||||||
);
|
|
||||||
expect(video.checksum).toStrictEqual(checksum);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /asset/thumbnail/:id', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get(
|
|
||||||
`/asset/thumbnail/${assetLocation.id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not include gps data for webp thumbnails', async () => {
|
|
||||||
const { status, body, type } = await request(app)
|
|
||||||
.get(`/asset/thumbnail/${assetLocation.id}?format=WEBP`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
await wsUtils.waitForEvent({
|
|
||||||
event: 'upload',
|
|
||||||
assetId: assetLocation.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toBeDefined();
|
|
||||||
expect(type).toBe('image/webp');
|
|
||||||
|
|
||||||
const exifData = await readTags(body, 'thumbnail.webp');
|
|
||||||
expect(exifData).not.toHaveProperty('GPSLongitude');
|
|
||||||
expect(exifData).not.toHaveProperty('GPSLatitude');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not include gps data for jpeg thumbnails', async () => {
|
|
||||||
const { status, body, type } = await request(app)
|
|
||||||
.get(`/asset/thumbnail/${assetLocation.id}?format=JPEG`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toBeDefined();
|
|
||||||
expect(type).toBe('image/jpeg');
|
|
||||||
|
|
||||||
const exifData = await readTags(body, 'thumbnail.jpg');
|
|
||||||
expect(exifData).not.toHaveProperty('GPSLongitude');
|
|
||||||
expect(exifData).not.toHaveProperty('GPSLatitude');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /asset/file/:id', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get(
|
|
||||||
`/asset/thumbnail/${assetLocation.id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should download the original', async () => {
|
|
||||||
const { status, body, type } = await request(app)
|
|
||||||
.get(`/asset/file/${assetLocation.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toBeDefined();
|
|
||||||
expect(type).toBe('image/jpeg');
|
|
||||||
|
|
||||||
const asset = await apiUtils.getAssetInfo(
|
|
||||||
admin.accessToken,
|
|
||||||
assetLocation.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
const original = await readFile(locationAssetFilepath);
|
|
||||||
const originalChecksum = sha1(original);
|
|
||||||
const downloadChecksum = sha1(body);
|
|
||||||
|
|
||||||
expect(originalChecksum).toBe(downloadChecksum);
|
|
||||||
expect(downloadChecksum).toBe(asset.checksum);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -29,14 +29,14 @@ describe('/audit', () => {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
deleteAssets(
|
deleteAssets(
|
||||||
{ assetBulkDeleteDto: { ids: [trashedAsset.id] } },
|
{ assetBulkDeleteDto: { ids: [trashedAsset.id] } },
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
),
|
),
|
||||||
updateAsset(
|
updateAsset(
|
||||||
{
|
{
|
||||||
id: archivedAsset.id,
|
id: archivedAsset.id,
|
||||||
updateAssetDto: { isArchived: true },
|
updateAssetDto: { isArchived: true },
|
||||||
},
|
},
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk';
|
import { AssetResponseDto, LoginResponseDto } from '@immich/sdk';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { apiUtils, app, dbUtils } from 'src/utils';
|
import { apiUtils, app, dbUtils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
@@ -6,7 +6,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
|
|||||||
|
|
||||||
describe('/download', () => {
|
describe('/download', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let asset1: AssetFileUploadResponseDto;
|
let asset1: AssetResponseDto;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
apiUtils.setup();
|
apiUtils.setup();
|
||||||
@@ -35,7 +35,7 @@ describe('/download', () => {
|
|||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
archives: [expect.objectContaining({ assetIds: [asset1.id] })],
|
archives: [expect.objectContaining({ assetIds: [asset1.id] })],
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -43,7 +43,7 @@ describe('/download', () => {
|
|||||||
describe('POST /download/asset/:id', () => {
|
describe('POST /download/asset/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post(
|
const { status, body } = await request(app).post(
|
||||||
`/download/asset/${asset1.id}`,
|
`/download/asset/${asset1.id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
AssetFileUploadResponseDto,
|
AssetResponseDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
|
SharedLinkCreateDto,
|
||||||
SharedLinkResponseDto,
|
SharedLinkResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
|
createSharedLink as create,
|
||||||
createAlbum,
|
createAlbum,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
@@ -15,8 +17,8 @@ import { beforeAll, describe, expect, it } from 'vitest';
|
|||||||
|
|
||||||
describe('/shared-link', () => {
|
describe('/shared-link', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let asset1: AssetFileUploadResponseDto;
|
let asset1: AssetResponseDto;
|
||||||
let asset2: AssetFileUploadResponseDto;
|
let asset2: AssetResponseDto;
|
||||||
let user1: LoginResponseDto;
|
let user1: LoginResponseDto;
|
||||||
let user2: LoginResponseDto;
|
let user2: LoginResponseDto;
|
||||||
let album: AlbumResponseDto;
|
let album: AlbumResponseDto;
|
||||||
@@ -48,11 +50,11 @@ describe('/shared-link', () => {
|
|||||||
[album, deletedAlbum, metadataAlbum] = await Promise.all([
|
[album, deletedAlbum, metadataAlbum] = await Promise.all([
|
||||||
createAlbum(
|
createAlbum(
|
||||||
{ createAlbumDto: { albumName: 'album' } },
|
{ createAlbumDto: { albumName: 'album' } },
|
||||||
{ headers: asBearerAuth(user1.accessToken) },
|
{ headers: asBearerAuth(user1.accessToken) }
|
||||||
),
|
),
|
||||||
createAlbum(
|
createAlbum(
|
||||||
{ createAlbumDto: { albumName: 'deleted album' } },
|
{ createAlbumDto: { albumName: 'deleted album' } },
|
||||||
{ headers: asBearerAuth(user2.accessToken) },
|
{ headers: asBearerAuth(user2.accessToken) }
|
||||||
),
|
),
|
||||||
createAlbum(
|
createAlbum(
|
||||||
{
|
{
|
||||||
@@ -61,7 +63,7 @@ describe('/shared-link', () => {
|
|||||||
assetIds: [asset1.id],
|
assetIds: [asset1.id],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ headers: asBearerAuth(user1.accessToken) },
|
{ headers: asBearerAuth(user1.accessToken) }
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -104,7 +106,7 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
await deleteUser(
|
await deleteUser(
|
||||||
{ id: user2.userId },
|
{ id: user2.userId },
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -130,7 +132,7 @@ describe('/shared-link', () => {
|
|||||||
expect.objectContaining({ id: linkWithPassword.id }),
|
expect.objectContaining({ id: linkWithPassword.id }),
|
||||||
expect.objectContaining({ id: linkWithMetadata.id }),
|
expect.objectContaining({ id: linkWithMetadata.id }),
|
||||||
expect.objectContaining({ id: linkWithoutMetadata.id }),
|
expect.objectContaining({ id: linkWithoutMetadata.id }),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -164,7 +166,7 @@ describe('/shared-link', () => {
|
|||||||
album,
|
album,
|
||||||
userId: user1.userId,
|
userId: user1.userId,
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -206,7 +208,7 @@ describe('/shared-link', () => {
|
|||||||
album,
|
album,
|
||||||
userId: user1.userId,
|
userId: user1.userId,
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -223,7 +225,7 @@ describe('/shared-link', () => {
|
|||||||
localDateTime: expect.any(String),
|
localDateTime: expect.any(String),
|
||||||
fileCreatedAt: expect.any(String),
|
fileCreatedAt: expect.any(String),
|
||||||
exifInfo: expect.any(Object),
|
exifInfo: expect.any(Object),
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
expect(body.album).toBeDefined();
|
expect(body.album).toBeDefined();
|
||||||
});
|
});
|
||||||
@@ -248,7 +250,7 @@ describe('/shared-link', () => {
|
|||||||
describe('GET /shared-link/:id', () => {
|
describe('GET /shared-link/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get(
|
const { status, body } = await request(app).get(
|
||||||
`/shared-link/${linkWithAlbum.id}`,
|
`/shared-link/${linkWithAlbum.id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
@@ -266,7 +268,7 @@ describe('/shared-link', () => {
|
|||||||
album,
|
album,
|
||||||
userId: user1.userId,
|
userId: user1.userId,
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -277,7 +279,7 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.objectContaining({ message: 'Shared link not found' }),
|
expect.objectContaining({ message: 'Shared link not found' })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -309,7 +311,7 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.objectContaining({ message: 'Invalid albumId' }),
|
expect.objectContaining({ message: 'Invalid albumId' })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -321,7 +323,7 @@ describe('/shared-link', () => {
|
|||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.objectContaining({ message: 'Invalid assetIds' }),
|
expect.objectContaining({ message: 'Invalid assetIds' })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -336,7 +338,7 @@ describe('/shared-link', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
userId: user1.userId,
|
userId: user1.userId,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -373,7 +375,7 @@ describe('/shared-link', () => {
|
|||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
userId: user1.userId,
|
userId: user1.userId,
|
||||||
description: 'foo',
|
description: 'foo',
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -425,7 +427,7 @@ describe('/shared-link', () => {
|
|||||||
describe('DELETE /shared-link/:id', () => {
|
describe('DELETE /shared-link/:id', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).delete(
|
const { status, body } = await request(app).delete(
|
||||||
`/shared-link/${linkWithAlbum.id}`,
|
`/shared-link/${linkWithAlbum.id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
import { LoginResponseDto, getAllAssets } from '@immich/sdk';
|
|
||||||
import { Socket } from 'socket.io-client';
|
|
||||||
import { errorDto } from 'src/responses';
|
|
||||||
import { apiUtils, app, asBearerAuth, dbUtils, wsUtils } from 'src/utils';
|
|
||||||
import request from 'supertest';
|
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
describe('/trash', () => {
|
|
||||||
let admin: LoginResponseDto;
|
|
||||||
let ws: Socket;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
apiUtils.setup();
|
|
||||||
await dbUtils.reset();
|
|
||||||
admin = await apiUtils.adminSetup({ onboarding: false });
|
|
||||||
ws = await wsUtils.connect(admin.accessToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
wsUtils.disconnect(ws);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('POST /trash/empty', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).post('/trash/empty');
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should empty the trash', async () => {
|
|
||||||
const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
|
|
||||||
await apiUtils.deleteAssets(admin.accessToken, [assetId]);
|
|
||||||
|
|
||||||
const before = await getAllAssets(
|
|
||||||
{},
|
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(before.length).toBeGreaterThanOrEqual(1);
|
|
||||||
|
|
||||||
const { status } = await request(app)
|
|
||||||
.post('/trash/empty')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
await wsUtils.waitForEvent({ event: 'delete', assetId });
|
|
||||||
|
|
||||||
const after = await getAllAssets(
|
|
||||||
{},
|
|
||||||
{ headers: asBearerAuth(admin.accessToken) },
|
|
||||||
);
|
|
||||||
expect(after.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('POST /trash/restore', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).post('/trash/restore');
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should restore all trashed assets', async () => {
|
|
||||||
const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
|
|
||||||
await apiUtils.deleteAssets(admin.accessToken, [assetId]);
|
|
||||||
|
|
||||||
const before = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
|
||||||
expect(before.isTrashed).toBe(true);
|
|
||||||
|
|
||||||
const { status } = await request(app)
|
|
||||||
.post('/trash/restore')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
const after = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
|
||||||
expect(after.isTrashed).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('POST /trash/restore/assets', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).post('/trash/restore/assets');
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should restore a trashed asset by id', async () => {
|
|
||||||
const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
|
|
||||||
await apiUtils.deleteAssets(admin.accessToken, [assetId]);
|
|
||||||
|
|
||||||
const before = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
|
||||||
expect(before.isTrashed).toBe(true);
|
|
||||||
|
|
||||||
const { status } = await request(app)
|
|
||||||
.post('/trash/restore/assets')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send({ ids: [assetId] });
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
const after = await apiUtils.getAssetInfo(admin.accessToken, assetId);
|
|
||||||
expect(after.isTrashed).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { apiUtils, cliUtils, dbUtils, immichCli } from 'src/utils';
|
import { apiUtils, cliUtils, dbUtils, immichCli } from 'src/utils';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe(`immich server-info`, () => {
|
describe(`immich server-info`, () => {
|
||||||
beforeAll(async () => {
|
beforeAll(() => {
|
||||||
apiUtils.setup();
|
apiUtils.setup();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
await dbUtils.reset();
|
await dbUtils.reset();
|
||||||
await cliUtils.login();
|
await cliUtils.login();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { getAllAlbums, getAllAssets } from '@immich/sdk';
|
import { getAllAlbums, getAllAssets } from '@immich/sdk';
|
||||||
import { mkdir, readdir, rm, symlink } from 'fs/promises';
|
|
||||||
import {
|
import {
|
||||||
apiUtils,
|
apiUtils,
|
||||||
asKeyAuth,
|
asKeyAuth,
|
||||||
@@ -9,18 +8,18 @@ import {
|
|||||||
testAssetDir,
|
testAssetDir,
|
||||||
} from 'src/utils';
|
} from 'src/utils';
|
||||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
import { mkdir, readdir, rm, symlink } from 'fs/promises';
|
||||||
|
|
||||||
describe(`immich upload`, () => {
|
describe(`immich upload`, () => {
|
||||||
let key: string;
|
let key: string;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(() => {
|
||||||
apiUtils.setup();
|
apiUtils.setup();
|
||||||
await dbUtils.reset();
|
|
||||||
key = await cliUtils.login();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await dbUtils.reset(['assets', 'albums']);
|
await dbUtils.reset();
|
||||||
|
key = await cliUtils.login();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('immich upload --recursive', () => {
|
describe('immich upload --recursive', () => {
|
||||||
@@ -34,7 +33,7 @@ describe(`immich upload`, () => {
|
|||||||
expect(stdout.split('\n')).toEqual(
|
expect(stdout.split('\n')).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|
||||||
@@ -56,7 +55,7 @@ describe(`immich upload`, () => {
|
|||||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||||
expect.stringContaining('Successfully created 1 new album'),
|
expect.stringContaining('Successfully created 1 new album'),
|
||||||
expect.stringContaining('Successfully updated 9 assets'),
|
expect.stringContaining('Successfully updated 9 assets'),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
expect(stderr).toBe('');
|
expect(stderr).toBe('');
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
@@ -78,7 +77,7 @@ describe(`immich upload`, () => {
|
|||||||
expect(response1.stdout.split('\n')).toEqual(
|
expect(response1.stdout.split('\n')).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
expect(response1.stderr).toBe('');
|
expect(response1.stderr).toBe('');
|
||||||
expect(response1.exitCode).toBe(0);
|
expect(response1.exitCode).toBe(0);
|
||||||
@@ -98,10 +97,10 @@ describe(`immich upload`, () => {
|
|||||||
expect(response2.stdout.split('\n')).toEqual(
|
expect(response2.stdout.split('\n')).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.stringContaining(
|
expect.stringContaining(
|
||||||
'All assets were already uploaded, nothing to do.',
|
'All assets were already uploaded, nothing to do.'
|
||||||
),
|
),
|
||||||
expect.stringContaining('Successfully updated 9 assets'),
|
expect.stringContaining('Successfully updated 9 assets'),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
expect(response2.stderr).toBe('');
|
expect(response2.stderr).toBe('');
|
||||||
expect(response2.exitCode).toBe(0);
|
expect(response2.exitCode).toBe(0);
|
||||||
@@ -128,7 +127,7 @@ describe(`immich upload`, () => {
|
|||||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||||
expect.stringContaining('Successfully created 1 new album'),
|
expect.stringContaining('Successfully created 1 new album'),
|
||||||
expect.stringContaining('Successfully updated 9 assets'),
|
expect.stringContaining('Successfully updated 9 assets'),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
expect(stderr).toBe('');
|
expect(stderr).toBe('');
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
@@ -149,7 +148,7 @@ describe(`immich upload`, () => {
|
|||||||
for (const file of filesToLink) {
|
for (const file of filesToLink) {
|
||||||
await symlink(
|
await symlink(
|
||||||
`${testAssetDir}/albums/nature/${file}`,
|
`${testAssetDir}/albums/nature/${file}`,
|
||||||
`/tmp/albums/nature/${file}`,
|
`/tmp/albums/nature/${file}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +166,7 @@ describe(`immich upload`, () => {
|
|||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||||
expect.stringContaining('Deleting assets that have been uploaded'),
|
expect.stringContaining('Deleting assets that have been uploaded'),
|
||||||
]),
|
])
|
||||||
);
|
);
|
||||||
expect(stderr).toBe('');
|
expect(stderr).toBe('');
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
|
|||||||
+4
-4
@@ -2,25 +2,25 @@ import { spawn, exec } from 'child_process';
|
|||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
let _resolve: () => unknown;
|
let _resolve: () => unknown;
|
||||||
const ready = new Promise<void>((resolve) => (_resolve = resolve));
|
const promise = new Promise<void>((resolve) => (_resolve = resolve));
|
||||||
|
|
||||||
const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' });
|
const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' });
|
||||||
|
|
||||||
child.stdout.on('data', (data) => {
|
child.stdout.on('data', (data) => {
|
||||||
const input = data.toString();
|
const input = data.toString();
|
||||||
console.log(input);
|
console.log(input);
|
||||||
if (input.includes('Immich Microservices is listening')) {
|
if (input.includes('Immich Server is listening')) {
|
||||||
_resolve();
|
_resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stderr.on('data', (data) => console.log(data.toString()));
|
child.stderr.on('data', (data) => console.log(data.toString()));
|
||||||
|
|
||||||
await ready;
|
await promise;
|
||||||
|
|
||||||
return async () => {
|
return async () => {
|
||||||
await new Promise<void>((resolve) =>
|
await new Promise<void>((resolve) =>
|
||||||
exec('docker compose down', () => resolve()),
|
exec('docker compose down', () => resolve())
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
+27
-149
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
AssetFileUploadResponseDto,
|
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
CreateAlbumDto,
|
CreateAlbumDto,
|
||||||
CreateAssetDto,
|
CreateAssetDto,
|
||||||
@@ -12,23 +11,18 @@ import {
|
|||||||
createSharedLink,
|
createSharedLink,
|
||||||
createUser,
|
createUser,
|
||||||
defaults,
|
defaults,
|
||||||
deleteAssets,
|
|
||||||
getAssetInfo,
|
|
||||||
login,
|
login,
|
||||||
setAdminOnboarding,
|
setAdminOnboarding,
|
||||||
signUpAdmin,
|
signUpAdmin,
|
||||||
updatePerson,
|
updatePerson,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { BrowserContext } from '@playwright/test';
|
import { BrowserContext } from '@playwright/test';
|
||||||
import { exec, spawn } from 'node:child_process';
|
import { exec, spawn } from 'child_process';
|
||||||
import { randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
import { access } from 'node:fs/promises';
|
import { access } from 'node:fs/promises';
|
||||||
import { tmpdir } from 'node:os';
|
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { EventEmitter } from 'node:stream';
|
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import pg from 'pg';
|
import pg from 'pg';
|
||||||
import { io, type Socket } from 'socket.io-client';
|
|
||||||
import { loginDto, signupDto } from 'src/fixtures';
|
import { loginDto, signupDto } from 'src/fixtures';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
|
||||||
@@ -43,23 +37,17 @@ const directoryExists = (directory: string) =>
|
|||||||
|
|
||||||
// TODO move test assets into e2e/assets
|
// TODO move test assets into e2e/assets
|
||||||
export const testAssetDir = path.resolve(`./../server/test/assets/`);
|
export const testAssetDir = path.resolve(`./../server/test/assets/`);
|
||||||
export const tempDir = tmpdir();
|
|
||||||
|
|
||||||
const serverContainerName = 'immich-e2e-server';
|
const serverContainerName = 'immich-e2e-server';
|
||||||
const mediaDir = '/usr/src/app/upload';
|
const uploadMediaDir = '/usr/src/app/upload/upload';
|
||||||
const dirs = [
|
|
||||||
`"${mediaDir}/thumbs"`,
|
|
||||||
`"${mediaDir}/upload"`,
|
|
||||||
`"${mediaDir}/library"`,
|
|
||||||
`"${mediaDir}/encoded-video"`,
|
|
||||||
].join(' ');
|
|
||||||
|
|
||||||
if (!(await directoryExists(`${testAssetDir}/albums`))) {
|
if (!(await directoryExists(`${testAssetDir}/albums`))) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`,
|
`Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setBaseUrl = () => (defaults.baseUrl = app);
|
||||||
export const asBearerAuth = (accessToken: string) => ({
|
export const asBearerAuth = (accessToken: string) => ({
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
});
|
});
|
||||||
@@ -71,7 +59,7 @@ let client: pg.Client | null = null;
|
|||||||
export const fileUtils = {
|
export const fileUtils = {
|
||||||
reset: async () => {
|
reset: async () => {
|
||||||
await execPromise(
|
await execPromise(
|
||||||
`docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`,
|
`docker exec -i "${serverContainerName}" rm -R "${uploadMediaDir}"`
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -93,7 +81,7 @@ export const dbUtils = {
|
|||||||
|
|
||||||
await client.query(
|
await client.query(
|
||||||
'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)',
|
'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)',
|
||||||
[assetId, personId, embedding],
|
[assetId, personId, embedding]
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
setPersonThumbnail: async (personId: string) => {
|
setPersonThumbnail: async (personId: string) => {
|
||||||
@@ -103,14 +91,14 @@ export const dbUtils = {
|
|||||||
|
|
||||||
await client.query(
|
await client.query(
|
||||||
`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`,
|
`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`,
|
||||||
[personId],
|
[personId]
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
reset: async (tables?: string[]) => {
|
reset: async (tables?: string[]) => {
|
||||||
try {
|
try {
|
||||||
if (!client) {
|
if (!client) {
|
||||||
client = new pg.Client(
|
client = new pg.Client(
|
||||||
'postgres://postgres:postgres@127.0.0.1:5433/immich',
|
'postgres://postgres:postgres@127.0.0.1:5433/immich'
|
||||||
);
|
);
|
||||||
await client.connect();
|
await client.connect();
|
||||||
}
|
}
|
||||||
@@ -182,94 +170,10 @@ export interface AdminSetupOptions {
|
|||||||
onboarding?: boolean;
|
onboarding?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SocketEvent {
|
|
||||||
UPLOAD = 'upload',
|
|
||||||
DELETE = 'delete',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventType = 'upload' | 'delete';
|
|
||||||
export interface WaitOptions {
|
|
||||||
event: EventType;
|
|
||||||
assetId: string;
|
|
||||||
timeout?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const events: Record<EventType, Set<string>> = {
|
|
||||||
upload: new Set<string>(),
|
|
||||||
delete: new Set<string>(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const callbacks: Record<string, () => void> = {};
|
|
||||||
|
|
||||||
const onEvent = ({ event, assetId }: { event: EventType; assetId: string }) => {
|
|
||||||
events[event].add(assetId);
|
|
||||||
const callback = callbacks[assetId];
|
|
||||||
if (callback) {
|
|
||||||
callback();
|
|
||||||
delete callbacks[assetId];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const wsUtils = {
|
|
||||||
connect: async (accessToken: string) => {
|
|
||||||
const websocket = io('http://127.0.0.1:2283', {
|
|
||||||
path: '/api/socket.io',
|
|
||||||
transports: ['websocket'],
|
|
||||||
extraHeaders: { Authorization: `Bearer ${accessToken}` },
|
|
||||||
autoConnect: true,
|
|
||||||
forceNew: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise<Socket>((resolve) => {
|
|
||||||
websocket
|
|
||||||
.on('connect', () => resolve(websocket))
|
|
||||||
.on('on_upload_success', (data: AssetResponseDto) =>
|
|
||||||
onEvent({ event: 'upload', assetId: data.id }),
|
|
||||||
)
|
|
||||||
.on('on_asset_delete', (assetId: string) =>
|
|
||||||
onEvent({ event: 'delete', assetId }),
|
|
||||||
)
|
|
||||||
.connect();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
disconnect: (ws: Socket) => {
|
|
||||||
if (ws?.connected) {
|
|
||||||
ws.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const set of Object.values(events)) {
|
|
||||||
set.clear();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
waitForEvent: async ({
|
|
||||||
event,
|
|
||||||
assetId,
|
|
||||||
timeout: ms,
|
|
||||||
}: WaitOptions): Promise<void> => {
|
|
||||||
const set = events[event];
|
|
||||||
if (set.has(assetId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
const timeout = setTimeout(
|
|
||||||
() => reject(new Error(`Timed out waiting for ${event} event`)),
|
|
||||||
ms || 5000,
|
|
||||||
);
|
|
||||||
|
|
||||||
callbacks[assetId] = () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const apiUtils = {
|
export const apiUtils = {
|
||||||
setup: () => {
|
setup: () => {
|
||||||
defaults.baseUrl = app;
|
setBaseUrl();
|
||||||
},
|
},
|
||||||
|
|
||||||
adminSetup: async (options?: AdminSetupOptions) => {
|
adminSetup: async (options?: AdminSetupOptions) => {
|
||||||
options = options || { onboarding: true };
|
options = options || { onboarding: true };
|
||||||
|
|
||||||
@@ -283,7 +187,7 @@ export const apiUtils = {
|
|||||||
userSetup: async (accessToken: string, dto: CreateUserDto) => {
|
userSetup: async (accessToken: string, dto: CreateUserDto) => {
|
||||||
await createUser(
|
await createUser(
|
||||||
{ createUserDto: dto },
|
{ createUserDto: dto },
|
||||||
{ headers: asBearerAuth(accessToken) },
|
{ headers: asBearerAuth(accessToken) }
|
||||||
);
|
);
|
||||||
return login({
|
return login({
|
||||||
loginCredentialDto: { email: dto.email, password: dto.password },
|
loginCredentialDto: { email: dto.email, password: dto.password },
|
||||||
@@ -292,74 +196,48 @@ export const apiUtils = {
|
|||||||
createApiKey: (accessToken: string) => {
|
createApiKey: (accessToken: string) => {
|
||||||
return createApiKey(
|
return createApiKey(
|
||||||
{ apiKeyCreateDto: { name: 'e2e' } },
|
{ apiKeyCreateDto: { name: 'e2e' } },
|
||||||
{ headers: asBearerAuth(accessToken) },
|
{ headers: asBearerAuth(accessToken) }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
|
createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
|
||||||
createAlbum(
|
createAlbum(
|
||||||
{ createAlbumDto: dto },
|
{ createAlbumDto: dto },
|
||||||
{ headers: asBearerAuth(accessToken) },
|
{ headers: asBearerAuth(accessToken) }
|
||||||
),
|
),
|
||||||
createAsset: async (
|
createAsset: async (
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
dto?: Partial<Omit<CreateAssetDto, 'assetData'>>,
|
dto?: Omit<CreateAssetDto, 'assetData'>
|
||||||
data?: {
|
|
||||||
bytes?: Buffer;
|
|
||||||
filename?: string;
|
|
||||||
},
|
|
||||||
) => {
|
) => {
|
||||||
const _dto = {
|
dto = dto || {
|
||||||
deviceAssetId: 'test-1',
|
deviceAssetId: 'test-1',
|
||||||
deviceId: 'test',
|
deviceId: 'test',
|
||||||
fileCreatedAt: new Date().toISOString(),
|
fileCreatedAt: new Date().toISOString(),
|
||||||
fileModifiedAt: new Date().toISOString(),
|
fileModifiedAt: new Date().toISOString(),
|
||||||
...(dto || {}),
|
|
||||||
};
|
};
|
||||||
|
const { body } = await request(app)
|
||||||
const _assetData = {
|
|
||||||
bytes: randomBytes(32),
|
|
||||||
filename: 'example.jpg',
|
|
||||||
...(data || {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const builder = request(app)
|
|
||||||
.post(`/asset/upload`)
|
.post(`/asset/upload`)
|
||||||
.attach('assetData', _assetData.bytes, _assetData.filename)
|
.field('deviceAssetId', dto.deviceAssetId)
|
||||||
|
.field('deviceId', dto.deviceId)
|
||||||
|
.field('fileCreatedAt', dto.fileCreatedAt)
|
||||||
|
.field('fileModifiedAt', dto.fileModifiedAt)
|
||||||
|
.attach('assetData', randomBytes(32), 'example.jpg')
|
||||||
.set('Authorization', `Bearer ${accessToken}`);
|
.set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(_dto)) {
|
return body as AssetResponseDto;
|
||||||
builder.field(key, String(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
const { body } = await builder;
|
|
||||||
|
|
||||||
return body as AssetFileUploadResponseDto;
|
|
||||||
},
|
},
|
||||||
getAssetInfo: (accessToken: string, id: string) =>
|
createPerson: async (accessToken: string, dto: PersonUpdateDto) => {
|
||||||
getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
|
||||||
deleteAssets: (accessToken: string, ids: string[]) =>
|
|
||||||
deleteAssets(
|
|
||||||
{ assetBulkDeleteDto: { ids } },
|
|
||||||
{ headers: asBearerAuth(accessToken) },
|
|
||||||
),
|
|
||||||
createPerson: async (accessToken: string, dto?: PersonUpdateDto) => {
|
|
||||||
// TODO fix createPerson to accept a body
|
// TODO fix createPerson to accept a body
|
||||||
let person = await createPerson({ headers: asBearerAuth(accessToken) });
|
const { id } = await createPerson({ headers: asBearerAuth(accessToken) });
|
||||||
await dbUtils.setPersonThumbnail(person.id);
|
await dbUtils.setPersonThumbnail(id);
|
||||||
|
|
||||||
if (!dto) {
|
|
||||||
return person;
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatePerson(
|
return updatePerson(
|
||||||
{ id: person.id, personUpdateDto: dto },
|
{ id, personUpdateDto: dto },
|
||||||
{ headers: asBearerAuth(accessToken) },
|
{ headers: asBearerAuth(accessToken) }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
|
createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
|
||||||
createSharedLink(
|
createSharedLink(
|
||||||
{ sharedLinkCreateDto: dto },
|
{ sharedLinkCreateDto: dto },
|
||||||
{ headers: asBearerAuth(accessToken) },
|
{ headers: asBearerAuth(accessToken) }
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ test.describe('Shared Links', () => {
|
|||||||
let asset: AssetResponseDto;
|
let asset: AssetResponseDto;
|
||||||
let album: AlbumResponseDto;
|
let album: AlbumResponseDto;
|
||||||
let sharedLink: SharedLinkResponseDto;
|
let sharedLink: SharedLinkResponseDto;
|
||||||
let sharedLinkPassword: SharedLinkResponseDto;
|
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
apiUtils.setup();
|
apiUtils.setup();
|
||||||
@@ -30,16 +29,17 @@ test.describe('Shared Links', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ headers: asBearerAuth(admin.accessToken) }
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
|
// { headers: asBearerAuth(admin.accessToken)},
|
||||||
|
);
|
||||||
|
sharedLink = await createSharedLink(
|
||||||
|
{
|
||||||
|
sharedLinkCreateDto: {
|
||||||
|
type: SharedLinkType.Album,
|
||||||
|
albumId: album.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ headers: asBearerAuth(admin.accessToken) }
|
||||||
);
|
);
|
||||||
sharedLink = await apiUtils.createSharedLink(admin.accessToken, {
|
|
||||||
type: SharedLinkType.Album,
|
|
||||||
albumId: album.id,
|
|
||||||
});
|
|
||||||
sharedLinkPassword = await apiUtils.createSharedLink(admin.accessToken, {
|
|
||||||
type: SharedLinkType.Album,
|
|
||||||
albumId: album.id,
|
|
||||||
password: 'test-password',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterAll(async () => {
|
test.afterAll(async () => {
|
||||||
@@ -55,16 +55,4 @@ test.describe('Shared Links', () => {
|
|||||||
await page.getByRole('button', { name: 'Download' }).click();
|
await page.getByRole('button', { name: 'Download' }).click();
|
||||||
await page.getByText('DOWNLOADING').waitFor();
|
await page.getByText('DOWNLOADING').waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('enter password for a shared link', async ({ page }) => {
|
|
||||||
await page.goto(`/share/${sharedLinkPassword.key}`);
|
|
||||||
await page.getByPlaceholder('Password').fill('test-password');
|
|
||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
|
||||||
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('show error for invalid shared link', async ({ page }) => {
|
|
||||||
await page.goto('/share/invalid');
|
|
||||||
await page.getByRole('heading', { name: 'Invalid share key' }).waitFor();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -165,14 +165,6 @@ class InferenceModel(ABC):
|
|||||||
def providers_default(self) -> list[str]:
|
def providers_default(self) -> list[str]:
|
||||||
available_providers = set(ort.get_available_providers())
|
available_providers = set(ort.get_available_providers())
|
||||||
log.debug(f"Available ORT providers: {available_providers}")
|
log.debug(f"Available ORT providers: {available_providers}")
|
||||||
if (openvino := "OpenVINOExecutionProvider") in available_providers:
|
|
||||||
device_ids: list[str] = ort.capi._pybind_state.get_available_openvino_device_ids()
|
|
||||||
log.debug(f"Available OpenVINO devices: {device_ids}")
|
|
||||||
|
|
||||||
gpu_devices = [device_id for device_id in device_ids if device_id.startswith("GPU")]
|
|
||||||
if not gpu_devices:
|
|
||||||
log.warning("No GPU device found in OpenVINO. Falling back to CPU.")
|
|
||||||
available_providers.remove(openvino)
|
|
||||||
return [provider for provider in SUPPORTED_PROVIDERS if provider in available_providers]
|
return [provider for provider in SUPPORTED_PROVIDERS if provider in available_providers]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -192,7 +184,15 @@ class InferenceModel(ABC):
|
|||||||
case "CPUExecutionProvider" | "CUDAExecutionProvider":
|
case "CPUExecutionProvider" | "CUDAExecutionProvider":
|
||||||
option = {"arena_extend_strategy": "kSameAsRequested"}
|
option = {"arena_extend_strategy": "kSameAsRequested"}
|
||||||
case "OpenVINOExecutionProvider":
|
case "OpenVINOExecutionProvider":
|
||||||
option = {"device_type": "GPU_FP32"}
|
try:
|
||||||
|
device_ids: list[str] = ort.capi._pybind_state.get_available_openvino_device_ids()
|
||||||
|
log.debug(f"Available OpenVINO devices: {device_ids}")
|
||||||
|
gpu_devices = [device_id for device_id in device_ids if device_id.startswith("GPU")]
|
||||||
|
option = {"device_id": gpu_devices[0]} if gpu_devices else {}
|
||||||
|
except AttributeError as e:
|
||||||
|
log.warning("Failed to get OpenVINO device IDs. Using default options.")
|
||||||
|
log.error(e)
|
||||||
|
option = {}
|
||||||
case _:
|
case _:
|
||||||
option = {}
|
option = {}
|
||||||
options.append(option)
|
options.append(option)
|
||||||
|
|||||||
@@ -45,23 +45,11 @@ class TestBase:
|
|||||||
assert encoder.providers == self.CUDA_EP
|
assert encoder.providers == self.CUDA_EP
|
||||||
|
|
||||||
@pytest.mark.providers(OV_EP)
|
@pytest.mark.providers(OV_EP)
|
||||||
def test_sets_openvino_provider_if_available(self, providers: list[str], mocker: MockerFixture) -> None:
|
def test_sets_openvino_provider_if_available(self, providers: list[str]) -> None:
|
||||||
mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
|
|
||||||
mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"]
|
|
||||||
|
|
||||||
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
||||||
|
|
||||||
assert encoder.providers == self.OV_EP
|
assert encoder.providers == self.OV_EP
|
||||||
|
|
||||||
@pytest.mark.providers(OV_EP)
|
|
||||||
def test_avoids_openvino_if_gpu_not_available(self, providers: list[str], mocker: MockerFixture) -> None:
|
|
||||||
mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
|
|
||||||
mocked.get_available_openvino_device_ids.return_value = ["CPU"]
|
|
||||||
|
|
||||||
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
|
||||||
|
|
||||||
assert encoder.providers == self.CPU_EP
|
|
||||||
|
|
||||||
@pytest.mark.providers(CUDA_EP_OUT_OF_ORDER)
|
@pytest.mark.providers(CUDA_EP_OUT_OF_ORDER)
|
||||||
def test_sets_providers_in_correct_order(self, providers: list[str]) -> None:
|
def test_sets_providers_in_correct_order(self, providers: list[str]) -> None:
|
||||||
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
||||||
@@ -80,14 +68,22 @@ class TestBase:
|
|||||||
|
|
||||||
assert encoder.providers == providers
|
assert encoder.providers == providers
|
||||||
|
|
||||||
def test_sets_default_provider_options(self, mocker: MockerFixture) -> None:
|
def test_sets_default_provider_options(self) -> None:
|
||||||
|
encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"])
|
||||||
|
|
||||||
|
assert encoder.provider_options == [
|
||||||
|
{},
|
||||||
|
{"arena_extend_strategy": "kSameAsRequested"},
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_sets_openvino_device_id_if_possible(self, mocker: MockerFixture) -> None:
|
||||||
mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
|
mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
|
||||||
mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"]
|
mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"]
|
||||||
|
|
||||||
encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"])
|
encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"])
|
||||||
|
|
||||||
assert encoder.provider_options == [
|
assert encoder.provider_options == [
|
||||||
{"device_type": "GPU_FP32"},
|
{"device_id": "GPU.0"},
|
||||||
{"arena_extend_strategy": "kSameAsRequested"},
|
{"arena_extend_strategy": "kSameAsRequested"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM mambaorg/micromamba:bookworm-slim@sha256:6038b89363c9181215f3d9e8ce2720c880e224537f4028a854482e43a9b4998a as builder
|
FROM mambaorg/micromamba:bookworm-slim@sha256:926cac38640709f90f3fef2a3f730733b5c350be612f0d14706be8833b79ad8c as builder
|
||||||
|
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
TRANSFORMERS_CACHE=/cache \
|
TRANSFORMERS_CACHE=/cache \
|
||||||
|
|||||||
Generated
+75
-75
@@ -1250,13 +1250,13 @@ test = ["Cython (>=0.29.24,<0.30.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpx"
|
name = "httpx"
|
||||||
version = "0.27.0"
|
version = "0.26.0"
|
||||||
description = "The next generation HTTP client."
|
description = "The next generation HTTP client."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
|
{file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"},
|
||||||
{file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
|
{file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -2101,61 +2101,61 @@ numpy = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "orjson"
|
name = "orjson"
|
||||||
version = "3.9.15"
|
version = "3.9.14"
|
||||||
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
|
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "orjson-3.9.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d61f7ce4727a9fa7680cd6f3986b0e2c732639f46a5e0156e550e35258aa313a"},
|
{file = "orjson-3.9.14-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:793f6c9448ab6eb7d4974b4dde3f230345c08ca6c7995330fbceeb43a5c8aa5e"},
|
||||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4feeb41882e8aa17634b589533baafdceb387e01e117b1ec65534ec724023d04"},
|
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bc7928d161840096adc956703494b5c0193ede887346f028216cac0af87500"},
|
||||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fbbeb3c9b2edb5fd044b2a070f127a0ac456ffd079cb82746fc84af01ef021a4"},
|
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58b36f54da759602d8e2f7dad958752d453dfe2c7122767bc7f765e17dc59959"},
|
||||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b66bcc5670e8a6b78f0313bcb74774c8291f6f8aeef10fe70e910b8040f3ab75"},
|
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:abcda41ecdc950399c05eff761c3de91485d9a70d8227cb599ad3a66afe93bcc"},
|
||||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2973474811db7b35c30248d1129c64fd2bdf40d57d84beed2a9a379a6f57d0ab"},
|
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df76ecd17b1b3627bddfd689faaf206380a1a38cc9f6c4075bd884eaedcf46c2"},
|
||||||
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fe41b6f72f52d3da4db524c8653e46243c8c92df826ab5ffaece2dba9cccd58"},
|
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d450a8e0656efb5d0fcb062157b918ab02dcca73278975b4ee9ea49e2fcf5bd5"},
|
||||||
{file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4228aace81781cc9d05a3ec3a6d2673a1ad0d8725b4e915f1089803e9efd2b99"},
|
{file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:95c03137b0cf66517c8baa65770507a756d3a89489d8ecf864ea92348e1beabe"},
|
||||||
{file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f7b65bfaf69493c73423ce9db66cfe9138b2f9ef62897486417a8fcb0a92bfe"},
|
{file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20837e10835c98973673406d6798e10f821e7744520633811a5a3d809762d8cc"},
|
||||||
{file = "orjson-3.9.15-cp310-none-win32.whl", hash = "sha256:2d99e3c4c13a7b0fb3792cc04c2829c9db07838fb6973e578b85c1745e7d0ce7"},
|
{file = "orjson-3.9.14-cp310-none-win32.whl", hash = "sha256:1f7b6f3ef10ae8e3558abb729873d033dbb5843507c66b1c0767e32502ba96bb"},
|
||||||
{file = "orjson-3.9.15-cp310-none-win_amd64.whl", hash = "sha256:b725da33e6e58e4a5d27958568484aa766e825e93aa20c26c91168be58e08cbb"},
|
{file = "orjson-3.9.14-cp310-none-win_amd64.whl", hash = "sha256:ea890e6dc1711aeec0a33b8520e395c2f3d59ead5b4351a788e06bf95fc7ba81"},
|
||||||
{file = "orjson-3.9.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c8e8fe01e435005d4421f183038fc70ca85d2c1e490f51fb972db92af6e047c2"},
|
{file = "orjson-3.9.14-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c19009ff37f033c70acd04b636380379499dac2cba27ae7dfc24f304deabbc81"},
|
||||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87f1097acb569dde17f246faa268759a71a2cb8c96dd392cd25c668b104cad2f"},
|
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19cdea0664aec0b7f385be84986d4defd3334e9c3c799407686ee1c26f7b8251"},
|
||||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff0f9913d82e1d1fadbd976424c316fbc4d9c525c81d047bbdd16bd27dd98cfc"},
|
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:135d518f73787ce323b1a5e21fb854fe22258d7a8ae562b81a49d6c7f826f2a3"},
|
||||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8055ec598605b0077e29652ccfe9372247474375e0e3f5775c91d9434e12d6b1"},
|
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2cf1d0557c61c75e18cf7d69fb689b77896e95553e212c0cc64cf2087944b84"},
|
||||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6768a327ea1ba44c9114dba5fdda4a214bdb70129065cd0807eb5f010bfcbb5"},
|
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7c11667421df2d8b18b021223505dcc3ee51be518d54e4dc49161ac88ac2b87"},
|
||||||
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12365576039b1a5a47df01aadb353b68223da413e2e7f98c02403061aad34bde"},
|
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eefc41ba42e75ed88bc396d8fe997beb20477f3e7efa000cd7a47eda452fbb2"},
|
||||||
{file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:71c6b009d431b3839d7c14c3af86788b3cfac41e969e3e1c22f8a6ea13139404"},
|
{file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:917311d6a64d1c327c0dfda1e41f3966a7fb72b11ca7aa2e7a68fcccc7db35d9"},
|
||||||
{file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e18668f1bd39e69b7fed19fa7cd1cd110a121ec25439328b5c89934e6d30d357"},
|
{file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4dc1c132259b38d12c6587d190cd09cd76e3b5273ce71fe1372437b4cbc65f6f"},
|
||||||
{file = "orjson-3.9.15-cp311-none-win32.whl", hash = "sha256:62482873e0289cf7313461009bf62ac8b2e54bc6f00c6fabcde785709231a5d7"},
|
{file = "orjson-3.9.14-cp311-none-win32.whl", hash = "sha256:6f39a10408478f4c05736a74da63727a1ae0e83e3533d07b19443400fe8591ca"},
|
||||||
{file = "orjson-3.9.15-cp311-none-win_amd64.whl", hash = "sha256:b3d336ed75d17c7b1af233a6561cf421dee41d9204aa3cfcc6c9c65cd5bb69a8"},
|
{file = "orjson-3.9.14-cp311-none-win_amd64.whl", hash = "sha256:26280a7fcb62d8257f634c16acebc3bec626454f9ab13558bbf7883b9140760e"},
|
||||||
{file = "orjson-3.9.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:82425dd5c7bd3adfe4e94c78e27e2fa02971750c2b7ffba648b0f5d5cc016a73"},
|
{file = "orjson-3.9.14-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:08e722a8d06b13b67a51f247a24938d1a94b4b3862e40e0eef3b2e98c99cd04c"},
|
||||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c51378d4a8255b2e7c1e5cc430644f0939539deddfa77f6fac7b56a9784160a"},
|
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2591faa0c031cf3f57e5bce1461cfbd6160f3f66b5a72609a130924917cb07d"},
|
||||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae4e06be04dc00618247c4ae3f7c3e561d5bc19ab6941427f6d3722a0875ef7"},
|
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2450d87dd7b4f277f4c5598faa8b49a0c197b91186c47a2c0b88e15531e4e3e"},
|
||||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcef128f970bb63ecf9a65f7beafd9b55e3aaf0efc271a4154050fc15cdb386e"},
|
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90903d2908158a2c9077a06f11e27545de610af690fb178fd3ba6b32492d4d1c"},
|
||||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b72758f3ffc36ca566ba98a8e7f4f373b6c17c646ff8ad9b21ad10c29186f00d"},
|
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce6f095eef0026eae76fc212f20f786011ecf482fc7df2f4c272a8ae6dd7b1ef"},
|
||||||
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c57bc7b946cf2efa67ac55766e41764b66d40cbd9489041e637c1304400494"},
|
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:751250a31fef2bac05a2da2449aae7142075ea26139271f169af60456d8ad27a"},
|
||||||
{file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:946c3a1ef25338e78107fba746f299f926db408d34553b4754e90a7de1d44068"},
|
{file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a1af21160a38ee8be3f4fcf24ee4b99e6184cadc7f915d599f073f478a94d2c"},
|
||||||
{file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2f256d03957075fcb5923410058982aea85455d035607486ccb847f095442bda"},
|
{file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:449bf090b2aa4e019371d7511a6ea8a5a248139205c27d1834bb4b1e3c44d936"},
|
||||||
{file = "orjson-3.9.15-cp312-none-win_amd64.whl", hash = "sha256:5bb399e1b49db120653a31463b4a7b27cf2fbfe60469546baf681d1b39f4edf2"},
|
{file = "orjson-3.9.14-cp312-none-win_amd64.whl", hash = "sha256:a603161318ff699784943e71f53899983b7dee571b4dd07c336437c9c5a272b0"},
|
||||||
{file = "orjson-3.9.15-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b17f0f14a9c0ba55ff6279a922d1932e24b13fc218a3e968ecdbf791b3682b25"},
|
{file = "orjson-3.9.14-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:814f288c011efdf8f115c5ebcc1ab94b11da64b207722917e0ceb42f52ef30a3"},
|
||||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f6cbd8e6e446fb7e4ed5bac4661a29e43f38aeecbf60c4b900b825a353276a1"},
|
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88cafb100af68af3b9b29b5ccd09fdf7a48c63327916c8c923a94c336d38dd3"},
|
||||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76bc6356d07c1d9f4b782813094d0caf1703b729d876ab6a676f3aaa9a47e37c"},
|
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba3518b999f88882ade6686f1b71e207b52e23546e180499be5bbb63a2f9c6e6"},
|
||||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdfa97090e2d6f73dced247a2f2d8004ac6449df6568f30e7fa1a045767c69a6"},
|
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978f416bbff9da8d2091e3cf011c92da68b13f2c453dcc2e8109099b2a19d234"},
|
||||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7413070a3e927e4207d00bd65f42d1b780fb0d32d7b1d951f6dc6ade318e1b5a"},
|
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75fc593cf836f631153d0e21beaeb8d26e144445c73645889335c2247fcd71a0"},
|
||||||
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cf1596680ac1f01839dba32d496136bdd5d8ffb858c280fa82bbfeb173bdd40"},
|
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d1528db3c7554f9d6eeb09df23cb80dd5177ec56eeb55cc5318826928de506"},
|
||||||
{file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:809d653c155e2cc4fd39ad69c08fdff7f4016c355ae4b88905219d3579e31eb7"},
|
{file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:7183cc68ee2113b19b0b8714221e5e3b07b3ba10ca2bb108d78fd49cefaae101"},
|
||||||
{file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:920fa5a0c5175ab14b9c78f6f820b75804fb4984423ee4c4f1e6d748f8b22bc1"},
|
{file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df3266d54246cb56b8bb17fa908660d2a0f2e3f63fbc32451ffc1b1505051d07"},
|
||||||
{file = "orjson-3.9.15-cp38-none-win32.whl", hash = "sha256:2b5c0f532905e60cf22a511120e3719b85d9c25d0e1c2a8abb20c4dede3b05a5"},
|
{file = "orjson-3.9.14-cp38-none-win32.whl", hash = "sha256:7913079b029e1b3501854c9a78ad938ed40d61fe09bebab3c93e60ff1301b189"},
|
||||||
{file = "orjson-3.9.15-cp38-none-win_amd64.whl", hash = "sha256:67384f588f7f8daf040114337d34a5188346e3fae6c38b6a19a2fe8c663a2f9b"},
|
{file = "orjson-3.9.14-cp38-none-win_amd64.whl", hash = "sha256:29512eb925b620e5da2fd7585814485c67cc6ba4fe739a0a700c50467a8a8065"},
|
||||||
{file = "orjson-3.9.15-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6fc2fe4647927070df3d93f561d7e588a38865ea0040027662e3e541d592811e"},
|
{file = "orjson-3.9.14-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5bf597530544db27a8d76aced49cfc817ee9503e0a4ebf0109cd70331e7bbe0c"},
|
||||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34cbcd216e7af5270f2ffa63a963346845eb71e174ea530867b7443892d77180"},
|
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac650d49366fa41fe702e054cb560171a8634e2865537e91f09a8d05ea5b1d37"},
|
||||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f541587f5c558abd93cb0de491ce99a9ef8d1ae29dd6ab4dbb5a13281ae04cbd"},
|
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:236230433a9a4968ab895140514c308fdf9f607cb8bee178a04372b771123860"},
|
||||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92255879280ef9c3c0bcb327c5a1b8ed694c290d61a6a532458264f887f052cb"},
|
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3014ccbda9be0b1b5f8ea895121df7e6524496b3908f4397ff02e923bcd8f6dd"},
|
||||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05a1f57fb601c426635fcae9ddbe90dfc1ed42245eb4c75e4960440cac667262"},
|
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac0c7eae7ad3a223bde690565442f8a3d620056bd01196f191af8be58a5248e1"},
|
||||||
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ede0bde16cc6e9b96633df1631fbcd66491d1063667f260a4f2386a098393790"},
|
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fca33fdd0b38839b01912c57546d4f412ba7bfa0faf9bf7453432219aec2df07"},
|
||||||
{file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e88b97ef13910e5f87bcbc4dd7979a7de9ba8702b54d3204ac587e83639c0c2b"},
|
{file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f75823cc1674a840a151e999a7dfa0d86c911150dd6f951d0736ee9d383bf415"},
|
||||||
{file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57d5d8cf9c27f7ef6bc56a5925c7fbc76b61288ab674eb352c26ac780caa5b10"},
|
{file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f52ac2eb49e99e7373f62e2a68428c6946cda52ce89aa8fe9f890c7278e2d3a"},
|
||||||
{file = "orjson-3.9.15-cp39-none-win32.whl", hash = "sha256:001f4eb0ecd8e9ebd295722d0cbedf0748680fb9998d3993abaed2f40587257a"},
|
{file = "orjson-3.9.14-cp39-none-win32.whl", hash = "sha256:0572f174f50b673b7df78680fb52cd0087a8585a6d06d295a5f790568e1064c6"},
|
||||||
{file = "orjson-3.9.15-cp39-none-win_amd64.whl", hash = "sha256:ea0b183a5fe6b2b45f3b854b0d19c4e932d6f5934ae1f723b07cf9560edd4ec7"},
|
{file = "orjson-3.9.14-cp39-none-win_amd64.whl", hash = "sha256:ab90c02cb264250b8a58cedcc72ed78a4a257d956c8d3c8bebe9751b818dfad8"},
|
||||||
{file = "orjson-3.9.15.tar.gz", hash = "sha256:95cae920959d772f30ab36d3b25f83bb0f3be671e986c72ce22f8fa700dae061"},
|
{file = "orjson-3.9.14.tar.gz", hash = "sha256:06fb40f8e49088ecaa02f1162581d39e2cf3fd9dbbfe411eb2284147c99bad79"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2465,13 +2465,13 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "8.0.2"
|
version = "8.0.0"
|
||||||
description = "pytest: simple powerful testing with Python"
|
description = "pytest: simple powerful testing with Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"},
|
{file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"},
|
||||||
{file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"},
|
{file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -2836,28 +2836,28 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.2.2"
|
version = "0.2.1"
|
||||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"},
|
{file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080"},
|
||||||
{file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"},
|
{file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"},
|
||||||
{file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"},
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"},
|
||||||
{file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"},
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"},
|
||||||
{file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"},
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"},
|
||||||
{file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"},
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"},
|
||||||
{file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"},
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"},
|
||||||
{file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"},
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"},
|
||||||
{file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"},
|
{file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"},
|
||||||
{file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"},
|
{file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"},
|
||||||
{file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"},
|
{file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"},
|
||||||
{file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"},
|
{file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"},
|
||||||
{file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"},
|
{file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"},
|
||||||
{file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"},
|
{file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"},
|
||||||
{file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"},
|
{file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"},
|
||||||
{file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"},
|
{file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"},
|
||||||
{file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"},
|
{file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "machine-learning"
|
name = "machine-learning"
|
||||||
version = "1.97.0"
|
version = "1.95.1"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 125,
|
"android.injected.version.code" => 123,
|
||||||
"android.injected.version.name" => "1.97.0",
|
"android.injected.version.name" => "1.95.1",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
|||||||
@@ -5,17 +5,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000271">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000232">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="74.334294">
|
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="78.881681">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="29.507669">
|
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="32.080999">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -379,7 +379,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 140;
|
CURRENT_PROJECT_VERSION = 139;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -515,7 +515,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 140;
|
CURRENT_PROJECT_VERSION = 139;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -543,7 +543,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 140;
|
CURRENT_PROJECT_VERSION = 139;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
|||||||
@@ -55,11 +55,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.96.0</string>
|
<string>1.95.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>140</string>
|
<string>139</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true />
|
<true />
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.97.0"
|
version_number: "1.95.1"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -5,32 +5,32 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000316">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000255">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.190055">
|
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.157832">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.109364">
|
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.825919">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.15926">
|
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.18815">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="4: build_app" time="80.90681">
|
<testcase classname="fastlane.lanes" name="4: build_app" time="110.912709">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="71.634559">
|
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="78.396901">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
@@ -133,7 +132,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
void toggleFavorite(Asset asset) =>
|
void toggleFavorite(Asset asset) =>
|
||||||
ref.read(assetProvider.notifier).toggleFavorite([asset]);
|
ref.read(assetProvider.notifier).toggleFavorite([asset]);
|
||||||
|
|
||||||
Future<void> precacheNextImage(int index) async {
|
void precacheNextImage(int index) {
|
||||||
void onError(Object exception, StackTrace? stackTrace) {
|
void onError(Object exception, StackTrace? stackTrace) {
|
||||||
// swallow error silently
|
// swallow error silently
|
||||||
debugPrint('Error precaching next image: $exception, $stackTrace');
|
debugPrint('Error precaching next image: $exception, $stackTrace');
|
||||||
@@ -141,7 +140,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (index < totalAssets && index >= 0) {
|
if (index < totalAssets && index >= 0) {
|
||||||
final asset = loadAsset(index);
|
final asset = loadAsset(index);
|
||||||
await precacheImage(
|
precacheImage(
|
||||||
ImmichImage.imageProvider(asset: asset),
|
ImmichImage.imageProvider(asset: asset),
|
||||||
context,
|
context,
|
||||||
onError: onError,
|
onError: onError,
|
||||||
@@ -712,21 +711,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() {
|
|
||||||
// No need to await this
|
|
||||||
unawaited(
|
|
||||||
// Delay this a bit so we can finish loading the page
|
|
||||||
Future.delayed(const Duration(milliseconds: 400)).then(
|
|
||||||
// Precache the next image
|
|
||||||
(_) => precacheNextImage(currentIndex.value + 1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
ref.listen(showControlsProvider, (_, show) {
|
ref.listen(showControlsProvider, (_, show) {
|
||||||
if (show) {
|
if (show) {
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
@@ -751,21 +735,14 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
isZoomed.value = state != PhotoViewScaleState.initial;
|
isZoomed.value = state != PhotoViewScaleState.initial;
|
||||||
ref.read(showControlsProvider.notifier).show = !isZoomed.value;
|
ref.read(showControlsProvider.notifier).show = !isZoomed.value;
|
||||||
},
|
},
|
||||||
loadingBuilder: (context, event, index) => ClipRect(
|
loadingBuilder: (context, event, index) => ImageFiltered(
|
||||||
child: Stack(
|
imageFilter: ui.ImageFilter.blur(
|
||||||
fit: StackFit.expand,
|
sigmaX: 1,
|
||||||
children: [
|
sigmaY: 1,
|
||||||
BackdropFilter(
|
),
|
||||||
filter: ui.ImageFilter.blur(
|
child: ImmichThumbnail(
|
||||||
sigmaX: 10,
|
asset: asset(),
|
||||||
sigmaY: 10,
|
fit: BoxFit.contain,
|
||||||
),
|
|
||||||
),
|
|
||||||
ImmichThumbnail(
|
|
||||||
asset: asset(),
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
pageController: controller,
|
pageController: controller,
|
||||||
@@ -777,16 +754,12 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
itemCount: totalAssets,
|
itemCount: totalAssets,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
onPageChanged: (value) async {
|
onPageChanged: (value) {
|
||||||
final next = currentIndex.value < value ? value + 1 : value - 1;
|
final next = currentIndex.value < value ? value + 1 : value - 1;
|
||||||
HapticFeedback.selectionClick();
|
precacheNextImage(next);
|
||||||
currentIndex.value = value;
|
currentIndex.value = value;
|
||||||
stackIndex.value = -1;
|
stackIndex.value = -1;
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
// Wait for page change animation to finish
|
|
||||||
await Future.delayed(const Duration(milliseconds: 400));
|
|
||||||
// Then precache the next image
|
|
||||||
unawaited(precacheNextImage(next));
|
|
||||||
},
|
},
|
||||||
builder: (context, index) {
|
builder: (context, index) {
|
||||||
final a =
|
final a =
|
||||||
@@ -845,7 +818,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
isMotionVideo: isPlayingMotionVideo.value,
|
isMotionVideo: isPlayingMotionVideo.value,
|
||||||
placeholder: Image(
|
placeholder: Image(
|
||||||
image: provider,
|
image: provider,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.fitWidth,
|
||||||
height: context.height,
|
height: context.height,
|
||||||
width: context.width,
|
width: context.width,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
|
|||||||
@@ -59,12 +59,8 @@ class VideoViewerPage extends HookWidget {
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
if (placeholder != null) placeholder!,
|
if (placeholder != null) placeholder!,
|
||||||
const Positioned.fill(
|
const DelayedLoadingIndicator(
|
||||||
child: Center(
|
fadeInDuration: Duration(milliseconds: 500),
|
||||||
child: DelayedLoadingIndicator(
|
|
||||||
fadeInDuration: Duration(milliseconds: 500),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
|
||||||
|
|
||||||
|
class BlurhashThumb extends HookWidget {
|
||||||
|
final double height;
|
||||||
|
final double width;
|
||||||
|
final Asset asset;
|
||||||
|
final EdgeInsets margin;
|
||||||
|
|
||||||
|
const BlurhashThumb({
|
||||||
|
super.key,
|
||||||
|
required this.height,
|
||||||
|
required this.width,
|
||||||
|
required this.asset,
|
||||||
|
required this.margin,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final blurhash = useBlurHashRef(asset).value;
|
||||||
|
if (blurhash == null) {
|
||||||
|
return SizedBox(
|
||||||
|
height: height,
|
||||||
|
width: width,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: margin,
|
||||||
|
child: Image.memory(
|
||||||
|
blurhash,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
frameBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
Widget child,
|
||||||
|
int? frame,
|
||||||
|
bool wasSynchronouslyLoaded,
|
||||||
|
) {
|
||||||
|
if (wasSynchronouslyLoaded) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
child: frame != null
|
||||||
|
? child
|
||||||
|
: SizedBox(
|
||||||
|
height: height,
|
||||||
|
width: width,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
height: height,
|
||||||
|
width: width,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
@@ -6,12 +7,15 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_grid/blurhash_thumb.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart';
|
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
|
||||||
import 'asset_grid_data_structure.dart';
|
import 'asset_grid_data_structure.dart';
|
||||||
@@ -325,32 +329,33 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A single row of all placeholder widgets
|
/// A single row of all placeholder widgets
|
||||||
class _PlaceholderRow extends StatelessWidget {
|
class _PlaceholderRow extends HookWidget {
|
||||||
final int number;
|
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
final double margin;
|
final double margin;
|
||||||
|
final List<Asset> assets;
|
||||||
|
|
||||||
const _PlaceholderRow({
|
const _PlaceholderRow({
|
||||||
super.key,
|
super.key,
|
||||||
required this.number,
|
|
||||||
required this.width,
|
required this.width,
|
||||||
required this.height,
|
required this.height,
|
||||||
required this.margin,
|
required this.margin,
|
||||||
|
required this.assets,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
for (int i = 0; i < number; i++)
|
for (int i = 0; i < assets.length; i++)
|
||||||
ThumbnailPlaceholder(
|
BlurhashThumb(
|
||||||
key: ValueKey(i),
|
key: ValueKey(i),
|
||||||
|
asset: assets[i],
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
margin: EdgeInsets.only(
|
margin: EdgeInsets.only(
|
||||||
bottom: margin,
|
bottom: margin,
|
||||||
right: i + 1 == number ? 0.0 : margin,
|
right: i + 1 == assets.length ? 0.0 : margin,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -401,9 +406,9 @@ class _Section extends StatelessWidget {
|
|||||||
final width = constraints.maxWidth / assetsPerRow -
|
final width = constraints.maxWidth / assetsPerRow -
|
||||||
margin * (assetsPerRow - 1) / assetsPerRow;
|
margin * (assetsPerRow - 1) / assetsPerRow;
|
||||||
final rows = (section.count + assetsPerRow - 1) ~/ assetsPerRow;
|
final rows = (section.count + assetsPerRow - 1) ~/ assetsPerRow;
|
||||||
final List<Asset> assetsToRender = scrolling
|
final List<Asset> assetsToRender = //scrolling
|
||||||
? []
|
//? []
|
||||||
: renderList.loadAssets(section.offset, section.count);
|
renderList.loadAssets(section.offset, section.count);
|
||||||
return Column(
|
return Column(
|
||||||
key: ValueKey(section.offset),
|
key: ValueKey(section.offset),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -422,18 +427,22 @@ class _Section extends StatelessWidget {
|
|||||||
selectAssets: selectAssets,
|
selectAssets: selectAssets,
|
||||||
deselectAssets: deselectAssets,
|
deselectAssets: deselectAssets,
|
||||||
),
|
),
|
||||||
for (int i = 0; i < rows; i++)
|
Stack(
|
||||||
scrolling
|
children: [
|
||||||
? _PlaceholderRow(
|
for (int i = 0; i < rows; i++)
|
||||||
key: ValueKey(i),
|
_PlaceholderRow(
|
||||||
number: i + 1 == rows
|
key: ValueKey('placeholder-$i'),
|
||||||
? section.count - i * assetsPerRow
|
assets: assetsToRender.nestedSlice(
|
||||||
: assetsPerRow,
|
i * assetsPerRow,
|
||||||
width: width,
|
min((i + 1) * assetsPerRow, section.count),
|
||||||
height: width,
|
),
|
||||||
margin: margin,
|
width: width,
|
||||||
)
|
height: width,
|
||||||
: _AssetRow(
|
margin: margin,
|
||||||
|
),
|
||||||
|
if (!scrolling)
|
||||||
|
for (int i = 0; i < rows; i++)
|
||||||
|
_AssetRow(
|
||||||
key: ValueKey(i),
|
key: ValueKey(i),
|
||||||
assets: assetsToRender.nestedSlice(
|
assets: assetsToRender.nestedSlice(
|
||||||
i * assetsPerRow,
|
i * assetsPerRow,
|
||||||
@@ -454,6 +463,8 @@ class _Section extends StatelessWidget {
|
|||||||
onSelect: (asset) => selectAssets([asset]),
|
onSelect: (asset) => selectAssets([asset]),
|
||||||
onDeselect: (asset) => deselectAssets([asset]),
|
onDeselect: (asset) => deselectAssets([asset]),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
|
||||||
|
|
||||||
class MemoryCard extends StatelessWidget {
|
class MemoryCard extends StatelessWidget {
|
||||||
final Asset asset;
|
final Asset asset;
|
||||||
@@ -22,6 +22,8 @@ class MemoryCard extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
String get accessToken => Store.get(StoreKey.accessToken);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
@@ -36,8 +38,19 @@ class MemoryCard extends StatelessWidget {
|
|||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
SizedBox.expand(
|
ImageFiltered(
|
||||||
child: _BlurredBackdrop(asset: asset),
|
imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: ImmichThumbnail.imageProvider(
|
||||||
|
asset: asset,
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Container(color: Colors.black.withOpacity(0.2)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
LayoutBuilder(
|
LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
@@ -100,50 +113,3 @@ class MemoryCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BlurredBackdrop extends HookWidget {
|
|
||||||
final Asset asset;
|
|
||||||
|
|
||||||
const _BlurredBackdrop({required this.asset});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final blurhash = useBlurHashRef(asset).value;
|
|
||||||
if (blurhash != null) {
|
|
||||||
// Use a nice cheap blur hash image decoration
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
image: DecorationImage(
|
|
||||||
image: MemoryImage(
|
|
||||||
blurhash,
|
|
||||||
),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
color: Colors.black.withOpacity(0.2),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Fall back to using a more expensive image filtered
|
|
||||||
// Since the ImmichImage is already precached, we can
|
|
||||||
// safely use that as the image provider
|
|
||||||
return ImageFiltered(
|
|
||||||
imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
image: DecorationImage(
|
|
||||||
image: ImmichImage.imageProvider(
|
|
||||||
asset: asset,
|
|
||||||
),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
color: Colors.black.withOpacity(0.2),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/modules/memories/ui/memory_epilogue.dart';
|
|||||||
import 'package:immich_mobile/modules/memories/ui/memory_progress_indicator.dart';
|
import 'package:immich_mobile/modules/memories/ui/memory_progress_indicator.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class MemoryPage extends HookConsumerWidget {
|
class MemoryPage extends HookConsumerWidget {
|
||||||
@@ -109,13 +110,24 @@ class MemoryPage extends HookConsumerWidget {
|
|||||||
asset = memories[nextMemoryIndex].assets.first;
|
asset = memories[nextMemoryIndex].assets.first;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Precache the asset
|
// Gets the thumbnail url and precaches it
|
||||||
await precacheImage(
|
final precaches = <Future<dynamic>>[];
|
||||||
ImmichImage.imageProvider(
|
|
||||||
asset: asset,
|
precaches.addAll([
|
||||||
|
precacheImage(
|
||||||
|
ImmichImage.imageProvider(
|
||||||
|
asset: asset,
|
||||||
|
),
|
||||||
|
context,
|
||||||
),
|
),
|
||||||
context,
|
precacheImage(
|
||||||
);
|
ImmichThumbnail.imageProvider(
|
||||||
|
asset: asset,
|
||||||
|
),
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
await Future.wait(precaches);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Precache the next page right away if we are on the first page
|
// Precache the next page right away if we are on the first page
|
||||||
@@ -124,14 +136,11 @@ class MemoryPage extends HookConsumerWidget {
|
|||||||
.then((_) => precacheAsset(1));
|
.then((_) => precacheAsset(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onAssetChanged(int otherIndex) async {
|
onAssetChanged(int otherIndex) {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
currentAssetPage.value = otherIndex;
|
currentAssetPage.value = otherIndex;
|
||||||
|
precacheAsset(otherIndex + 1);
|
||||||
updateProgressText();
|
updateProgressText();
|
||||||
// Wait for page change animation to finish
|
|
||||||
await Future.delayed(const Duration(milliseconds: 400));
|
|
||||||
// And then precache the next asset
|
|
||||||
await precacheAsset(otherIndex + 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Notification listener is used instead of OnPageChanged callback since OnPageChanged is called
|
/* Notification listener is used instead of OnPageChanged callback since OnPageChanged is called
|
||||||
|
|||||||
@@ -20,24 +20,21 @@ class DelayedLoadingIndicator extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder(
|
return AnimatedSwitcher(
|
||||||
future: Future.delayed(delay),
|
duration: fadeInDuration ?? Duration.zero,
|
||||||
builder: (context, snapshot) {
|
child: FutureBuilder(
|
||||||
late Widget c;
|
future: Future.delayed(delay),
|
||||||
if (snapshot.connectionState == ConnectionState.done) {
|
builder: (context, snapshot) {
|
||||||
c = child ??
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
const ImmichLoadingIndicator(
|
return child ??
|
||||||
key: ValueKey('loading'),
|
const ImmichLoadingIndicator(
|
||||||
);
|
key: ValueKey('loading'),
|
||||||
} else {
|
);
|
||||||
c = Container(key: const ValueKey('hiding'));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return AnimatedSwitcher(
|
return Container(key: const ValueKey('hiding'));
|
||||||
duration: fadeInDuration ?? Duration.zero,
|
},
|
||||||
child: c,
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,11 +58,9 @@ class ImmichImage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whether to use the local asset image provider or a remote one
|
|
||||||
static bool useLocal(Asset asset) =>
|
static bool useLocal(Asset asset) =>
|
||||||
!asset.isRemote ||
|
!asset.isRemote ||
|
||||||
asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false);
|
asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (asset == null) {
|
if (asset == null) {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_local_
|
|||||||
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/asset.dart';
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
|
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/thumbhash_placeholder.dart';
|
import 'package:immich_mobile/shared/ui/thumbhash_placeholder.dart';
|
||||||
import 'package:octo_image/octo_image.dart';
|
import 'package:octo_image/octo_image.dart';
|
||||||
|
|
||||||
@@ -44,7 +43,7 @@ class ImmichThumbnail extends HookWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImmichImage.useLocal(asset)) {
|
if (useLocal(asset)) {
|
||||||
return ImmichLocalThumbnailProvider(
|
return ImmichLocalThumbnailProvider(
|
||||||
asset: asset,
|
asset: asset,
|
||||||
height: thumbnailSize,
|
height: thumbnailSize,
|
||||||
@@ -58,6 +57,8 @@ class ImmichThumbnail extends HookWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool useLocal(Asset asset) => !asset.isRemote || asset.isLocal;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Uint8List? blurhash = useBlurHashRef(asset).value;
|
Uint8List? blurhash = useBlurHashRef(asset).value;
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ OctoPlaceholderBuilder blurHashPlaceholderBuilder(
|
|||||||
}) {
|
}) {
|
||||||
return (context) => blurhash == null
|
return (context) => blurhash == null
|
||||||
? const ThumbnailPlaceholder()
|
? const ThumbnailPlaceholder()
|
||||||
: FadeInPlaceholderImage(
|
: Image(
|
||||||
placeholder: const ThumbnailPlaceholder(),
|
|
||||||
image: MemoryImage(blurhash),
|
image: MemoryImage(blurhash),
|
||||||
fit: fit ?? BoxFit.cover,
|
fit: fit ?? BoxFit.cover,
|
||||||
);
|
);
|
||||||
|
|||||||
Generated
+1
-1
@@ -3,7 +3,7 @@ Immich API
|
|||||||
|
|
||||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.97.0
|
- API version: 1.95.1
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import 'package:openapi/api.dart';
|
|||||||
Name | Type | Description | Notes
|
Name | Type | Description | Notes
|
||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
**enabled** | **bool** | |
|
**enabled** | **bool** | |
|
||||||
|
**interval** | **int** | |
|
||||||
|
**usePolling** | **bool** | |
|
||||||
|
|
||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|||||||
+19
-3
@@ -14,25 +14,37 @@ class SystemConfigLibraryWatchDto {
|
|||||||
/// Returns a new [SystemConfigLibraryWatchDto] instance.
|
/// Returns a new [SystemConfigLibraryWatchDto] instance.
|
||||||
SystemConfigLibraryWatchDto({
|
SystemConfigLibraryWatchDto({
|
||||||
required this.enabled,
|
required this.enabled,
|
||||||
|
required this.interval,
|
||||||
|
required this.usePolling,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool enabled;
|
bool enabled;
|
||||||
|
|
||||||
|
int interval;
|
||||||
|
|
||||||
|
bool usePolling;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigLibraryWatchDto &&
|
bool operator ==(Object other) => identical(this, other) || other is SystemConfigLibraryWatchDto &&
|
||||||
other.enabled == enabled;
|
other.enabled == enabled &&
|
||||||
|
other.interval == interval &&
|
||||||
|
other.usePolling == usePolling;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(enabled.hashCode);
|
(enabled.hashCode) +
|
||||||
|
(interval.hashCode) +
|
||||||
|
(usePolling.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SystemConfigLibraryWatchDto[enabled=$enabled]';
|
String toString() => 'SystemConfigLibraryWatchDto[enabled=$enabled, interval=$interval, usePolling=$usePolling]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'enabled'] = this.enabled;
|
json[r'enabled'] = this.enabled;
|
||||||
|
json[r'interval'] = this.interval;
|
||||||
|
json[r'usePolling'] = this.usePolling;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +57,8 @@ class SystemConfigLibraryWatchDto {
|
|||||||
|
|
||||||
return SystemConfigLibraryWatchDto(
|
return SystemConfigLibraryWatchDto(
|
||||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
|
interval: mapValueOfType<int>(json, r'interval')!,
|
||||||
|
usePolling: mapValueOfType<bool>(json, r'usePolling')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -93,6 +107,8 @@ class SystemConfigLibraryWatchDto {
|
|||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'enabled',
|
'enabled',
|
||||||
|
'interval',
|
||||||
|
'usePolling',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// int interval
|
||||||
|
test('to test the property `interval`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// bool usePolling
|
||||||
|
test('to test the property `usePolling`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.97.0+125
|
version: 1.95.1+123
|
||||||
isar_version: &isar_version 3.1.0+1
|
isar_version: &isar_version 3.1.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -6458,7 +6458,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.97.0",
|
"version": "1.95.1",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
@@ -9831,10 +9831,18 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"interval": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"usePolling": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"enabled"
|
"enabled",
|
||||||
|
"interval",
|
||||||
|
"usePolling"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
|||||||
+13
-1
@@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.97.0
|
* The version of the OpenAPI document: 1.95.1
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
@@ -4401,6 +4401,18 @@ export interface SystemConfigLibraryWatchDto {
|
|||||||
* @memberof SystemConfigLibraryWatchDto
|
* @memberof SystemConfigLibraryWatchDto
|
||||||
*/
|
*/
|
||||||
'enabled': boolean;
|
'enabled': boolean;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof SystemConfigLibraryWatchDto
|
||||||
|
*/
|
||||||
|
'interval': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof SystemConfigLibraryWatchDto
|
||||||
|
*/
|
||||||
|
'usePolling': boolean;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.97.0
|
* The version of the OpenAPI document: 1.95.1
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.97.0
|
* The version of the OpenAPI document: 1.95.1
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.97.0
|
* The version of the OpenAPI document: 1.95.1
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.97.0
|
* The version of the OpenAPI document: 1.95.1
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
|||||||
Generated
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Immich
|
* Immich
|
||||||
* 1.97.0
|
* 1.95.1
|
||||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||||
* See https://www.npmjs.com/package/oazapfts
|
* See https://www.npmjs.com/package/oazapfts
|
||||||
*/
|
*/
|
||||||
@@ -835,6 +835,8 @@ export type SystemConfigLibraryScanDto = {
|
|||||||
};
|
};
|
||||||
export type SystemConfigLibraryWatchDto = {
|
export type SystemConfigLibraryWatchDto = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
interval: number;
|
||||||
|
usePolling: boolean;
|
||||||
};
|
};
|
||||||
export type SystemConfigLibraryDto = {
|
export type SystemConfigLibraryDto = {
|
||||||
scan: SystemConfigLibraryScanDto;
|
scan: SystemConfigLibraryScanDto;
|
||||||
|
|||||||
+6
-6
@@ -23,15 +23,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oazapfts/runtime": {
|
"node_modules/@oazapfts/runtime": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oazapfts/runtime/-/runtime-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@oazapfts/runtime/-/runtime-1.0.0.tgz",
|
||||||
"integrity": "sha512-CMl7f1gXYpjIyEtDhg4YfXwr2MXfbadbvqwKbMsaHkVtSglmuz5A8jSyefTqaJlmh0MOA2ZNS9jnbfIdtcoDiw==",
|
"integrity": "sha512-1ovqeaeEvShbYge5/7ctJokpvqB0anBdfDNfU5jWstjV2/Gbe+vvcBM274Z0abM3IM0b9MmSNWYBXnJXYO8KCw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.11.20",
|
"version": "20.11.19",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
|
||||||
"integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
|
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
# dev build
|
# dev build
|
||||||
FROM ghcr.io/immich-app/base-server-dev:20240227@sha256:b1e212c106ce2318a587e0b2ef377215c958e877f61993ed9310534e4589cce4 as dev
|
FROM ghcr.io/immich-app/base-server-dev:20240222@sha256:2ff467d6ae5c00a2317eb7b13cb40ba5be0fd33c160175dba621b1bf72bc1cd1 as dev
|
||||||
|
|
||||||
RUN apt-get install --no-install-recommends -yqq tini
|
RUN apt-get install --no-install-recommends -yqq tini
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
@@ -40,7 +40,7 @@ RUN npm run build
|
|||||||
|
|
||||||
|
|
||||||
# prod build
|
# prod build
|
||||||
FROM ghcr.io/immich-app/base-server-prod:20240227@sha256:d47f5f7f2b6c53957c6353352b2fa24f2845da50e6491a7c74eb779ace10628c
|
FROM ghcr.io/immich-app/base-server-prod:20240222@sha256:9ae5eebf95cf7759eec9dcfbd9e48a722701075ac855209f2e0b01c631b76f5c
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
|
|||||||
@@ -538,6 +538,90 @@ describe(`${AssetController.name} (e2e)`, () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('GET /asset/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(server).get(`/asset/${uuidStub.notFound}`);
|
||||||
|
expect(body).toEqual(errorStub.unauthorized);
|
||||||
|
expect(status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require a valid id', async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.get(`/asset/${uuidStub.invalid}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorStub.badRequest(['id must be a UUID']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require access', async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.get(`/asset/${asset4.id}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorStub.noPermission);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get the asset info', async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.get(`/asset/${asset1.id}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({ id: asset1.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with a shared link', async () => {
|
||||||
|
const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, {
|
||||||
|
type: SharedLinkType.INDIVIDUAL,
|
||||||
|
assetIds: [asset1.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { status, body } = await request(server).get(`/asset/${asset1.id}?key=${sharedLink.key}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({ id: asset1.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not send people data for shared links for un-authenticated users', async () => {
|
||||||
|
const personRepository = app.get<IPersonRepository>(IPersonRepository);
|
||||||
|
const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' });
|
||||||
|
|
||||||
|
await personRepository.createFaces([
|
||||||
|
{
|
||||||
|
assetId: asset1.id,
|
||||||
|
personId: person.id,
|
||||||
|
embedding: Array.from({ length: 512 }, Math.random),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.put(`/asset/${asset1.id}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({ isFavorite: true });
|
||||||
|
expect(status).toEqual(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
id: asset1.id,
|
||||||
|
isFavorite: true,
|
||||||
|
people: [
|
||||||
|
{
|
||||||
|
birthDate: null,
|
||||||
|
id: expect.any(String),
|
||||||
|
isHidden: false,
|
||||||
|
name: 'Test Person',
|
||||||
|
thumbnailPath: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, {
|
||||||
|
type: SharedLinkType.INDIVIDUAL,
|
||||||
|
assetIds: [asset1.id],
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await request(server).get(`/asset/${asset1.id}?key=${sharedLink.key}`);
|
||||||
|
expect(data.status).toBe(200);
|
||||||
|
expect(data.body).toMatchObject({ people: [] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('POST /asset/upload', () => {
|
describe('POST /asset/upload', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(server)
|
const { status, body } = await request(server)
|
||||||
@@ -675,6 +759,286 @@ describe(`${AssetController.name} (e2e)`, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('PUT /asset/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(server).put(`/asset/:${uuidStub.notFound}`);
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorStub.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require a valid id', async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.put(`/asset/${uuidStub.invalid}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorStub.badRequest(['id must be a UUID']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require access', async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.put(`/asset/${asset4.id}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorStub.noPermission);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should favorite an asset', async () => {
|
||||||
|
expect(asset1).toMatchObject({ isFavorite: false });
|
||||||
|
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.put(`/asset/${asset1.id}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({ isFavorite: true });
|
||||||
|
expect(body).toMatchObject({ id: asset1.id, isFavorite: true });
|
||||||
|
expect(status).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should archive an asset', async () => {
|
||||||
|
expect(asset1).toMatchObject({ isArchived: false });
|
||||||
|
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.put(`/asset/${asset1.id}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({ isArchived: true });
|
||||||
|
expect(body).toMatchObject({ id: asset1.id, isArchived: true });
|
||||||
|
expect(status).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update date time original', async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.put(`/asset/${asset1.id}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({ dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' });
|
||||||
|
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
id: asset1.id,
|
||||||
|
exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00.000Z' }),
|
||||||
|
});
|
||||||
|
expect(status).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject invalid gps coordinates', async () => {
|
||||||
|
for (const test of [
|
||||||
|
{ latitude: 12 },
|
||||||
|
{ longitude: 12 },
|
||||||
|
{ latitude: 12, longitude: 'abc' },
|
||||||
|
{ latitude: 'abc', longitude: 12 },
|
||||||
|
{ latitude: null, longitude: 12 },
|
||||||
|
{ latitude: 12, longitude: null },
|
||||||
|
{ latitude: 91, longitude: 12 },
|
||||||
|
{ latitude: -91, longitude: 12 },
|
||||||
|
{ latitude: 12, longitude: -181 },
|
||||||
|
{ latitude: 12, longitude: 181 },
|
||||||
|
]) {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.put(`/asset/${asset1.id}`)
|
||||||
|
.send(test)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorStub.badRequest());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update gps data', async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.put(`/asset/${asset1.id}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({ latitude: 12, longitude: 12 });
|
||||||
|
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
id: asset1.id,
|
||||||
|
exifInfo: expect.objectContaining({ latitude: 12, longitude: 12 }),
|
||||||
|
});
|
||||||
|
expect(status).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the description', async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.put(`/asset/${asset1.id}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({ description: 'Test asset description' });
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
id: asset1.id,
|
||||||
|
exifInfo: expect.objectContaining({ description: 'Test asset description' }),
|
||||||
|
});
|
||||||
|
expect(status).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return tagged people', async () => {
|
||||||
|
const personRepository = app.get<IPersonRepository>(IPersonRepository);
|
||||||
|
const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' });
|
||||||
|
|
||||||
|
await personRepository.createFaces([
|
||||||
|
{
|
||||||
|
assetId: asset1.id,
|
||||||
|
personId: person.id,
|
||||||
|
embedding: Array.from({ length: 512 }, Math.random),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.put(`/asset/${asset1.id}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({ isFavorite: true });
|
||||||
|
expect(status).toEqual(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
id: asset1.id,
|
||||||
|
isFavorite: true,
|
||||||
|
people: [
|
||||||
|
{
|
||||||
|
birthDate: null,
|
||||||
|
id: expect.any(String),
|
||||||
|
isHidden: false,
|
||||||
|
name: 'Test Person',
|
||||||
|
thumbnailPath: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /asset/statistics', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await api.assetApi.upload(server, user1.accessToken, 'favored_asset', { isFavorite: true });
|
||||||
|
await api.assetApi.upload(server, user1.accessToken, 'archived_asset', { isArchived: true });
|
||||||
|
await api.assetApi.upload(server, user1.accessToken, 'favored_archived_asset', {
|
||||||
|
isFavorite: true,
|
||||||
|
isArchived: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(server).get('/asset/statistics');
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorStub.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return stats of all assets', async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.get('/asset/statistics')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
|
expect(body).toEqual({ images: 6, videos: 1, total: 7 });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return stats of all favored assets', async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.get('/asset/statistics')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.query({ isFavorite: true });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({ images: 2, videos: 1, total: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return stats of all archived assets', async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.get('/asset/statistics')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.query({ isArchived: true });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({ images: 3, videos: 0, total: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return stats of all favored and archived assets', async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.get('/asset/statistics')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.query({ isFavorite: true, isArchived: true });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({ images: 1, videos: 0, total: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return stats of all assets neither favored nor archived', async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.get('/asset/statistics')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.query({ isFavorite: false, isArchived: false });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({ images: 2, videos: 0, total: 2 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /asset/random', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
createAsset(user1, new Date('1970-02-01')),
|
||||||
|
createAsset(user1, new Date('1970-02-01')),
|
||||||
|
createAsset(user1, new Date('1970-02-01')),
|
||||||
|
createAsset(user1, new Date('1970-02-01')),
|
||||||
|
createAsset(user1, new Date('1970-02-01')),
|
||||||
|
createAsset(user1, new Date('1970-02-01')),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(server).get('/asset/random');
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorStub.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(Array(10))('should return 1 random assets', async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.get('/asset/random')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
|
||||||
|
const assets: AssetResponseDto[] = body;
|
||||||
|
expect(assets.length).toBe(1);
|
||||||
|
expect(assets[0].ownerId).toBe(user1.userId);
|
||||||
|
//
|
||||||
|
// assets owned by user2
|
||||||
|
expect(assets[0].id).not.toBe(asset4.id);
|
||||||
|
// assets owned by user1
|
||||||
|
expect([asset1.id, asset2.id, asset3.id]).toContain(assets[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(Array(10))('should return 2 random assets', async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.get('/asset/random?count=2')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
|
||||||
|
const assets: AssetResponseDto[] = body;
|
||||||
|
expect(assets.length).toBe(2);
|
||||||
|
|
||||||
|
for (const asset of assets) {
|
||||||
|
expect(asset.ownerId).toBe(user1.userId);
|
||||||
|
// assets owned by user1
|
||||||
|
expect([asset1.id, asset2.id, asset3.id]).toContain(asset.id);
|
||||||
|
// assets owned by user2
|
||||||
|
expect(asset.id).not.toBe(asset4.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(Array(10))(
|
||||||
|
'should return 1 asset if there are 10 assets in the database but user 2 only has 1',
|
||||||
|
async () => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.get('/[]asset/random')
|
||||||
|
.set('Authorization', `Bearer ${user2.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual([expect.objectContaining({ id: asset4.id })]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should return error', async () => {
|
||||||
|
const { status } = await request(server)
|
||||||
|
.get('/asset/random?count=ABC')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('GET /asset/time-buckets', () => {
|
describe('GET /asset/time-buckets', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(server).get('/asset/time-buckets').query({ size: TimeBucketSize.MONTH });
|
const { status, body } = await request(server).get('/asset/time-buckets').query({ size: TimeBucketSize.MONTH });
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AssetResponseDto } from '@app/domain';
|
import { AssetBulkDeleteDto, AssetResponseDto } from '@app/domain';
|
||||||
import { CreateAssetDto } from '@app/immich/api-v1/asset/dto/create-asset.dto';
|
import { CreateAssetDto } from '@app/immich/api-v1/asset/dto/create-asset.dto';
|
||||||
import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
|
import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
|
||||||
import { randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
@@ -74,4 +74,8 @@ export const assetApi = {
|
|||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
return body;
|
return body;
|
||||||
},
|
},
|
||||||
|
delete: async (server: any, accessToken: string, dto: AssetBulkDeleteDto) => {
|
||||||
|
const { status } = await request(server).delete('/asset').set('Authorization', `Bearer ${accessToken}`).send(dto);
|
||||||
|
expect(status).toBe(204);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { LoginResponseDto } from '@app/domain';
|
||||||
|
import { AssetType } from '@app/infra/entities';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { basename, join } from 'node:path';
|
||||||
|
import { IMMICH_TEST_ASSET_PATH, testApp } from '../../../src/test-utils/utils';
|
||||||
|
import { api } from '../../client';
|
||||||
|
|
||||||
|
const JPEG = {
|
||||||
|
type: AssetType.IMAGE,
|
||||||
|
originalFileName: 'el_torcal_rocks',
|
||||||
|
resized: true,
|
||||||
|
exifInfo: {
|
||||||
|
dateTimeOriginal: '2012-08-05T11:39:59.000Z',
|
||||||
|
exifImageWidth: 512,
|
||||||
|
exifImageHeight: 341,
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
focalLength: 75,
|
||||||
|
iso: 200,
|
||||||
|
fNumber: 11,
|
||||||
|
exposureTime: '1/160',
|
||||||
|
fileSizeInByte: 53_493,
|
||||||
|
make: 'SONY',
|
||||||
|
model: 'DSLR-A550',
|
||||||
|
orientation: null,
|
||||||
|
description: 'SONY DSC',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tests = [
|
||||||
|
{ input: 'formats/jpg/el_torcal_rocks.jpg', expected: JPEG },
|
||||||
|
{ input: 'formats/jpeg/el_torcal_rocks.jpeg', expected: JPEG },
|
||||||
|
{
|
||||||
|
input: 'formats/heic/IMG_2682.heic',
|
||||||
|
expected: {
|
||||||
|
type: AssetType.IMAGE,
|
||||||
|
originalFileName: 'IMG_2682',
|
||||||
|
resized: true,
|
||||||
|
fileCreatedAt: '2019-03-21T16:04:22.348Z',
|
||||||
|
exifInfo: {
|
||||||
|
dateTimeOriginal: '2019-03-21T16:04:22.348Z',
|
||||||
|
exifImageWidth: 4032,
|
||||||
|
exifImageHeight: 3024,
|
||||||
|
latitude: 41.2203,
|
||||||
|
longitude: -96.071_625,
|
||||||
|
make: 'Apple',
|
||||||
|
model: 'iPhone 7',
|
||||||
|
lensModel: 'iPhone 7 back camera 3.99mm f/1.8',
|
||||||
|
fileSizeInByte: 880_703,
|
||||||
|
exposureTime: '1/887',
|
||||||
|
iso: 20,
|
||||||
|
focalLength: 3.99,
|
||||||
|
fNumber: 1.8,
|
||||||
|
timeZone: 'America/Chicago',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'formats/png/density_plot.png',
|
||||||
|
expected: {
|
||||||
|
type: AssetType.IMAGE,
|
||||||
|
originalFileName: 'density_plot',
|
||||||
|
resized: true,
|
||||||
|
exifInfo: {
|
||||||
|
exifImageWidth: 800,
|
||||||
|
exifImageHeight: 800,
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
fileSizeInByte: 25_408,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'formats/raw/Nikon/D80/glarus.nef',
|
||||||
|
expected: {
|
||||||
|
type: AssetType.IMAGE,
|
||||||
|
originalFileName: 'glarus',
|
||||||
|
resized: true,
|
||||||
|
fileCreatedAt: '2010-07-20T17:27:12.000Z',
|
||||||
|
exifInfo: {
|
||||||
|
make: 'NIKON CORPORATION',
|
||||||
|
model: 'NIKON D80',
|
||||||
|
exposureTime: '1/200',
|
||||||
|
fNumber: 10,
|
||||||
|
focalLength: 18,
|
||||||
|
iso: 100,
|
||||||
|
fileSizeInByte: 9_057_784,
|
||||||
|
dateTimeOriginal: '2010-07-20T17:27:12.000Z',
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
orientation: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'formats/raw/Nikon/D700/philadelphia.nef',
|
||||||
|
expected: {
|
||||||
|
type: AssetType.IMAGE,
|
||||||
|
originalFileName: 'philadelphia',
|
||||||
|
resized: true,
|
||||||
|
fileCreatedAt: '2016-09-22T22:10:29.060Z',
|
||||||
|
exifInfo: {
|
||||||
|
make: 'NIKON CORPORATION',
|
||||||
|
model: 'NIKON D700',
|
||||||
|
exposureTime: '1/400',
|
||||||
|
fNumber: 11,
|
||||||
|
focalLength: 85,
|
||||||
|
iso: 200,
|
||||||
|
fileSizeInByte: 15_856_335,
|
||||||
|
dateTimeOriginal: '2016-09-22T22:10:29.060Z',
|
||||||
|
latitude: null,
|
||||||
|
longitude: null,
|
||||||
|
orientation: '1',
|
||||||
|
timeZone: 'UTC-5',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe(`Format (e2e)`, () => {
|
||||||
|
let server: any;
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const app = await testApp.create();
|
||||||
|
server = app.getHttpServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await testApp.reset();
|
||||||
|
await api.authApi.adminSignUp(server);
|
||||||
|
admin = await api.authApi.adminLogin(server);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await testApp.teardown();
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const { input, expected } of tests) {
|
||||||
|
it(`should generate a thumbnail for ${input}`, async () => {
|
||||||
|
const filepath = join(IMMICH_TEST_ASSET_PATH, input);
|
||||||
|
const content = await readFile(filepath);
|
||||||
|
await api.assetApi.upload(server, admin.accessToken, 'test-device-id', {
|
||||||
|
content,
|
||||||
|
filename: basename(filepath),
|
||||||
|
});
|
||||||
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||||
|
|
||||||
|
expect(assets).toHaveLength(1);
|
||||||
|
|
||||||
|
const asset = assets[0];
|
||||||
|
|
||||||
|
expect(asset.exifInfo).toBeDefined();
|
||||||
|
expect(asset.exifInfo).toMatchObject(expected.exifInfo);
|
||||||
|
expect(asset).toMatchObject(expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { AssetResponseDto, LoginResponseDto } from '@app/domain';
|
||||||
|
import { AssetController } from '@app/immich';
|
||||||
|
import { exiftool } from 'exiftool-vendored';
|
||||||
|
import { readFile, writeFile } from 'fs/promises';
|
||||||
|
import {
|
||||||
|
IMMICH_TEST_ASSET_PATH,
|
||||||
|
IMMICH_TEST_ASSET_TEMP_PATH,
|
||||||
|
db,
|
||||||
|
restoreTempFolder,
|
||||||
|
testApp,
|
||||||
|
} from '../../../src/test-utils/utils';
|
||||||
|
import { api } from '../../client';
|
||||||
|
|
||||||
|
describe(`${AssetController.name} (e2e)`, () => {
|
||||||
|
let server: any;
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
server = (await testApp.create()).getHttpServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await testApp.reset();
|
||||||
|
await restoreTempFolder();
|
||||||
|
await api.authApi.adminSignUp(server);
|
||||||
|
admin = await api.authApi.adminLogin(server);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await testApp.teardown();
|
||||||
|
await restoreTempFolder();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should strip metadata of', () => {
|
||||||
|
let assetWithLocation: AssetResponseDto;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const fileContent = await readFile(`${IMMICH_TEST_ASSET_PATH}/metadata/gps-position/thompson-springs.jpg`);
|
||||||
|
|
||||||
|
await api.assetApi.upload(server, admin.accessToken, 'test-asset-id', { content: fileContent });
|
||||||
|
|
||||||
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||||
|
|
||||||
|
expect(assets).toHaveLength(1);
|
||||||
|
assetWithLocation = assets[0];
|
||||||
|
|
||||||
|
expect(assetWithLocation).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
exifInfo: expect.objectContaining({ latitude: 39.115, longitude: -108.400968333333 }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('small webp thumbnails', async () => {
|
||||||
|
const assetId = assetWithLocation.id;
|
||||||
|
|
||||||
|
const thumbnail = await api.assetApi.getWebpThumbnail(server, admin.accessToken, assetId);
|
||||||
|
|
||||||
|
await writeFile(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.webp`, thumbnail);
|
||||||
|
|
||||||
|
const exifData = await exiftool.read(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.webp`);
|
||||||
|
|
||||||
|
expect(exifData).not.toHaveProperty('GPSLongitude');
|
||||||
|
expect(exifData).not.toHaveProperty('GPSLatitude');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('large jpeg thumbnails', async () => {
|
||||||
|
const assetId = assetWithLocation.id;
|
||||||
|
|
||||||
|
const thumbnail = await api.assetApi.getJpegThumbnail(server, admin.accessToken, assetId);
|
||||||
|
|
||||||
|
await writeFile(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.jpg`, thumbnail);
|
||||||
|
|
||||||
|
const exifData = await exiftool.read(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.jpg`);
|
||||||
|
|
||||||
|
expect(exifData).not.toHaveProperty('GPSLongitude');
|
||||||
|
expect(exifData).not.toHaveProperty('GPSLatitude');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
// These hashes were created by copying the image files to a Samsung phone,
|
||||||
|
// exporting the video from Samsung's stock Gallery app, and hashing them locally.
|
||||||
|
// This ensures that immich+exiftool are extracting the videos the same way Samsung does.
|
||||||
|
// DO NOT assume immich+exiftool are doing things correctly and just copy whatever hash it gives
|
||||||
|
// into the test here.
|
||||||
|
['Samsung One UI 5.jpg', 'fr14niqCq6N20HB8rJYEvpsUVtI='],
|
||||||
|
['Samsung One UI 6.jpg', 'lT9Uviw/FFJYCjfIxAGPTjzAmmw='],
|
||||||
|
['Samsung One UI 6.heic', '/ejgzywvgvzvVhUYVfvkLzFBAF0='],
|
||||||
|
])('should extract motionphoto video', (file, checksum) => {
|
||||||
|
it(`with checksum ${checksum} from ${file}`, async () => {
|
||||||
|
const fileContent = await readFile(`${IMMICH_TEST_ASSET_PATH}/formats/motionphoto/${file}`);
|
||||||
|
|
||||||
|
const response = await api.assetApi.upload(server, admin.accessToken, 'test-asset-id', { content: fileContent });
|
||||||
|
const asset = await api.assetApi.get(server, admin.accessToken, response.id);
|
||||||
|
expect(asset).toHaveProperty('livePhotoVideoId');
|
||||||
|
const video = await api.assetApi.get(server, admin.accessToken, asset.livePhotoVideoId as string);
|
||||||
|
|
||||||
|
expect(video.checksum).toStrictEqual(checksum);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { LoginResponseDto } from '@app/domain';
|
||||||
|
import { api } from 'e2e/client';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { basename, join } from 'node:path';
|
||||||
|
import type { App } from 'supertest/types';
|
||||||
|
import { IMMICH_TEST_ASSET_PATH, testApp } from '../../../src/test-utils/utils';
|
||||||
|
|
||||||
|
const assetFilePath = join(IMMICH_TEST_ASSET_PATH, 'formats/png/density_plot.png');
|
||||||
|
|
||||||
|
describe(`Trash (e2e)`, () => {
|
||||||
|
let server: App;
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const app = await testApp.create();
|
||||||
|
server = app.getHttpServer();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await testApp.reset();
|
||||||
|
await api.authApi.adminSignUp(server);
|
||||||
|
admin = await api.authApi.adminLogin(server);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await testApp.teardown();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should move an asset to trash', async () => {
|
||||||
|
const content = await readFile(assetFilePath);
|
||||||
|
const { id: assetId } = await api.assetApi.upload(server, admin.accessToken, 'test-device-id', {
|
||||||
|
content,
|
||||||
|
filename: basename(assetFilePath),
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadedAsset = await api.assetApi.get(server, admin.accessToken, assetId);
|
||||||
|
expect(uploadedAsset.isTrashed).toBe(false);
|
||||||
|
|
||||||
|
await api.assetApi.delete(server, admin.accessToken, { ids: [assetId] });
|
||||||
|
|
||||||
|
const deletedAsset = await api.assetApi.get(server, admin.accessToken, assetId);
|
||||||
|
expect(deletedAsset.isTrashed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete all trashed assets', async () => {
|
||||||
|
const content = await readFile(assetFilePath);
|
||||||
|
const { id: assetId } = await api.assetApi.upload(server, admin.accessToken, 'test-device-id', {
|
||||||
|
content,
|
||||||
|
filename: basename(assetFilePath),
|
||||||
|
});
|
||||||
|
|
||||||
|
await api.assetApi.delete(server, admin.accessToken, { ids: [assetId] });
|
||||||
|
|
||||||
|
const assetsBeforeEmpty = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||||
|
expect(assetsBeforeEmpty.length).toBe(1);
|
||||||
|
|
||||||
|
await api.trashApi.empty(server, admin.accessToken);
|
||||||
|
|
||||||
|
const assetsAfterEmpty = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||||
|
expect(assetsAfterEmpty.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should restore all trashed assets', async () => {
|
||||||
|
const content = await readFile(assetFilePath);
|
||||||
|
const { id: assetId } = await api.assetApi.upload(server, admin.accessToken, 'test-device-id', {
|
||||||
|
content,
|
||||||
|
filename: basename(assetFilePath),
|
||||||
|
});
|
||||||
|
|
||||||
|
await api.assetApi.delete(server, admin.accessToken, { ids: [assetId] });
|
||||||
|
|
||||||
|
const deletedAsset = await api.assetApi.get(server, admin.accessToken, assetId);
|
||||||
|
expect(deletedAsset.isTrashed).toBe(true);
|
||||||
|
|
||||||
|
await api.trashApi.restore(server, admin.accessToken);
|
||||||
|
|
||||||
|
const restoredAsset = await api.assetApi.get(server, admin.accessToken, assetId);
|
||||||
|
expect(restoredAsset.isTrashed).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Generated
+110
-110
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.97.0",
|
"version": "1.95.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.97.0",
|
"version": "1.95.1",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.22.11",
|
"@babel/runtime": "^7.22.11",
|
||||||
@@ -3179,9 +3179,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.11.20",
|
"version": "20.11.19",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
|
||||||
"integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
|
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
@@ -3281,9 +3281,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/semver": {
|
"node_modules/@types/semver": {
|
||||||
"version": "7.5.8",
|
"version": "7.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
|
||||||
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
|
"integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/send": {
|
"node_modules/@types/send": {
|
||||||
@@ -3398,16 +3398,16 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz",
|
||||||
"integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==",
|
"integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.5.1",
|
"@eslint-community/regexpp": "^4.5.1",
|
||||||
"@typescript-eslint/scope-manager": "7.0.2",
|
"@typescript-eslint/scope-manager": "7.0.1",
|
||||||
"@typescript-eslint/type-utils": "7.0.2",
|
"@typescript-eslint/type-utils": "7.0.1",
|
||||||
"@typescript-eslint/utils": "7.0.2",
|
"@typescript-eslint/utils": "7.0.1",
|
||||||
"@typescript-eslint/visitor-keys": "7.0.2",
|
"@typescript-eslint/visitor-keys": "7.0.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"graphemer": "^1.4.0",
|
"graphemer": "^1.4.0",
|
||||||
"ignore": "^5.2.4",
|
"ignore": "^5.2.4",
|
||||||
@@ -3433,15 +3433,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz",
|
||||||
"integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==",
|
"integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "7.0.2",
|
"@typescript-eslint/scope-manager": "7.0.1",
|
||||||
"@typescript-eslint/types": "7.0.2",
|
"@typescript-eslint/types": "7.0.1",
|
||||||
"@typescript-eslint/typescript-estree": "7.0.2",
|
"@typescript-eslint/typescript-estree": "7.0.1",
|
||||||
"@typescript-eslint/visitor-keys": "7.0.2",
|
"@typescript-eslint/visitor-keys": "7.0.1",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3461,13 +3461,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz",
|
||||||
"integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==",
|
"integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "7.0.2",
|
"@typescript-eslint/types": "7.0.1",
|
||||||
"@typescript-eslint/visitor-keys": "7.0.2"
|
"@typescript-eslint/visitor-keys": "7.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^16.0.0 || >=18.0.0"
|
"node": "^16.0.0 || >=18.0.0"
|
||||||
@@ -3478,13 +3478,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz",
|
||||||
"integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==",
|
"integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "7.0.2",
|
"@typescript-eslint/typescript-estree": "7.0.1",
|
||||||
"@typescript-eslint/utils": "7.0.2",
|
"@typescript-eslint/utils": "7.0.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^1.0.1"
|
"ts-api-utils": "^1.0.1"
|
||||||
},
|
},
|
||||||
@@ -3505,9 +3505,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz",
|
||||||
"integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==",
|
"integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^16.0.0 || >=18.0.0"
|
"node": "^16.0.0 || >=18.0.0"
|
||||||
@@ -3518,13 +3518,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz",
|
||||||
"integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==",
|
"integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "7.0.2",
|
"@typescript-eslint/types": "7.0.1",
|
||||||
"@typescript-eslint/visitor-keys": "7.0.2",
|
"@typescript-eslint/visitor-keys": "7.0.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"globby": "^11.1.0",
|
"globby": "^11.1.0",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
@@ -3570,17 +3570,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz",
|
||||||
"integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==",
|
"integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
"@types/json-schema": "^7.0.12",
|
"@types/json-schema": "^7.0.12",
|
||||||
"@types/semver": "^7.5.0",
|
"@types/semver": "^7.5.0",
|
||||||
"@typescript-eslint/scope-manager": "7.0.2",
|
"@typescript-eslint/scope-manager": "7.0.1",
|
||||||
"@typescript-eslint/types": "7.0.2",
|
"@typescript-eslint/types": "7.0.1",
|
||||||
"@typescript-eslint/typescript-estree": "7.0.2",
|
"@typescript-eslint/typescript-estree": "7.0.1",
|
||||||
"semver": "^7.5.4"
|
"semver": "^7.5.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3595,12 +3595,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz",
|
||||||
"integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==",
|
"integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "7.0.2",
|
"@typescript-eslint/types": "7.0.1",
|
||||||
"eslint-visitor-keys": "^3.4.1"
|
"eslint-visitor-keys": "^3.4.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -5504,9 +5504,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.4.5",
|
"version": "16.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.4.tgz",
|
||||||
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
|
"integrity": "sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -8139,9 +8139,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/joi": {
|
"node_modules/joi": {
|
||||||
"version": "17.12.2",
|
"version": "17.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz",
|
"resolved": "https://registry.npmjs.org/joi/-/joi-17.12.1.tgz",
|
||||||
"integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==",
|
"integrity": "sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hapi/hoek": "^9.3.0",
|
"@hapi/hoek": "^9.3.0",
|
||||||
"@hapi/topo": "^5.1.0",
|
"@hapi/topo": "^5.1.0",
|
||||||
@@ -14730,9 +14730,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "20.11.20",
|
"version": "20.11.19",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
|
||||||
"integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
|
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
@@ -14819,9 +14819,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/semver": {
|
"@types/semver": {
|
||||||
"version": "7.5.8",
|
"version": "7.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
|
||||||
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
|
"integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/send": {
|
"@types/send": {
|
||||||
@@ -14936,16 +14936,16 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@typescript-eslint/eslint-plugin": {
|
"@typescript-eslint/eslint-plugin": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz",
|
||||||
"integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==",
|
"integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@eslint-community/regexpp": "^4.5.1",
|
"@eslint-community/regexpp": "^4.5.1",
|
||||||
"@typescript-eslint/scope-manager": "7.0.2",
|
"@typescript-eslint/scope-manager": "7.0.1",
|
||||||
"@typescript-eslint/type-utils": "7.0.2",
|
"@typescript-eslint/type-utils": "7.0.1",
|
||||||
"@typescript-eslint/utils": "7.0.2",
|
"@typescript-eslint/utils": "7.0.1",
|
||||||
"@typescript-eslint/visitor-keys": "7.0.2",
|
"@typescript-eslint/visitor-keys": "7.0.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"graphemer": "^1.4.0",
|
"graphemer": "^1.4.0",
|
||||||
"ignore": "^5.2.4",
|
"ignore": "^5.2.4",
|
||||||
@@ -14955,54 +14955,54 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/parser": {
|
"@typescript-eslint/parser": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz",
|
||||||
"integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==",
|
"integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/scope-manager": "7.0.2",
|
"@typescript-eslint/scope-manager": "7.0.1",
|
||||||
"@typescript-eslint/types": "7.0.2",
|
"@typescript-eslint/types": "7.0.1",
|
||||||
"@typescript-eslint/typescript-estree": "7.0.2",
|
"@typescript-eslint/typescript-estree": "7.0.1",
|
||||||
"@typescript-eslint/visitor-keys": "7.0.2",
|
"@typescript-eslint/visitor-keys": "7.0.1",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/scope-manager": {
|
"@typescript-eslint/scope-manager": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz",
|
||||||
"integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==",
|
"integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/types": "7.0.2",
|
"@typescript-eslint/types": "7.0.1",
|
||||||
"@typescript-eslint/visitor-keys": "7.0.2"
|
"@typescript-eslint/visitor-keys": "7.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/type-utils": {
|
"@typescript-eslint/type-utils": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz",
|
||||||
"integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==",
|
"integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/typescript-estree": "7.0.2",
|
"@typescript-eslint/typescript-estree": "7.0.1",
|
||||||
"@typescript-eslint/utils": "7.0.2",
|
"@typescript-eslint/utils": "7.0.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^1.0.1"
|
"ts-api-utils": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/types": {
|
"@typescript-eslint/types": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz",
|
||||||
"integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==",
|
"integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@typescript-eslint/typescript-estree": {
|
"@typescript-eslint/typescript-estree": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz",
|
||||||
"integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==",
|
"integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/types": "7.0.2",
|
"@typescript-eslint/types": "7.0.1",
|
||||||
"@typescript-eslint/visitor-keys": "7.0.2",
|
"@typescript-eslint/visitor-keys": "7.0.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"globby": "^11.1.0",
|
"globby": "^11.1.0",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
@@ -15032,27 +15032,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/utils": {
|
"@typescript-eslint/utils": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz",
|
||||||
"integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==",
|
"integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
"@types/json-schema": "^7.0.12",
|
"@types/json-schema": "^7.0.12",
|
||||||
"@types/semver": "^7.5.0",
|
"@types/semver": "^7.5.0",
|
||||||
"@typescript-eslint/scope-manager": "7.0.2",
|
"@typescript-eslint/scope-manager": "7.0.1",
|
||||||
"@typescript-eslint/types": "7.0.2",
|
"@typescript-eslint/types": "7.0.1",
|
||||||
"@typescript-eslint/typescript-estree": "7.0.2",
|
"@typescript-eslint/typescript-estree": "7.0.1",
|
||||||
"semver": "^7.5.4"
|
"semver": "^7.5.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/visitor-keys": {
|
"@typescript-eslint/visitor-keys": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz",
|
||||||
"integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==",
|
"integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/types": "7.0.2",
|
"@typescript-eslint/types": "7.0.1",
|
||||||
"eslint-visitor-keys": "^3.4.1"
|
"eslint-visitor-keys": "^3.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -16494,9 +16494,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dotenv": {
|
"dotenv": {
|
||||||
"version": "16.4.5",
|
"version": "16.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.4.tgz",
|
||||||
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg=="
|
"integrity": "sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg=="
|
||||||
},
|
},
|
||||||
"dotenv-expand": {
|
"dotenv-expand": {
|
||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
@@ -18453,9 +18453,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"joi": {
|
"joi": {
|
||||||
"version": "17.12.2",
|
"version": "17.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz",
|
"resolved": "https://registry.npmjs.org/joi/-/joi-17.12.1.tgz",
|
||||||
"integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==",
|
"integrity": "sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@hapi/hoek": "^9.3.0",
|
"@hapi/hoek": "^9.3.0",
|
||||||
"@hapi/topo": "^5.1.0",
|
"@hapi/topo": "^5.1.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.97.0",
|
"version": "1.95.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -112,13 +112,20 @@ export class LibraryService extends EventEmitter {
|
|||||||
ignore: library.exclusionPatterns,
|
ignore: library.exclusionPatterns,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const config = await this.configCore.getConfig();
|
||||||
|
const { usePolling, interval } = config.library.watch;
|
||||||
|
|
||||||
|
this.logger.debug(`Settings for watcher: usePolling: ${usePolling}, interval: ${interval}`);
|
||||||
|
|
||||||
let _resolve: () => void;
|
let _resolve: () => void;
|
||||||
const ready$ = new Promise<void>((resolve) => (_resolve = resolve));
|
const ready$ = new Promise<void>((resolve) => (_resolve = resolve));
|
||||||
|
|
||||||
this.watchers[id] = this.storageRepository.watch(
|
this.watchers[id] = this.storageRepository.watch(
|
||||||
library.importPaths,
|
library.importPaths,
|
||||||
{
|
{
|
||||||
usePolling: false,
|
usePolling,
|
||||||
|
interval,
|
||||||
|
binaryInterval: interval,
|
||||||
ignoreInitial: true,
|
ignoreInitial: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1799,9 +1799,9 @@ describe(MediaService.name, () => {
|
|||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga'],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
`-c:v hevc_rkmpp`,
|
`-c:v hevc_rkmpp_encoder`,
|
||||||
'-c:a copy',
|
'-c:a copy',
|
||||||
'-movflags faststart',
|
'-movflags faststart',
|
||||||
'-fps_mode passthrough',
|
'-fps_mode passthrough',
|
||||||
@@ -1810,12 +1810,17 @@ describe(MediaService.name, () => {
|
|||||||
'-g 256',
|
'-g 256',
|
||||||
'-tag:v hvc1',
|
'-tag:v hvc1',
|
||||||
'-v verbose',
|
'-v verbose',
|
||||||
'-vf scale_rkrga=-2:720:format=nv12:afbc=1',
|
|
||||||
'-level 153',
|
'-level 153',
|
||||||
'-rc_mode AVBR',
|
'-rc_mode 3',
|
||||||
|
'-quality_min 0',
|
||||||
|
'-quality_max 100',
|
||||||
'-b:v 10000k',
|
'-b:v 10000k',
|
||||||
|
'-width 1280',
|
||||||
|
'-height 720',
|
||||||
],
|
],
|
||||||
twoPass: false,
|
twoPass: false,
|
||||||
|
ffmpegPath: 'ffmpeg_mpp',
|
||||||
|
ldLibraryPath: '/lib/aarch64-linux-gnu:/lib/ffmpeg-mpp',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1834,9 +1839,9 @@ describe(MediaService.name, () => {
|
|||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
{
|
{
|
||||||
inputOptions: ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga'],
|
inputOptions: [],
|
||||||
outputOptions: [
|
outputOptions: [
|
||||||
`-c:v h264_rkmpp`,
|
`-c:v h264_rkmpp_encoder`,
|
||||||
'-c:a copy',
|
'-c:a copy',
|
||||||
'-movflags faststart',
|
'-movflags faststart',
|
||||||
'-fps_mode passthrough',
|
'-fps_mode passthrough',
|
||||||
@@ -1844,12 +1849,16 @@ describe(MediaService.name, () => {
|
|||||||
'-map 0:1',
|
'-map 0:1',
|
||||||
'-g 256',
|
'-g 256',
|
||||||
'-v verbose',
|
'-v verbose',
|
||||||
'-vf scale_rkrga=-2:720:format=nv12:afbc=1',
|
|
||||||
'-level 51',
|
'-level 51',
|
||||||
'-rc_mode CQP',
|
'-rc_mode 2',
|
||||||
'-qp_init 30',
|
'-quality_min 51',
|
||||||
|
'-quality_max 51',
|
||||||
|
'-width 1280',
|
||||||
|
'-height 720',
|
||||||
],
|
],
|
||||||
twoPass: false,
|
twoPass: false,
|
||||||
|
ffmpegPath: 'ffmpeg_mpp',
|
||||||
|
ldLibraryPath: '/lib/aarch64-linux-gnu:/lib/ffmpeg-mpp',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class BaseConfig implements VideoCodecSWConfig {
|
|||||||
|
|
||||||
getOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
|
getOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
|
||||||
const options = {
|
const options = {
|
||||||
inputOptions: this.getBaseInputOptions(videoStream),
|
inputOptions: this.getBaseInputOptions(),
|
||||||
outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v verbose'],
|
outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v verbose'],
|
||||||
twoPass: this.eligibleForTwoPass(),
|
twoPass: this.eligibleForTwoPass(),
|
||||||
} as TranscodeOptions;
|
} as TranscodeOptions;
|
||||||
@@ -30,8 +30,7 @@ class BaseConfig implements VideoCodecSWConfig {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
getBaseInputOptions(): string[] {
|
||||||
getBaseInputOptions(videoStream: VideoStreamInfo): string[] {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,28 +607,35 @@ export class VAAPIConfig extends BaseHWConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class RKMPPConfig extends BaseHWConfig {
|
export class RKMPPConfig extends BaseHWConfig {
|
||||||
|
getOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo): TranscodeOptions {
|
||||||
|
const options = super.getOptions(target, videoStream, audioStream);
|
||||||
|
options.ffmpegPath = 'ffmpeg_mpp';
|
||||||
|
options.ldLibraryPath = '/lib/aarch64-linux-gnu:/lib/ffmpeg-mpp';
|
||||||
|
if ([TranscodeTarget.ALL, TranscodeTarget.VIDEO].includes(target)) {
|
||||||
|
options.outputOptions.push(...this.getSizeOptions(videoStream));
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
eligibleForTwoPass(): boolean {
|
eligibleForTwoPass(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getBaseInputOptions(videoStream: VideoStreamInfo) {
|
getBaseInputOptions() {
|
||||||
if (this.devices.length === 0) {
|
if (this.devices.length === 0) {
|
||||||
throw new Error('No RKMPP device found');
|
throw new Error('No RKMPP device found');
|
||||||
}
|
}
|
||||||
if (this.shouldToneMap(videoStream)) {
|
return [];
|
||||||
// disable hardware decoding
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilterOptions(videoStream: VideoStreamInfo) {
|
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||||
if (this.shouldToneMap(videoStream)) {
|
return this.shouldToneMap(videoStream) ? this.getToneMapping() : [];
|
||||||
// use software filter options
|
}
|
||||||
return super.getFilterOptions(videoStream);
|
|
||||||
}
|
getSizeOptions(videoStream: VideoStreamInfo) {
|
||||||
if (this.shouldScale(videoStream)) {
|
if (this.shouldScale(videoStream)) {
|
||||||
return [`scale_rkrga=${this.getScaling(videoStream)}:format=nv12:afbc=1`];
|
const { width, height } = this.getSize(videoStream);
|
||||||
|
return [`-width ${width}`, `-height ${height}`];
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -653,11 +659,12 @@ export class RKMPPConfig extends BaseHWConfig {
|
|||||||
getBitrateOptions() {
|
getBitrateOptions() {
|
||||||
const bitrate = this.getMaxBitrateValue();
|
const bitrate = this.getMaxBitrateValue();
|
||||||
if (bitrate > 0) {
|
if (bitrate > 0) {
|
||||||
// -b:v specifies max bitrate, average bitrate is derived automatically...
|
return ['-rc_mode 3', '-quality_min 0', '-quality_max 100', `-b:v ${bitrate}${this.getBitrateUnit()}`];
|
||||||
return ['-rc_mode AVBR', `-b:v ${bitrate}${this.getBitrateUnit()}`];
|
} else {
|
||||||
|
// convert CQP from 51-10 to 0-100, values below 10 are set to 10
|
||||||
|
const quality = Math.floor(125 - Math.max(this.config.crf, 10) * (125 / 51));
|
||||||
|
return ['-rc_mode 2', `-quality_min ${quality}`, `-quality_max ${quality}`];
|
||||||
}
|
}
|
||||||
// use CRF value as QP value
|
|
||||||
return ['-rc_mode CQP', `-qp_init ${this.config.crf}`];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSupportedCodecs() {
|
getSupportedCodecs() {
|
||||||
@@ -665,6 +672,6 @@ export class RKMPPConfig extends BaseHWConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getVideoCodec(): string {
|
getVideoCodec(): string {
|
||||||
return `${this.config.targetVideoCodec}_rkmpp`;
|
return `${this.config.targetVideoCodec}_rkmpp_encoder`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export interface TranscodeOptions {
|
|||||||
inputOptions: string[];
|
inputOptions: string[];
|
||||||
outputOptions: string[];
|
outputOptions: string[];
|
||||||
twoPass: boolean;
|
twoPass: boolean;
|
||||||
|
ffmpegPath?: string;
|
||||||
|
ldLibraryPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BitrateDistribution {
|
export interface BitrateDistribution {
|
||||||
|
|||||||
@@ -187,5 +187,4 @@ export interface ISearchRepository {
|
|||||||
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
|
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
|
||||||
upsert(smartInfo: Partial<SmartInfoEntity>, embedding?: Embedding): Promise<void>;
|
upsert(smartInfo: Partial<SmartInfoEntity>, embedding?: Embedding): Promise<void>;
|
||||||
searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]>;
|
searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]>;
|
||||||
deleteAllSearchEmbeddings(): Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ describe(SmartInfoService.name, () => {
|
|||||||
|
|
||||||
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]);
|
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]);
|
||||||
expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.SMART_SEARCH);
|
expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.SMART_SEARCH);
|
||||||
expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should queue all the assets', async () => {
|
it('should queue all the assets', async () => {
|
||||||
@@ -84,7 +83,6 @@ describe(SmartInfoService.name, () => {
|
|||||||
|
|
||||||
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]);
|
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]);
|
||||||
expect(assetMock.getAll).toHaveBeenCalled();
|
expect(assetMock.getAll).toHaveBeenCalled();
|
||||||
expect(searchMock.deleteAllSearchEmbeddings).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -50,10 +50,6 @@ export class SmartInfoService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (force) {
|
|
||||||
await this.repository.deleteAllSearchEmbeddings();
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||||
return force
|
return force
|
||||||
? this.assetRepository.getAll(pagination)
|
? this.assetRepository.getAll(pagination)
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { validateCronExpression } from '@app/domain';
|
import { validateCronExpression } from '@app/domain';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
|
IsInt,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsObject,
|
IsObject,
|
||||||
|
IsPositive,
|
||||||
IsString,
|
IsString,
|
||||||
Validate,
|
Validate,
|
||||||
ValidateIf,
|
ValidateIf,
|
||||||
@@ -35,6 +38,14 @@ export class SystemConfigLibraryScanDto {
|
|||||||
export class SystemConfigLibraryWatchDto {
|
export class SystemConfigLibraryWatchDto {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
enabled!: boolean;
|
enabled!: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
usePolling!: boolean;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@IsPositive()
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
interval!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SystemConfigLibraryDto {
|
export class SystemConfigLibraryDto {
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
usePolling: false,
|
||||||
|
interval: 10_000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
usePolling: false,
|
||||||
|
interval: 10_000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export enum SystemConfigKey {
|
|||||||
LIBRARY_SCAN_CRON_EXPRESSION = 'library.scan.cronExpression',
|
LIBRARY_SCAN_CRON_EXPRESSION = 'library.scan.cronExpression',
|
||||||
|
|
||||||
LIBRARY_WATCH_ENABLED = 'library.watch.enabled',
|
LIBRARY_WATCH_ENABLED = 'library.watch.enabled',
|
||||||
|
LIBRARY_WATCH_USE_POLLING = 'library.watch.usePolling',
|
||||||
|
LIBRARY_WATCH_INTERVAL = 'library.watch.interval',
|
||||||
|
|
||||||
LOGGING_ENABLED = 'logging.enabled',
|
LOGGING_ENABLED = 'logging.enabled',
|
||||||
LOGGING_LEVEL = 'logging.level',
|
LOGGING_LEVEL = 'logging.level',
|
||||||
@@ -266,6 +268,8 @@ export interface SystemConfig {
|
|||||||
};
|
};
|
||||||
watch: {
|
watch: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
usePolling: boolean;
|
||||||
|
interval: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
server: {
|
server: {
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
||||||
|
|
||||||
export class RemoveLibraryWatchPollingOption1709150004123 implements MigrationInterface {
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`DELETE FROM "system_config" WHERE key = 'library.watch.usePolling'`);
|
|
||||||
await queryRunner.query(`DELETE FROM "system_config" WHERE key = 'library.watch.interval'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(): Promise<void> {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -76,7 +76,18 @@ export class MediaRepository implements IMediaRepository {
|
|||||||
transcode(input: string, output: string | Writable, options: TranscodeOptions): Promise<void> {
|
transcode(input: string, output: string | Writable, options: TranscodeOptions): Promise<void> {
|
||||||
if (!options.twoPass) {
|
if (!options.twoPass) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.configureFfmpegCall(input, output, options).on('error', reject).on('end', resolve).run();
|
const oldLdLibraryPath = process.env.LD_LIBRARY_PATH;
|
||||||
|
if (options.ldLibraryPath) {
|
||||||
|
// fluent ffmpeg does not allow to set environment variables, so we do it manually
|
||||||
|
process.env.LD_LIBRARY_PATH = this.chainPath(oldLdLibraryPath || '', options.ldLibraryPath);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.configureFfmpegCall(input, output, options).on('error', reject).on('end', resolve).run();
|
||||||
|
} finally {
|
||||||
|
if (options.ldLibraryPath) {
|
||||||
|
process.env.LD_LIBRARY_PATH = oldLdLibraryPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +121,7 @@ export class MediaRepository implements IMediaRepository {
|
|||||||
|
|
||||||
configureFfmpegCall(input: string, output: string | Writable, options: TranscodeOptions) {
|
configureFfmpegCall(input: string, output: string | Writable, options: TranscodeOptions) {
|
||||||
return ffmpeg(input, { niceness: 10 })
|
return ffmpeg(input, { niceness: 10 })
|
||||||
|
.setFfmpegPath(options.ffmpegPath || 'ffmpeg')
|
||||||
.inputOptions(options.inputOptions)
|
.inputOptions(options.inputOptions)
|
||||||
.outputOptions(options.outputOptions)
|
.outputOptions(options.outputOptions)
|
||||||
.output(output)
|
.output(output)
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ export class PersonRepository implements IPersonRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteAll(): Promise<void> {
|
async deleteAll(): Promise<void> {
|
||||||
await this.personRepository.clear();
|
await this.personRepository.delete({});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAllFaces(): Promise<void> {
|
async deleteAllFaces(): Promise<void> {
|
||||||
await this.assetFaceRepository.query('TRUNCATE TABLE asset_faces CASCADE');
|
await this.assetFaceRepository.delete({});
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllFaces(
|
getAllFaces(
|
||||||
|
|||||||
@@ -229,17 +229,25 @@ export class SearchRepository implements ISearchRepository {
|
|||||||
this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`);
|
this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`);
|
||||||
|
|
||||||
await this.smartSearchRepository.manager.transaction(async (manager) => {
|
await this.smartSearchRepository.manager.transaction(async (manager) => {
|
||||||
await manager.clear(SmartSearchEntity);
|
await manager.query(`DROP TABLE smart_search`);
|
||||||
await manager.query(`ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE vector(${dimSize})`);
|
|
||||||
|
await manager.query(`
|
||||||
|
CREATE TABLE smart_search (
|
||||||
|
"assetId" uuid PRIMARY KEY REFERENCES assets(id) ON DELETE CASCADE,
|
||||||
|
embedding vector(${dimSize}) NOT NULL )`);
|
||||||
|
|
||||||
|
await manager.query(`
|
||||||
|
CREATE INDEX clip_index ON smart_search
|
||||||
|
USING vectors (embedding vector_cos_ops) WITH (options = $$
|
||||||
|
[indexing.hnsw]
|
||||||
|
m = 16
|
||||||
|
ef_construction = 300
|
||||||
|
$$)`);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`Successfully updated database CLIP dimension size from ${curDimSize} to ${dimSize}.`);
|
this.logger.log(`Successfully updated database CLIP dimension size from ${curDimSize} to ${dimSize}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAllSearchEmbeddings(): Promise<void> {
|
|
||||||
return this.smartSearchRepository.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getDimSize(): Promise<number> {
|
private async getDimSize(): Promise<number> {
|
||||||
const res = await this.smartSearchRepository.manager.query(`
|
const res = await this.smartSearchRepository.manager.query(`
|
||||||
SELECT atttypmod as dimsize
|
SELECT atttypmod as dimsize
|
||||||
|
|||||||
@@ -8,6 +8,5 @@ export const newSearchRepositoryMock = (): jest.Mocked<ISearchRepository> => {
|
|||||||
searchFaces: jest.fn(),
|
searchFaces: jest.fn(),
|
||||||
upsert: jest.fn(),
|
upsert: jest.fn(),
|
||||||
searchPlaces: jest.fn(),
|
searchPlaces: jest.fn(),
|
||||||
deleteAllSearchEmbeddings: jest.fn(),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
+7
-13
@@ -13,7 +13,6 @@ module.exports = {
|
|||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
ecmaVersion: 2022,
|
ecmaVersion: 2022,
|
||||||
extraFileExtensions: ['.svelte'],
|
extraFileExtensions: ['.svelte'],
|
||||||
project: ['./tsconfig.json'],
|
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
@@ -33,6 +32,13 @@ module.exports = {
|
|||||||
NodeJS: true,
|
NodeJS: true,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
'unicorn/no-useless-undefined': 'off',
|
||||||
|
'unicorn/prefer-spread': 'off',
|
||||||
|
'unicorn/no-null': 'off',
|
||||||
|
'unicorn/prevent-abbreviations': 'off',
|
||||||
|
'unicorn/no-nested-ternary': 'off',
|
||||||
|
'unicorn/consistent-function-scoping': 'off',
|
||||||
|
'unicorn/prefer-top-level-await': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'warn',
|
'warn',
|
||||||
{
|
{
|
||||||
@@ -42,17 +48,5 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
curly: 2,
|
curly: 2,
|
||||||
'unicorn/no-useless-undefined': 'off',
|
|
||||||
'unicorn/prefer-spread': 'off',
|
|
||||||
'unicorn/no-null': 'off',
|
|
||||||
'unicorn/prevent-abbreviations': 'off',
|
|
||||||
'unicorn/no-nested-ternary': 'off',
|
|
||||||
'unicorn/consistent-function-scoping': 'off',
|
|
||||||
'unicorn/prefer-top-level-await': 'off',
|
|
||||||
// TODO: set recommended-type-checked and remove these rules
|
|
||||||
'@typescript-eslint/await-thenable': 'error',
|
|
||||||
'@typescript-eslint/no-floating-promises': 'error',
|
|
||||||
'@typescript-eslint/no-misused-promises': 'error',
|
|
||||||
'@typescript-eslint/require-await': 'error',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+171
-175
@@ -1,67 +1,67 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.3.0",
|
"version": "1.1.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.3.0",
|
"version": "1.1.1",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.3.67",
|
||||||
"@photo-sphere-viewer/core": "^5.7.1",
|
"@photo-sphere-viewer/core": "^5.7.0",
|
||||||
"@zoom-image/svelte": "^0.2.6",
|
"@zoom-image/svelte": "^0.2.0",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"copy-image-clipboard": "^2.1.2",
|
"copy-image-clipboard": "^2.1.2",
|
||||||
"dom-to-image": "^2.6.0",
|
"dom-to-image": "^2.6.0",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.7",
|
||||||
"justified-layout": "^4.1.0",
|
"justified-layout": "^4.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.2.1",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.6.1",
|
||||||
"svelte-local-storage-store": "^0.6.4",
|
"svelte-local-storage-store": "^0.6.0",
|
||||||
"svelte-maplibre": "^0.8.1",
|
"svelte-maplibre": "^0.8.0",
|
||||||
"thumbhash": "^0.1.1"
|
"thumbhash": "^0.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.0.0",
|
||||||
"@floating-ui/dom": "^1.6.3",
|
"@floating-ui/dom": "^1.5.1",
|
||||||
"@socket.io/component-emitter": "^3.1.0",
|
"@socket.io/component-emitter": "^3.1.0",
|
||||||
"@sveltejs/adapter-static": "^3.0.1",
|
"@sveltejs/adapter-static": "^3.0.1",
|
||||||
"@sveltejs/enhanced-img": "^0.1.8",
|
"@sveltejs/enhanced-img": "^0.1.8",
|
||||||
"@sveltejs/kit": "^2.5.2",
|
"@sveltejs/kit": "^2.5.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||||
"@testing-library/jest-dom": "^6.4.2",
|
"@testing-library/jest-dom": "^6.1.5",
|
||||||
"@testing-library/svelte": "^4.1.0",
|
"@testing-library/svelte": "^4.0.3",
|
||||||
"@types/dom-to-image": "^2.6.7",
|
"@types/dom-to-image": "^2.6.4",
|
||||||
"@types/justified-layout": "^4.1.4",
|
"@types/justified-layout": "^4.1.0",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.6",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.2.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
"@typescript-eslint/parser": "^7.1.0",
|
"@typescript-eslint/parser": "^7.0.0",
|
||||||
"@vitest/coverage-v8": "^1.3.1",
|
"@vitest/coverage-v8": "^1.0.4",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.13",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.34.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-svelte": "^2.35.1",
|
"eslint-plugin-svelte": "^2.30.0",
|
||||||
"eslint-plugin-unicorn": "^51.0.1",
|
"eslint-plugin-unicorn": "^51.0.0",
|
||||||
"factory.ts": "^1.4.1",
|
"factory.ts": "^1.3.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.21",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.1.0",
|
||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
"prettier-plugin-svelte": "^3.2.1",
|
"prettier-plugin-svelte": "^3.1.2",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"svelte": "^4.2.12",
|
"svelte": "^4.2.11",
|
||||||
"svelte-check": "^3.6.5",
|
"svelte-check": "^3.6.4",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.2.7",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.5.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^5.1.4",
|
"vite": "^5.1.1",
|
||||||
"vitest": "^1.3.1"
|
"vitest": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
@@ -898,9 +898,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "8.57.0",
|
"version": "8.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
|
||||||
"integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
|
"integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
@@ -948,13 +948,13 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.14",
|
"version": "0.11.13",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
||||||
"integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
|
"integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@humanwhocodes/object-schema": "^2.0.2",
|
"@humanwhocodes/object-schema": "^2.0.1",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.1.1",
|
||||||
"minimatch": "^3.0.5"
|
"minimatch": "^3.0.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -975,9 +975,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@humanwhocodes/object-schema": {
|
"node_modules/@humanwhocodes/object-schema": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
|
||||||
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
|
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@img/sharp-darwin-arm64": {
|
"node_modules/@img/sharp-darwin-arm64": {
|
||||||
@@ -1859,9 +1859,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sveltejs/kit": {
|
"node_modules/@sveltejs/kit": {
|
||||||
"version": "2.5.2",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.1.tgz",
|
||||||
"integrity": "sha512-1Pm2lsBYURQsjnLyZa+jw75eVD4gYHxGRwPyFe4DAmB3FjTVR8vRNWGeuDLGFcKMh/B1ij6FTUrc9GrerogCng==",
|
"integrity": "sha512-TKj08o3mJCoQNLTdRdGkHPePTCPUGTgkew65RDqjVU3MtPVxljsofXQYfXndHfq0P7KoPRO/0/reF6HesU0Djw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2284,9 +2284,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/semver": {
|
"node_modules/@types/semver": {
|
||||||
"version": "7.5.8",
|
"version": "7.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
|
||||||
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
|
"integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/supercluster": {
|
"node_modules/@types/supercluster": {
|
||||||
@@ -2298,16 +2298,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "7.1.0",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz",
|
||||||
"integrity": "sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==",
|
"integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.5.1",
|
"@eslint-community/regexpp": "^4.5.1",
|
||||||
"@typescript-eslint/scope-manager": "7.1.0",
|
"@typescript-eslint/scope-manager": "7.0.1",
|
||||||
"@typescript-eslint/type-utils": "7.1.0",
|
"@typescript-eslint/type-utils": "7.0.1",
|
||||||
"@typescript-eslint/utils": "7.1.0",
|
"@typescript-eslint/utils": "7.0.1",
|
||||||
"@typescript-eslint/visitor-keys": "7.1.0",
|
"@typescript-eslint/visitor-keys": "7.0.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"graphemer": "^1.4.0",
|
"graphemer": "^1.4.0",
|
||||||
"ignore": "^5.2.4",
|
"ignore": "^5.2.4",
|
||||||
@@ -2366,15 +2366,15 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "7.1.0",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz",
|
||||||
"integrity": "sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==",
|
"integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "7.1.0",
|
"@typescript-eslint/scope-manager": "7.0.1",
|
||||||
"@typescript-eslint/types": "7.1.0",
|
"@typescript-eslint/types": "7.0.1",
|
||||||
"@typescript-eslint/typescript-estree": "7.1.0",
|
"@typescript-eslint/typescript-estree": "7.0.1",
|
||||||
"@typescript-eslint/visitor-keys": "7.1.0",
|
"@typescript-eslint/visitor-keys": "7.0.1",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2394,13 +2394,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "7.1.0",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz",
|
||||||
"integrity": "sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==",
|
"integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "7.1.0",
|
"@typescript-eslint/types": "7.0.1",
|
||||||
"@typescript-eslint/visitor-keys": "7.1.0"
|
"@typescript-eslint/visitor-keys": "7.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^16.0.0 || >=18.0.0"
|
"node": "^16.0.0 || >=18.0.0"
|
||||||
@@ -2411,13 +2411,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "7.1.0",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz",
|
||||||
"integrity": "sha512-UZIhv8G+5b5skkcuhgvxYWHjk7FW7/JP5lPASMEUoliAPwIH/rxoUSQPia2cuOj9AmDZmwUl1usKm85t5VUMew==",
|
"integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "7.1.0",
|
"@typescript-eslint/typescript-estree": "7.0.1",
|
||||||
"@typescript-eslint/utils": "7.1.0",
|
"@typescript-eslint/utils": "7.0.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^1.0.1"
|
"ts-api-utils": "^1.0.1"
|
||||||
},
|
},
|
||||||
@@ -2438,9 +2438,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "7.1.0",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz",
|
||||||
"integrity": "sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==",
|
"integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^16.0.0 || >=18.0.0"
|
"node": "^16.0.0 || >=18.0.0"
|
||||||
@@ -2451,13 +2451,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "7.1.0",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz",
|
||||||
"integrity": "sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==",
|
"integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "7.1.0",
|
"@typescript-eslint/types": "7.0.1",
|
||||||
"@typescript-eslint/visitor-keys": "7.1.0",
|
"@typescript-eslint/visitor-keys": "7.0.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"globby": "^11.1.0",
|
"globby": "^11.1.0",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
@@ -2536,17 +2536,17 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "7.1.0",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz",
|
||||||
"integrity": "sha512-WUFba6PZC5OCGEmbweGpnNJytJiLG7ZvDBJJoUcX4qZYf1mGZ97mO2Mps6O2efxJcJdRNpqweCistDbZMwIVHw==",
|
"integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
"@types/json-schema": "^7.0.12",
|
"@types/json-schema": "^7.0.12",
|
||||||
"@types/semver": "^7.5.0",
|
"@types/semver": "^7.5.0",
|
||||||
"@typescript-eslint/scope-manager": "7.1.0",
|
"@typescript-eslint/scope-manager": "7.0.1",
|
||||||
"@typescript-eslint/types": "7.1.0",
|
"@typescript-eslint/types": "7.0.1",
|
||||||
"@typescript-eslint/typescript-estree": "7.1.0",
|
"@typescript-eslint/typescript-estree": "7.0.1",
|
||||||
"semver": "^7.5.4"
|
"semver": "^7.5.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2594,12 +2594,12 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "7.1.0",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz",
|
||||||
"integrity": "sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==",
|
"integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "7.1.0",
|
"@typescript-eslint/types": "7.0.1",
|
||||||
"eslint-visitor-keys": "^3.4.1"
|
"eslint-visitor-keys": "^3.4.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2617,23 +2617,24 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/browser": {
|
"node_modules/@vitest/browser": {
|
||||||
"version": "1.3.1",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-1.0.4.tgz",
|
||||||
"integrity": "sha512-pRof8G8nqRWwg3ouyIctyhfIVk5jXgF056uF//sqdi37+pVtDz9kBI/RMu0xlc8tgCyJ2aEMfbgJZPUydlEVaQ==",
|
"integrity": "sha512-qMT1NhClex73eA2sOwnlwLcSIVCW8B7NFVzIKuXLKxSJD3LsNq8PCKhwOkBxklbSAcZdkOgL/d3/gzQT7k9eng==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/utils": "1.3.1",
|
"estree-walker": "^3.0.3",
|
||||||
"magic-string": "^0.30.5",
|
"magic-string": "^0.30.5",
|
||||||
"sirv": "^2.0.4"
|
"sirv": "^2.0.3"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"playwright": "*",
|
"playwright": "*",
|
||||||
"vitest": "1.3.1",
|
"safaridriver": "*",
|
||||||
|
"vitest": "^1.0.0",
|
||||||
"webdriverio": "*"
|
"webdriverio": "*"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
@@ -2649,9 +2650,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/coverage-v8": {
|
"node_modules/@vitest/coverage-v8": {
|
||||||
"version": "1.3.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.2.2.tgz",
|
||||||
"integrity": "sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==",
|
"integrity": "sha512-IHyKnDz18SFclIEEAHb9Y4Uxx0sPKC2VO1kdDCs1BF6Ip4S8rQprs971zIsooLUn7Afs71GRxWMWpkCGZpRMhw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.1",
|
"@ampproject/remapping": "^2.2.1",
|
||||||
@@ -2672,17 +2673,17 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vitest": "1.3.1"
|
"vitest": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "1.3.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz",
|
||||||
"integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==",
|
"integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/spy": "1.3.1",
|
"@vitest/spy": "1.2.2",
|
||||||
"@vitest/utils": "1.3.1",
|
"@vitest/utils": "1.2.2",
|
||||||
"chai": "^4.3.10"
|
"chai": "^4.3.10"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -2690,12 +2691,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/runner": {
|
"node_modules/@vitest/runner": {
|
||||||
"version": "1.3.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz",
|
||||||
"integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==",
|
"integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/utils": "1.3.1",
|
"@vitest/utils": "1.2.2",
|
||||||
"p-limit": "^5.0.0",
|
"p-limit": "^5.0.0",
|
||||||
"pathe": "^1.1.1"
|
"pathe": "^1.1.1"
|
||||||
},
|
},
|
||||||
@@ -2731,9 +2732,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/snapshot": {
|
"node_modules/@vitest/snapshot": {
|
||||||
"version": "1.3.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz",
|
||||||
"integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==",
|
"integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"magic-string": "^0.30.5",
|
"magic-string": "^0.30.5",
|
||||||
@@ -2777,9 +2778,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/spy": {
|
"node_modules/@vitest/spy": {
|
||||||
"version": "1.3.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz",
|
||||||
"integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==",
|
"integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tinyspy": "^2.2.0"
|
"tinyspy": "^2.2.0"
|
||||||
@@ -2789,9 +2790,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/utils": {
|
"node_modules/@vitest/utils": {
|
||||||
"version": "1.3.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz",
|
||||||
"integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==",
|
"integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"diff-sequences": "^29.6.3",
|
"diff-sequences": "^29.6.3",
|
||||||
@@ -2836,9 +2837,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@zoom-image/core": {
|
"node_modules/@zoom-image/core": {
|
||||||
"version": "0.33.0",
|
"version": "0.32.1",
|
||||||
"resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.32.1.tgz",
|
||||||
"integrity": "sha512-wkMV8+aE7PeknLFhpIb/6vwRl09Z2gWM4UqKdnXO6Mb0pP9BiuDLcLvGGGB4o++uAPINgDwmNn+Loo641XSjDA==",
|
"integrity": "sha512-R56D749Ck+/1yLWlEJ2FctxjdpTQEje3jPhOAbeEZGzLndIumskO42UqRNixcER6sAzCi01oYopmqnCpDElF0g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@namnode/store": "^0.1.0"
|
"@namnode/store": "^0.1.0"
|
||||||
},
|
},
|
||||||
@@ -2848,11 +2849,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@zoom-image/svelte": {
|
"node_modules/@zoom-image/svelte": {
|
||||||
"version": "0.2.6",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.4.tgz",
|
||||||
"integrity": "sha512-dEpA/egmTjVcptwhtcKHvkhVMTzQCpH17erfcXuJByt+nn5Oo4LnZOxE8gwSVEdPp65Ns6Y/byYD0GSQ/vv+DQ==",
|
"integrity": "sha512-rgfgn7Q60VrwmE4MPBzDWaFplc+411Lxg1nMdAnq/UTv4HTWSpiwm1IOg8gQZjRp92a8RXcRmUYXU+wFKEMjSg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zoom-image/core": "0.33.0"
|
"@zoom-image/core": "0.32.1"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -4054,16 +4055,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "8.57.0",
|
"version": "8.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
|
||||||
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
|
"integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.6.1",
|
"@eslint-community/regexpp": "^4.6.1",
|
||||||
"@eslint/eslintrc": "^2.1.4",
|
"@eslint/eslintrc": "^2.1.4",
|
||||||
"@eslint/js": "8.57.0",
|
"@eslint/js": "8.56.0",
|
||||||
"@humanwhocodes/config-array": "^0.11.14",
|
"@humanwhocodes/config-array": "^0.11.13",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
"@nodelib/fs.walk": "^1.2.8",
|
"@nodelib/fs.walk": "^1.2.8",
|
||||||
"@ungap/structured-clone": "^1.2.0",
|
"@ungap/structured-clone": "^1.2.0",
|
||||||
@@ -7990,23 +7991,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strip-literal": {
|
"node_modules/strip-literal": {
|
||||||
"version": "2.0.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz",
|
||||||
"integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==",
|
"integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"js-tokens": "^8.0.2"
|
"acorn": "^8.10.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strip-literal/node_modules/js-tokens": {
|
|
||||||
"version": "8.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz",
|
|
||||||
"integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/sucrase": {
|
"node_modules/sucrase": {
|
||||||
"version": "3.34.0",
|
"version": "3.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
|
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
|
||||||
@@ -8082,9 +8077,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte": {
|
"node_modules/svelte": {
|
||||||
"version": "4.2.12",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.11.tgz",
|
||||||
"integrity": "sha512-d8+wsh5TfPwqVzbm4/HCXC783/KPHV60NvwitJnyTA5lWn1elhXMNWhXGCJ7PwPa8qFUnyJNIyuIRt2mT0WMug==",
|
"integrity": "sha512-YIQk3J4X89wOLhjsqIW8tqY3JHPuBdtdOIkASP2PZeAMcSW9RsIjQzMesCrxOF3gdWYC0mKknlKF7OqmLM+Zqg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.1",
|
"@ampproject/remapping": "^2.2.1",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||||
@@ -8106,9 +8101,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte-check": {
|
"node_modules/svelte-check": {
|
||||||
"version": "3.6.5",
|
"version": "3.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.4.tgz",
|
||||||
"integrity": "sha512-5aLgoQEdadvp8ypvKQ2avhnQ+V9YPQQaWrTFlXFw5g/v8xIQBvo+X/WqxTyD+V/ItDqXg3+abUA53rdDHgUjCA==",
|
"integrity": "sha512-mY/dqucqm46p72M8yZmn81WPZx9mN6uuw8UVfR3ZKQeLxQg5HDGO3HHm5AZuWZPYNMLJ+TRMn+TeN53HfQ/vsw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/trace-mapping": "^0.3.17",
|
"@jridgewell/trace-mapping": "^0.3.17",
|
||||||
@@ -8178,9 +8173,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte-maplibre": {
|
"node_modules/svelte-maplibre": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.8.0.tgz",
|
||||||
"integrity": "sha512-CTm/s0+mJzBHSoO5zPKBo3ORmUyiWS3Ex4xvVdNgVg+sDesHasEAJ0N1/NUrd56S33zgRdFZGzRnRguCnKFAzw==",
|
"integrity": "sha512-sRSj/zQa7LTfHNIzKcYe+sa9qHClt/OAXcdPQ0w3ksLbCMmVHGk4B2yIXHCVk0g4sc18M85N8KGsHVtZoNC+Mw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"d3-geo": "^3.1.0",
|
"d3-geo": "^3.1.0",
|
||||||
"just-compare": "^2.3.0",
|
"just-compare": "^2.3.0",
|
||||||
@@ -8461,9 +8456,9 @@
|
|||||||
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="
|
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="
|
||||||
},
|
},
|
||||||
"node_modules/tinyspy": {
|
"node_modules/tinyspy": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz",
|
||||||
"integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
|
"integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -8738,9 +8733,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.1.4",
|
"version": "5.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz",
|
||||||
"integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==",
|
"integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.19.3",
|
"esbuild": "^0.19.3",
|
||||||
@@ -8806,9 +8801,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-node": {
|
"node_modules/vite-node": {
|
||||||
"version": "1.3.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz",
|
||||||
"integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==",
|
"integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cac": "^6.7.14",
|
"cac": "^6.7.14",
|
||||||
@@ -8842,17 +8837,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vitest": {
|
"node_modules/vitest": {
|
||||||
"version": "1.3.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz",
|
||||||
"integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==",
|
"integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/expect": "1.3.1",
|
"@vitest/expect": "1.2.2",
|
||||||
"@vitest/runner": "1.3.1",
|
"@vitest/runner": "1.2.2",
|
||||||
"@vitest/snapshot": "1.3.1",
|
"@vitest/snapshot": "1.2.2",
|
||||||
"@vitest/spy": "1.3.1",
|
"@vitest/spy": "1.2.2",
|
||||||
"@vitest/utils": "1.3.1",
|
"@vitest/utils": "1.2.2",
|
||||||
"acorn-walk": "^8.3.2",
|
"acorn-walk": "^8.3.2",
|
||||||
|
"cac": "^6.7.14",
|
||||||
"chai": "^4.3.10",
|
"chai": "^4.3.10",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"execa": "^8.0.1",
|
"execa": "^8.0.1",
|
||||||
@@ -8861,11 +8857,11 @@
|
|||||||
"pathe": "^1.1.1",
|
"pathe": "^1.1.1",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
"std-env": "^3.5.0",
|
"std-env": "^3.5.0",
|
||||||
"strip-literal": "^2.0.0",
|
"strip-literal": "^1.3.0",
|
||||||
"tinybench": "^2.5.1",
|
"tinybench": "^2.5.1",
|
||||||
"tinypool": "^0.8.2",
|
"tinypool": "^0.8.2",
|
||||||
"vite": "^5.0.0",
|
"vite": "^5.0.0",
|
||||||
"vite-node": "1.3.1",
|
"vite-node": "1.2.2",
|
||||||
"why-is-node-running": "^2.2.2"
|
"why-is-node-running": "^2.2.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -8880,8 +8876,8 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@edge-runtime/vm": "*",
|
"@edge-runtime/vm": "*",
|
||||||
"@types/node": "^18.0.0 || >=20.0.0",
|
"@types/node": "^18.0.0 || >=20.0.0",
|
||||||
"@vitest/browser": "1.3.1",
|
"@vitest/browser": "^1.0.0",
|
||||||
"@vitest/ui": "1.3.1",
|
"@vitest/ui": "^1.0.0",
|
||||||
"happy-dom": "*",
|
"happy-dom": "*",
|
||||||
"jsdom": "*"
|
"jsdom": "*"
|
||||||
},
|
},
|
||||||
|
|||||||
+35
-35
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.3.0",
|
"version": "1.1.1",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
||||||
@@ -22,59 +22,59 @@
|
|||||||
"prepare": "svelte-kit sync"
|
"prepare": "svelte-kit sync"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.0.0",
|
||||||
"@floating-ui/dom": "^1.6.3",
|
"@floating-ui/dom": "^1.5.1",
|
||||||
"@socket.io/component-emitter": "^3.1.0",
|
"@socket.io/component-emitter": "^3.1.0",
|
||||||
"@sveltejs/adapter-static": "^3.0.1",
|
"@sveltejs/adapter-static": "^3.0.1",
|
||||||
"@sveltejs/enhanced-img": "^0.1.8",
|
"@sveltejs/enhanced-img": "^0.1.8",
|
||||||
"@sveltejs/kit": "^2.5.2",
|
"@sveltejs/kit": "^2.5.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||||
"@testing-library/jest-dom": "^6.4.2",
|
"@testing-library/jest-dom": "^6.1.5",
|
||||||
"@testing-library/svelte": "^4.1.0",
|
"@testing-library/svelte": "^4.0.3",
|
||||||
"@types/dom-to-image": "^2.6.7",
|
"@types/dom-to-image": "^2.6.4",
|
||||||
"@types/justified-layout": "^4.1.4",
|
"@types/justified-layout": "^4.1.0",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.6",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.2.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
"@typescript-eslint/parser": "^7.1.0",
|
"@typescript-eslint/parser": "^7.0.0",
|
||||||
"@vitest/coverage-v8": "^1.3.1",
|
"@vitest/coverage-v8": "^1.0.4",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.13",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.34.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-svelte": "^2.35.1",
|
"eslint-plugin-svelte": "^2.30.0",
|
||||||
"eslint-plugin-unicorn": "^51.0.1",
|
"eslint-plugin-unicorn": "^51.0.0",
|
||||||
"factory.ts": "^1.4.1",
|
"factory.ts": "^1.3.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.21",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.1.0",
|
||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
"prettier-plugin-svelte": "^3.2.1",
|
"prettier-plugin-svelte": "^3.1.2",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"svelte": "^4.2.12",
|
"svelte": "^4.2.11",
|
||||||
"svelte-check": "^3.6.5",
|
"svelte-check": "^3.6.4",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.2.7",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.5.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^5.1.4",
|
"vite": "^5.1.1",
|
||||||
"vitest": "^1.3.1"
|
"vitest": "^1.0.4"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.3.67",
|
||||||
"@photo-sphere-viewer/core": "^5.7.1",
|
"@photo-sphere-viewer/core": "^5.7.0",
|
||||||
"@zoom-image/svelte": "^0.2.6",
|
"@zoom-image/svelte": "^0.2.0",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"copy-image-clipboard": "^2.1.2",
|
"copy-image-clipboard": "^2.1.2",
|
||||||
"dom-to-image": "^2.6.0",
|
"dom-to-image": "^2.6.0",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.7",
|
||||||
"justified-layout": "^4.1.0",
|
"justified-layout": "^4.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.2.1",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.6.1",
|
||||||
"svelte-local-storage-store": "^0.6.4",
|
"svelte-local-storage-store": "^0.6.0",
|
||||||
"svelte-maplibre": "^0.8.1",
|
"svelte-maplibre": "^0.8.0",
|
||||||
"thumbhash": "^0.1.1"
|
"thumbhash": "^0.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { isHttpError } from '@immich/sdk';
|
import { isHttpError } from '@immich/sdk';
|
||||||
import type { HandleClientError } from '@sveltejs/kit';
|
import type { HandleClientError } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
const LOG_PREFIX = '[hooks.client.ts]';
|
||||||
const DEFAULT_MESSAGE = 'Hmm, not sure about that. Check the logs or open a ticket?';
|
const DEFAULT_MESSAGE = 'Hmm, not sure about that. Check the logs or open a ticket?';
|
||||||
|
|
||||||
const parseError = (error: unknown) => {
|
const parseError = (error: unknown) => {
|
||||||
@@ -22,6 +23,6 @@ const parseError = (error: unknown) => {
|
|||||||
|
|
||||||
export const handleError: HandleClientError = ({ error }) => {
|
export const handleError: HandleClientError = ({ error }) => {
|
||||||
const result = parseError(error);
|
const result = parseError(error);
|
||||||
console.error(`[hooks.client.ts]:handleError ${result.message}`);
|
console.error(`${LOG_PREFIX}:handleError ${result.message}`);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,11 +48,11 @@
|
|||||||
await handleCommand(jobId, dto);
|
await handleCommand(jobId, dto);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onConfirm = async () => {
|
const onConfirm = () => {
|
||||||
if (!confirmJob) {
|
if (!confirmJob) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await handleCommand(confirmJob, { command: JobCommand.Start, force: true });
|
handleCommand(confirmJob, { command: JobCommand.Start, force: true });
|
||||||
confirmJob = null;
|
confirmJob = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetToDefault = (configKeys: Array<keyof SystemConfigDto>) => {
|
const resetToDefault = async (configKeys: Array<keyof SystemConfigDto>) => {
|
||||||
for (const key of configKeys) {
|
for (const key of configKeys) {
|
||||||
config = { ...config, [key]: defaultConfig[key] };
|
config = { ...config, [key]: defaultConfig[key] };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,28 @@
|
|||||||
subtitle="Watch external libraries for file changes"
|
subtitle="Watch external libraries for file changes"
|
||||||
bind:checked={config.library.watch.enabled}
|
bind:checked={config.library.watch.enabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SettingSwitch
|
||||||
|
title="Use filesystem polling (EXPERIMENTAL)"
|
||||||
|
disabled={disabled || !config.library.watch.enabled}
|
||||||
|
subtitle="Use polling instead of native filesystem watching. This is required for network shares but can be very resource intensive. Use with care!"
|
||||||
|
bind:checked={config.library.watch.usePolling}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingInputField
|
||||||
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
|
required={config.library.watch.usePolling}
|
||||||
|
disabled={disabled || !config.library.watch.usePolling || !config.library.watch.enabled}
|
||||||
|
label="Polling interval"
|
||||||
|
bind:value={config.library.watch.interval}
|
||||||
|
isEdited={config.library.watch.interval !== savedConfig.library.watch.interval}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="desc">
|
||||||
|
<p class="text-sm dark:text-immich-dark-fg">
|
||||||
|
Interval of filesystem polling, in milliseconds. Lower values will result in higher CPU usage.
|
||||||
|
</p>
|
||||||
|
</svelte:fragment>
|
||||||
|
</SettingInputField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { autoGrowHeight } from '$lib/utils/autogrow';
|
|
||||||
import { updateAlbumInfo } from '@immich/sdk';
|
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
|
||||||
|
|
||||||
export let id: string;
|
|
||||||
export let description: string;
|
|
||||||
export let isOwned: boolean;
|
|
||||||
|
|
||||||
$: newDescription = description;
|
|
||||||
|
|
||||||
const handleUpdateDescription = async () => {
|
|
||||||
if (newDescription === description) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateAlbumInfo({
|
|
||||||
id,
|
|
||||||
updateAlbumDto: {
|
|
||||||
description: newDescription,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, 'Error updating album description');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
description = newDescription;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if isOwned}
|
|
||||||
<textarea
|
|
||||||
class="w-full mt-2 resize-none overflow-hidden text-black dark:text-white border-b-2 border-transparent border-gray-500 bg-transparent text-base outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:focus:border-immich-dark-primary hover:border-gray-400"
|
|
||||||
bind:value={newDescription}
|
|
||||||
on:input={(e) => autoGrowHeight(e.currentTarget)}
|
|
||||||
on:focusout={handleUpdateDescription}
|
|
||||||
use:autoGrowHeight
|
|
||||||
placeholder="Add description"
|
|
||||||
/>
|
|
||||||
{:else if description}
|
|
||||||
<p class="break-words whitespace-pre-line w-full text-black dark:text-white text-base">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { updateAlbumInfo } from '@immich/sdk';
|
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
|
||||||
|
|
||||||
export let id: string;
|
|
||||||
export let albumName: string;
|
|
||||||
export let isOwned: boolean;
|
|
||||||
|
|
||||||
$: newAlbumName = albumName;
|
|
||||||
|
|
||||||
const handleUpdateName = async () => {
|
|
||||||
if (newAlbumName === albumName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateAlbumInfo({
|
|
||||||
id,
|
|
||||||
updateAlbumDto: {
|
|
||||||
albumName: newAlbumName,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, 'Unable to update album name');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
albumName = newAlbumName;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<input
|
|
||||||
on:keydown={(e) => e.key === 'Enter' && e.currentTarget.blur()}
|
|
||||||
on:blur={handleUpdateName}
|
|
||||||
class="w-[99%] mb-2 border-b-2 border-transparent text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned
|
|
||||||
? 'hover:border-gray-400'
|
|
||||||
: 'hover:border-transparent'} bg-immich-bg focus:border-b-2 focus:border-immich-primary focus:outline-none dark:bg-immich-dark-bg dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray"
|
|
||||||
type="text"
|
|
||||||
bind:value={newAlbumName}
|
|
||||||
disabled={!isOwned}
|
|
||||||
title="Edit Title"
|
|
||||||
placeholder="Add a title"
|
|
||||||
/>
|
|
||||||
@@ -21,7 +21,6 @@
|
|||||||
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
||||||
import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
|
||||||
import UpdatePanel from '../shared-components/update-panel.svelte';
|
import UpdatePanel from '../shared-components/update-panel.svelte';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
|
||||||
|
|
||||||
export let sharedLink: SharedLinkResponseDto;
|
export let sharedLink: SharedLinkResponseDto;
|
||||||
export let user: UserResponseDto | undefined = undefined;
|
export let user: UserResponseDto | undefined = undefined;
|
||||||
@@ -36,7 +35,7 @@
|
|||||||
|
|
||||||
dragAndDropFilesStore.subscribe((value) => {
|
dragAndDropFilesStore.subscribe((value) => {
|
||||||
if (value.isDragging && value.files.length > 0) {
|
if (value.isDragging && value.files.length > 0) {
|
||||||
handlePromiseError(fileUploadHandler(value.files, album.id));
|
fileUploadHandler(value.files, album.id);
|
||||||
dragAndDropFilesStore.set({ isDragging: false, files: [] });
|
dragAndDropFilesStore.set({ isDragging: false, files: [] });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -68,7 +67,7 @@
|
|||||||
|
|
||||||
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
|
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(async () => {
|
||||||
document.addEventListener('keydown', onKeyboardPress);
|
document.addEventListener('keydown', onKeyboardPress);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
||||||
import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
|
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||||
import { getAssetType } from '$lib/utils/asset-utils';
|
import { getAssetType } from '$lib/utils/asset-utils';
|
||||||
import { autoGrowHeight } from '$lib/utils/autogrow';
|
import { autoGrowHeight } from '$lib/utils/autogrow';
|
||||||
import { clickOutside } from '$lib/utils/click-outside';
|
import { clickOutside } from '$lib/utils/click-outside';
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (assetId && previousAssetId != assetId) {
|
if (assetId && previousAssetId != assetId) {
|
||||||
handlePromiseError(getReactions());
|
getReactions();
|
||||||
previousAssetId = assetId;
|
previousAssetId = assetId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,10 +95,10 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEnter = async (event: KeyboardEvent) => {
|
const handleEnter = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await handleSendComment();
|
handleSendComment();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||||
import { stackAssetsStore } from '$lib/stores/stacked-asset.store';
|
import { stackAssetsStore } from '$lib/stores/stacked-asset.store';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { getAssetJobMessage, isSharedLink, handlePromiseError } from '$lib/utils';
|
import { getAssetJobMessage, isSharedLink } from '$lib/utils';
|
||||||
import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils';
|
import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
||||||
@@ -174,8 +174,8 @@
|
|||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (isShared && asset.id) {
|
if (isShared && asset.id) {
|
||||||
handlePromiseError(getFavorite());
|
getFavorite();
|
||||||
handlePromiseError(getNumberOfComments());
|
getNumberOfComments();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,9 +184,9 @@
|
|||||||
if (value === SlideshowState.PlaySlideshow) {
|
if (value === SlideshowState.PlaySlideshow) {
|
||||||
slideshowHistory.reset();
|
slideshowHistory.reset();
|
||||||
slideshowHistory.queue(asset.id);
|
slideshowHistory.queue(asset.id);
|
||||||
handlePromiseError(handlePlaySlideshow());
|
handlePlaySlideshow();
|
||||||
} else if (value === SlideshowState.StopSlideshow) {
|
} else if (value === SlideshowState.StopSlideshow) {
|
||||||
handlePromiseError(handleStopSlideshow());
|
handleStopSlideshow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$: asset.id && !sharedLink && handlePromiseError(handleGetAllAlbums()); // Update the album information when the asset ID changes
|
$: asset.id && !sharedLink && handleGetAllAlbums(); // Update the album information when the asset ID changes
|
||||||
|
|
||||||
const handleGetAllAlbums = async () => {
|
const handleGetAllAlbums = async () => {
|
||||||
if (isSharedLink()) {
|
if (isSharedLink()) {
|
||||||
@@ -247,7 +247,7 @@
|
|||||||
isShowActivity = !isShowActivity;
|
isShowActivity = !isShowActivity;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeypress = async (event: KeyboardEvent) => {
|
const handleKeypress = (event: KeyboardEvent) => {
|
||||||
if (shouldIgnoreShortcut(event)) {
|
if (shouldIgnoreShortcut(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -264,7 +264,7 @@
|
|||||||
case 'a':
|
case 'a':
|
||||||
case 'A': {
|
case 'A': {
|
||||||
if (shiftKey) {
|
if (shiftKey) {
|
||||||
await toggleArchive();
|
toggleArchive();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -273,18 +273,18 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'ArrowRight': {
|
case 'ArrowRight': {
|
||||||
await navigateAssetForward();
|
navigateAssetForward();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'd':
|
case 'd':
|
||||||
case 'D': {
|
case 'D': {
|
||||||
if (shiftKey) {
|
if (shiftKey) {
|
||||||
await downloadFile(asset);
|
downloadFile(asset);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'Delete': {
|
case 'Delete': {
|
||||||
await trashOrDelete(shiftKey);
|
trashOrDelete(shiftKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'Escape': {
|
case 'Escape': {
|
||||||
@@ -296,7 +296,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'f': {
|
case 'f': {
|
||||||
await toggleFavorite();
|
toggleFavorite();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'i': {
|
case 'i': {
|
||||||
@@ -326,7 +326,7 @@
|
|||||||
|
|
||||||
slideshowHistory.queue(asset.id);
|
slideshowHistory.queue(asset.id);
|
||||||
|
|
||||||
await setAssetId(asset.id);
|
setAssetId(asset.id);
|
||||||
$restartSlideshowProgress = true;
|
$restartSlideshowProgress = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -369,17 +369,17 @@
|
|||||||
$isShowDetail = !$isShowDetail;
|
$isShowDetail = !$isShowDetail;
|
||||||
};
|
};
|
||||||
|
|
||||||
const trashOrDelete = async (force: boolean = false) => {
|
const trashOrDelete = (force: boolean = false) => {
|
||||||
if (force || !isTrashEnabled) {
|
if (force || !isTrashEnabled) {
|
||||||
if ($showDeleteModal) {
|
if ($showDeleteModal) {
|
||||||
isShowDeleteConfirmation = true;
|
isShowDeleteConfirmation = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await deleteAsset();
|
deleteAsset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await trashAsset();
|
trashAsset();
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -432,7 +432,7 @@
|
|||||||
message: asset.isFavorite ? `Added to favorites` : `Removed from favorites`,
|
message: asset.isFavorite ? `Added to favorites` : `Removed from favorites`,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Unable to ${asset.isFavorite ? `add asset to` : `remove asset from`} favorites`);
|
await handleError(error, `Unable to ${asset.isFavorite ? `add asset to` : `remove asset from`} favorites`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -472,7 +472,7 @@
|
|||||||
message: asset.isArchived ? `Added to archive` : `Removed from archive`,
|
message: asset.isArchived ? `Added to archive` : `Removed from archive`,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Unable to ${asset.isArchived ? `add asset to` : `remove asset from`} archive`);
|
await handleError(error, `Unable to ${asset.isArchived ? `add asset to` : `remove asset from`} archive`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -481,7 +481,7 @@
|
|||||||
await runAssetJobs({ assetJobsDto: { assetIds: [asset.id], name } });
|
await runAssetJobs({ assetJobsDto: { assetIds: [asset.id], name } });
|
||||||
notificationController.show({ type: NotificationType.Info, message: getAssetJobMessage(name) });
|
notificationController.show({ type: NotificationType.Info, message: getAssetJobMessage(name) });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Unable to submit job`);
|
await handleError(error, `Unable to submit job`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -492,7 +492,7 @@
|
|||||||
let assetViewerHtmlElement: HTMLElement;
|
let assetViewerHtmlElement: HTMLElement;
|
||||||
|
|
||||||
const slideshowHistory = new SlideshowHistory((assetId: string) => {
|
const slideshowHistory = new SlideshowHistory((assetId: string) => {
|
||||||
handlePromiseError(setAssetId(assetId));
|
setAssetId(assetId);
|
||||||
$restartSlideshowProgress = true;
|
$restartSlideshowProgress = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -550,7 +550,7 @@
|
|||||||
dispatch('close');
|
dispatch('close');
|
||||||
notificationController.show({ type: NotificationType.Info, message: 'Un-stacked', timeout: 1500 });
|
notificationController.show({ type: NotificationType.Info, message: 'Un-stacked', timeout: 1500 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Unable to unstack`);
|
await handleError(error, `Unable to unstack`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { websocketEvents } from '$lib/stores/websocket';
|
import { websocketEvents } from '$lib/stores/websocket';
|
||||||
import { getAssetThumbnailUrl, getPeopleThumbnailUrl, isSharedLink, handlePromiseError } from '$lib/utils';
|
import { getAssetThumbnailUrl, getPeopleThumbnailUrl, isSharedLink } from '$lib/utils';
|
||||||
import { delay, getAssetFilename } from '$lib/utils/asset-utils';
|
import { delay, getAssetFilename } from '$lib/utils/asset-utils';
|
||||||
import { autoGrowHeight } from '$lib/utils/autogrow';
|
import { autoGrowHeight } from '$lib/utils/autogrow';
|
||||||
import { clickOutside } from '$lib/utils/click-outside';
|
import { clickOutside } from '$lib/utils/click-outside';
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
originalDescription = description;
|
originalDescription = description;
|
||||||
};
|
};
|
||||||
|
|
||||||
$: handlePromiseError(handleNewAsset(asset));
|
$: handleNewAsset(asset);
|
||||||
|
|
||||||
$: latlng = (() => {
|
$: latlng = (() => {
|
||||||
const lat = asset.exifInfo?.latitude;
|
const lat = asset.exifInfo?.latitude;
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'Enter': {
|
case 'Enter': {
|
||||||
if (ctrl && event.target === textArea) {
|
if (ctrl && event.target === textArea) {
|
||||||
await handleFocusOut();
|
handleFocusOut();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||||
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
|
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
|
||||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||||
import { getKey, handlePromiseError } from '$lib/utils';
|
import { getKey } from '$lib/utils';
|
||||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||||
import { getBoundingBox } from '$lib/utils/people-utils';
|
import { getBoundingBox } from '$lib/utils/people-utils';
|
||||||
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const doZoomImage = () => {
|
const doZoomImage = async () => {
|
||||||
setZoomImageWheelState({
|
setZoomImageWheelState({
|
||||||
currentZoom: $zoomImageWheelState.currentZoom === 1 ? 2 : 1,
|
currentZoom: $zoomImageWheelState.currentZoom === 1 ? 2 : 1,
|
||||||
});
|
});
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
if (state.currentZoom > 1 && isWebCompatibleImage(asset) && !hasZoomed && !$alwaysLoadOriginalFile) {
|
if (state.currentZoom > 1 && isWebCompatibleImage(asset) && !hasZoomed && !$alwaysLoadOriginalFile) {
|
||||||
hasZoomed = true;
|
hasZoomed = true;
|
||||||
|
|
||||||
handlePromiseError(loadAssetData({ loadOriginal: true }));
|
loadAssetData({ loadOriginal: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
video.muted = false;
|
video.muted = false;
|
||||||
dispatch('onVideoStarted');
|
dispatch('onVideoStarted');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to play video');
|
await handleError(error, 'Unable to play video');
|
||||||
} finally {
|
} finally {
|
||||||
isVideoLoading = false;
|
isVideoLoading = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
if (assetType === AssetTypeEnum.Image) {
|
if (assetType === AssetTypeEnum.Image) {
|
||||||
image = $photoViewer;
|
image = $photoViewer;
|
||||||
} else if (assetType === AssetTypeEnum.Video) {
|
} else if (assetType === AssetTypeEnum.Video) {
|
||||||
const data = getAssetThumbnailUrl(assetId, ThumbnailFormat.Webp);
|
const data = await getAssetThumbnailUrl(assetId, ThumbnailFormat.Webp);
|
||||||
const img: HTMLImageElement = new Image();
|
const img: HTMLImageElement = new Image();
|
||||||
img.src = data;
|
img.src = data;
|
||||||
|
|
||||||
|
|||||||
@@ -43,10 +43,10 @@
|
|||||||
dispatch('back');
|
dispatch('back');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSwapPeople = async () => {
|
const handleSwapPeople = () => {
|
||||||
[person, selectedPeople[0]] = [selectedPeople[0], person];
|
[person, selectedPeople[0]] = [selectedPeople[0], person];
|
||||||
$page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE);
|
$page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE);
|
||||||
await goto(`${AppRoute.PEOPLE}/${person.id}?${$page.url.searchParams.toString()}`);
|
goto(`${AppRoute.PEOPLE}/${person.id}?${$page.url.searchParams.toString()}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelect = (selected: PersonResponseDto) => {
|
const onSelect = (selected: PersonResponseDto) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
||||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||||
import { websocketEvents } from '$lib/stores/websocket';
|
import { websocketEvents } from '$lib/stores/websocket';
|
||||||
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
|
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { getPersonNameWithHiddenValue } from '$lib/utils/person';
|
import { getPersonNameWithHiddenValue } from '$lib/utils/person';
|
||||||
import {
|
import {
|
||||||
@@ -46,8 +46,8 @@
|
|||||||
let allPeople: PersonResponseDto[] = [];
|
let allPeople: PersonResponseDto[] = [];
|
||||||
|
|
||||||
// timers
|
// timers
|
||||||
let loaderLoadingDoneTimeout: ReturnType<typeof setTimeout>;
|
let loaderLoadingDoneTimeout: NodeJS.Timeout;
|
||||||
let automaticRefreshTimeout: ReturnType<typeof setTimeout>;
|
let automaticRefreshTimeout: NodeJS.Timeout;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
close: void;
|
close: void;
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
handlePromiseError(loadPeople());
|
loadPeople();
|
||||||
return websocketEvents.on('on_person_thumbnail', onPersonThumbnail);
|
return websocketEvents.on('on_person_thumbnail', onPersonThumbnail);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePersonPicker = (index: number) => {
|
const handlePersonPicker = async (index: number) => {
|
||||||
editedPersonIndex = index;
|
editedPersonIndex = index;
|
||||||
showSeletecFaces = true;
|
showSeletecFaces = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -132,7 +132,9 @@
|
|||||||
title={'Assign selected assets to a new person'}
|
title={'Assign selected assets to a new person'}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
disabled={disableButtons || hasSelection}
|
disabled={disableButtons || hasSelection}
|
||||||
on:click={handleCreate}
|
on:click={() => {
|
||||||
|
handleCreate();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{#if !showLoadingSpinnerCreate}
|
{#if !showLoadingSpinnerCreate}
|
||||||
<Icon path={mdiPlus} size={18} />
|
<Icon path={mdiPlus} size={18} />
|
||||||
@@ -145,7 +147,9 @@
|
|||||||
size={'sm'}
|
size={'sm'}
|
||||||
title={'Assign selected assets to an existing person'}
|
title={'Assign selected assets to an existing person'}
|
||||||
disabled={disableButtons || !hasSelection}
|
disabled={disableButtons || !hasSelection}
|
||||||
on:click={handleReassign}
|
on:click={() => {
|
||||||
|
handleReassign();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{#if !showLoadingSpinnerReassign}
|
{#if !showLoadingSpinnerReassign}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user