Compare commits

..

42 Commits

Author SHA1 Message Date
Alex The Bot
6f53e83d49 Version v1.97.0 2024-02-28 22:34:00 +00:00
martyfuhry
b1a896ba61 feat(mobile): Adds better precaching for assets in gallery view and memory lane (#7486)
Adds better precaching for assets in gallery view and memory lane
2024-02-28 16:13:15 -06:00
martyfuhry
d28abaad7b fix(mobile): Fixes prefer remote assets in thumbnail provider (#7485)
Fixes prefer remote assets in thumbnail provider

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-02-28 21:55:29 +00:00
martyfuhry
79442fc8a1 fix(mobile): Fixes thumbnail size with blur and alignment in video player (#7483)
* Clip the edges of the image filter

* Fixes thumbnail blur effect expanding outside of the image

* Fixes thumbs with video player and delayed loader
2024-02-28 15:48:59 -06:00
Michel Heusschen
93f0a866a3 fix(web): settings accordion open state (#7504) 2024-02-28 15:40:08 -06:00
martin
84fe41df31 fix(web): re-render albums (#7403)
* fix: re-render albums

* fix: album description

* fix: reactivity

* fix album reactivity + components for title and description

* only update AssetGrid when albumId changes

* remove title and description bindings

* remove console.log

* chore: fix merge

* pr feedback

* pr feedback

---------

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
2024-02-28 15:39:53 -06:00
Jonathan Jogenfors
e4f32a045d chore: remove watcher polling option (#7480)
* remove watcher polling

* fix lint

* add db migration
2024-02-28 21:20:10 +01:00
Simon Séhier
784d92dbb3 chore(deployment): add explicit registry to docker image names (#7496) 2024-02-28 09:15:32 -06:00
Michel Heusschen
c88184673a fix(web): keep notifications in view when scrolling (#7493) 2024-02-28 07:25:08 -05:00
Jason Rasmussen
74d431f881 refactor(server): format and metadata e2e (#7477)
* refactor(server): format and metadata e2e

* refactor: on upload success waiting
2024-02-28 10:21:31 +01:00
Alex
e2c0945bc1 chore: post release tasks 2024-02-27 23:09:48 -06:00
Alex
a02a24f349 chore: post release tasks 2024-02-27 23:09:40 -06:00
Fynn Petersen-Frey
87a7825cbc feat(server): rkmpp hardware decoding scaling (#7472)
* feat(server): RKMPP hardware decode & scaling

* disable hardware decoding for HDR
2024-02-27 20:32:07 -05:00
renovate[bot]
f0ea99cea9 chore(deps): update dependency @types/node to v20.11.20 (#7474)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-27 22:45:29 +01:00
Jonathan Jogenfors
0d2a656aa1 chore: add agpl milestone (#7479)
add agpl milestone
2024-02-27 21:43:12 +00:00
Alex The Bot
6d91c23f65 Version v1.96.0 2024-02-27 20:14:58 +00:00
renovate[bot]
df02a9f5ed chore(deps): update dependency @types/node to v20.11.20 (#7476)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-27 19:16:37 +00:00
renovate[bot]
2702bcc407 chore(deps): update dependency @types/node to v20.11.20 (#7475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-27 19:09:26 +00:00
Jason Rasmussen
807cd245f4 refactor(server): e2e (#7462)
* refactor: trash e2e

* refactor: asset e2e
2024-02-27 14:04:38 -05:00
renovate[bot]
dc0f8756f5 chore(deps): update dependency @types/node to v20.11.20 (#7473)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-27 14:03:34 -05:00
renovate[bot]
df9ab8943d chore(deps): update base-image to v20240227 (major) (#7470)
chore(deps): update base-image to v20240227

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-27 14:03:02 -05:00
Ben McCann
79409438a7 chore(web): upgrade dependencies (#7471) 2024-02-27 14:01:11 -05:00
martyfuhry
b15eec7ca7 refactor(mobile): Uses blurhash for memory card instead of blurred thumbnail (#7469)
* Uses blurhash for memory card instead of blurred thumbnail

New blurred backdrop widget

Comments

* Fixes video placeholder image placement

* unused import
2024-02-27 12:38:14 -06:00
Alex
908104299d chore(web): remove album's action notification (#7467) 2024-02-27 12:16:52 -06:00
renovate[bot]
c94874296c chore(deps): update web (#7448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-27 12:19:21 -05:00
renovate[bot]
8361130351 chore(deps): update @immich/cli (#7445)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-27 12:19:04 -05:00
Ben McCann
907a95a746 chore(web): cleanup promise handling (#7382)
* no-misused-promises

* no-floating-promises

* format

* revert for now

* remove load function

* require-await

* revert a few no-floating-promises changes that would cause no-misused-promises failures

* format

* fix a few more

* fix most remaining errors

* executor-queue

* executor-queue.spec

* remove duplicate comments by grouping rules

* upgrade sveltekit and enforce rules

* oops. move await

* try this

* just ignore for now since it's only a test

* run in parallel

* Update web/src/routes/admin/jobs-status/+page.svelte

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* remove Promise.resolve call

* rename function

* remove unnecessary warning silencing

* make handleError sync

* fix new errors from recently merged PR to main

* extract method

* use handlePromiseError

---------

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-02-27 10:37:37 -06:00
Alex
57f25855d3 fix(web/server): revert renovate hash update (#7464)
* Revert "chore(deps): update node.js to f3299f1 (#7444)"

This reverts commit cfb49c8be0.

* Revert "chore(deps): update node.js to f3299f1 (#7443)"

This reverts commit 2f121af9ec.
2024-02-27 10:28:00 -06:00
renovate[bot]
e02964ca0d chore(deps): update server (#7447)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-27 10:26:37 -06:00
renovate[bot]
fd301a3261 fix(deps): update dependency orjson to v3.9.15 [security] (#7438) 2024-02-27 10:59:28 -05:00
martyfuhry
d76baee50d refactor(mobile): Use ImmichThumbnail and local thumbnail image provider (#7279)
* Refactor to use ImmichThumbnail and local thumbnail image provider

format

* dart format

linter errors

linter

* Adds blurhash

format

* Fixes image blur

* uses hook instead of stateful widget to be more consistent

* Uses blurhash hook state

* Uses blurhash ref instead of state

* Fixes fade in duration for fade in placeholder

* Fixes an issue where thumbnails fail to load if too many thumbnail requests are made simultaenously

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-02-27 09:51:19 -06:00
Fynn Petersen-Frey
5e485e35e9 feat(server): easy RKMPP video encoding (#7460)
* feat(server): easy RKMPP video encoding

* make linter happy
2024-02-27 09:47:04 -06:00
renovate[bot]
cfb49c8be0 chore(deps): update node.js to f3299f1 (#7444)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-27 09:29:25 -06:00
renovate[bot]
2f121af9ec chore(deps): update node.js to f3299f1 (#7443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-27 09:29:08 -06:00
Michel Heusschen
21feb69083 fix(web): don't ask password for invalid shared link (#7456)
* fix(web): don't ask password for invalid shared link

* use apiUtils for e2e test
2024-02-27 09:25:57 -06:00
Mert
fb18129843 fix(server): truncate embedding tables (#7449)
truncate
2024-02-27 09:24:23 -06:00
Michel Heusschen
9fa2424652 fix(web): shared links page broken by enhanced:img (#7453) 2024-02-27 07:44:32 -05:00
dependabot[bot]
7d1edddd51 chore(deps): bump docker/setup-buildx-action from 3.0.0 to 3.1.0 (#7459)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.0.0 to 3.1.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.0.0...v3.1.0)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-27 07:43:06 -05:00
renovate[bot]
3f18a936b2 chore(deps): update dependency @oazapfts/runtime to v1.0.1 (#7446)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-27 07:37:12 -05:00
Alex
e20d3048cd chore(ci): move e2e test to macos runner (#7452)
* chore(ci): move e2e test to macos runner

* setup docker

* ubuntu test
2024-02-27 07:35:14 -05:00
renovate[bot]
8965c25f54 chore(deps): update machine-learning (#7451)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-27 00:53:37 -05:00
Mert
7e18e69c1c fix(ml): only use openvino if a gpu is available (#7450)
use `device_type`
2024-02-27 00:45:14 -05:00
163 changed files with 3072 additions and 2615 deletions

View File

@@ -58,7 +58,7 @@ jobs:
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
uses: docker/setup-buildx-action@v3.1.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v3

View File

@@ -66,7 +66,7 @@ jobs:
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
uses: docker/setup-buildx-action@v3.1.0
# Workaround to fix error:
# failed to push: failed to copy: io: read/write on closed pipe
# See https://github.com/docker/build-push-action/issues/761

View File

@@ -12,7 +12,7 @@ concurrency:
jobs:
server-e2e-api:
name: Server (e2e-api)
runs-on: mich
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./server
@@ -29,7 +29,7 @@ jobs:
server-e2e-jobs:
name: Server (e2e-jobs)
runs-on: mich
runs-on: ubuntu-latest
steps:
- name: Checkout code

818
cli/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -60,12 +60,12 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
restart: always
database:
container_name: immich_postgres
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
image: registry.hub.docker.com/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}

View File

@@ -38,12 +38,6 @@ services:
- /dev/dri:/dev/dri
- /dev/dma_heap:/dev/dma_heap
- /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:
devices:

View File

@@ -88,10 +88,7 @@ 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.
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.
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.
### Nightly job

View File

@@ -50,12 +50,22 @@ import {
mdiVectorCombine,
mdiVideo,
mdiWeb,
mdiScaleBalance,
} from '@mdi/js';
import Layout from '@theme/Layout';
import React from 'react';
import Timeline, { DateType, Item } from '../components/timeline';
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,
description: 'Automatically import files in external libraries when the operating system detects changes.',

View File

@@ -4,7 +4,6 @@ name: immich-e2e
x-server-build: &server-common
image: immich-server:latest
container_name: immich-e2e-server
build:
context: ../
dockerfile: server/Dockerfile
@@ -23,14 +22,16 @@ x-server-build: &server-common
services:
immich-server:
container_name: immich-e2e-server
command: [ "./start.sh", "immich" ]
<<: *server-common
ports:
- 2283:3001
# immich-microservices:
# command: [ "./start.sh", "microservices" ]
# <<: *server-common
immich-microservices:
container_name: immich-e2e-microservices
command: [ "./start.sh", "microservices" ]
<<: *server-common
redis:
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5

172
e2e/package-lock.json generated
View File

@@ -12,11 +12,15 @@
"@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.41.2",
"@types/luxon": "^3.4.2",
"@types/node": "^20.11.17",
"@types/pg": "^8.11.0",
"@types/supertest": "^6.0.2",
"@vitest/coverage-v8": "^1.3.0",
"exiftool-vendored": "^24.5.0",
"luxon": "^3.4.4",
"pg": "^8.11.3",
"socket.io-client": "^4.7.4",
"supertest": "^6.3.4",
"typescript": "^5.3.3",
"vitest": "^1.3.0"
@@ -591,6 +595,12 @@
"@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": {
"version": "1.41.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz",
@@ -781,6 +791,12 @@
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"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": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
@@ -799,6 +815,12 @@
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
"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": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@@ -806,9 +828,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"version": "20.11.20",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
"integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -1059,6 +1081,15 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"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": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -1263,6 +1294,28 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
@@ -1354,6 +1407,43 @@
"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": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
@@ -1547,6 +1637,15 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
@@ -1704,6 +1803,15 @@
"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": {
"version": "0.30.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
@@ -2346,6 +2454,34 @@
"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": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -2743,6 +2879,36 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"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": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@@ -16,11 +16,15 @@
"@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.41.2",
"@types/luxon": "^3.4.2",
"@types/node": "^20.11.17",
"@types/pg": "^8.11.0",
"@types/supertest": "^6.0.2",
"@vitest/coverage-v8": "^1.3.0",
"exiftool-vendored": "^24.5.0",
"luxon": "^3.4.4",
"pg": "^8.11.3",
"socket.io-client": "^4.7.4",
"supertest": "^6.3.4",
"typescript": "^5.3.3",
"vitest": "^1.3.0"

View File

@@ -1,7 +1,7 @@
import {
ActivityCreateDto,
AlbumResponseDto,
AssetResponseDto,
AssetFileUploadResponseDto,
LoginResponseDto,
ReactionType,
createActivity as create,
@@ -16,13 +16,13 @@ import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe('/activity', () => {
let admin: LoginResponseDto;
let nonOwner: LoginResponseDto;
let asset: AssetResponseDto;
let asset: AssetFileUploadResponseDto;
let album: AlbumResponseDto;
const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
create(
{ activityCreateDto: dto },
{ headers: asBearerAuth(accessToken || admin.accessToken) }
{ headers: asBearerAuth(accessToken || admin.accessToken) },
);
beforeAll(async () => {
@@ -40,7 +40,7 @@ describe('/activity', () => {
sharedWithUserIds: [nonOwner.userId],
},
},
{ headers: asBearerAuth(admin.accessToken) }
{ headers: asBearerAuth(admin.accessToken) },
);
});
@@ -61,7 +61,7 @@ describe('/activity', () => {
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
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}`);
expect(status).toEqual(400);
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}`);
expect(status).toEqual(400);
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],
},
},
{ headers: asBearerAuth(admin.accessToken) }
{ headers: asBearerAuth(admin.accessToken) },
);
const [reaction] = await Promise.all([
@@ -216,7 +216,7 @@ describe('/activity', () => {
.send({ albumId: uuidDto.invalid });
expect(status).toEqual(400);
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([
'comment must be a string',
'comment should not be empty',
])
]),
);
});
@@ -357,7 +357,7 @@ describe('/activity', () => {
describe('DELETE /activity/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(
`/activity/${uuidDto.notFound}`
`/activity/${uuidDto.notFound}`,
);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -421,7 +421,7 @@ describe('/activity', () => {
expect(status).toBe(400);
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,
comment: 'This is a test comment',
},
nonOwner.accessToken
nonOwner.accessToken,
);
const { status } = await request(app)

View File

@@ -1,6 +1,6 @@
import {
AlbumResponseDto,
AssetResponseDto,
AssetFileUploadResponseDto,
LoginResponseDto,
SharedLinkType,
deleteUser,
@@ -21,8 +21,8 @@ const user2NotShared = 'user2NotShared';
describe('/album', () => {
let admin: LoginResponseDto;
let user1: LoginResponseDto;
let user1Asset1: AssetResponseDto;
let user1Asset2: AssetResponseDto;
let user1Asset1: AssetFileUploadResponseDto;
let user1Asset2: AssetFileUploadResponseDto;
let user1Albums: AlbumResponseDto[];
let user2: LoginResponseDto;
let user2Albums: AlbumResponseDto[];
@@ -95,7 +95,7 @@ describe('/album', () => {
await deleteUser(
{ id: user3.userId },
{ headers: asBearerAuth(admin.accessToken) }
{ headers: asBearerAuth(admin.accessToken) },
);
});
@@ -112,7 +112,7 @@ describe('/album', () => {
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400);
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,
shared: true,
}),
])
]),
);
});
@@ -175,7 +175,7 @@ describe('/album', () => {
albumName: user1NotShared,
shared: false,
}),
])
]),
);
});
@@ -202,7 +202,7 @@ describe('/album', () => {
albumName: user2SharedUser,
shared: true,
}),
])
]),
);
});
@@ -219,7 +219,7 @@ describe('/album', () => {
albumName: user1NotShared,
shared: false,
}),
])
]),
);
});
@@ -251,7 +251,7 @@ describe('/album', () => {
describe('GET /album/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(
`/album/${user1Albums[0].id}`
`/album/${user1Albums[0].id}`,
);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -361,7 +361,7 @@ describe('/album', () => {
describe('PUT /album/:id/assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(
`/album/${user1Albums[0].id}/assets`
`/album/${user1Albums[0].id}/assets`,
);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -519,7 +519,7 @@ describe('/album', () => {
expect(body).toEqual(
expect.objectContaining({
sharedUsers: [expect.objectContaining({ id: user2.userId })],
})
}),
);
});

View File

@@ -0,0 +1,808 @@
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);
});
});
});

View File

@@ -29,14 +29,14 @@ describe('/audit', () => {
await Promise.all([
deleteAssets(
{ assetBulkDeleteDto: { ids: [trashedAsset.id] } },
{ headers: asBearerAuth(admin.accessToken) }
{ headers: asBearerAuth(admin.accessToken) },
),
updateAsset(
{
id: archivedAsset.id,
updateAssetDto: { isArchived: true },
},
{ headers: asBearerAuth(admin.accessToken) }
{ headers: asBearerAuth(admin.accessToken) },
),
]);

View File

@@ -1,4 +1,4 @@
import { AssetResponseDto, LoginResponseDto } from '@immich/sdk';
import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk';
import { errorDto } from 'src/responses';
import { apiUtils, app, dbUtils } from 'src/utils';
import request from 'supertest';
@@ -6,7 +6,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
describe('/download', () => {
let admin: LoginResponseDto;
let asset1: AssetResponseDto;
let asset1: AssetFileUploadResponseDto;
beforeAll(async () => {
apiUtils.setup();
@@ -35,7 +35,7 @@ describe('/download', () => {
expect(body).toEqual(
expect.objectContaining({
archives: [expect.objectContaining({ assetIds: [asset1.id] })],
})
}),
);
});
});
@@ -43,7 +43,7 @@ describe('/download', () => {
describe('POST /download/asset/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(
`/download/asset/${asset1.id}`
`/download/asset/${asset1.id}`,
);
expect(status).toBe(401);

View File

@@ -1,11 +1,9 @@
import {
AlbumResponseDto,
AssetResponseDto,
AssetFileUploadResponseDto,
LoginResponseDto,
SharedLinkCreateDto,
SharedLinkResponseDto,
SharedLinkType,
createSharedLink as create,
createAlbum,
deleteUser,
} from '@immich/sdk';
@@ -17,8 +15,8 @@ import { beforeAll, describe, expect, it } from 'vitest';
describe('/shared-link', () => {
let admin: LoginResponseDto;
let asset1: AssetResponseDto;
let asset2: AssetResponseDto;
let asset1: AssetFileUploadResponseDto;
let asset2: AssetFileUploadResponseDto;
let user1: LoginResponseDto;
let user2: LoginResponseDto;
let album: AlbumResponseDto;
@@ -50,11 +48,11 @@ describe('/shared-link', () => {
[album, deletedAlbum, metadataAlbum] = await Promise.all([
createAlbum(
{ createAlbumDto: { albumName: 'album' } },
{ headers: asBearerAuth(user1.accessToken) }
{ headers: asBearerAuth(user1.accessToken) },
),
createAlbum(
{ createAlbumDto: { albumName: 'deleted album' } },
{ headers: asBearerAuth(user2.accessToken) }
{ headers: asBearerAuth(user2.accessToken) },
),
createAlbum(
{
@@ -63,7 +61,7 @@ describe('/shared-link', () => {
assetIds: [asset1.id],
},
},
{ headers: asBearerAuth(user1.accessToken) }
{ headers: asBearerAuth(user1.accessToken) },
),
]);
@@ -106,7 +104,7 @@ describe('/shared-link', () => {
await deleteUser(
{ id: user2.userId },
{ headers: asBearerAuth(admin.accessToken) }
{ headers: asBearerAuth(admin.accessToken) },
);
});
@@ -132,7 +130,7 @@ describe('/shared-link', () => {
expect.objectContaining({ id: linkWithPassword.id }),
expect.objectContaining({ id: linkWithMetadata.id }),
expect.objectContaining({ id: linkWithoutMetadata.id }),
])
]),
);
});
@@ -166,7 +164,7 @@ describe('/shared-link', () => {
album,
userId: user1.userId,
type: SharedLinkType.Album,
})
}),
);
});
@@ -208,7 +206,7 @@ describe('/shared-link', () => {
album,
userId: user1.userId,
type: SharedLinkType.Album,
})
}),
);
});
@@ -225,7 +223,7 @@ describe('/shared-link', () => {
localDateTime: expect.any(String),
fileCreatedAt: expect.any(String),
exifInfo: expect.any(Object),
})
}),
);
expect(body.album).toBeDefined();
});
@@ -250,7 +248,7 @@ describe('/shared-link', () => {
describe('GET /shared-link/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(
`/shared-link/${linkWithAlbum.id}`
`/shared-link/${linkWithAlbum.id}`,
);
expect(status).toBe(401);
@@ -268,7 +266,7 @@ describe('/shared-link', () => {
album,
userId: user1.userId,
type: SharedLinkType.Album,
})
}),
);
});
@@ -279,7 +277,7 @@ describe('/shared-link', () => {
expect(status).toBe(400);
expect(body).toEqual(
expect.objectContaining({ message: 'Shared link not found' })
expect.objectContaining({ message: 'Shared link not found' }),
);
});
});
@@ -311,7 +309,7 @@ describe('/shared-link', () => {
expect(status).toBe(400);
expect(body).toEqual(
expect.objectContaining({ message: 'Invalid albumId' })
expect.objectContaining({ message: 'Invalid albumId' }),
);
});
@@ -323,7 +321,7 @@ describe('/shared-link', () => {
expect(status).toBe(400);
expect(body).toEqual(
expect.objectContaining({ message: 'Invalid assetIds' })
expect.objectContaining({ message: 'Invalid assetIds' }),
);
});
@@ -338,7 +336,7 @@ describe('/shared-link', () => {
expect.objectContaining({
type: SharedLinkType.Album,
userId: user1.userId,
})
}),
);
});
});
@@ -375,7 +373,7 @@ describe('/shared-link', () => {
type: SharedLinkType.Album,
userId: user1.userId,
description: 'foo',
})
}),
);
});
});
@@ -427,7 +425,7 @@ describe('/shared-link', () => {
describe('DELETE /shared-link/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(
`/shared-link/${linkWithAlbum.id}`
`/shared-link/${linkWithAlbum.id}`,
);
expect(status).toBe(401);

View File

@@ -0,0 +1,107 @@
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);
});
});
});

View File

@@ -1,12 +1,9 @@
import { apiUtils, cliUtils, dbUtils, immichCli } from 'src/utils';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { beforeAll, describe, expect, it } from 'vitest';
describe(`immich server-info`, () => {
beforeAll(() => {
beforeAll(async () => {
apiUtils.setup();
});
beforeEach(async () => {
await dbUtils.reset();
await cliUtils.login();
});

View File

@@ -1,4 +1,5 @@
import { getAllAlbums, getAllAssets } from '@immich/sdk';
import { mkdir, readdir, rm, symlink } from 'fs/promises';
import {
apiUtils,
asKeyAuth,
@@ -8,18 +9,18 @@ import {
testAssetDir,
} from 'src/utils';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { mkdir, readdir, rm, symlink } from 'fs/promises';
describe(`immich upload`, () => {
let key: string;
beforeAll(() => {
beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
key = await cliUtils.login();
});
beforeEach(async () => {
await dbUtils.reset();
key = await cliUtils.login();
await dbUtils.reset(['assets', 'albums']);
});
describe('immich upload --recursive', () => {
@@ -33,7 +34,7 @@ describe(`immich upload`, () => {
expect(stdout.split('\n')).toEqual(
expect.arrayContaining([
expect.stringContaining('Successfully uploaded 9 assets'),
])
]),
);
expect(exitCode).toBe(0);
@@ -55,7 +56,7 @@ describe(`immich upload`, () => {
expect.stringContaining('Successfully uploaded 9 assets'),
expect.stringContaining('Successfully created 1 new album'),
expect.stringContaining('Successfully updated 9 assets'),
])
]),
);
expect(stderr).toBe('');
expect(exitCode).toBe(0);
@@ -77,7 +78,7 @@ describe(`immich upload`, () => {
expect(response1.stdout.split('\n')).toEqual(
expect.arrayContaining([
expect.stringContaining('Successfully uploaded 9 assets'),
])
]),
);
expect(response1.stderr).toBe('');
expect(response1.exitCode).toBe(0);
@@ -97,10 +98,10 @@ describe(`immich upload`, () => {
expect(response2.stdout.split('\n')).toEqual(
expect.arrayContaining([
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(response2.stderr).toBe('');
expect(response2.exitCode).toBe(0);
@@ -127,7 +128,7 @@ describe(`immich upload`, () => {
expect.stringContaining('Successfully uploaded 9 assets'),
expect.stringContaining('Successfully created 1 new album'),
expect.stringContaining('Successfully updated 9 assets'),
])
]),
);
expect(stderr).toBe('');
expect(exitCode).toBe(0);
@@ -148,7 +149,7 @@ describe(`immich upload`, () => {
for (const file of filesToLink) {
await symlink(
`${testAssetDir}/albums/nature/${file}`,
`/tmp/albums/nature/${file}`
`/tmp/albums/nature/${file}`,
);
}
@@ -166,7 +167,7 @@ describe(`immich upload`, () => {
expect.arrayContaining([
expect.stringContaining('Successfully uploaded 9 assets'),
expect.stringContaining('Deleting assets that have been uploaded'),
])
]),
);
expect(stderr).toBe('');
expect(exitCode).toBe(0);

View File

@@ -2,25 +2,25 @@ import { spawn, exec } from 'child_process';
export default async () => {
let _resolve: () => unknown;
const promise = new Promise<void>((resolve) => (_resolve = resolve));
const ready = new Promise<void>((resolve) => (_resolve = resolve));
const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' });
child.stdout.on('data', (data) => {
const input = data.toString();
console.log(input);
if (input.includes('Immich Server is listening')) {
if (input.includes('Immich Microservices is listening')) {
_resolve();
}
});
child.stderr.on('data', (data) => console.log(data.toString()));
await promise;
await ready;
return async () => {
await new Promise<void>((resolve) =>
exec('docker compose down', () => resolve())
exec('docker compose down', () => resolve()),
);
};
};

View File

@@ -1,4 +1,5 @@
import {
AssetFileUploadResponseDto,
AssetResponseDto,
CreateAlbumDto,
CreateAssetDto,
@@ -11,18 +12,23 @@ import {
createSharedLink,
createUser,
defaults,
deleteAssets,
getAssetInfo,
login,
setAdminOnboarding,
signUpAdmin,
updatePerson,
} from '@immich/sdk';
import { BrowserContext } from '@playwright/test';
import { exec, spawn } from 'child_process';
import { exec, spawn } from 'node:child_process';
import { randomBytes } from 'node:crypto';
import { access } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { EventEmitter } from 'node:stream';
import { promisify } from 'node:util';
import pg from 'pg';
import { io, type Socket } from 'socket.io-client';
import { loginDto, signupDto } from 'src/fixtures';
import request from 'supertest';
@@ -37,17 +43,23 @@ const directoryExists = (directory: string) =>
// TODO move test assets into e2e/assets
export const testAssetDir = path.resolve(`./../server/test/assets/`);
export const tempDir = tmpdir();
const serverContainerName = 'immich-e2e-server';
const uploadMediaDir = '/usr/src/app/upload/upload';
const mediaDir = '/usr/src/app/upload';
const dirs = [
`"${mediaDir}/thumbs"`,
`"${mediaDir}/upload"`,
`"${mediaDir}/library"`,
`"${mediaDir}/encoded-video"`,
].join(' ');
if (!(await directoryExists(`${testAssetDir}/albums`))) {
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) => ({
Authorization: `Bearer ${accessToken}`,
});
@@ -59,7 +71,7 @@ let client: pg.Client | null = null;
export const fileUtils = {
reset: async () => {
await execPromise(
`docker exec -i "${serverContainerName}" rm -R "${uploadMediaDir}"`
`docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`,
);
},
};
@@ -81,7 +93,7 @@ export const dbUtils = {
await client.query(
'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)',
[assetId, personId, embedding]
[assetId, personId, embedding],
);
},
setPersonThumbnail: async (personId: string) => {
@@ -91,14 +103,14 @@ export const dbUtils = {
await client.query(
`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`,
[personId]
[personId],
);
},
reset: async (tables?: string[]) => {
try {
if (!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();
}
@@ -170,10 +182,94 @@ export interface AdminSetupOptions {
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 = {
setup: () => {
setBaseUrl();
defaults.baseUrl = app;
},
adminSetup: async (options?: AdminSetupOptions) => {
options = options || { onboarding: true };
@@ -187,7 +283,7 @@ export const apiUtils = {
userSetup: async (accessToken: string, dto: CreateUserDto) => {
await createUser(
{ createUserDto: dto },
{ headers: asBearerAuth(accessToken) }
{ headers: asBearerAuth(accessToken) },
);
return login({
loginCredentialDto: { email: dto.email, password: dto.password },
@@ -196,48 +292,74 @@ export const apiUtils = {
createApiKey: (accessToken: string) => {
return createApiKey(
{ apiKeyCreateDto: { name: 'e2e' } },
{ headers: asBearerAuth(accessToken) }
{ headers: asBearerAuth(accessToken) },
);
},
createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
createAlbum(
{ createAlbumDto: dto },
{ headers: asBearerAuth(accessToken) }
{ headers: asBearerAuth(accessToken) },
),
createAsset: async (
accessToken: string,
dto?: Omit<CreateAssetDto, 'assetData'>
dto?: Partial<Omit<CreateAssetDto, 'assetData'>>,
data?: {
bytes?: Buffer;
filename?: string;
},
) => {
dto = dto || {
const _dto = {
deviceAssetId: 'test-1',
deviceId: 'test',
fileCreatedAt: 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`)
.field('deviceAssetId', dto.deviceAssetId)
.field('deviceId', dto.deviceId)
.field('fileCreatedAt', dto.fileCreatedAt)
.field('fileModifiedAt', dto.fileModifiedAt)
.attach('assetData', randomBytes(32), 'example.jpg')
.attach('assetData', _assetData.bytes, _assetData.filename)
.set('Authorization', `Bearer ${accessToken}`);
return body as AssetResponseDto;
for (const [key, value] of Object.entries(_dto)) {
builder.field(key, String(value));
}
const { body } = await builder;
return body as AssetFileUploadResponseDto;
},
createPerson: async (accessToken: string, dto: PersonUpdateDto) => {
getAssetInfo: (accessToken: string, id: string) =>
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
const { id } = await createPerson({ headers: asBearerAuth(accessToken) });
await dbUtils.setPersonThumbnail(id);
let person = await createPerson({ headers: asBearerAuth(accessToken) });
await dbUtils.setPersonThumbnail(person.id);
if (!dto) {
return person;
}
return updatePerson(
{ id, personUpdateDto: dto },
{ headers: asBearerAuth(accessToken) }
{ id: person.id, personUpdateDto: dto },
{ headers: asBearerAuth(accessToken) },
);
},
createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
createSharedLink(
{ sharedLinkCreateDto: dto },
{ headers: asBearerAuth(accessToken) }
{ headers: asBearerAuth(accessToken) },
),
};

View File

@@ -15,6 +15,7 @@ test.describe('Shared Links', () => {
let asset: AssetResponseDto;
let album: AlbumResponseDto;
let sharedLink: SharedLinkResponseDto;
let sharedLinkPassword: SharedLinkResponseDto;
test.beforeAll(async () => {
apiUtils.setup();
@@ -29,17 +30,16 @@ test.describe('Shared Links', () => {
},
},
{ 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 () => {
@@ -55,4 +55,16 @@ test.describe('Shared Links', () => {
await page.getByRole('button', { name: 'Download' }).click();
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();
});
});

View File

@@ -165,6 +165,14 @@ class InferenceModel(ABC):
def providers_default(self) -> list[str]:
available_providers = set(ort.get_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]
@property
@@ -184,15 +192,7 @@ class InferenceModel(ABC):
case "CPUExecutionProvider" | "CUDAExecutionProvider":
option = {"arena_extend_strategy": "kSameAsRequested"}
case "OpenVINOExecutionProvider":
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 = {}
option = {"device_type": "GPU_FP32"}
case _:
option = {}
options.append(option)

View File

@@ -45,11 +45,23 @@ class TestBase:
assert encoder.providers == self.CUDA_EP
@pytest.mark.providers(OV_EP)
def test_sets_openvino_provider_if_available(self, providers: list[str]) -> None:
def test_sets_openvino_provider_if_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 = ["GPU.0", "CPU"]
encoder = OpenCLIPEncoder("ViT-B-32__openai")
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)
def test_sets_providers_in_correct_order(self, providers: list[str]) -> None:
encoder = OpenCLIPEncoder("ViT-B-32__openai")
@@ -68,22 +80,14 @@ class TestBase:
assert encoder.providers == providers
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:
def test_sets_default_provider_options(self, mocker: MockerFixture) -> 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", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"])
assert encoder.provider_options == [
{"device_id": "GPU.0"},
{"device_type": "GPU_FP32"},
{"arena_extend_strategy": "kSameAsRequested"},
]

View File

@@ -1,4 +1,4 @@
FROM mambaorg/micromamba:bookworm-slim@sha256:926cac38640709f90f3fef2a3f730733b5c350be612f0d14706be8833b79ad8c as builder
FROM mambaorg/micromamba:bookworm-slim@sha256:6038b89363c9181215f3d9e8ce2720c880e224537f4028a854482e43a9b4998a as builder
ENV NODE_ENV=production \
TRANSFORMERS_CACHE=/cache \

View File

@@ -1250,13 +1250,13 @@ test = ["Cython (>=0.29.24,<0.30.0)"]
[[package]]
name = "httpx"
version = "0.26.0"
version = "0.27.0"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
files = [
{file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"},
{file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"},
{file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
{file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
]
[package.dependencies]
@@ -2101,61 +2101,61 @@ numpy = [
[[package]]
name = "orjson"
version = "3.9.14"
version = "3.9.15"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.8"
files = [
{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.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bc7928d161840096adc956703494b5c0193ede887346f028216cac0af87500"},
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58b36f54da759602d8e2f7dad958752d453dfe2c7122767bc7f765e17dc59959"},
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:abcda41ecdc950399c05eff761c3de91485d9a70d8227cb599ad3a66afe93bcc"},
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df76ecd17b1b3627bddfd689faaf206380a1a38cc9f6c4075bd884eaedcf46c2"},
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d450a8e0656efb5d0fcb062157b918ab02dcca73278975b4ee9ea49e2fcf5bd5"},
{file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:95c03137b0cf66517c8baa65770507a756d3a89489d8ecf864ea92348e1beabe"},
{file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20837e10835c98973673406d6798e10f821e7744520633811a5a3d809762d8cc"},
{file = "orjson-3.9.14-cp310-none-win32.whl", hash = "sha256:1f7b6f3ef10ae8e3558abb729873d033dbb5843507c66b1c0767e32502ba96bb"},
{file = "orjson-3.9.14-cp310-none-win_amd64.whl", hash = "sha256:ea890e6dc1711aeec0a33b8520e395c2f3d59ead5b4351a788e06bf95fc7ba81"},
{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.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19cdea0664aec0b7f385be84986d4defd3334e9c3c799407686ee1c26f7b8251"},
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:135d518f73787ce323b1a5e21fb854fe22258d7a8ae562b81a49d6c7f826f2a3"},
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2cf1d0557c61c75e18cf7d69fb689b77896e95553e212c0cc64cf2087944b84"},
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7c11667421df2d8b18b021223505dcc3ee51be518d54e4dc49161ac88ac2b87"},
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eefc41ba42e75ed88bc396d8fe997beb20477f3e7efa000cd7a47eda452fbb2"},
{file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:917311d6a64d1c327c0dfda1e41f3966a7fb72b11ca7aa2e7a68fcccc7db35d9"},
{file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4dc1c132259b38d12c6587d190cd09cd76e3b5273ce71fe1372437b4cbc65f6f"},
{file = "orjson-3.9.14-cp311-none-win32.whl", hash = "sha256:6f39a10408478f4c05736a74da63727a1ae0e83e3533d07b19443400fe8591ca"},
{file = "orjson-3.9.14-cp311-none-win_amd64.whl", hash = "sha256:26280a7fcb62d8257f634c16acebc3bec626454f9ab13558bbf7883b9140760e"},
{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.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2591faa0c031cf3f57e5bce1461cfbd6160f3f66b5a72609a130924917cb07d"},
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2450d87dd7b4f277f4c5598faa8b49a0c197b91186c47a2c0b88e15531e4e3e"},
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90903d2908158a2c9077a06f11e27545de610af690fb178fd3ba6b32492d4d1c"},
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce6f095eef0026eae76fc212f20f786011ecf482fc7df2f4c272a8ae6dd7b1ef"},
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:751250a31fef2bac05a2da2449aae7142075ea26139271f169af60456d8ad27a"},
{file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a1af21160a38ee8be3f4fcf24ee4b99e6184cadc7f915d599f073f478a94d2c"},
{file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:449bf090b2aa4e019371d7511a6ea8a5a248139205c27d1834bb4b1e3c44d936"},
{file = "orjson-3.9.14-cp312-none-win_amd64.whl", hash = "sha256:a603161318ff699784943e71f53899983b7dee571b4dd07c336437c9c5a272b0"},
{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.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88cafb100af68af3b9b29b5ccd09fdf7a48c63327916c8c923a94c336d38dd3"},
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba3518b999f88882ade6686f1b71e207b52e23546e180499be5bbb63a2f9c6e6"},
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978f416bbff9da8d2091e3cf011c92da68b13f2c453dcc2e8109099b2a19d234"},
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75fc593cf836f631153d0e21beaeb8d26e144445c73645889335c2247fcd71a0"},
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d1528db3c7554f9d6eeb09df23cb80dd5177ec56eeb55cc5318826928de506"},
{file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:7183cc68ee2113b19b0b8714221e5e3b07b3ba10ca2bb108d78fd49cefaae101"},
{file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df3266d54246cb56b8bb17fa908660d2a0f2e3f63fbc32451ffc1b1505051d07"},
{file = "orjson-3.9.14-cp38-none-win32.whl", hash = "sha256:7913079b029e1b3501854c9a78ad938ed40d61fe09bebab3c93e60ff1301b189"},
{file = "orjson-3.9.14-cp38-none-win_amd64.whl", hash = "sha256:29512eb925b620e5da2fd7585814485c67cc6ba4fe739a0a700c50467a8a8065"},
{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.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac650d49366fa41fe702e054cb560171a8634e2865537e91f09a8d05ea5b1d37"},
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:236230433a9a4968ab895140514c308fdf9f607cb8bee178a04372b771123860"},
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3014ccbda9be0b1b5f8ea895121df7e6524496b3908f4397ff02e923bcd8f6dd"},
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac0c7eae7ad3a223bde690565442f8a3d620056bd01196f191af8be58a5248e1"},
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fca33fdd0b38839b01912c57546d4f412ba7bfa0faf9bf7453432219aec2df07"},
{file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f75823cc1674a840a151e999a7dfa0d86c911150dd6f951d0736ee9d383bf415"},
{file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f52ac2eb49e99e7373f62e2a68428c6946cda52ce89aa8fe9f890c7278e2d3a"},
{file = "orjson-3.9.14-cp39-none-win32.whl", hash = "sha256:0572f174f50b673b7df78680fb52cd0087a8585a6d06d295a5f790568e1064c6"},
{file = "orjson-3.9.14-cp39-none-win_amd64.whl", hash = "sha256:ab90c02cb264250b8a58cedcc72ed78a4a257d956c8d3c8bebe9751b818dfad8"},
{file = "orjson-3.9.14.tar.gz", hash = "sha256:06fb40f8e49088ecaa02f1162581d39e2cf3fd9dbbfe411eb2284147c99bad79"},
{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.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4feeb41882e8aa17634b589533baafdceb387e01e117b1ec65534ec724023d04"},
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fbbeb3c9b2edb5fd044b2a070f127a0ac456ffd079cb82746fc84af01ef021a4"},
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b66bcc5670e8a6b78f0313bcb74774c8291f6f8aeef10fe70e910b8040f3ab75"},
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2973474811db7b35c30248d1129c64fd2bdf40d57d84beed2a9a379a6f57d0ab"},
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fe41b6f72f52d3da4db524c8653e46243c8c92df826ab5ffaece2dba9cccd58"},
{file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4228aace81781cc9d05a3ec3a6d2673a1ad0d8725b4e915f1089803e9efd2b99"},
{file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f7b65bfaf69493c73423ce9db66cfe9138b2f9ef62897486417a8fcb0a92bfe"},
{file = "orjson-3.9.15-cp310-none-win32.whl", hash = "sha256:2d99e3c4c13a7b0fb3792cc04c2829c9db07838fb6973e578b85c1745e7d0ce7"},
{file = "orjson-3.9.15-cp310-none-win_amd64.whl", hash = "sha256:b725da33e6e58e4a5d27958568484aa766e825e93aa20c26c91168be58e08cbb"},
{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.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87f1097acb569dde17f246faa268759a71a2cb8c96dd392cd25c668b104cad2f"},
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff0f9913d82e1d1fadbd976424c316fbc4d9c525c81d047bbdd16bd27dd98cfc"},
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8055ec598605b0077e29652ccfe9372247474375e0e3f5775c91d9434e12d6b1"},
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6768a327ea1ba44c9114dba5fdda4a214bdb70129065cd0807eb5f010bfcbb5"},
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12365576039b1a5a47df01aadb353b68223da413e2e7f98c02403061aad34bde"},
{file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:71c6b009d431b3839d7c14c3af86788b3cfac41e969e3e1c22f8a6ea13139404"},
{file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e18668f1bd39e69b7fed19fa7cd1cd110a121ec25439328b5c89934e6d30d357"},
{file = "orjson-3.9.15-cp311-none-win32.whl", hash = "sha256:62482873e0289cf7313461009bf62ac8b2e54bc6f00c6fabcde785709231a5d7"},
{file = "orjson-3.9.15-cp311-none-win_amd64.whl", hash = "sha256:b3d336ed75d17c7b1af233a6561cf421dee41d9204aa3cfcc6c9c65cd5bb69a8"},
{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.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c51378d4a8255b2e7c1e5cc430644f0939539deddfa77f6fac7b56a9784160a"},
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae4e06be04dc00618247c4ae3f7c3e561d5bc19ab6941427f6d3722a0875ef7"},
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcef128f970bb63ecf9a65f7beafd9b55e3aaf0efc271a4154050fc15cdb386e"},
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b72758f3ffc36ca566ba98a8e7f4f373b6c17c646ff8ad9b21ad10c29186f00d"},
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c57bc7b946cf2efa67ac55766e41764b66d40cbd9489041e637c1304400494"},
{file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:946c3a1ef25338e78107fba746f299f926db408d34553b4754e90a7de1d44068"},
{file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2f256d03957075fcb5923410058982aea85455d035607486ccb847f095442bda"},
{file = "orjson-3.9.15-cp312-none-win_amd64.whl", hash = "sha256:5bb399e1b49db120653a31463b4a7b27cf2fbfe60469546baf681d1b39f4edf2"},
{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.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f6cbd8e6e446fb7e4ed5bac4661a29e43f38aeecbf60c4b900b825a353276a1"},
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76bc6356d07c1d9f4b782813094d0caf1703b729d876ab6a676f3aaa9a47e37c"},
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdfa97090e2d6f73dced247a2f2d8004ac6449df6568f30e7fa1a045767c69a6"},
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7413070a3e927e4207d00bd65f42d1b780fb0d32d7b1d951f6dc6ade318e1b5a"},
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cf1596680ac1f01839dba32d496136bdd5d8ffb858c280fa82bbfeb173bdd40"},
{file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:809d653c155e2cc4fd39ad69c08fdff7f4016c355ae4b88905219d3579e31eb7"},
{file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:920fa5a0c5175ab14b9c78f6f820b75804fb4984423ee4c4f1e6d748f8b22bc1"},
{file = "orjson-3.9.15-cp38-none-win32.whl", hash = "sha256:2b5c0f532905e60cf22a511120e3719b85d9c25d0e1c2a8abb20c4dede3b05a5"},
{file = "orjson-3.9.15-cp38-none-win_amd64.whl", hash = "sha256:67384f588f7f8daf040114337d34a5188346e3fae6c38b6a19a2fe8c663a2f9b"},
{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.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34cbcd216e7af5270f2ffa63a963346845eb71e174ea530867b7443892d77180"},
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f541587f5c558abd93cb0de491ce99a9ef8d1ae29dd6ab4dbb5a13281ae04cbd"},
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92255879280ef9c3c0bcb327c5a1b8ed694c290d61a6a532458264f887f052cb"},
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05a1f57fb601c426635fcae9ddbe90dfc1ed42245eb4c75e4960440cac667262"},
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ede0bde16cc6e9b96633df1631fbcd66491d1063667f260a4f2386a098393790"},
{file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e88b97ef13910e5f87bcbc4dd7979a7de9ba8702b54d3204ac587e83639c0c2b"},
{file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57d5d8cf9c27f7ef6bc56a5925c7fbc76b61288ab674eb352c26ac780caa5b10"},
{file = "orjson-3.9.15-cp39-none-win32.whl", hash = "sha256:001f4eb0ecd8e9ebd295722d0cbedf0748680fb9998d3993abaed2f40587257a"},
{file = "orjson-3.9.15-cp39-none-win_amd64.whl", hash = "sha256:ea0b183a5fe6b2b45f3b854b0d19c4e932d6f5934ae1f723b07cf9560edd4ec7"},
{file = "orjson-3.9.15.tar.gz", hash = "sha256:95cae920959d772f30ab36d3b25f83bb0f3be671e986c72ce22f8fa700dae061"},
]
[[package]]
@@ -2465,13 +2465,13 @@ files = [
[[package]]
name = "pytest"
version = "8.0.0"
version = "8.0.2"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"},
{file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"},
{file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"},
{file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"},
]
[package.dependencies]
@@ -2836,28 +2836,28 @@ files = [
[[package]]
name = "ruff"
version = "0.2.1"
version = "0.2.2"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{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.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"},
{file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"},
{file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"},
{file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"},
{file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"},
{file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"},
{file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"},
{file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"},
{file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"},
{file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"},
{file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"},
{file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"},
{file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"},
{file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"},
{file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"},
{file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"},
{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.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"},
{file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"},
{file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"},
{file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"},
{file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"},
{file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"},
{file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"},
{file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"},
{file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"},
]
[[package]]

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.95.1"
version = "1.97.0"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 123,
"android.injected.version.name" => "1.95.1",
"android.injected.version.code" => 125,
"android.injected.version.name" => "1.97.0",
}
)
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')

View File

@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000232">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000271">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="78.881681">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="74.334294">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="32.080999">
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="29.507669">
</testcase>

View File

@@ -379,7 +379,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 139;
CURRENT_PROJECT_VERSION = 140;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -515,7 +515,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 139;
CURRENT_PROJECT_VERSION = 140;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -543,7 +543,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 139;
CURRENT_PROJECT_VERSION = 140;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -55,11 +55,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.95.0</string>
<string>1.96.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>139</string>
<string>140</string>
<key>FLTEnableImpeller</key>
<true />
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.95.1"
version_number: "1.97.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -5,32 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000255">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000316">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.157832">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.190055">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.825919">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.109364">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.18815">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.15926">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="110.912709">
<testcase classname="fastlane.lanes" name="4: build_app" time="80.90681">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="78.396901">
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="71.634559">
</testcase>

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:ui' as ui;
@@ -132,7 +133,7 @@ class GalleryViewerPage extends HookConsumerWidget {
void toggleFavorite(Asset asset) =>
ref.read(assetProvider.notifier).toggleFavorite([asset]);
void precacheNextImage(int index) {
Future<void> precacheNextImage(int index) async {
void onError(Object exception, StackTrace? stackTrace) {
// swallow error silently
debugPrint('Error precaching next image: $exception, $stackTrace');
@@ -140,7 +141,7 @@ class GalleryViewerPage extends HookConsumerWidget {
if (index < totalAssets && index >= 0) {
final asset = loadAsset(index);
precacheImage(
await precacheImage(
ImmichImage.imageProvider(asset: asset),
context,
onError: onError,
@@ -711,6 +712,21 @@ 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) {
if (show) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
@@ -735,14 +751,21 @@ class GalleryViewerPage extends HookConsumerWidget {
isZoomed.value = state != PhotoViewScaleState.initial;
ref.read(showControlsProvider.notifier).show = !isZoomed.value;
},
loadingBuilder: (context, event, index) => ImageFiltered(
imageFilter: ui.ImageFilter.blur(
sigmaX: 1,
sigmaY: 1,
),
child: ImmichThumbnail(
asset: asset(),
fit: BoxFit.contain,
loadingBuilder: (context, event, index) => ClipRect(
child: Stack(
fit: StackFit.expand,
children: [
BackdropFilter(
filter: ui.ImageFilter.blur(
sigmaX: 10,
sigmaY: 10,
),
),
ImmichThumbnail(
asset: asset(),
fit: BoxFit.contain,
),
],
),
),
pageController: controller,
@@ -754,12 +777,16 @@ class GalleryViewerPage extends HookConsumerWidget {
),
itemCount: totalAssets,
scrollDirection: Axis.horizontal,
onPageChanged: (value) {
onPageChanged: (value) async {
final next = currentIndex.value < value ? value + 1 : value - 1;
precacheNextImage(next);
HapticFeedback.selectionClick();
currentIndex.value = value;
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) {
final a =
@@ -818,7 +845,7 @@ class GalleryViewerPage extends HookConsumerWidget {
isMotionVideo: isPlayingMotionVideo.value,
placeholder: Image(
image: provider,
fit: BoxFit.fitWidth,
fit: BoxFit.contain,
height: context.height,
width: context.width,
alignment: Alignment.center,

View File

@@ -59,8 +59,12 @@ class VideoViewerPage extends HookWidget {
return Stack(
children: [
if (placeholder != null) placeholder!,
const DelayedLoadingIndicator(
fadeInDuration: Duration(milliseconds: 500),
const Positioned.fill(
child: Center(
child: DelayedLoadingIndicator(
fadeInDuration: Duration(milliseconds: 500),
),
),
),
],
);

View File

@@ -1,60 +0,0 @@
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,
),
);
}
}

View File

@@ -1,5 +1,4 @@
import 'dart:collection';
import 'dart:convert';
import 'dart:developer';
import 'dart:math';
@@ -7,15 +6,12 @@ import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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/collection_extensions.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_placeholder.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 'asset_grid_data_structure.dart';
@@ -329,33 +325,32 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
}
/// A single row of all placeholder widgets
class _PlaceholderRow extends HookWidget {
class _PlaceholderRow extends StatelessWidget {
final int number;
final double width;
final double height;
final double margin;
final List<Asset> assets;
const _PlaceholderRow({
super.key,
required this.number,
required this.width,
required this.height,
required this.margin,
required this.assets,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
for (int i = 0; i < assets.length; i++)
BlurhashThumb(
for (int i = 0; i < number; i++)
ThumbnailPlaceholder(
key: ValueKey(i),
asset: assets[i],
width: width,
height: height,
margin: EdgeInsets.only(
bottom: margin,
right: i + 1 == assets.length ? 0.0 : margin,
right: i + 1 == number ? 0.0 : margin,
),
),
],
@@ -406,9 +401,9 @@ class _Section extends StatelessWidget {
final width = constraints.maxWidth / assetsPerRow -
margin * (assetsPerRow - 1) / assetsPerRow;
final rows = (section.count + assetsPerRow - 1) ~/ assetsPerRow;
final List<Asset> assetsToRender = //scrolling
//? []
renderList.loadAssets(section.offset, section.count);
final List<Asset> assetsToRender = scrolling
? []
: renderList.loadAssets(section.offset, section.count);
return Column(
key: ValueKey(section.offset),
crossAxisAlignment: CrossAxisAlignment.start,
@@ -427,22 +422,18 @@ class _Section extends StatelessWidget {
selectAssets: selectAssets,
deselectAssets: deselectAssets,
),
Stack(
children: [
for (int i = 0; i < rows; i++)
_PlaceholderRow(
key: ValueKey('placeholder-$i'),
assets: assetsToRender.nestedSlice(
i * assetsPerRow,
min((i + 1) * assetsPerRow, section.count),
),
width: width,
height: width,
margin: margin,
),
if (!scrolling)
for (int i = 0; i < rows; i++)
_AssetRow(
for (int i = 0; i < rows; i++)
scrolling
? _PlaceholderRow(
key: ValueKey(i),
number: i + 1 == rows
? section.count - i * assetsPerRow
: assetsPerRow,
width: width,
height: width,
margin: margin,
)
: _AssetRow(
key: ValueKey(i),
assets: assetsToRender.nestedSlice(
i * assetsPerRow,
@@ -463,8 +454,6 @@ class _Section extends StatelessWidget {
onSelect: (asset) => selectAssets([asset]),
onDeselect: (asset) => deselectAssets([asset]),
),
],
),
],
);
},

View File

@@ -1,12 +1,12 @@
import 'dart:ui';
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/modules/asset_viewer/views/video_viewer_page.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.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/immich_thumbnail.dart';
class MemoryCard extends StatelessWidget {
final Asset asset;
@@ -22,8 +22,6 @@ class MemoryCard extends StatelessWidget {
super.key,
});
String get accessToken => Store.get(StoreKey.accessToken);
@override
Widget build(BuildContext context) {
return Card(
@@ -38,19 +36,8 @@ class MemoryCard extends StatelessWidget {
clipBehavior: Clip.hardEdge,
child: Stack(
children: [
ImageFiltered(
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)),
),
SizedBox.expand(
child: _BlurredBackdrop(asset: asset),
),
LayoutBuilder(
builder: (context, constraints) {
@@ -113,3 +100,50 @@ 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),
),
),
);
}
}
}

View File

@@ -10,7 +10,6 @@ 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/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
@RoutePage()
class MemoryPage extends HookConsumerWidget {
@@ -110,24 +109,13 @@ class MemoryPage extends HookConsumerWidget {
asset = memories[nextMemoryIndex].assets.first;
}
// Gets the thumbnail url and precaches it
final precaches = <Future<dynamic>>[];
precaches.addAll([
precacheImage(
ImmichImage.imageProvider(
asset: asset,
),
context,
// Precache the asset
await precacheImage(
ImmichImage.imageProvider(
asset: asset,
),
precacheImage(
ImmichThumbnail.imageProvider(
asset: asset,
),
context,
),
]);
await Future.wait(precaches);
context,
);
}
// Precache the next page right away if we are on the first page
@@ -136,11 +124,14 @@ class MemoryPage extends HookConsumerWidget {
.then((_) => precacheAsset(1));
}
onAssetChanged(int otherIndex) {
Future<void> onAssetChanged(int otherIndex) async {
HapticFeedback.selectionClick();
currentAssetPage.value = otherIndex;
precacheAsset(otherIndex + 1);
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

View File

@@ -20,21 +20,24 @@ class DelayedLoadingIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
duration: fadeInDuration ?? Duration.zero,
child: FutureBuilder(
future: Future.delayed(delay),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return child ??
const ImmichLoadingIndicator(
key: ValueKey('loading'),
);
}
return FutureBuilder(
future: Future.delayed(delay),
builder: (context, snapshot) {
late Widget c;
if (snapshot.connectionState == ConnectionState.done) {
c = child ??
const ImmichLoadingIndicator(
key: ValueKey('loading'),
);
} else {
c = Container(key: const ValueKey('hiding'));
}
return Container(key: const ValueKey('hiding'));
},
),
return AnimatedSwitcher(
duration: fadeInDuration ?? Duration.zero,
child: c,
);
},
);
}
}

View File

@@ -58,9 +58,11 @@ class ImmichImage extends StatelessWidget {
}
}
// Whether to use the local asset image provider or a remote one
static bool useLocal(Asset asset) =>
!asset.isRemote ||
asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false);
@override
Widget build(BuildContext context) {
if (asset == null) {

View File

@@ -6,6 +6,7 @@ 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/shared/models/asset.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:octo_image/octo_image.dart';
@@ -43,7 +44,7 @@ class ImmichThumbnail extends HookWidget {
);
}
if (useLocal(asset)) {
if (ImmichImage.useLocal(asset)) {
return ImmichLocalThumbnailProvider(
asset: asset,
height: thumbnailSize,
@@ -57,8 +58,6 @@ class ImmichThumbnail extends HookWidget {
}
}
static bool useLocal(Asset asset) => !asset.isRemote || asset.isLocal;
@override
Widget build(BuildContext context) {
Uint8List? blurhash = useBlurHashRef(asset).value;

View File

@@ -23,7 +23,8 @@ OctoPlaceholderBuilder blurHashPlaceholderBuilder(
}) {
return (context) => blurhash == null
? const ThumbnailPlaceholder()
: Image(
: FadeInPlaceholderImage(
placeholder: const ThumbnailPlaceholder(),
image: MemoryImage(blurhash),
fit: fit ?? BoxFit.cover,
);

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.95.1
- API version: 1.97.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements

View File

@@ -9,8 +9,6 @@ import 'package:openapi/api.dart';
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**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)

View File

@@ -14,37 +14,25 @@ class SystemConfigLibraryWatchDto {
/// Returns a new [SystemConfigLibraryWatchDto] instance.
SystemConfigLibraryWatchDto({
required this.enabled,
required this.interval,
required this.usePolling,
});
bool enabled;
int interval;
bool usePolling;
@override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigLibraryWatchDto &&
other.enabled == enabled &&
other.interval == interval &&
other.usePolling == usePolling;
other.enabled == enabled;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(enabled.hashCode) +
(interval.hashCode) +
(usePolling.hashCode);
(enabled.hashCode);
@override
String toString() => 'SystemConfigLibraryWatchDto[enabled=$enabled, interval=$interval, usePolling=$usePolling]';
String toString() => 'SystemConfigLibraryWatchDto[enabled=$enabled]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'enabled'] = this.enabled;
json[r'interval'] = this.interval;
json[r'usePolling'] = this.usePolling;
return json;
}
@@ -57,8 +45,6 @@ class SystemConfigLibraryWatchDto {
return SystemConfigLibraryWatchDto(
enabled: mapValueOfType<bool>(json, r'enabled')!,
interval: mapValueOfType<int>(json, r'interval')!,
usePolling: mapValueOfType<bool>(json, r'usePolling')!,
);
}
return null;
@@ -107,8 +93,6 @@ class SystemConfigLibraryWatchDto {
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'enabled',
'interval',
'usePolling',
};
}

View File

@@ -21,16 +21,6 @@ void main() {
// TODO
});
// int interval
test('to test the property `interval`', () async {
// TODO
});
// bool usePolling
test('to test the property `usePolling`', () async {
// TODO
});
});

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.95.1+123
version: 1.97.0+125
isar_version: &isar_version 3.1.0+1
environment:

View File

@@ -6458,7 +6458,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.95.1",
"version": "1.97.0",
"contact": {}
},
"tags": [],
@@ -9831,18 +9831,10 @@
"properties": {
"enabled": {
"type": "boolean"
},
"interval": {
"type": "integer"
},
"usePolling": {
"type": "boolean"
}
},
"required": [
"enabled",
"interval",
"usePolling"
"enabled"
],
"type": "object"
},

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.95.1
* The version of the OpenAPI document: 1.97.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@@ -4401,18 +4401,6 @@ export interface SystemConfigLibraryWatchDto {
* @memberof SystemConfigLibraryWatchDto
*/
'enabled': boolean;
/**
*
* @type {number}
* @memberof SystemConfigLibraryWatchDto
*/
'interval': number;
/**
*
* @type {boolean}
* @memberof SystemConfigLibraryWatchDto
*/
'usePolling': boolean;
}
/**
*

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.95.1
* The version of the OpenAPI document: 1.97.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.95.1
* The version of the OpenAPI document: 1.97.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.95.1
* The version of the OpenAPI document: 1.97.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.95.1
* The version of the OpenAPI document: 1.97.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 1.95.1
* 1.97.0
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/
@@ -835,8 +835,6 @@ export type SystemConfigLibraryScanDto = {
};
export type SystemConfigLibraryWatchDto = {
enabled: boolean;
interval: number;
usePolling: boolean;
};
export type SystemConfigLibraryDto = {
scan: SystemConfigLibraryScanDto;

View File

@@ -23,15 +23,15 @@
}
},
"node_modules/@oazapfts/runtime": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@oazapfts/runtime/-/runtime-1.0.0.tgz",
"integrity": "sha512-1ovqeaeEvShbYge5/7ctJokpvqB0anBdfDNfU5jWstjV2/Gbe+vvcBM274Z0abM3IM0b9MmSNWYBXnJXYO8KCw==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@oazapfts/runtime/-/runtime-1.0.1.tgz",
"integrity": "sha512-CMl7f1gXYpjIyEtDhg4YfXwr2MXfbadbvqwKbMsaHkVtSglmuz5A8jSyefTqaJlmh0MOA2ZNS9jnbfIdtcoDiw==",
"dev": true
},
"node_modules/@types/node": {
"version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"version": "20.11.20",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
"integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"

View File

@@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:20240222@sha256:2ff467d6ae5c00a2317eb7b13cb40ba5be0fd33c160175dba621b1bf72bc1cd1 as dev
FROM ghcr.io/immich-app/base-server-dev:20240227@sha256:b1e212c106ce2318a587e0b2ef377215c958e877f61993ed9310534e4589cce4 as dev
RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app
@@ -40,7 +40,7 @@ RUN npm run build
# prod build
FROM ghcr.io/immich-app/base-server-prod:20240222@sha256:9ae5eebf95cf7759eec9dcfbd9e48a722701075ac855209f2e0b01c631b76f5c
FROM ghcr.io/immich-app/base-server-prod:20240227@sha256:d47f5f7f2b6c53957c6353352b2fa24f2845da50e6491a7c74eb779ace10628c
WORKDIR /usr/src/app
ENV NODE_ENV=production \

View File

@@ -538,90 +538,6 @@ 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', () => {
it('should require authentication', async () => {
const { status, body } = await request(server)
@@ -759,286 +675,6 @@ 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', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get('/asset/time-buckets').query({ size: TimeBucketSize.MONTH });

View File

@@ -1,4 +1,4 @@
import { AssetBulkDeleteDto, AssetResponseDto } from '@app/domain';
import { AssetResponseDto } from '@app/domain';
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 { randomBytes } from 'node:crypto';
@@ -74,8 +74,4 @@ export const assetApi = {
expect(status).toBe(200);
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);
},
};

View File

@@ -1,158 +0,0 @@
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);
});
}
});

View File

@@ -1,102 +0,0 @@
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);
});
});
});

View File

@@ -1,80 +0,0 @@
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);
});
});

220
server/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.95.1",
"version": "1.97.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.95.1",
"version": "1.97.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@babel/runtime": "^7.22.11",
@@ -3179,9 +3179,9 @@
}
},
"node_modules/@types/node": {
"version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"version": "20.11.20",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
"integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -3281,9 +3281,9 @@
}
},
"node_modules/@types/semver": {
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
"integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true
},
"node_modules/@types/send": {
@@ -3398,16 +3398,16 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz",
"integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz",
"integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/type-utils": "7.0.1",
"@typescript-eslint/utils": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.1",
"@typescript-eslint/scope-manager": "7.0.2",
"@typescript-eslint/type-utils": "7.0.2",
"@typescript-eslint/utils": "7.0.2",
"@typescript-eslint/visitor-keys": "7.0.2",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@@ -3433,15 +3433,15 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz",
"integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz",
"integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/types": "7.0.1",
"@typescript-eslint/typescript-estree": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.1",
"@typescript-eslint/scope-manager": "7.0.2",
"@typescript-eslint/types": "7.0.2",
"@typescript-eslint/typescript-estree": "7.0.2",
"@typescript-eslint/visitor-keys": "7.0.2",
"debug": "^4.3.4"
},
"engines": {
@@ -3461,13 +3461,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz",
"integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz",
"integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.1"
"@typescript-eslint/types": "7.0.2",
"@typescript-eslint/visitor-keys": "7.0.2"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -3478,13 +3478,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz",
"integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz",
"integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/typescript-estree": "7.0.1",
"@typescript-eslint/utils": "7.0.1",
"@typescript-eslint/typescript-estree": "7.0.2",
"@typescript-eslint/utils": "7.0.2",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
},
@@ -3505,9 +3505,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz",
"integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz",
"integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==",
"dev": true,
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -3518,13 +3518,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz",
"integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz",
"integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.1",
"@typescript-eslint/types": "7.0.2",
"@typescript-eslint/visitor-keys": "7.0.2",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -3570,17 +3570,17 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz",
"integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz",
"integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/types": "7.0.1",
"@typescript-eslint/typescript-estree": "7.0.1",
"@typescript-eslint/scope-manager": "7.0.2",
"@typescript-eslint/types": "7.0.2",
"@typescript-eslint/typescript-estree": "7.0.2",
"semver": "^7.5.4"
},
"engines": {
@@ -3595,12 +3595,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz",
"integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz",
"integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.0.1",
"@typescript-eslint/types": "7.0.2",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
@@ -5504,9 +5504,9 @@
}
},
"node_modules/dotenv": {
"version": "16.4.4",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.4.tgz",
"integrity": "sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg==",
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"engines": {
"node": ">=12"
},
@@ -8139,9 +8139,9 @@
}
},
"node_modules/joi": {
"version": "17.12.1",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.12.1.tgz",
"integrity": "sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ==",
"version": "17.12.2",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz",
"integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==",
"dependencies": {
"@hapi/hoek": "^9.3.0",
"@hapi/topo": "^5.1.0",
@@ -14730,9 +14730,9 @@
}
},
"@types/node": {
"version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"version": "20.11.20",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
"integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
"requires": {
"undici-types": "~5.26.4"
}
@@ -14819,9 +14819,9 @@
}
},
"@types/semver": {
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
"integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true
},
"@types/send": {
@@ -14936,16 +14936,16 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz",
"integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz",
"integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==",
"dev": true,
"requires": {
"@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/type-utils": "7.0.1",
"@typescript-eslint/utils": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.1",
"@typescript-eslint/scope-manager": "7.0.2",
"@typescript-eslint/type-utils": "7.0.2",
"@typescript-eslint/utils": "7.0.2",
"@typescript-eslint/visitor-keys": "7.0.2",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@@ -14955,54 +14955,54 @@
}
},
"@typescript-eslint/parser": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz",
"integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz",
"integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==",
"dev": true,
"requires": {
"@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/types": "7.0.1",
"@typescript-eslint/typescript-estree": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.1",
"@typescript-eslint/scope-manager": "7.0.2",
"@typescript-eslint/types": "7.0.2",
"@typescript-eslint/typescript-estree": "7.0.2",
"@typescript-eslint/visitor-keys": "7.0.2",
"debug": "^4.3.4"
}
},
"@typescript-eslint/scope-manager": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz",
"integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz",
"integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==",
"dev": true,
"requires": {
"@typescript-eslint/types": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.1"
"@typescript-eslint/types": "7.0.2",
"@typescript-eslint/visitor-keys": "7.0.2"
}
},
"@typescript-eslint/type-utils": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz",
"integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz",
"integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==",
"dev": true,
"requires": {
"@typescript-eslint/typescript-estree": "7.0.1",
"@typescript-eslint/utils": "7.0.1",
"@typescript-eslint/typescript-estree": "7.0.2",
"@typescript-eslint/utils": "7.0.2",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
}
},
"@typescript-eslint/types": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz",
"integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz",
"integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz",
"integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz",
"integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==",
"dev": true,
"requires": {
"@typescript-eslint/types": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.1",
"@typescript-eslint/types": "7.0.2",
"@typescript-eslint/visitor-keys": "7.0.2",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -15032,27 +15032,27 @@
}
},
"@typescript-eslint/utils": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz",
"integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz",
"integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/types": "7.0.1",
"@typescript-eslint/typescript-estree": "7.0.1",
"@typescript-eslint/scope-manager": "7.0.2",
"@typescript-eslint/types": "7.0.2",
"@typescript-eslint/typescript-estree": "7.0.2",
"semver": "^7.5.4"
}
},
"@typescript-eslint/visitor-keys": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz",
"integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz",
"integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==",
"dev": true,
"requires": {
"@typescript-eslint/types": "7.0.1",
"@typescript-eslint/types": "7.0.2",
"eslint-visitor-keys": "^3.4.1"
}
},
@@ -16494,9 +16494,9 @@
}
},
"dotenv": {
"version": "16.4.4",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.4.tgz",
"integrity": "sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg=="
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg=="
},
"dotenv-expand": {
"version": "10.0.0",
@@ -18453,9 +18453,9 @@
}
},
"joi": {
"version": "17.12.1",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.12.1.tgz",
"integrity": "sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ==",
"version": "17.12.2",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz",
"integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==",
"requires": {
"@hapi/hoek": "^9.3.0",
"@hapi/topo": "^5.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.95.1",
"version": "1.97.0",
"description": "",
"author": "",
"private": true,

View File

@@ -112,20 +112,13 @@ export class LibraryService extends EventEmitter {
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;
const ready$ = new Promise<void>((resolve) => (_resolve = resolve));
this.watchers[id] = this.storageRepository.watch(
library.importPaths,
{
usePolling,
interval,
binaryInterval: interval,
usePolling: false,
ignoreInitial: true,
},
{

View File

@@ -1799,9 +1799,9 @@ describe(MediaService.name, () => {
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
{
inputOptions: [],
inputOptions: ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga'],
outputOptions: [
`-c:v hevc_rkmpp_encoder`,
`-c:v hevc_rkmpp`,
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
@@ -1810,17 +1810,12 @@ describe(MediaService.name, () => {
'-g 256',
'-tag:v hvc1',
'-v verbose',
'-vf scale_rkrga=-2:720:format=nv12:afbc=1',
'-level 153',
'-rc_mode 3',
'-quality_min 0',
'-quality_max 100',
'-rc_mode AVBR',
'-b:v 10000k',
'-width 1280',
'-height 720',
],
twoPass: false,
ffmpegPath: 'ffmpeg_mpp',
ldLibraryPath: '/lib/aarch64-linux-gnu:/lib/ffmpeg-mpp',
},
);
});
@@ -1839,9 +1834,9 @@ describe(MediaService.name, () => {
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
{
inputOptions: [],
inputOptions: ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga'],
outputOptions: [
`-c:v h264_rkmpp_encoder`,
`-c:v h264_rkmpp`,
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
@@ -1849,16 +1844,12 @@ describe(MediaService.name, () => {
'-map 0:1',
'-g 256',
'-v verbose',
'-vf scale_rkrga=-2:720:format=nv12:afbc=1',
'-level 51',
'-rc_mode 2',
'-quality_min 51',
'-quality_max 51',
'-width 1280',
'-height 720',
'-rc_mode CQP',
'-qp_init 30',
],
twoPass: false,
ffmpegPath: 'ffmpeg_mpp',
ldLibraryPath: '/lib/aarch64-linux-gnu:/lib/ffmpeg-mpp',
},
);
});

View File

@@ -14,7 +14,7 @@ class BaseConfig implements VideoCodecSWConfig {
getOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
const options = {
inputOptions: this.getBaseInputOptions(),
inputOptions: this.getBaseInputOptions(videoStream),
outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v verbose'],
twoPass: this.eligibleForTwoPass(),
} as TranscodeOptions;
@@ -30,7 +30,8 @@ class BaseConfig implements VideoCodecSWConfig {
return options;
}
getBaseInputOptions(): string[] {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getBaseInputOptions(videoStream: VideoStreamInfo): string[] {
return [];
}
@@ -607,35 +608,28 @@ export class VAAPIConfig 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 {
return false;
}
getBaseInputOptions() {
getBaseInputOptions(videoStream: VideoStreamInfo) {
if (this.devices.length === 0) {
throw new Error('No RKMPP device found');
}
return [];
if (this.shouldToneMap(videoStream)) {
// disable hardware decoding
return [];
}
return ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga'];
}
getFilterOptions(videoStream: VideoStreamInfo) {
return this.shouldToneMap(videoStream) ? this.getToneMapping() : [];
}
getSizeOptions(videoStream: VideoStreamInfo) {
if (this.shouldToneMap(videoStream)) {
// use software filter options
return super.getFilterOptions(videoStream);
}
if (this.shouldScale(videoStream)) {
const { width, height } = this.getSize(videoStream);
return [`-width ${width}`, `-height ${height}`];
return [`scale_rkrga=${this.getScaling(videoStream)}:format=nv12:afbc=1`];
}
return [];
}
@@ -659,12 +653,11 @@ export class RKMPPConfig extends BaseHWConfig {
getBitrateOptions() {
const bitrate = this.getMaxBitrateValue();
if (bitrate > 0) {
return ['-rc_mode 3', '-quality_min 0', '-quality_max 100', `-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}`];
// -b:v specifies max bitrate, average bitrate is derived automatically...
return ['-rc_mode AVBR', `-b:v ${bitrate}${this.getBitrateUnit()}`];
}
// use CRF value as QP value
return ['-rc_mode CQP', `-qp_init ${this.config.crf}`];
}
getSupportedCodecs() {
@@ -672,6 +665,6 @@ export class RKMPPConfig extends BaseHWConfig {
}
getVideoCodec(): string {
return `${this.config.targetVideoCodec}_rkmpp_encoder`;
return `${this.config.targetVideoCodec}_rkmpp`;
}
}

View File

@@ -51,8 +51,6 @@ export interface TranscodeOptions {
inputOptions: string[];
outputOptions: string[];
twoPass: boolean;
ffmpegPath?: string;
ldLibraryPath?: string;
}
export interface BitrateDistribution {

View File

@@ -187,4 +187,5 @@ export interface ISearchRepository {
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
upsert(smartInfo: Partial<SmartInfoEntity>, embedding?: Embedding): Promise<void>;
searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]>;
deleteAllSearchEmbeddings(): Promise<void>;
}

View File

@@ -71,6 +71,7 @@ describe(SmartInfoService.name, () => {
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(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
});
it('should queue all the assets', async () => {
@@ -83,6 +84,7 @@ describe(SmartInfoService.name, () => {
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]);
expect(assetMock.getAll).toHaveBeenCalled();
expect(searchMock.deleteAllSearchEmbeddings).toHaveBeenCalled();
});
});

View File

@@ -50,6 +50,10 @@ export class SmartInfoService {
return true;
}
if (force) {
await this.repository.deleteAllSearchEmbeddings();
}
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force
? this.assetRepository.getAll(pagination)

View File

@@ -1,12 +1,9 @@
import { validateCronExpression } from '@app/domain';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsBoolean,
IsInt,
IsNotEmpty,
IsObject,
IsPositive,
IsString,
Validate,
ValidateIf,
@@ -38,14 +35,6 @@ export class SystemConfigLibraryScanDto {
export class SystemConfigLibraryWatchDto {
@IsBoolean()
enabled!: boolean;
@IsBoolean()
usePolling!: boolean;
@IsInt()
@IsPositive()
@ApiProperty({ type: 'integer' })
interval!: number;
}
export class SystemConfigLibraryDto {

View File

@@ -132,8 +132,6 @@ export const defaults = Object.freeze<SystemConfig>({
},
watch: {
enabled: false,
usePolling: false,
interval: 10_000,
},
},
server: {

View File

@@ -136,8 +136,6 @@ const updatedConfig = Object.freeze<SystemConfig>({
},
watch: {
enabled: false,
usePolling: false,
interval: 10_000,
},
},
});

View File

@@ -51,8 +51,6 @@ export enum SystemConfigKey {
LIBRARY_SCAN_CRON_EXPRESSION = 'library.scan.cronExpression',
LIBRARY_WATCH_ENABLED = 'library.watch.enabled',
LIBRARY_WATCH_USE_POLLING = 'library.watch.usePolling',
LIBRARY_WATCH_INTERVAL = 'library.watch.interval',
LOGGING_ENABLED = 'logging.enabled',
LOGGING_LEVEL = 'logging.level',
@@ -268,8 +266,6 @@ export interface SystemConfig {
};
watch: {
enabled: boolean;
usePolling: boolean;
interval: number;
};
};
server: {

View File

@@ -0,0 +1,12 @@
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
}
}

View File

@@ -76,18 +76,7 @@ export class MediaRepository implements IMediaRepository {
transcode(input: string, output: string | Writable, options: TranscodeOptions): Promise<void> {
if (!options.twoPass) {
return new Promise((resolve, reject) => {
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;
}
}
this.configureFfmpegCall(input, output, options).on('error', reject).on('end', resolve).run();
});
}
@@ -121,7 +110,6 @@ export class MediaRepository implements IMediaRepository {
configureFfmpegCall(input: string, output: string | Writable, options: TranscodeOptions) {
return ffmpeg(input, { niceness: 10 })
.setFfmpegPath(options.ffmpegPath || 'ffmpeg')
.inputOptions(options.inputOptions)
.outputOptions(options.outputOptions)
.output(output)

View File

@@ -40,11 +40,11 @@ export class PersonRepository implements IPersonRepository {
}
async deleteAll(): Promise<void> {
await this.personRepository.delete({});
await this.personRepository.clear();
}
async deleteAllFaces(): Promise<void> {
await this.assetFaceRepository.delete({});
await this.assetFaceRepository.query('TRUNCATE TABLE asset_faces CASCADE');
}
getAllFaces(

View File

@@ -229,25 +229,17 @@ export class SearchRepository implements ISearchRepository {
this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`);
await this.smartSearchRepository.manager.transaction(async (manager) => {
await manager.query(`DROP TABLE smart_search`);
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
$$)`);
await manager.clear(SmartSearchEntity);
await manager.query(`ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE vector(${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> {
const res = await this.smartSearchRepository.manager.query(`
SELECT atttypmod as dimsize

View File

@@ -8,5 +8,6 @@ export const newSearchRepositoryMock = (): jest.Mocked<ISearchRepository> => {
searchFaces: jest.fn(),
upsert: jest.fn(),
searchPlaces: jest.fn(),
deleteAllSearchEmbeddings: jest.fn(),
};
};

View File

@@ -13,6 +13,7 @@ module.exports = {
sourceType: 'module',
ecmaVersion: 2022,
extraFileExtensions: ['.svelte'],
project: ['./tsconfig.json'],
},
env: {
browser: true,
@@ -32,13 +33,6 @@ module.exports = {
NodeJS: true,
},
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': [
'warn',
{
@@ -48,5 +42,17 @@ module.exports = {
},
],
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',
},
};

346
web/package-lock.json generated
View File

@@ -1,67 +1,67 @@
{
"name": "immich-web",
"version": "1.1.1",
"version": "1.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-web",
"version": "1.1.1",
"version": "1.3.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@immich/sdk": "file:../open-api/typescript-sdk",
"@mdi/js": "^7.3.67",
"@photo-sphere-viewer/core": "^5.7.0",
"@zoom-image/svelte": "^0.2.0",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.7.1",
"@zoom-image/svelte": "^0.2.6",
"axios": "^1.6.7",
"buffer": "^6.0.3",
"copy-image-clipboard": "^2.1.2",
"dom-to-image": "^2.6.0",
"handlebars": "^4.7.7",
"handlebars": "^4.7.8",
"justified-layout": "^4.1.0",
"lodash-es": "^4.17.21",
"luxon": "^3.2.1",
"socket.io-client": "^4.6.1",
"svelte-local-storage-store": "^0.6.0",
"svelte-maplibre": "^0.8.0",
"luxon": "^3.4.4",
"socket.io-client": "^4.7.4",
"svelte-local-storage-store": "^0.6.4",
"svelte-maplibre": "^0.8.1",
"thumbhash": "^0.1.1"
},
"devDependencies": {
"@faker-js/faker": "^8.0.0",
"@floating-ui/dom": "^1.5.1",
"@faker-js/faker": "^8.4.1",
"@floating-ui/dom": "^1.6.3",
"@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/enhanced-img": "^0.1.8",
"@sveltejs/kit": "^2.5.1",
"@sveltejs/kit": "^2.5.2",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/svelte": "^4.0.3",
"@types/dom-to-image": "^2.6.4",
"@types/justified-layout": "^4.1.0",
"@types/lodash-es": "^4.17.6",
"@types/luxon": "^3.2.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"@vitest/coverage-v8": "^1.0.4",
"autoprefixer": "^10.4.13",
"eslint": "^8.34.0",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/svelte": "^4.1.0",
"@types/dom-to-image": "^2.6.7",
"@types/justified-layout": "^4.1.4",
"@types/lodash-es": "^4.17.12",
"@types/luxon": "^3.4.2",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"@vitest/coverage-v8": "^1.3.1",
"autoprefixer": "^10.4.17",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.30.0",
"eslint-plugin-unicorn": "^51.0.0",
"factory.ts": "^1.3.0",
"eslint-plugin-svelte": "^2.35.1",
"eslint-plugin-unicorn": "^51.0.1",
"factory.ts": "^1.4.1",
"identity-obj-proxy": "^3.0.0",
"postcss": "^8.4.21",
"prettier": "^3.1.0",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2",
"prettier-plugin-svelte": "^3.2.1",
"rollup-plugin-visualizer": "^5.12.0",
"svelte": "^4.2.11",
"svelte-check": "^3.6.4",
"tailwindcss": "^3.2.7",
"tslib": "^2.5.0",
"svelte": "^4.2.12",
"svelte-check": "^3.6.5",
"tailwindcss": "^3.4.1",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"vite": "^5.1.1",
"vitest": "^1.0.4"
"vite": "^5.1.4",
"vitest": "^1.3.1"
}
},
"../open-api/typescript-sdk": {
@@ -898,9 +898,9 @@
}
},
"node_modules/@eslint/js": {
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
"integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
"version": "8.57.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
"integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -948,13 +948,13 @@
"dev": true
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.13",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
"integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
"integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
"dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^2.0.1",
"debug": "^4.1.1",
"@humanwhocodes/object-schema": "^2.0.2",
"debug": "^4.3.1",
"minimatch": "^3.0.5"
},
"engines": {
@@ -975,9 +975,9 @@
}
},
"node_modules/@humanwhocodes/object-schema": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
"dev": true
},
"node_modules/@img/sharp-darwin-arm64": {
@@ -1859,9 +1859,9 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.1.tgz",
"integrity": "sha512-TKj08o3mJCoQNLTdRdGkHPePTCPUGTgkew65RDqjVU3MtPVxljsofXQYfXndHfq0P7KoPRO/0/reF6HesU0Djw==",
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.2.tgz",
"integrity": "sha512-1Pm2lsBYURQsjnLyZa+jw75eVD4gYHxGRwPyFe4DAmB3FjTVR8vRNWGeuDLGFcKMh/B1ij6FTUrc9GrerogCng==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@@ -2284,9 +2284,9 @@
"dev": true
},
"node_modules/@types/semver": {
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
"integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true
},
"node_modules/@types/supercluster": {
@@ -2298,16 +2298,16 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz",
"integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.0.tgz",
"integrity": "sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/type-utils": "7.0.1",
"@typescript-eslint/utils": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.1",
"@typescript-eslint/scope-manager": "7.1.0",
"@typescript-eslint/type-utils": "7.1.0",
"@typescript-eslint/utils": "7.1.0",
"@typescript-eslint/visitor-keys": "7.1.0",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@@ -2366,15 +2366,15 @@
"dev": true
},
"node_modules/@typescript-eslint/parser": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz",
"integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.0.tgz",
"integrity": "sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/types": "7.0.1",
"@typescript-eslint/typescript-estree": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.1",
"@typescript-eslint/scope-manager": "7.1.0",
"@typescript-eslint/types": "7.1.0",
"@typescript-eslint/typescript-estree": "7.1.0",
"@typescript-eslint/visitor-keys": "7.1.0",
"debug": "^4.3.4"
},
"engines": {
@@ -2394,13 +2394,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz",
"integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.0.tgz",
"integrity": "sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.1"
"@typescript-eslint/types": "7.1.0",
"@typescript-eslint/visitor-keys": "7.1.0"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -2411,13 +2411,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz",
"integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.0.tgz",
"integrity": "sha512-UZIhv8G+5b5skkcuhgvxYWHjk7FW7/JP5lPASMEUoliAPwIH/rxoUSQPia2cuOj9AmDZmwUl1usKm85t5VUMew==",
"dev": true,
"dependencies": {
"@typescript-eslint/typescript-estree": "7.0.1",
"@typescript-eslint/utils": "7.0.1",
"@typescript-eslint/typescript-estree": "7.1.0",
"@typescript-eslint/utils": "7.1.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
},
@@ -2438,9 +2438,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz",
"integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.0.tgz",
"integrity": "sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==",
"dev": true,
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -2451,13 +2451,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz",
"integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.0.tgz",
"integrity": "sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.1",
"@typescript-eslint/types": "7.1.0",
"@typescript-eslint/visitor-keys": "7.1.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -2536,17 +2536,17 @@
"dev": true
},
"node_modules/@typescript-eslint/utils": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz",
"integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.0.tgz",
"integrity": "sha512-WUFba6PZC5OCGEmbweGpnNJytJiLG7ZvDBJJoUcX4qZYf1mGZ97mO2Mps6O2efxJcJdRNpqweCistDbZMwIVHw==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/types": "7.0.1",
"@typescript-eslint/typescript-estree": "7.0.1",
"@typescript-eslint/scope-manager": "7.1.0",
"@typescript-eslint/types": "7.1.0",
"@typescript-eslint/typescript-estree": "7.1.0",
"semver": "^7.5.4"
},
"engines": {
@@ -2594,12 +2594,12 @@
"dev": true
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz",
"integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.0.tgz",
"integrity": "sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.0.1",
"@typescript-eslint/types": "7.1.0",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
@@ -2617,24 +2617,23 @@
"dev": true
},
"node_modules/@vitest/browser": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-1.0.4.tgz",
"integrity": "sha512-qMT1NhClex73eA2sOwnlwLcSIVCW8B7NFVzIKuXLKxSJD3LsNq8PCKhwOkBxklbSAcZdkOgL/d3/gzQT7k9eng==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-1.3.1.tgz",
"integrity": "sha512-pRof8G8nqRWwg3ouyIctyhfIVk5jXgF056uF//sqdi37+pVtDz9kBI/RMu0xlc8tgCyJ2aEMfbgJZPUydlEVaQ==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"estree-walker": "^3.0.3",
"@vitest/utils": "1.3.1",
"magic-string": "^0.30.5",
"sirv": "^2.0.3"
"sirv": "^2.0.4"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"playwright": "*",
"safaridriver": "*",
"vitest": "^1.0.0",
"vitest": "1.3.1",
"webdriverio": "*"
},
"peerDependenciesMeta": {
@@ -2650,9 +2649,9 @@
}
},
"node_modules/@vitest/coverage-v8": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.2.2.tgz",
"integrity": "sha512-IHyKnDz18SFclIEEAHb9Y4Uxx0sPKC2VO1kdDCs1BF6Ip4S8rQprs971zIsooLUn7Afs71GRxWMWpkCGZpRMhw==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.1.tgz",
"integrity": "sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.1",
@@ -2673,17 +2672,17 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"vitest": "^1.0.0"
"vitest": "1.3.1"
}
},
"node_modules/@vitest/expect": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz",
"integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz",
"integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==",
"dev": true,
"dependencies": {
"@vitest/spy": "1.2.2",
"@vitest/utils": "1.2.2",
"@vitest/spy": "1.3.1",
"@vitest/utils": "1.3.1",
"chai": "^4.3.10"
},
"funding": {
@@ -2691,12 +2690,12 @@
}
},
"node_modules/@vitest/runner": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz",
"integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz",
"integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==",
"dev": true,
"dependencies": {
"@vitest/utils": "1.2.2",
"@vitest/utils": "1.3.1",
"p-limit": "^5.0.0",
"pathe": "^1.1.1"
},
@@ -2732,9 +2731,9 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz",
"integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz",
"integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==",
"dev": true,
"dependencies": {
"magic-string": "^0.30.5",
@@ -2778,9 +2777,9 @@
"dev": true
},
"node_modules/@vitest/spy": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz",
"integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz",
"integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==",
"dev": true,
"dependencies": {
"tinyspy": "^2.2.0"
@@ -2790,9 +2789,9 @@
}
},
"node_modules/@vitest/utils": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz",
"integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz",
"integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==",
"dev": true,
"dependencies": {
"diff-sequences": "^29.6.3",
@@ -2837,9 +2836,9 @@
"dev": true
},
"node_modules/@zoom-image/core": {
"version": "0.32.1",
"resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.32.1.tgz",
"integrity": "sha512-R56D749Ck+/1yLWlEJ2FctxjdpTQEje3jPhOAbeEZGzLndIumskO42UqRNixcER6sAzCi01oYopmqnCpDElF0g==",
"version": "0.33.0",
"resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.33.0.tgz",
"integrity": "sha512-wkMV8+aE7PeknLFhpIb/6vwRl09Z2gWM4UqKdnXO6Mb0pP9BiuDLcLvGGGB4o++uAPINgDwmNn+Loo641XSjDA==",
"dependencies": {
"@namnode/store": "^0.1.0"
},
@@ -2849,11 +2848,11 @@
}
},
"node_modules/@zoom-image/svelte": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.4.tgz",
"integrity": "sha512-rgfgn7Q60VrwmE4MPBzDWaFplc+411Lxg1nMdAnq/UTv4HTWSpiwm1IOg8gQZjRp92a8RXcRmUYXU+wFKEMjSg==",
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.6.tgz",
"integrity": "sha512-dEpA/egmTjVcptwhtcKHvkhVMTzQCpH17erfcXuJByt+nn5Oo4LnZOxE8gwSVEdPp65Ns6Y/byYD0GSQ/vv+DQ==",
"dependencies": {
"@zoom-image/core": "0.32.1"
"@zoom-image/core": "0.33.0"
},
"funding": {
"type": "github",
@@ -4055,16 +4054,16 @@
}
},
"node_modules/eslint": {
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
"integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
"version": "8.57.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.4",
"@eslint/js": "8.56.0",
"@humanwhocodes/config-array": "^0.11.13",
"@eslint/js": "8.57.0",
"@humanwhocodes/config-array": "^0.11.14",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"@ungap/structured-clone": "^1.2.0",
@@ -7991,17 +7990,23 @@
}
},
"node_modules/strip-literal": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz",
"integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz",
"integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==",
"dev": true,
"dependencies": {
"acorn": "^8.10.0"
"js-tokens": "^8.0.2"
},
"funding": {
"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": {
"version": "3.34.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
@@ -8077,9 +8082,9 @@
}
},
"node_modules/svelte": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.11.tgz",
"integrity": "sha512-YIQk3J4X89wOLhjsqIW8tqY3JHPuBdtdOIkASP2PZeAMcSW9RsIjQzMesCrxOF3gdWYC0mKknlKF7OqmLM+Zqg==",
"version": "4.2.12",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.12.tgz",
"integrity": "sha512-d8+wsh5TfPwqVzbm4/HCXC783/KPHV60NvwitJnyTA5lWn1elhXMNWhXGCJ7PwPa8qFUnyJNIyuIRt2mT0WMug==",
"dependencies": {
"@ampproject/remapping": "^2.2.1",
"@jridgewell/sourcemap-codec": "^1.4.15",
@@ -8101,9 +8106,9 @@
}
},
"node_modules/svelte-check": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.4.tgz",
"integrity": "sha512-mY/dqucqm46p72M8yZmn81WPZx9mN6uuw8UVfR3ZKQeLxQg5HDGO3HHm5AZuWZPYNMLJ+TRMn+TeN53HfQ/vsw==",
"version": "3.6.5",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.5.tgz",
"integrity": "sha512-5aLgoQEdadvp8ypvKQ2avhnQ+V9YPQQaWrTFlXFw5g/v8xIQBvo+X/WqxTyD+V/ItDqXg3+abUA53rdDHgUjCA==",
"dev": true,
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.17",
@@ -8173,9 +8178,9 @@
}
},
"node_modules/svelte-maplibre": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.8.0.tgz",
"integrity": "sha512-sRSj/zQa7LTfHNIzKcYe+sa9qHClt/OAXcdPQ0w3ksLbCMmVHGk4B2yIXHCVk0g4sc18M85N8KGsHVtZoNC+Mw==",
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.8.1.tgz",
"integrity": "sha512-CTm/s0+mJzBHSoO5zPKBo3ORmUyiWS3Ex4xvVdNgVg+sDesHasEAJ0N1/NUrd56S33zgRdFZGzRnRguCnKFAzw==",
"dependencies": {
"d3-geo": "^3.1.0",
"just-compare": "^2.3.0",
@@ -8456,9 +8461,9 @@
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="
},
"node_modules/tinyspy": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz",
"integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
"integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
"dev": true,
"engines": {
"node": ">=14.0.0"
@@ -8733,9 +8738,9 @@
}
},
"node_modules/vite": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz",
"integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==",
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz",
"integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==",
"dev": true,
"dependencies": {
"esbuild": "^0.19.3",
@@ -8801,9 +8806,9 @@
}
},
"node_modules/vite-node": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz",
"integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz",
"integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==",
"dev": true,
"dependencies": {
"cac": "^6.7.14",
@@ -8837,18 +8842,17 @@
}
},
"node_modules/vitest": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz",
"integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz",
"integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==",
"dev": true,
"dependencies": {
"@vitest/expect": "1.2.2",
"@vitest/runner": "1.2.2",
"@vitest/snapshot": "1.2.2",
"@vitest/spy": "1.2.2",
"@vitest/utils": "1.2.2",
"@vitest/expect": "1.3.1",
"@vitest/runner": "1.3.1",
"@vitest/snapshot": "1.3.1",
"@vitest/spy": "1.3.1",
"@vitest/utils": "1.3.1",
"acorn-walk": "^8.3.2",
"cac": "^6.7.14",
"chai": "^4.3.10",
"debug": "^4.3.4",
"execa": "^8.0.1",
@@ -8857,11 +8861,11 @@
"pathe": "^1.1.1",
"picocolors": "^1.0.0",
"std-env": "^3.5.0",
"strip-literal": "^1.3.0",
"strip-literal": "^2.0.0",
"tinybench": "^2.5.1",
"tinypool": "^0.8.2",
"vite": "^5.0.0",
"vite-node": "1.2.2",
"vite-node": "1.3.1",
"why-is-node-running": "^2.2.2"
},
"bin": {
@@ -8876,8 +8880,8 @@
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "^1.0.0",
"@vitest/ui": "^1.0.0",
"@vitest/browser": "1.3.1",
"@vitest/ui": "1.3.1",
"happy-dom": "*",
"jsdom": "*"
},

View File

@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "1.1.1",
"version": "1.3.0",
"license": "GNU Affero General Public License version 3",
"scripts": {
"dev": "vite dev --host 0.0.0.0 --port 3000",
@@ -22,59 +22,59 @@
"prepare": "svelte-kit sync"
},
"devDependencies": {
"@faker-js/faker": "^8.0.0",
"@floating-ui/dom": "^1.5.1",
"@faker-js/faker": "^8.4.1",
"@floating-ui/dom": "^1.6.3",
"@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/enhanced-img": "^0.1.8",
"@sveltejs/kit": "^2.5.1",
"@sveltejs/kit": "^2.5.2",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/svelte": "^4.0.3",
"@types/dom-to-image": "^2.6.4",
"@types/justified-layout": "^4.1.0",
"@types/lodash-es": "^4.17.6",
"@types/luxon": "^3.2.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"@vitest/coverage-v8": "^1.0.4",
"autoprefixer": "^10.4.13",
"eslint": "^8.34.0",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/svelte": "^4.1.0",
"@types/dom-to-image": "^2.6.7",
"@types/justified-layout": "^4.1.4",
"@types/lodash-es": "^4.17.12",
"@types/luxon": "^3.4.2",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"@vitest/coverage-v8": "^1.3.1",
"autoprefixer": "^10.4.17",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.30.0",
"eslint-plugin-unicorn": "^51.0.0",
"factory.ts": "^1.3.0",
"eslint-plugin-svelte": "^2.35.1",
"eslint-plugin-unicorn": "^51.0.1",
"factory.ts": "^1.4.1",
"identity-obj-proxy": "^3.0.0",
"postcss": "^8.4.21",
"prettier": "^3.1.0",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2",
"prettier-plugin-svelte": "^3.2.1",
"rollup-plugin-visualizer": "^5.12.0",
"svelte": "^4.2.11",
"svelte-check": "^3.6.4",
"tailwindcss": "^3.2.7",
"tslib": "^2.5.0",
"svelte": "^4.2.12",
"svelte-check": "^3.6.5",
"tailwindcss": "^3.4.1",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"vite": "^5.1.1",
"vitest": "^1.0.4"
"vite": "^5.1.4",
"vitest": "^1.3.1"
},
"type": "module",
"dependencies": {
"@immich/sdk": "file:../open-api/typescript-sdk",
"@mdi/js": "^7.3.67",
"@photo-sphere-viewer/core": "^5.7.0",
"@zoom-image/svelte": "^0.2.0",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.7.1",
"@zoom-image/svelte": "^0.2.6",
"axios": "^1.6.7",
"buffer": "^6.0.3",
"copy-image-clipboard": "^2.1.2",
"dom-to-image": "^2.6.0",
"handlebars": "^4.7.7",
"handlebars": "^4.7.8",
"justified-layout": "^4.1.0",
"lodash-es": "^4.17.21",
"luxon": "^3.2.1",
"socket.io-client": "^4.6.1",
"svelte-local-storage-store": "^0.6.0",
"svelte-maplibre": "^0.8.0",
"luxon": "^3.4.4",
"socket.io-client": "^4.7.4",
"svelte-local-storage-store": "^0.6.4",
"svelte-maplibre": "^0.8.1",
"thumbhash": "^0.1.1"
}
}

View File

@@ -1,7 +1,6 @@
import { isHttpError } from '@immich/sdk';
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 parseError = (error: unknown) => {
@@ -23,6 +22,6 @@ const parseError = (error: unknown) => {
export const handleError: HandleClientError = ({ error }) => {
const result = parseError(error);
console.error(`${LOG_PREFIX}:handleError ${result.message}`);
console.error(`[hooks.client.ts]:handleError ${result.message}`);
return result;
};

View File

@@ -48,11 +48,11 @@
await handleCommand(jobId, dto);
};
const onConfirm = () => {
const onConfirm = async () => {
if (!confirmJob) {
return;
}
handleCommand(confirmJob, { command: JobCommand.Start, force: true });
await handleCommand(confirmJob, { command: JobCommand.Start, force: true });
confirmJob = null;
};

View File

@@ -54,7 +54,7 @@
});
};
const resetToDefault = async (configKeys: Array<keyof SystemConfigDto>) => {
const resetToDefault = (configKeys: Array<keyof SystemConfigDto>) => {
for (const key of configKeys) {
config = { ...config, [key]: defaultConfig[key] };
}

View File

@@ -42,28 +42,6 @@
subtitle="Watch external libraries for file changes"
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 class="ml-4">

View File

@@ -0,0 +1,45 @@
<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}

View File

@@ -0,0 +1,42 @@
<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"
/>

View File

@@ -21,6 +21,7 @@
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
import UpdatePanel from '../shared-components/update-panel.svelte';
import { handlePromiseError } from '$lib/utils';
export let sharedLink: SharedLinkResponseDto;
export let user: UserResponseDto | undefined = undefined;
@@ -35,7 +36,7 @@
dragAndDropFilesStore.subscribe((value) => {
if (value.isDragging && value.files.length > 0) {
fileUploadHandler(value.files, album.id);
handlePromiseError(fileUploadHandler(value.files, album.id));
dragAndDropFilesStore.set({ isDragging: false, files: [] });
}
});
@@ -67,7 +68,7 @@
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
onMount(async () => {
onMount(() => {
document.addEventListener('keydown', onKeyboardPress);
});

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
import { getAssetThumbnailUrl } from '$lib/utils';
import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
import { getAssetType } from '$lib/utils/asset-utils';
import { autoGrowHeight } from '$lib/utils/autogrow';
import { clickOutside } from '$lib/utils/click-outside';
@@ -79,7 +79,7 @@
$: {
if (assetId && previousAssetId != assetId) {
getReactions();
handlePromiseError(getReactions());
previousAssetId = assetId;
}
}
@@ -95,10 +95,10 @@
}
};
const handleEnter = (event: KeyboardEvent) => {
const handleEnter = async (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault();
handleSendComment();
await handleSendComment();
return;
}
};

View File

@@ -10,7 +10,7 @@
import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { stackAssetsStore } from '$lib/stores/stacked-asset.store';
import { user } from '$lib/stores/user.store';
import { getAssetJobMessage, isSharedLink } from '$lib/utils';
import { getAssetJobMessage, isSharedLink, handlePromiseError } from '$lib/utils';
import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error';
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
@@ -174,8 +174,8 @@
$: {
if (isShared && asset.id) {
getFavorite();
getNumberOfComments();
handlePromiseError(getFavorite());
handlePromiseError(getNumberOfComments());
}
}
@@ -184,9 +184,9 @@
if (value === SlideshowState.PlaySlideshow) {
slideshowHistory.reset();
slideshowHistory.queue(asset.id);
handlePlaySlideshow();
handlePromiseError(handlePlaySlideshow());
} else if (value === SlideshowState.StopSlideshow) {
handleStopSlideshow();
handlePromiseError(handleStopSlideshow());
}
});
@@ -226,7 +226,7 @@
}
});
$: asset.id && !sharedLink && handleGetAllAlbums(); // Update the album information when the asset ID changes
$: asset.id && !sharedLink && handlePromiseError(handleGetAllAlbums()); // Update the album information when the asset ID changes
const handleGetAllAlbums = async () => {
if (isSharedLink()) {
@@ -247,7 +247,7 @@
isShowActivity = !isShowActivity;
};
const handleKeypress = (event: KeyboardEvent) => {
const handleKeypress = async (event: KeyboardEvent) => {
if (shouldIgnoreShortcut(event)) {
return;
}
@@ -264,7 +264,7 @@
case 'a':
case 'A': {
if (shiftKey) {
toggleArchive();
await toggleArchive();
}
return;
}
@@ -273,18 +273,18 @@
return;
}
case 'ArrowRight': {
navigateAssetForward();
await navigateAssetForward();
return;
}
case 'd':
case 'D': {
if (shiftKey) {
downloadFile(asset);
await downloadFile(asset);
}
return;
}
case 'Delete': {
trashOrDelete(shiftKey);
await trashOrDelete(shiftKey);
return;
}
case 'Escape': {
@@ -296,7 +296,7 @@
return;
}
case 'f': {
toggleFavorite();
await toggleFavorite();
return;
}
case 'i': {
@@ -326,7 +326,7 @@
slideshowHistory.queue(asset.id);
setAssetId(asset.id);
await setAssetId(asset.id);
$restartSlideshowProgress = true;
};
@@ -369,17 +369,17 @@
$isShowDetail = !$isShowDetail;
};
const trashOrDelete = (force: boolean = false) => {
const trashOrDelete = async (force: boolean = false) => {
if (force || !isTrashEnabled) {
if ($showDeleteModal) {
isShowDeleteConfirmation = true;
return;
}
deleteAsset();
await deleteAsset();
return;
}
trashAsset();
await trashAsset();
return;
};
@@ -432,7 +432,7 @@
message: asset.isFavorite ? `Added to favorites` : `Removed from favorites`,
});
} catch (error) {
await handleError(error, `Unable to ${asset.isFavorite ? `add asset to` : `remove asset from`} favorites`);
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`,
});
} catch (error) {
await handleError(error, `Unable to ${asset.isArchived ? `add asset to` : `remove asset from`} archive`);
handleError(error, `Unable to ${asset.isArchived ? `add asset to` : `remove asset from`} archive`);
}
};
@@ -481,7 +481,7 @@
await runAssetJobs({ assetJobsDto: { assetIds: [asset.id], name } });
notificationController.show({ type: NotificationType.Info, message: getAssetJobMessage(name) });
} catch (error) {
await handleError(error, `Unable to submit job`);
handleError(error, `Unable to submit job`);
}
};
@@ -492,7 +492,7 @@
let assetViewerHtmlElement: HTMLElement;
const slideshowHistory = new SlideshowHistory((assetId: string) => {
setAssetId(assetId);
handlePromiseError(setAssetId(assetId));
$restartSlideshowProgress = true;
});
@@ -550,7 +550,7 @@
dispatch('close');
notificationController.show({ type: NotificationType.Info, message: 'Un-stacked', timeout: 1500 });
} catch (error) {
await handleError(error, `Unable to unstack`);
handleError(error, `Unable to unstack`);
}
};
</script>

View File

@@ -7,7 +7,7 @@
import { featureFlags } from '$lib/stores/server-config.store';
import { user } from '$lib/stores/user.store';
import { websocketEvents } from '$lib/stores/websocket';
import { getAssetThumbnailUrl, getPeopleThumbnailUrl, isSharedLink } from '$lib/utils';
import { getAssetThumbnailUrl, getPeopleThumbnailUrl, isSharedLink, handlePromiseError } from '$lib/utils';
import { delay, getAssetFilename } from '$lib/utils/asset-utils';
import { autoGrowHeight } from '$lib/utils/autogrow';
import { clickOutside } from '$lib/utils/click-outside';
@@ -78,7 +78,7 @@
originalDescription = description;
};
$: handleNewAsset(asset);
$: handlePromiseError(handleNewAsset(asset));
$: latlng = (() => {
const lat = asset.exifInfo?.latitude;
@@ -113,7 +113,7 @@
switch (event.key) {
case 'Enter': {
if (ctrl && event.target === textArea) {
handleFocusOut();
await handleFocusOut();
}
}
}

View File

@@ -4,7 +4,7 @@
import { boundingBoxesArray } from '$lib/stores/people.store';
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
import { photoZoomState } from '$lib/stores/zoom-image.store';
import { getKey } from '$lib/utils';
import { getKey, handlePromiseError } from '$lib/utils';
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
import { getBoundingBox } from '$lib/utils/people-utils';
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
@@ -102,7 +102,7 @@
}
};
const doZoomImage = async () => {
const doZoomImage = () => {
setZoomImageWheelState({
currentZoom: $zoomImageWheelState.currentZoom === 1 ? 2 : 1,
});
@@ -120,7 +120,7 @@
if (state.currentZoom > 1 && isWebCompatibleImage(asset) && !hasZoomed && !$alwaysLoadOriginalFile) {
hasZoomed = true;
loadAssetData({ loadOriginal: true });
handlePromiseError(loadAssetData({ loadOriginal: true }));
}
});
</script>

View File

@@ -20,7 +20,7 @@
video.muted = false;
dispatch('onVideoStarted');
} catch (error) {
await handleError(error, 'Unable to play video');
handleError(error, 'Unable to play video');
} finally {
isVideoLoading = false;
}

View File

@@ -49,7 +49,7 @@
if (assetType === AssetTypeEnum.Image) {
image = $photoViewer;
} else if (assetType === AssetTypeEnum.Video) {
const data = await getAssetThumbnailUrl(assetId, ThumbnailFormat.Webp);
const data = getAssetThumbnailUrl(assetId, ThumbnailFormat.Webp);
const img: HTMLImageElement = new Image();
img.src = data;

View File

@@ -43,10 +43,10 @@
dispatch('back');
};
const handleSwapPeople = () => {
const handleSwapPeople = async () => {
[person, selectedPeople[0]] = [selectedPeople[0], person];
$page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE);
goto(`${AppRoute.PEOPLE}/${person.id}?${$page.url.searchParams.toString()}`);
await goto(`${AppRoute.PEOPLE}/${person.id}?${$page.url.searchParams.toString()}`);
};
const onSelect = (selected: PersonResponseDto) => {

View File

@@ -3,7 +3,7 @@
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
import { boundingBoxesArray } from '$lib/stores/people.store';
import { websocketEvents } from '$lib/stores/websocket';
import { getPeopleThumbnailUrl } from '$lib/utils';
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { getPersonNameWithHiddenValue } from '$lib/utils/person';
import {
@@ -46,8 +46,8 @@
let allPeople: PersonResponseDto[] = [];
// timers
let loaderLoadingDoneTimeout: NodeJS.Timeout;
let automaticRefreshTimeout: NodeJS.Timeout;
let loaderLoadingDoneTimeout: ReturnType<typeof setTimeout>;
let automaticRefreshTimeout: ReturnType<typeof setTimeout>;
const dispatch = createEventDispatcher<{
close: void;
@@ -85,7 +85,7 @@
};
onMount(() => {
loadPeople();
handlePromiseError(loadPeople());
return websocketEvents.on('on_person_thumbnail', onPersonThumbnail);
});
@@ -164,7 +164,7 @@
}
};
const handlePersonPicker = async (index: number) => {
const handlePersonPicker = (index: number) => {
editedPersonIndex = index;
showSeletecFaces = true;
};

View File

@@ -132,9 +132,7 @@
title={'Assign selected assets to a new person'}
size={'sm'}
disabled={disableButtons || hasSelection}
on:click={() => {
handleCreate();
}}
on:click={handleCreate}
>
{#if !showLoadingSpinnerCreate}
<Icon path={mdiPlus} size={18} />
@@ -147,9 +145,7 @@
size={'sm'}
title={'Assign selected assets to an existing person'}
disabled={disableButtons || !hasSelection}
on:click={() => {
handleReassign();
}}
on:click={handleReassign}
>
{#if !showLoadingSpinnerReassign}
<div>

Some files were not shown because too many files have changed in this diff Show More